| // 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); |
| } |
| } |