728f837e18
Changes the error reporting logic in the `Scanner` class to use the literate diagnostic reporting API, along with the top level function `translateErrorToken`, which `Scanner` makes use of. This required changing the signature of the `ReportError` typedef, which is used to communicate scanner errors to front end unit tests. Change-Id: I6a6a6964c1402b30de96ee2555016934c4ba40d8 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/473423 Commit-Queue: Paul Berry <paulberry@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
279 lines
9.3 KiB
Dart
279 lines
9.3 KiB
Dart
// 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:_fe_analyzer_shared/src/base/errors.dart';
|
|
import 'package:_fe_analyzer_shared/src/scanner/error_token.dart';
|
|
import 'package:_fe_analyzer_shared/src/scanner/scanner.dart';
|
|
import 'package:_fe_analyzer_shared/src/scanner/token.dart';
|
|
import 'package:analyzer/src/dart/scanner/translate_error_token.dart'
|
|
show translateErrorToken;
|
|
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
|
|
import 'package:test/test.dart';
|
|
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
|
|
|
import 'scanner_test.dart';
|
|
|
|
void main() {
|
|
defineReflectiveSuite(() {
|
|
defineReflectiveTests(ScannerTest_Replacement);
|
|
});
|
|
}
|
|
|
|
/// Scanner tests that use the analyzer scanner, then convert the resulting
|
|
/// token stream into a Fasta token stream, then convert back to an analyzer
|
|
/// token stream before verifying assertions.
|
|
///
|
|
/// These tests help to validate the correctness of the analyzer->Fasta token
|
|
/// stream conversion.
|
|
@reflectiveTest
|
|
class ScannerTest_Replacement extends ScannerTestBase {
|
|
@override
|
|
Token scanWithListener(
|
|
String source,
|
|
ErrorListener listener, {
|
|
ScannerConfiguration? configuration,
|
|
}) {
|
|
// Process the source similar to
|
|
// pkg/analyzer/lib/src/dart/scanner/scanner.dart
|
|
// to simulate replacing the analyzer scanner
|
|
|
|
ScannerResult result = scanString(
|
|
source,
|
|
configuration: configuration,
|
|
includeComments: true,
|
|
);
|
|
|
|
Token tokens = result.tokens;
|
|
assertValidTokenStream(tokens, errorsFirst: true);
|
|
assertValidBeginTokens(tokens);
|
|
|
|
// The scanner pretends there is an additional line at EOF
|
|
result.lineStarts.removeLast();
|
|
|
|
return extractErrors(tokens, listener);
|
|
}
|
|
|
|
void _assertOpenClosePair(String source) {
|
|
BeginToken open = _scan(source) as BeginToken;
|
|
Token close = open.next!;
|
|
expect(close.next!.isEof, isTrue);
|
|
expect(open.endGroup, close);
|
|
expect(open.isSynthetic, isFalse);
|
|
expect(close.isSynthetic, isFalse);
|
|
}
|
|
|
|
void _assertOpenOnly(String source, String expectedCloser) {
|
|
ErrorListener listener = new ErrorListener();
|
|
BeginToken open = scanWithListener(source, listener) as BeginToken;
|
|
Token close = open.next!;
|
|
expect(close.next!.isEof, isTrue);
|
|
expect(open.endGroup, close);
|
|
expect(open.isSynthetic, isFalse);
|
|
expect(close.isSynthetic, isTrue);
|
|
listener.assertErrors([
|
|
new TestError(1, diag.expectedToken, [expectedCloser]),
|
|
]);
|
|
}
|
|
|
|
void test_double_error() {
|
|
String source = "3457e";
|
|
ErrorListener listener = new ErrorListener();
|
|
Token token = scanWithListener(source, listener);
|
|
expect(token.type, TokenType.DOUBLE);
|
|
expect(token.offset, 0);
|
|
expect(token.isSynthetic, isTrue);
|
|
// the invalid token is updated to be valid ...
|
|
expect(token.lexeme, source + "0");
|
|
// ... but the length does *not* include the additional character
|
|
// so as to be true to the original source.
|
|
expect(token.length, source.length);
|
|
expect(token.next!.isEof, isTrue);
|
|
expect(listener.errors, hasLength(1));
|
|
TestError error = listener.errors[0];
|
|
expect(error.diagnosticCode, diag.missingDigit);
|
|
expect(error.offset, source.length - 1);
|
|
}
|
|
|
|
@override
|
|
void test_lt() {
|
|
// The scanner does not automatically insert a closer for '<'
|
|
// because it could be part of an expression rather than an opener
|
|
BeginToken lt = _scan('<') as BeginToken;
|
|
expect(lt.next!.isEof, isTrue);
|
|
expect(lt.isSynthetic, isFalse);
|
|
}
|
|
|
|
void test_lt_gt() {
|
|
_assertOpenClosePair('< >');
|
|
}
|
|
|
|
@override
|
|
void test_open_curly_bracket() {
|
|
_assertOpenOnly('{', '}');
|
|
}
|
|
|
|
void test_open_curly_bracket_with_close() {
|
|
_assertOpenClosePair('{ }');
|
|
}
|
|
|
|
@override
|
|
void test_open_paren() {
|
|
_assertOpenOnly('(', ')');
|
|
}
|
|
|
|
void test_open_paren_with_close() {
|
|
_assertOpenClosePair('( )');
|
|
}
|
|
|
|
@override
|
|
void test_open_square_bracket() {
|
|
_assertOpenOnly('[', ']');
|
|
}
|
|
|
|
void test_open_square_bracket_with_close() {
|
|
_assertOpenClosePair('[ ]');
|
|
}
|
|
|
|
@override
|
|
void test_mismatched_opener_in_interpolation() {
|
|
// When openers and closers are mismatched,
|
|
// the scanner favors considering the opener to be mismatched
|
|
// and inserts synthetic closers as needed.
|
|
// r'"${({(}}"' is parsed as r'"${({()})}"'
|
|
// where both ')' are synthetic
|
|
ErrorListener listener = new ErrorListener();
|
|
var stringStart = scanWithListener(r'"${({(}}"', listener);
|
|
BeginToken interpolationStart = stringStart.next as BeginToken;
|
|
BeginToken openParen1 = interpolationStart.next as BeginToken;
|
|
BeginToken openBrace = openParen1.next as BeginToken;
|
|
BeginToken openParen2 = openBrace.next as BeginToken;
|
|
var closeParen2 = openParen2.next!;
|
|
var closeBrace = closeParen2.next!;
|
|
var closeParen1 = closeBrace.next!;
|
|
var interpolationEnd = closeParen1.next!;
|
|
var stringEnd = interpolationEnd.next!;
|
|
var eof = stringEnd.next!;
|
|
|
|
expect(interpolationStart.endToken, same(interpolationEnd));
|
|
expect(interpolationEnd.isSynthetic, isFalse);
|
|
expect(openParen1.endToken, same(closeParen1));
|
|
expect(closeParen1.isSynthetic, isTrue);
|
|
expect(openBrace.endToken, same(closeBrace));
|
|
expect(closeBrace.isSynthetic, isFalse);
|
|
expect(openParen2.endToken, same(closeParen2));
|
|
expect(closeParen2.isSynthetic, isTrue);
|
|
expect(eof.isEof, isTrue);
|
|
listener.assertErrors([
|
|
new TestError(6, diag.expectedToken, [')']),
|
|
new TestError(7, diag.expectedToken, [')']),
|
|
]);
|
|
}
|
|
|
|
@override
|
|
void test_unmatched_openers() {
|
|
ErrorListener listener = new ErrorListener();
|
|
// The scanner inserts missing closers except for '<'
|
|
BeginToken openBrace = scanWithListener('{[(<', listener) as BeginToken;
|
|
BeginToken openBracket = openBrace.next as BeginToken;
|
|
BeginToken openParen = openBracket.next as BeginToken;
|
|
BeginToken openLT = openParen.next as BeginToken;
|
|
var closeParen = openLT.next!;
|
|
var closeBracket = closeParen.next!;
|
|
var closeBrace = closeBracket.next!;
|
|
var eof = closeBrace.next!;
|
|
|
|
expect(openBrace.endGroup, same(closeBrace));
|
|
expect(openBracket.endGroup, same(closeBracket));
|
|
expect(openParen.endGroup, same(closeParen));
|
|
expect(eof.isEof, true);
|
|
|
|
listener.assertErrors([
|
|
new TestError(4, diag.expectedToken, [')']),
|
|
new TestError(4, diag.expectedToken, [']']),
|
|
new TestError(4, diag.expectedToken, ['}']),
|
|
]);
|
|
}
|
|
|
|
Token _scan(String source) {
|
|
ErrorListener listener = new ErrorListener();
|
|
Token token = scanWithListener(source, listener);
|
|
listener.assertNoErrors();
|
|
return token;
|
|
}
|
|
|
|
Token extractErrors(Token firstToken, ErrorListener listener) {
|
|
var token = firstToken;
|
|
// The default recovery strategy used by scanString
|
|
// places all error tokens at the head of the stream.
|
|
while (token.type == TokenType.BAD_INPUT) {
|
|
translateErrorToken(token as ErrorToken, (
|
|
LocatedDiagnostic locatedDiagnostic,
|
|
) {
|
|
listener.errors.add(
|
|
new TestError(
|
|
locatedDiagnostic.offset,
|
|
locatedDiagnostic.locatableDiagnostic.code,
|
|
locatedDiagnostic.locatableDiagnostic.arguments,
|
|
),
|
|
);
|
|
});
|
|
token = token.next!;
|
|
}
|
|
if (!token.previous!.isEof) {
|
|
new Token.eof(-1).setNext(token);
|
|
}
|
|
return token;
|
|
}
|
|
|
|
/// Assert that the tokens in the stream are correctly connected prev/next.
|
|
void assertValidTokenStream(Token firstToken, {bool errorsFirst = false}) {
|
|
Token token = firstToken;
|
|
Token previous = token.previous!;
|
|
expect(previous.isEof, isTrue, reason: 'Missing leading EOF');
|
|
expect(previous.next, token, reason: 'Invalid leading EOF');
|
|
expect(previous.previous, previous, reason: 'Invalid leading EOF');
|
|
if (errorsFirst) {
|
|
while (!token.isEof && token is ErrorToken) {
|
|
token = token.next!;
|
|
}
|
|
}
|
|
var isNotErrorToken = isNot(const TypeMatcher<ErrorToken>());
|
|
while (!token.isEof) {
|
|
if (errorsFirst) expect(token, isNotErrorToken);
|
|
previous = token;
|
|
token = token.next!;
|
|
expect(token, isNotNull, reason: previous.toString());
|
|
expect(token.previous, previous, reason: token.toString());
|
|
}
|
|
expect(token.next, token, reason: 'Invalid trailing EOF');
|
|
}
|
|
|
|
/// Assert that all [BeginToken] has a valid `endGroup`
|
|
/// that is in the stream.
|
|
void assertValidBeginTokens(Token firstToken) {
|
|
var openerStack = <BeginToken>[];
|
|
var errorStack = <ErrorToken>[];
|
|
Token token = firstToken;
|
|
while (!token.isEof) {
|
|
if (token is BeginToken) {
|
|
if (token.lexeme != '<') {
|
|
expect(token.endGroup, isNotNull, reason: token.lexeme);
|
|
}
|
|
if (token.endGroup != null) openerStack.add(token);
|
|
} else if (openerStack.isNotEmpty && openerStack.last.endGroup == token) {
|
|
BeginToken beginToken = openerStack.removeLast();
|
|
if (token.isSynthetic) {
|
|
ErrorToken errorToken = errorStack.removeAt(0);
|
|
expect(errorToken.begin, beginToken);
|
|
}
|
|
} else if (token is UnmatchedToken) {
|
|
errorStack.add(token);
|
|
}
|
|
token = token.next!;
|
|
}
|
|
expect(openerStack, isEmpty, reason: 'Missing closers');
|
|
expect(errorStack, isEmpty, reason: 'Extra error tokens');
|
|
}
|
|
}
|