blob: 2da851087581acd0336354e956aa5f1378ccc4c5 [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io' show File;
import 'dart:typed_data' show Uint8List;
import 'package:_fe_analyzer_shared/src/experiments/errors.dart'
show getExperimentNotEnabledMessage;
import 'package:_fe_analyzer_shared/src/experiments/flags.dart'
as shared
show ExperimentalFlag;
import 'package:_fe_analyzer_shared/src/parser/experimental_features.dart';
import 'package:_fe_analyzer_shared/src/parser/parser.dart'
show Parser, lengthOfSpan;
import 'package:_fe_analyzer_shared/src/scanner/scanner.dart'
show
ErrorToken,
LanguageVersionChanged,
LanguageVersionToken,
ScannerConfiguration,
ScannerResult,
Token,
scan,
scanDirectives;
import 'package:_fe_analyzer_shared/src/scanner/token.dart'
show SyntheticStringToken;
import 'package:front_end/src/api_prototype/experimental_flags.dart'
show ExperimentalFlag;
import 'package:front_end/src/base/command_line_reporting.dart'
as command_line_reporting;
import 'package:front_end/src/base/messages.dart' show Message;
import 'package:front_end/src/source/diet_parser.dart'
show useImplicitCreationExpressionInCfe;
import 'package:front_end/src/source/stack_listener_impl.dart'
show offsetForToken;
import 'package:kernel/ast.dart';
import 'package:testing/testing.dart' show TestDescription;
import 'parser_test_listener.dart' show ParserTestListener;
import 'testing/experimental_features.dart' show ExperimentalFeaturesFromFlags;
import 'testing/folder_options.dart' show FolderOptions, SuiteFolderOptions;
String? compareTestListeners(
ParserTestListenerWithMessageFormatting a,
ParserTestListenerWithMessageFormatting b, {
required Set<String> filters,
required Set<String> ignored,
}) {
List<String> aLines = a.sb.toString().split("\n");
List<String> bLines = b.sb.toString().split("\n");
bool doRemoveListenerArguments = filters.contains("ignoreListenerArguments");
int aIndex = 0;
int bIndex = 0;
while (aIndex < aLines.length && bIndex < bLines.length) {
String aLine = aLines[aIndex];
String bLine = bLines[bIndex];
if (doRemoveListenerArguments) {
aLine = removeListenerArguments(aLine);
bLine = removeListenerArguments(bLine);
}
bool anyIgnored = false;
if (ignored.contains(aLine.trim())) {
anyIgnored = true;
aIndex++;
}
if (ignored.contains(bLine.trim())) {
anyIgnored = true;
bIndex++;
}
if (anyIgnored) continue;
if (aLine.trim() != bLine.trim()) {
return "Disagreement: '${aLine}' vs '${bLine}'";
}
aIndex++;
bIndex++;
}
// Any trailing lines?
while (aIndex < aLines.length) {
String aLine = aLines[aIndex];
if (doRemoveListenerArguments) {
aLine = removeListenerArguments(aLine);
}
if (ignored.contains(aLine.trim())) {
aIndex++;
continue;
}
return "Unmatched line at end: '$aLine'";
}
while (bIndex < bLines.length) {
String bLine = bLines[bIndex];
if (doRemoveListenerArguments) {
bLine = removeListenerArguments(bLine);
}
if (ignored.contains(bLine.trim())) {
bIndex++;
continue;
}
return "Unmatched line at end: '$bLine'";
}
return null;
}
/// Scans the uri, parses it with the test listener and returns it.
///
/// Returns null if scanner doesn't return any Token.
ParserTestListenerWithMessageFormatting? doListenerParsing(
Uri uri,
String suiteName,
Map<ExperimentalFlag, bool> explicitExperimentalFlags,
String shortName, {
bool addTrace = false,
bool annotateLines = false,
bool scanDirectivesOnly = false,
bool parseDirectivesOnly = false,
}) {
ExperimentalFeaturesFromFlags experimentalFeatures =
new ExperimentalFeaturesFromFlags(explicitExperimentalFlags);
List<int> lineStarts = <int>[];
Token firstToken = scanUri(
uri,
experimentalFeatures,
lineStarts: lineStarts,
languageVersionChanged: experimentalFeatures.onLanguageVersionChanged,
directivesOnly: scanDirectivesOnly,
);
File f = new File.fromUri(uri);
Uint8List rawBytes = f.readAsBytesSync();
Source source = new Source(lineStarts, rawBytes, uri, uri);
String shortNameId = "${suiteName}/${shortName}";
ParserTestListenerWithMessageFormatting parserTestListener =
new ParserTestListenerWithMessageFormatting(
addTrace,
annotateLines,
source,
shortNameId,
);
Parser parser = new Parser(
parserTestListener,
useImplicitCreationExpression: useImplicitCreationExpressionInCfe,
experimentalFeatures: experimentalFeatures,
);
if (parseDirectivesOnly) {
parser.parseDirectives(firstToken);
} else {
parser.parseUnit(firstToken);
}
return parserTestListener;
}
String removeListenerArguments(String s) {
int index = s.indexOf("(");
if (index < 0) return s;
return s.substring(0, index);
}
Token scanRawBytes(
Uint8List rawBytes,
ScannerConfiguration config,
List<int>? lineStarts, {
LanguageVersionChanged? languageVersionChanged,
bool directivesOnly = false,
}) {
ScannerResult scanResult;
if (directivesOnly) {
scanResult = scanDirectives(
rawBytes,
configuration: config,
languageVersionChanged: languageVersionChanged,
);
} else {
scanResult = scan(
rawBytes,
configuration: config,
includeComments: true,
languageVersionChanged: languageVersionChanged,
);
}
Token firstToken = scanResult.tokens;
if (lineStarts != null) {
lineStarts.addAll(scanResult.lineStarts);
}
return firstToken;
}
Token scanUri(
Uri uri,
ExperimentalFeaturesFromFlags experimentalFeatures, {
List<int>? lineStarts,
LanguageVersionChanged? languageVersionChanged,
bool directivesOnly = false,
}) {
File f = new File.fromUri(uri);
Uint8List rawBytes = f.readAsBytesSync();
return scanRawBytes(
rawBytes,
experimentalFeatures.buildScannerConfiguration(),
lineStarts,
languageVersionChanged: languageVersionChanged,
directivesOnly: directivesOnly,
);
}
StringBuffer tokenStreamToString(
Token firstToken,
List<int> lineStarts, {
bool addTypes = false,
List<LanguageVersionToken>? languageVersionTokensSeen,
}) {
StringBuffer sb = new StringBuffer();
Token? token = firstToken;
if (languageVersionTokensSeen != null) {
for (LanguageVersionToken languageVersion in languageVersionTokensSeen) {
sb.writeln(
"// @dart = ${languageVersion.major}.${languageVersion.minor}",
);
}
}
Token? process(Token? token, bool errorTokens) {
bool printed = false;
int endOfLast = -1;
int lineStartsIteratorLine = 1;
Iterator<int> lineStartsIterator = lineStarts.iterator;
lineStartsIterator.moveNext();
lineStartsIterator.moveNext();
lineStartsIteratorLine++;
Set<Token> seenTokens = new Set<Token>.identity();
while (token != null) {
if (errorTokens && token is! ErrorToken) return token;
if (!errorTokens && token is ErrorToken) {
if (token == token.next) break;
token = token.next;
continue;
}
int prevLine = lineStartsIteratorLine;
while (token.offset >= lineStartsIterator.current &&
lineStartsIterator.moveNext()) {
lineStartsIteratorLine++;
}
if (printed &&
(token.offset > endOfLast || prevLine < lineStartsIteratorLine)) {
if (prevLine < lineStartsIteratorLine) {
for (int i = prevLine; i < lineStartsIteratorLine; i++) {
sb.write("\n");
}
} else {
sb.write(" ");
}
}
if (token is! ErrorToken) {
sb.write(token.lexeme);
if (!addTypes && token.lexeme == "" && token is SyntheticStringToken) {
sb.write("*synthetic*");
}
}
if (addTypes) {
// Avoid 6000+ changes caused by "Impl" being added to some token
// classes.
String type = token.runtimeType.toString().replaceFirst("Impl", "");
sb.write("[$type]");
}
printed = true;
endOfLast = token.end;
if (token == token.next) break;
token = token.next;
if (!seenTokens.add(token!)) {
// Loop in tokens: Print error and break to avoid infinite loop.
sb.write(
"\n\nERROR: Loop in tokens: $token "
"(${token.runtimeType}, ${token.type}, ${token.offset})) "
"was seen before "
"(linking to ${token.next}, ${token.next.runtimeType}, "
"${token.next!.type}, ${token.next!.offset})!\n\n",
);
break;
}
}
return token;
}
if (addTypes) {
token = process(token, true);
}
token = process(token, false);
return sb;
}
class ParserTestListenerWithMessageFormatting extends ParserTestListener {
final bool annotateLines;
final Source? source;
final String? shortName;
final List<String> errors = <String>[];
Location? latestSeenLocation;
new(bool trace, this.annotateLines, this.source, this.shortName)
: super(trace);
@override
bool checkEof(Token token) {
bool result = super.checkEof(token);
if (result) {
errors.add("WARNING: Reporting at eof --- see below for details.");
}
return result;
}
@override
void doPrint(String s) {
super.doPrint(s);
if (!annotateLines) {
if (s.startsWith("beginCompilationUnit(") ||
s.startsWith("endCompilationUnit(")) {
if (indent != 0) {
throw "Incorrect indents: '$s' (indent = $indent).\n\n"
"${sb.toString()}";
}
} else {
if (indent <= 0) {
throw "Incorrect indents: '$s' (indent = $indent).\n\n"
"${sb.toString()}";
}
}
}
}
@override
void handleExperimentNotEnabled(
shared.ExperimentalFlag experimentalFlag,
Token startToken,
Token endToken,
) {
_reportMessage(
getExperimentNotEnabledMessage(experimentalFlag),
startToken,
endToken,
);
super.handleExperimentNotEnabled(experimentalFlag, startToken, endToken);
}
@override
void handleRecoverableError(
Message message,
Token startToken,
Token endToken,
) {
_reportMessage(message, startToken, endToken);
super.handleRecoverableError(message, startToken, endToken);
}
@override
void seen(Token? token) {
if (!annotateLines) return;
if (token == null) return;
if (source == null) return;
if (offsetForToken(token) < 0) return;
Location location = source!.getLocation(
source!.fileUri!,
offsetForToken(token),
);
if (latestSeenLocation == null ||
location.line > latestSeenLocation!.line) {
latestSeenLocation = location;
String? sourceLine = source!.getTextLine(location.line);
doPrint("");
doPrint("// Line ${location.line}: $sourceLine");
}
}
void _reportMessage(Message message, Token startToken, Token endToken) {
if (source != null) {
Location location = source!.getLocation(
source!.fileUri!,
offsetForToken(startToken),
);
int length = lengthOfSpan(startToken, endToken);
if (length <= 0) length = 1;
errors.add(
command_line_reporting.formatErrorMessage(
source!.getTextLine(location.line),
location,
length,
shortName,
message.problemMessage,
),
);
} else {
errors.add(message.problemMessage);
}
}
}
abstract interface class StandardContextAdditions {
SuiteFolderOptions get folderOptions;
Map<ExperimentalFlag, bool> get forcedExperimentalFlags;
}
extension TestDescriptionHelper on TestDescription {
Map<ExperimentalFlag, bool> computeExplicitExperimentalFlags(
StandardContextAdditions context,
) {
return computeFolderOptions(context)
.computeExplicitExperimentalFlags(context.forcedExperimentalFlags);
}
FolderOptions computeFolderOptions(StandardContextAdditions context) {
return context.folderOptions.computeFolderOptions(this);
}
}