blob: 62a722fabecc81660298de5af43e00c2bbaefee8 [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/analyzer.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 CompilationUnit AdjustValidUnitBeforeComparison(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>[
new TestSuffix('annotation', '@annotation var f;'),
new TestSuffix('field', 'var f;'),
new TestSuffix('fieldConst', 'const f = 0;'),
new TestSuffix('fieldFinal', 'final f = 0;'),
new TestSuffix('methodNonVoid', 'int a(b) => 0;'),
new TestSuffix('methodVoid', 'void a(b) {}'),
new TestSuffix('getter', 'int get a => 0;'),
new 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>[
new TestSuffix('class', 'class A {}'),
new TestSuffix('typedef', 'typedef A = B Function(C, D);'),
new TestSuffix('functionVoid', 'void f() {}'),
new TestSuffix('functionNonVoid', 'int f() {}'),
new TestSuffix('var', 'var a;'),
new TestSuffix('const', 'const a = 0;'),
new TestSuffix('final', 'final a = 0;'),
new TestSuffix('getter', 'int get a => 0;'),
new 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>[
new TestSuffix('part', "part 'a.dart';")
]..addAll(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>[
new TestSuffix('import', "import 'a.dart';"),
new TestSuffix('export', "export 'a.dart';")
]..addAll(postPartSuffixes);
/**
* A list of suffixes that can be used by tests of statements.
*/
static final List<TestSuffix> statementSuffixes = <TestSuffix>[
new TestSuffix('assert', "assert (true);"),
new TestSuffix('block', "{}"),
new TestSuffix('break', "break;"),
new TestSuffix('continue', "continue;"),
new TestSuffix('do', "do {} while (true);"),
new TestSuffix('if', "if (true) {}"),
new TestSuffix('for', "for (var x in y) {}"),
new TestSuffix('labeled', "l: {}"),
new TestSuffix('localFunctionNonVoid', "int f() {}"),
new TestSuffix('localFunctionVoid', "void f() {}"),
new TestSuffix('localVariable', "var x;"),
new TestSuffix('switch', "switch (x) {}"),
new TestSuffix('try', "try {} finally {}"),
new TestSuffix('return', "return;"),
new 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,
{String head, bool includeEof: true, String tail}) {
group(groupName, () {
for (TestDescriptor descriptor in descriptors) {
if (includeEof) {
_buildTestForDescriptorAndSuffix(
descriptor, TestSuffix.eof, 0, head, tail);
}
for (int i = 0; i < suffixes.length; i++) {
_buildTestForDescriptorAndSuffix(
descriptor, suffixes[i], i + 1, head, tail);
}
if (descriptor.failing != null) {
test('${descriptor.name}_failingList', () {
Set<String> failing = new 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) {
test('${descriptor.name}_${suffix.name}', () {
//
// Compose the invalid and valid pieces of code.
//
StringBuffer invalid = new StringBuffer();
StringBuffer valid = new StringBuffer();
StringBuffer base = new 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 =
new GatheringErrorListener(checkRanges: true);
parseCompilationUnit2(base.toString(), listener);
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);
}
});
}
}
/**
* 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 = new 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);
}