blob: 9b357b406c6cf38299ae405176f0eb12488e87c3 [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 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/src/dart/error/syntactic_errors.dart';
import 'package:test/test.dart';
import '../../../../generated/test_support.dart';
import '../recovery_test_support.dart';
typedef AdjustValidUnitBeforeComparison = CompilationUnit Function(
CompilationUnit unit);
/// A base class that adds support for tests that test how well the parser
/// recovers when the user has entered an incomplete (but otherwise correct)
/// construct (such as a top-level declaration, class member, or statement).
///
/// Because users often add new constructs between two existing constructs,
/// these tests effectively test whether the parser is able to recognize where
/// the partially entered construct ends and where the next fully entered
/// construct begins. (The preceding construct is irrelevant.) Given the large
/// number of following constructs the are valid in most contexts, these tests
/// are designed to programmatically generate tests based on a list of possible
/// following constructs.
abstract class PartialCodeTest extends AbstractRecoveryTest {
/// A list of suffixes that can be used by tests of class members.
static final List<TestSuffix> classMemberSuffixes = <TestSuffix>[
TestSuffix('annotation', '@annotation var f;'),
TestSuffix('field', 'var f;'),
TestSuffix('fieldConst', 'const f = 0;'),
TestSuffix('fieldFinal', 'final f = 0;'),
TestSuffix('methodNonVoid', 'int a(b) => 0;'),
TestSuffix('methodVoid', 'void a(b) {}'),
TestSuffix('getter', 'int get a => 0;'),
TestSuffix('setter', 'set a(b) {}')
];
/// A list of suffixes that can be used by tests of top-level constructs that
/// can validly be followed by any declaration.
static final List<TestSuffix> declarationSuffixes = <TestSuffix>[
TestSuffix('class', 'class A {}'),
TestSuffix('enum', 'enum E { v }'),
// new TestSuffix('extension', 'extension E on A {}'),
TestSuffix('mixin', 'mixin M {}'),
TestSuffix('typedef', 'typedef A = B Function(C, D);'),
TestSuffix('functionVoid', 'void f() {}'),
TestSuffix('functionNonVoid', 'int f() {}'),
TestSuffix('var', 'var a;'),
TestSuffix('const', 'const a = 0;'),
TestSuffix('final', 'final a = 0;'),
TestSuffix('getter', 'int get a => 0;'),
TestSuffix('setter', 'set a(b) {}')
];
/// A list of suffixes that can be used by tests of top-level constructs that
/// can validly be followed by anything that is valid after a part directive.
static final List<TestSuffix> postPartSuffixes = <TestSuffix>[
TestSuffix('part', "part 'a.dart';"),
...declarationSuffixes
];
/// A list of suffixes that can be used by tests of top-level constructs that
/// can validly be followed by any directive or declaration other than a
/// library directive.
static final List<TestSuffix> prePartSuffixes = <TestSuffix>[
TestSuffix('import', "import 'a.dart';"),
TestSuffix('export', "export 'a.dart';"),
...postPartSuffixes
];
/// A list of suffixes that can be used by tests of statements.
static final List<TestSuffix> statementSuffixes = <TestSuffix>[
TestSuffix('assert', "assert (true);"),
TestSuffix('block', "{}"),
TestSuffix('break', "break;"),
TestSuffix('continue', "continue;"),
TestSuffix('do', "do {} while (true);"),
TestSuffix('if', "if (true) {}"),
TestSuffix('for', "for (var x in y) {}"),
TestSuffix('labeled', "l: {}"),
TestSuffix('localFunctionNonVoid', "int f() {}"),
TestSuffix('localFunctionVoid', "void f() {}"),
TestSuffix('localVariable', "var x;"),
TestSuffix('switch', "switch (x) {}"),
TestSuffix('try', "try {} finally {}"),
TestSuffix('return', "return;"),
TestSuffix('while', "while (true) {}"),
];
/// Build a group of tests with the given [groupName]. There will be one test
/// for every combination of elements in the cross-product of the lists of
/// [descriptors] and [suffixes], and one additional test for every descriptor
/// where the suffix is the empty string (to test partial declarations at the
/// end of the file). In total, there will be
/// `descriptors.length * (suffixes.length + 1)` tests generated.
buildTests(String groupName, List<TestDescriptor> descriptors,
List<TestSuffix> suffixes,
{FeatureSet featureSet,
String head,
bool includeEof = true,
String tail}) {
group(groupName, () {
for (TestDescriptor descriptor in descriptors) {
if (includeEof) {
_buildTestForDescriptorAndSuffix(
descriptor, TestSuffix.eof, 0, head, tail,
featureSet: featureSet);
}
for (int i = 0; i < suffixes.length; i++) {
_buildTestForDescriptorAndSuffix(
descriptor, suffixes[i], i + 1, head, tail,
featureSet: featureSet);
}
if (descriptor.failing != null) {
test('${descriptor.name}_failingList', () {
Set<String> failing = Set.from(descriptor.failing);
if (includeEof) {
failing.remove('eof');
}
failing.removeAll(suffixes.map((TestSuffix suffix) => suffix.name));
expect(failing, isEmpty,
reason:
'There are tests marked as failing that are not being run');
});
}
}
});
}
/// Build a single test based on the given [descriptor] and [suffix].
_buildTestForDescriptorAndSuffix(TestDescriptor descriptor, TestSuffix suffix,
int suffixIndex, String head, String tail,
{FeatureSet featureSet}) {
test('${descriptor.name}_${suffix.name}', () {
//
// Compose the invalid and valid pieces of code.
//
StringBuffer invalid = StringBuffer();
StringBuffer valid = StringBuffer();
StringBuffer base = StringBuffer();
if (head != null) {
invalid.write(head);
valid.write(head);
base.write(head);
}
invalid.write(descriptor.invalid);
valid.write(descriptor.valid);
if (suffix.text.isNotEmpty) {
invalid.write(' ');
invalid.write(suffix.text);
valid.write(' ');
valid.write(suffix.text);
base.write(' ');
base.write(suffix.text);
}
if (tail != null) {
invalid.write(tail);
valid.write(tail);
base.write(tail);
}
//
// Determine the existing errors in the code without either valid or
// invalid code.
//
GatheringErrorListener listener =
GatheringErrorListener(checkRanges: true);
parseCompilationUnit2(base.toString(), listener, featureSet: featureSet);
var baseErrorCodes = <ErrorCode>[];
listener.errors.forEach((AnalysisError error) {
if (error.errorCode == ParserErrorCode.BREAK_OUTSIDE_OF_LOOP ||
error.errorCode == ParserErrorCode.CONTINUE_OUTSIDE_OF_LOOP ||
error.errorCode == ParserErrorCode.CONTINUE_WITHOUT_LABEL_IN_CASE) {
baseErrorCodes.add(error.errorCode);
}
});
var expectedValidCodeErrors = <ErrorCode>[];
expectedValidCodeErrors.addAll(baseErrorCodes);
if (descriptor.expectedErrorsInValidCode != null) {
expectedValidCodeErrors.addAll(descriptor.expectedErrorsInValidCode);
}
var expectedInvalidCodeErrors = <ErrorCode>[];
expectedInvalidCodeErrors.addAll(baseErrorCodes);
if (descriptor.errorCodes != null) {
expectedInvalidCodeErrors.addAll(descriptor.errorCodes);
}
//
// Run the test.
//
List<String> failing = descriptor.failing;
if (descriptor.allFailing ||
(failing != null && failing.contains(suffix.name))) {
bool failed = false;
try {
testRecovery(
invalid.toString(), expectedInvalidCodeErrors, valid.toString(),
adjustValidUnitBeforeComparison:
descriptor.adjustValidUnitBeforeComparison,
expectedErrorsInValidCode: expectedValidCodeErrors);
failed = true;
} catch (e) {
// Expected to fail.
}
if (failed) {
fail('Expected to fail, but passed');
}
} else {
testRecovery(
invalid.toString(), expectedInvalidCodeErrors, valid.toString(),
adjustValidUnitBeforeComparison:
descriptor.adjustValidUnitBeforeComparison,
expectedErrorsInValidCode: expectedValidCodeErrors,
featureSet: featureSet);
}
});
}
}
/// A description of a set of tests that are to be built.
class TestDescriptor {
/// The name of the test.
final String name;
/// Invalid code that the parser is expected to recover from.
final String invalid;
/// Error codes that the parser is expected to produce.
final List<ErrorCode> errorCodes;
/// Valid code that is equivalent to what the parser should produce as part of
/// recovering from the invalid code.
final String valid;
/// Error codes that the parser is expected to produce in the valid code.
final List<ErrorCode> expectedErrorsInValidCode;
/// A flag indicating whether all of the tests are expected to fail.
final bool allFailing;
/// A list containing the names of the suffixes for which the test is expected
/// to fail.
final List<String> failing;
/// A function that modifies the valid compilation unit before it is compared
/// with the invalid compilation unit, or `null` if no modification needed.
AdjustValidUnitBeforeComparison adjustValidUnitBeforeComparison;
/// Initialize a newly created test descriptor.
TestDescriptor(this.name, this.invalid, this.errorCodes, this.valid,
{this.allFailing = false,
this.failing,
this.expectedErrorsInValidCode,
this.adjustValidUnitBeforeComparison});
}
/// A description of a set of suffixes that are to be used to construct tests.
class TestSuffix {
static final TestSuffix eof = TestSuffix('eof', '');
/// The name of the suffix.
final String name;
/// The code to be appended to the test code.
final String text;
/// Initialize a newly created suffix.
TestSuffix(this.name, this.text);
}