blob: b4dcab12d270339fb926fd7dec438e9c924e18a6 [file] [log] [blame]
// Copyright (c) 2017, 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';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:test/test.dart';
import '../../../generated/parser_test_base.dart';
import '../../../generated/test_support.dart';
/// The base class for tests that test how well the parser recovers from various
/// syntactic errors.
abstract class AbstractRecoveryTest extends FastaParserTestCase {
void testRecovery(
String invalidCode,
List<DiagnosticCode>? diagnosticCodes,
String validCode, {
CompilationUnitImpl Function(CompilationUnitImpl unit)?
adjustValidUnitBeforeComparison,
List<DiagnosticCode>? expectedDiagnosticsInValidCode,
FeatureSet? featureSet,
}) {
CompilationUnitImpl validUnit;
// Assert that the valid code is indeed valid.
try {
validUnit = parseCompilationUnit(
validCode,
codes: expectedDiagnosticsInValidCode,
featureSet: featureSet,
);
validateTokenStream(validUnit.beginToken);
} catch (e) {
// print('');
// print(' Errors in valid code.');
// print(' Error: $e');
// print(' Code: $validCode');
// print('');
rethrow;
}
// Compare the structures before asserting valid errors.
GatheringDiagnosticListener listener = GatheringDiagnosticListener();
var invalidUnit = parseCompilationUnit2(
invalidCode,
listener,
featureSet: featureSet,
);
validateTokenStream(invalidUnit.beginToken);
if (adjustValidUnitBeforeComparison != null) {
validUnit = adjustValidUnitBeforeComparison(validUnit);
}
ResultComparator.compare(invalidUnit, validUnit);
// Assert valid errors.
if (diagnosticCodes != null) {
listener.assertErrorsWithCodes(diagnosticCodes);
} else {
listener.assertNoErrors();
}
}
void validateTokenStream(Token token) {
while (!token.isEof) {
Token next = token.next!;
expect(token.end, lessThanOrEqualTo(next.offset));
if (next.isSynthetic) {
if (const [')', ']', '}'].contains(next.lexeme)) {
expect(next.beforeSynthetic, token);
}
}
token = next;
}
}
}
/// An object used to compare to AST structures and cause the test to fail if
/// they differ in any important ways.
class ResultComparator {
/// Compares two lists in a syntax tree, and if they don't match, returns a
/// description of how they fail to match.
///
/// The two lists being compared might be lists of AST nodes or lists of
/// tokens.
_Mismatch? _compareLists({
required List<Object?> actual,
required List<Object?> expected,
}) {
int length = actual.length;
if (expected.length != length) {
return _failDifferentLength(actual: actual, expected: expected);
}
for (int i = 0; i < length; i++) {
if (_compareTreeParts(actual: actual[i], expected: expected[i])
case var mismatch?) {
mismatch.reversePath.add(_IndexPathSegment(i));
return mismatch;
}
}
return null;
}
/// Compares two [AstNode]s in a syntax tree, and if they don't match, returns
/// a description of how they fail to match. The caller should have already
/// checked that the two nodes have the same [runtimeType].
///
/// The nodes are compared by using [AstNodeImpl.namedChildEntities] to
/// convert them each into a `Map<String, Object>`, whose keys typically match
/// the names of syntactic getters, and whose values typically match the
/// values returned by those getters. These maps are then compared using
/// [_compareTreeParts].
_Mismatch? _compareNodesWithMatchingTypes({
required AstNodeImpl actual,
required AstNodeImpl expected,
}) {
var allKeys = <String>{};
var actualChildEntities = _computeChildEntitiesMap(
actual,
keyAccumulator: allKeys,
);
var expectedChildEntities = _computeChildEntitiesMap(
expected,
keyAccumulator: allKeys,
);
for (var key in allKeys) {
var actualChild = actualChildEntities[key];
var expectedChild = expectedChildEntities[key];
if (_compareTreeParts(actual: actualChild, expected: expectedChild)
case var mismatch?) {
mismatch.reversePath.add(_GetterPathSegment(key));
return mismatch;
}
}
return null;
}
/// Compares two [Token]s in a syntax tree, and if they don't match, returns
/// a description of how they fail to match.
_Mismatch? _compareTokens({required Token actual, required Token expected}) {
StringBuffer buffer;
if (expected.lexeme == '_k_') {
// '_k_' matches a keyword.
if (actual.isKeyword) {
return null;
} else {
buffer = StringBuffer();
buffer.writeln('Expected a keyword');
}
} else if (expected.lexeme == '_s_') {
// '_s_' matches a synthetic identifier.
if (actual.isSynthetic && actual.type == expected.type) {
return null;
} else {
buffer = StringBuffer();
buffer.writeln('Expected a synthetic identifier');
}
} else {
if (actual.type == expected.type) {
if (actual.isSynthetic) {
if (expected.isSynthetic) {
// Synthetic tokens match each other regardless of their lexeme and
// length
return null;
} else {
// An actual synthetic token matches an expected non-synthetic token
// if the lexemes match. The lengths do not need to match because
// the length of a synthetic token is zero.
assert(
actual.length == 0,
'Length is expected to be zero because token is synthetic',
);
if (actual.lexeme == expected.lexeme) return null;
// Also, an actual synthetic empty string matches an expected empty
// string, even if the strings are quotated differently.
if (_isEmptyStringToken(actual) && _isEmptyStringToken(expected)) {
return null;
}
}
} else {
// An actual non-synthetic token only matches an expected token with
// the same length and lexeme.
if (actual.length == expected.length &&
actual.lexeme == expected.lexeme) {
return null;
}
}
}
buffer = StringBuffer();
buffer.write('Expected ');
_describeToken(expected, buffer);
}
buffer.write('But found ');
_describeToken(actual, buffer);
return _Mismatch(buffer.toString());
}
/// Compares two parts of a syntax tree, and if they don't match, returns a
/// description of how they fail to match.
///
/// The two parts being compared might be AST nodes, AST node lists, tokens,
/// or token lists.
_Mismatch? _compareTreeParts({
required Object? actual,
required Object? expected,
}) {
if (identical(actual, expected)) {
return null;
}
if (actual == null) {
return _failIfExpectingNonNull(expected: expected);
}
if (expected == null) {
return _failBecauseUnexpected(actual: actual);
}
if (actual is List<Object?> && expected is List<Object?>) {
return _compareLists(actual: actual, expected: expected);
}
if (actual is Token && expected is Token) {
return _compareTokens(actual: actual, expected: expected);
}
if (actual.runtimeType != expected.runtimeType) {
return _failBecauseUnexpectedType(actual: actual, expected: expected);
}
if (_compareNodesWithMatchingTypes(
actual: actual as AstNodeImpl,
expected: expected as AstNodeImpl,
)
case var mismatch?) {
var typeName = actual.runtimeType.toString();
if (typeName.endsWith('Impl')) {
typeName = typeName.substring(0, typeName.length - 4);
}
mismatch.reversePath.add(_CastPathSegment(typeName));
return mismatch;
}
return null;
}
/// Computes a map whose values are the [AstNode]s, [Token]s, and
/// [NodeList]s pointed to by [node], and whose keys are the names of the
/// getters that return those values.
///
/// This information is derived from the [ChildEntity] objects returned by
/// [AstNodeImpl.namedChildEntities].
Map<String, Object> _computeChildEntitiesMap(
AstNodeImpl node, {
required Set<String> keyAccumulator,
}) {
var result = <String, Object>{};
for (var ChildEntity(:name, :value) in node.namedChildEntities) {
if (node is FormalParameterListImpl && name == 'parameter') {
// FormalParameterListImpl.namedChildEntities splits up the parameter
// list into individual `parameter` entitites so that it can insert
// `leftDelimiter` in the middle of the list (see
// https://github.com/dart-lang/sdk/issues/60445 for details). To
// compensate for this, we need to reassemble the individual `parameter`
// entities back into a single list.
keyAccumulator.add('parameters');
((result['parameters'] ??= <AstNode>[]) as List<AstNode>).add(
value as AstNode,
);
} else {
assert(
!result.containsKey(name),
'Unexpected duplicate name in $runtimeType.childEntities: $name',
);
keyAccumulator.add(name);
result[name] = value;
}
}
return result;
}
/// Writes a human-readable description of [token] to [buffer].
void _describeToken(Token token, StringBuffer buffer) {
if (token.isSynthetic) buffer.write('synthetic ');
buffer.write('token ');
buffer.writeln(json.encode(token.lexeme));
buffer.write(' type=');
buffer.write(token.type.name);
buffer.write(', length=');
buffer.writeln(token.length);
}
/// Creates a [_Mismatch] representing a piece of syntax that was unexpected
/// (the corresponding part of the `expected` AST is `null`).
_Mismatch _failBecauseUnexpected({required Object actual}) {
StringBuffer buffer = StringBuffer();
buffer.write('Unexpected ');
buffer.writeln(actual.runtimeType);
return _Mismatch(buffer.toString());
}
/// Creates a [_Mismatch] representing a piece of syntax that has the wrong
/// runtime type.
_Mismatch _failBecauseUnexpectedType({
required Object actual,
required Object expected,
}) {
StringBuffer buffer = StringBuffer();
buffer.write('Expected a ');
buffer.write(expected.runtimeType);
buffer.write('; found ');
buffer.writeln(actual.runtimeType);
return _Mismatch(buffer.toString());
}
/// Creates a [_Mismatch] representing a piece of syntax that is a list with
/// an unexpected length.
_Mismatch _failDifferentLength({
required List<Object?> actual,
required List<Object?> expected,
}) {
StringBuffer buffer = StringBuffer();
buffer.writeln('Expected a list of length ${expected.length}');
buffer.writeln(' $expected');
buffer.writeln('But found a list of length ${actual.length}');
buffer.writeln(' $actual');
return _Mismatch(buffer.toString());
}
/// Handles the situation where a slot in the `actual` AST is `null`.
///
/// If the corresponding slot in the `expected` AST is also `null`, then there
/// is no mismatch. Otherwise, a [_Mismatch] is created to describe the
/// problem.
_Mismatch? _failIfExpectingNonNull({required Object? expected}) {
if (expected != null) {
StringBuffer buffer = StringBuffer();
buffer.write('Expected a ');
buffer.write(expected.runtimeType);
buffer.writeln('; found nothing');
return _Mismatch(buffer.toString());
}
return null;
}
bool _isEmptyStringToken(Token token) => const [
'""',
"''",
'""""""',
"''''''",
'r""',
"r''",
'r""""""',
"r''''''",
].contains(token.lexeme);
/// Compares the [actual] and [expected] nodes, failing the test if they are
/// different.
static void compare(AstNode actual, AstNode expected) {
ResultComparator comparator = ResultComparator();
if (comparator._compareTreeParts(actual: actual, expected: expected)
case _Mismatch(:var message, :var reversePath)) {
if (reversePath.isNotEmpty) {
var path = reversePath.reversed.fold(
'root',
(p, seg) => seg.combinePath(p),
);
fail('$message path: $path\n');
} else {
fail(message);
}
}
}
}
/// A [_PathSegment] representing a cast to a specific AST node type.
class _CastPathSegment extends _PathSegment {
final String typeName;
_CastPathSegment(this.typeName);
@override
String combinePath(String path) => '($path as $typeName)';
}
/// A [_PathSegment] representing a getter invocation.
class _GetterPathSegment extends _PathSegment {
final String getterName;
_GetterPathSegment(this.getterName);
@override
String combinePath(String path) => '$path.$getterName';
}
/// A [_PathSegment] representing the operation of indexing into a list.
class _IndexPathSegment extends _PathSegment {
final int index;
_IndexPathSegment(this.index);
@override
String combinePath(String path) => '$path[$index]';
}
/// A description of a mismatch between two ASTs.
///
/// Part of the implementation of [ResultComparator.compare].
class _Mismatch {
/// Description of the mismatch, followed by `\n`.
final String message;
/// A list of [_PathSegment] objects which, when taken in revers order,
/// describe the path from the AST nodes passed to [ResultComparator.compare]
/// to the point where the mismatch occurred.
final reversePath = <_PathSegment>[];
_Mismatch(this.message) : assert(message.endsWith('\n'));
}
/// A single step in the path from the root of an AST to a particular point in
/// the AST.
sealed class _PathSegment {
/// Combines this path segment with [path].
///
/// If [path] represents a particular AST (or a part of an AST), then the
/// returned string represents the particular substructure of that AST
/// selected by this path segment. For example, if `path` is `foo`, and `this`
/// is a [_GetterPathSegment] representing invocation of the getter
/// `.operand`, then the resulting string will be `foo.operand`.
String combinePath(String path);
}