blob: 5c630442ae417fb9283326925b9d8267b0ad5430 [file]
// Copyright (c) 2019, 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:convert' show jsonDecode;
import 'dart:io' show File;
import 'dart:typed_data' show Uint8List;
import 'package:_fe_analyzer_shared/src/parser/parser.dart' show Parser;
import 'package:_fe_analyzer_shared/src/scanner/scanner.dart'
show Token, Scanner, LanguageVersionToken;
import 'package:front_end/src/api_prototype/experimental_flags.dart';
import 'package:front_end/src/source/diet_parser.dart'
show useImplicitCreationExpressionInCfe;
import 'package:front_end/src/util/parser_ast.dart';
import 'package:front_end/src/util/parser_ast_helper.dart';
import 'package:kernel/ast.dart';
import 'package:testing/testing.dart'
show Chain, ChainContext, ExpectationSet, Result, Step, TestDescription;
import 'parser_suite_utils.dart';
import 'parser_test_listener.dart' show ParserTestListener;
import 'parser_test_parser.dart' show TestParser;
import 'testing/environment_keys.dart';
import 'testing/experimental_features.dart';
import 'testing/folder_options.dart';
import 'testing_utils.dart' show checkEnvironment;
import 'utils/kernel_chain.dart' show MatchContext;
import 'utils/suite_utils.dart';
const String EXPECTATIONS = '''
[
{
"name": "ExpectationFileMismatch",
"group": "Fail"
},
{
"name": "ExpectationFileMissing",
"group": "Fail"
}
]
''';
void main([List<String> arguments = const []]) => internalMain(
createContext,
arguments: arguments,
displayName: "parser suite",
configurationPath: "../testing.json",
);
Future<Context> createContext(Chain suite, Map<String, String> environment) {
const Set<String> knownEnvironmentKeys = {
EnvironmentKeys.updateExpectations,
EnvironmentKeys.trace,
EnvironmentKeys.annotateLines,
};
checkEnvironment(environment, knownEnvironmentKeys);
bool updateExpectations =
environment[EnvironmentKeys.updateExpectations] == "true";
bool trace = environment[EnvironmentKeys.trace] == "true";
bool annotateLines = environment[EnvironmentKeys.annotateLines] == "true";
return new Future.value(
new Context(
baseUri: suite.root,
suiteName: suite.name,
updateExpectations: updateExpectations,
addTrace: trace,
annotateLines: annotateLines,
environment: environment,
),
);
}
class Context extends ChainContext
with MatchContext
implements StandardContextAdditions {
@override
final bool updateExpectations;
@override
String get updateExpectationsOption =>
'${EnvironmentKeys.updateExpectations}=true';
@override
bool get canBeFixWithUpdateExpectations => true;
final bool addTrace;
final bool annotateLines;
final String suiteName;
@override
final SuiteFolderOptions folderOptions;
@override
final Map<ExperimentalFlag, bool> forcedExperimentalFlags;
new({
required Uri baseUri,
required this.suiteName,
required this.updateExpectations,
required this.addTrace,
required this.annotateLines,
required Map<String, String> environment,
}) : folderOptions = new SuiteFolderOptions(baseUri),
forcedExperimentalFlags =
SuiteFolderOptions.computeForcedExperimentalFlags(environment);
@override
final List<Step> steps = const <Step>[
const TokenStep(true, ".scanner.directives.expect", directivesOnly: true),
const TokenStep(true, ".scanner.expect"),
const TokenStep(false, ".parser.expect"),
const ParserAstStep(true),
const ListenerStep(true),
const CompareDirectivesStep(),
const IntertwinedStep(),
];
@override
final ExpectationSet expectationSet = new ExpectationSet.fromJsonList(
jsonDecode(EXPECTATIONS),
);
}
class ContextChecksOnly extends Context {
new({
required Uri baseUri,
required String suiteName,
required Map<String, String> environment,
}) : super(
baseUri: baseUri,
suiteName: suiteName,
updateExpectations: false,
addTrace: false,
annotateLines: false,
environment: environment,
);
@override
final List<Step> steps = const <Step>[
const ListenerStep(false),
const CompareDirectivesStep(),
const ParserAstStep(false),
];
@override
final ExpectationSet expectationSet = new ExpectationSet.fromJsonList(
jsonDecode(EXPECTATIONS),
);
}
class ParserAstStep extends Step<TestDescription, TestDescription, Context> {
final bool enablePossibleExpectFile;
const new(this.enablePossibleExpectFile);
@override
String get name => "ParserAst";
@override
Future<Result<TestDescription>> run(
TestDescription description,
Context context,
) {
FolderOptions folderOptions = description.computeFolderOptions(context);
Uri uri = description.uri;
File f = new File.fromUri(uri);
Uint8List rawBytes = f.readAsBytesSync();
ParserAstNode ast = getAST(rawBytes);
if (ast.what != "CompilationUnit") {
throw "Expected a single element for 'CompilationUnit' "
"but got ${ast.what}";
}
if (enablePossibleExpectFile && folderOptions.withOutline) {
ExtractSomeMembers indexer = new ExtractSomeMembers();
ast.accept(indexer);
return context.match<TestDescription>(
".outline.expect",
indexer.sb.toString(),
description.uri,
description,
);
}
return new Future.value(new Result<TestDescription>.pass(description));
}
}
class ExtractSomeMembers extends RecursiveParserAstVisitor {
StringBuffer sb = new StringBuffer();
String? currentContainerName;
@override
void visitClassDeclarationEnd(ClassDeclarationEnd node) {
currentContainerName = node.getClassIdentifier().token.lexeme;
sb.writeln("Class: $currentContainerName");
super.visitClassDeclarationEnd(node);
currentContainerName = null;
}
@override
void visitTopLevelMethodEnd(TopLevelMethodEnd node) {
String name = node.getNameIdentifier().token.lexeme;
sb.writeln("Top-level method: $name");
}
@override
void visitMethodEnd(MethodEnd node) {
sb.writeln("Method: $currentContainerName.${node.getNameIdentifier()}");
}
}
class ListenerStep extends Step<TestDescription, TestDescription, Context> {
final bool doExpects;
final bool compareDirectives;
const new(this.doExpects, {this.compareDirectives = false});
@override
String get name => "listener";
@override
Future<Result<TestDescription>> run(
TestDescription description,
Context context,
) {
Uri uri = description.uri;
if (compareDirectives) {
return _compareDirectives(uri, context, description);
}
ParserTestListenerWithMessageFormatting? parserTestListener =
doListenerParsing(
uri,
context.suiteName,
description.computeExplicitExperimentalFlags(context),
description.shortName,
addTrace: context.addTrace,
annotateLines: context.annotateLines,
);
if (parserTestListener == null) {
return Future.value(crash(description, StackTrace.current));
}
String errors = "";
if (parserTestListener.errors.isNotEmpty) {
errors =
"Problems reported:\n\n"
"${parserTestListener.errors.join("\n\n")}\n\n";
}
if (doExpects) {
return context.match<TestDescription>(
".expect",
"${errors}${parserTestListener.sb}",
uri,
description,
);
} else {
return new Future.value(new Result<TestDescription>.pass(description));
}
}
Future<Result<TestDescription>> _compareDirectives(
Uri uri,
Context context,
TestDescription description,
) {
ParserTestListenerWithMessageFormatting? scanParseDirectivesOnly =
doListenerParsing(
uri,
context.suiteName,
description.computeExplicitExperimentalFlags(context),
description.shortName,
addTrace: context.addTrace,
annotateLines: context.annotateLines,
parseDirectivesOnly: true,
scanDirectivesOnly: true,
);
ParserTestListenerWithMessageFormatting? parseDirectivesOnly =
doListenerParsing(
uri,
context.suiteName,
description.computeExplicitExperimentalFlags(context),
description.shortName,
addTrace: context.addTrace,
annotateLines: context.annotateLines,
parseDirectivesOnly: true,
scanDirectivesOnly: false,
);
String? scanParseResult = scanParseDirectivesOnly?.sb.toString();
String? parseResult = parseDirectivesOnly?.sb.toString();
if (scanParseResult != parseResult) {
return Future.value(
new Result<TestDescription>.fail(
description,
"Different definitions:\n$scanParseResult\n\nvs\n\n$parseResult",
),
);
}
return Future.value(new Result<TestDescription>.pass(description));
}
}
class CompareDirectivesStep
extends Step<TestDescription, TestDescription, Context> {
const new();
@override
String get name => "compare directives";
@override
Future<Result<TestDescription>> run(
TestDescription description,
Context context,
) {
Uri uri = description.uri;
ParserTestListenerWithMessageFormatting? scanParseDirectivesOnly =
doListenerParsing(
uri,
context.suiteName,
description.computeExplicitExperimentalFlags(context),
description.shortName,
addTrace: context.addTrace,
annotateLines: context.annotateLines,
parseDirectivesOnly: true,
scanDirectivesOnly: true,
);
ParserTestListenerWithMessageFormatting? parseDirectivesOnly =
doListenerParsing(
uri,
context.suiteName,
description.computeExplicitExperimentalFlags(context),
description.shortName,
addTrace: context.addTrace,
annotateLines: context.annotateLines,
parseDirectivesOnly: true,
scanDirectivesOnly: false,
);
if (scanParseDirectivesOnly == null || parseDirectivesOnly == null) {
throw "";
}
String? compareResult = compareTestListeners(
scanParseDirectivesOnly,
parseDirectivesOnly,
filters: {"ignoreListenerArguments"},
ignored: {"beginMetadataStar", "endMetadataStar", "handleDirectivesOnly"},
);
if (compareResult != null) {
return Future.value(
fail(
description,
"$compareResult\n\n"
"Comparing:\n(scan and parse only directives:)\n\n"
"${scanParseDirectivesOnly.sb.toString()}"
"\n\nwith\n(scan normally, parse only directives:)\n\n"
"${parseDirectivesOnly.sb.toString()}",
StackTrace.current,
),
);
}
return Future.value(new Result<TestDescription>.pass(description));
}
}
class IntertwinedStep extends Step<TestDescription, TestDescription, Context> {
const new();
@override
String get name => "intertwined";
@override
Future<Result<TestDescription>> run(
TestDescription description,
Context context,
) {
List<int> lineStarts = <int>[];
Map<ExperimentalFlag, bool> explicitExperimentalFlags = description
.computeExplicitExperimentalFlags(context);
ExperimentalFeaturesFromFlags experimentalFeatures =
new ExperimentalFeaturesFromFlags(explicitExperimentalFlags);
Token firstToken = scanUri(
description.uri,
experimentalFeatures,
lineStarts: lineStarts,
languageVersionChanged: experimentalFeatures.onLanguageVersionChanged,
);
File f = new File.fromUri(description.uri);
Uint8List rawBytes = f.readAsBytesSync();
Source source = new Source(
lineStarts,
rawBytes,
description.uri,
description.uri,
);
ParserTestListenerForIntertwined parserTestListener =
new ParserTestListenerForIntertwined(
context.addTrace,
context.annotateLines,
source,
);
TestParser parser = new TestParser(
parserTestListener,
context.addTrace,
experimentalFeatures: experimentalFeatures,
);
parserTestListener.parser = parser;
parser.sb = parserTestListener.sb;
parser.parseUnit(firstToken);
return context.match<TestDescription>(
".intertwined.expect",
"${parser.sb}",
description.uri,
description,
);
}
}
class TokenStep extends Step<TestDescription, TestDescription, Context> {
final bool onlyScanner;
final String suffix;
final bool directivesOnly;
const new(this.onlyScanner, this.suffix, {this.directivesOnly = false});
@override
String get name =>
"token (${directivesOnly
? "directives"
: onlyScanner
? "scanner"
: "parser"})";
@override
Future<Result<TestDescription>> run(
TestDescription description,
Context context,
) {
Map<ExperimentalFlag, bool> explicitExperimentalFlags = description
.computeExplicitExperimentalFlags(context);
ExperimentalFeaturesFromFlags experimentalFeatures =
new ExperimentalFeaturesFromFlags(explicitExperimentalFlags);
List<int> lineStarts = <int>[];
List<LanguageVersionToken> languageVersionTokensSeen = [];
Token firstToken = scanUri(
description.uri,
experimentalFeatures,
lineStarts: lineStarts,
languageVersionChanged:
(Scanner scanner, LanguageVersionToken languageVersion) {
languageVersionTokensSeen.add(languageVersion);
experimentalFeatures.onLanguageVersionChanged(
scanner,
languageVersion,
);
},
directivesOnly: directivesOnly,
);
if (directivesOnly &&
firstToken.isEof &&
languageVersionTokensSeen.isEmpty) {
// Expect no file.
return context.match<TestDescription>(
suffix,
// An empty actual parameter deletes any existing file if asked to
// update expectations.
"",
description.uri,
description,
);
}
StringBuffer beforeParser = tokenStreamToString(
firstToken,
lineStarts,
languageVersionTokensSeen: languageVersionTokensSeen,
);
StringBuffer beforeParserWithTypes = tokenStreamToString(
firstToken,
lineStarts,
addTypes: true,
);
if (onlyScanner) {
return context.match<TestDescription>(
suffix,
"${beforeParser}\n\n${beforeParserWithTypes}",
description.uri,
description,
);
}
ParserTestListener parserTestListener = new ParserTestListener(
context.addTrace,
);
Parser parser = new Parser(
parserTestListener,
useImplicitCreationExpression: useImplicitCreationExpressionInCfe,
experimentalFeatures: experimentalFeatures,
);
bool parserCrashed = false;
dynamic parserCrashedE;
StackTrace? parserCrashedSt;
try {
parser.parseUnit(firstToken);
} catch (e, st) {
parserCrashed = true;
parserCrashedE = e;
parserCrashedSt = st;
}
StringBuffer afterParser = tokenStreamToString(firstToken, lineStarts);
StringBuffer afterParserWithTypes = tokenStreamToString(
firstToken,
lineStarts,
addTypes: true,
);
bool rewritten =
beforeParserWithTypes.toString() != afterParserWithTypes.toString();
String rewrittenString = rewritten
? "NOTICE: Stream was rewritten by parser!\n\n"
: "";
Future<Result<TestDescription>> result = context.match<TestDescription>(
suffix,
"${rewrittenString}${afterParser}\n\n${afterParserWithTypes}",
description.uri,
description,
);
return result.then((result) {
if (parserCrashed) {
return crash("Parser crashed: $parserCrashedE", parserCrashedSt!);
} else {
return result;
}
});
}
}
class ParserTestListenerForIntertwined
extends ParserTestListenerWithMessageFormatting {
late TestParser parser;
new(bool trace, bool annotateLines, Source source)
: super(trace, annotateLines, source, null);
@override
void doPrint(String s) {
int prevIndent = super.indent;
super.indent = parser.indent;
if (s.trim() == "") {
super.doPrint("");
} else {
super.doPrint("listener: " + s);
}
super.indent = prevIndent;
}
}