1309dffc0a
Move the analyzer-only Diagnostic, DiagnosticMessage, Severity, and locatable diagnostic helper types out of _fe_analyzer_shared and into package:analyzer. I paln to make changes outlined in https://github.com/dart-lang/sdk/issues/63311 and chat discussion. Keeping these classes in the analyzer simplifies the migration and avoids introducing a shared abstraction before there is a concrete need for one. If we decide later need to have a shared abstraction, we can always extract one at that point. With coding agents internal code motion is cheap. Update analyzer, analysis server plugin, analyzer plugin, linter, and scanner call sites to import the moved APIs from analyzer libraries, and refresh API baselines to reflect the new public owner. Change-Id: Ie0ef0f01c6e4be7ebaac25619ac3e3fe991a44d9 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/501000 Reviewed-by: Samuel Rawlins <srawlins@google.com> Reviewed-by: Paul Berry <paulberry@google.com> Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
279 lines
9.4 KiB
Dart
279 lines
9.4 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/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' show LocatedDiagnostic;
|
|
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');
|
|
}
|
|
}
|