ee69f45a1f
Work towards #60635 In this change, we rework the AnalysisErrorListener deprecation to better support users who have their own class that implements AnalysisErrorListener. This change introduces a sealed supertype, DiagnosticOrErrorListener, with the old implementation, AnalysisErrorListener, and the new implementation, DiagnosticListener, as its sole direct subclasses. Users who have implemented AnalysisErrorListener should be able to instead implement DiagnosticListener, and their class is an acceptable instance of DiagnosticOrErrorListener, wherever that is needed. In a breaking change we can drop AnalysisErrorListener and deprecate DiagnosticOrErrorListener, and in the next breaking change, we can drop DiagnosticOrErrorListener. For reference, see the first API difference when deprecating AnalysisErrorListener and introducing DiagnosticListener: https://github.com/dart-lang/sdk/commit/903d77cc8229972a424941dcb7b7b79741e833eb#diff-dec15868961d7eadcd009f49d129bebcdb747aaa8dbfde5f5a08884e0cf11e32 Change-Id: I3ccf11d54b41fbca98d020d89978d250c16b4c04 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/436480 Reviewed-by: Paul Berry <paulberry@google.com> Commit-Queue: Samuel Rawlins <srawlins@google.com>
557 lines
16 KiB
Dart
557 lines
16 KiB
Dart
// Copyright (c) 2014, 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/diagnostic/diagnostic.dart';
|
|
import 'package:analyzer/error/error.dart';
|
|
import 'package:analyzer/error/listener.dart';
|
|
import 'package:analyzer/file_system/file_system.dart';
|
|
import 'package:analyzer/instrumentation/instrumentation.dart';
|
|
import 'package:analyzer/source/line_info.dart';
|
|
import 'package:analyzer/source/source.dart';
|
|
import 'package:analyzer/src/generated/engine.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
/// A description of a message that is expected to be reported with an error.
|
|
class ExpectedContextMessage {
|
|
/// The path of the file with which the message is associated.
|
|
final File file;
|
|
|
|
/// The offset of the beginning of the error's region.
|
|
final int offset;
|
|
|
|
/// The offset of the beginning of the error's region.
|
|
final int length;
|
|
|
|
/// The message text for the error.
|
|
final String? text;
|
|
|
|
/// A list of patterns that should be contained in the message test; empty if
|
|
/// the message contents should not be checked.
|
|
final List<Pattern> textContains;
|
|
|
|
ExpectedContextMessage(
|
|
this.file,
|
|
this.offset,
|
|
this.length, {
|
|
this.text,
|
|
this.textContains = const [],
|
|
});
|
|
|
|
/// Return `true` if the [message] matches this description of what it's
|
|
/// expected to be.
|
|
bool matches(DiagnosticMessage message) {
|
|
if (message.filePath != file.path) {
|
|
return false;
|
|
}
|
|
|
|
if (message.offset != offset) {
|
|
return false;
|
|
}
|
|
|
|
if (message.length != length) {
|
|
return false;
|
|
}
|
|
|
|
var messageText = message.messageText(includeUrl: true);
|
|
if (text != null && messageText != text) {
|
|
return false;
|
|
}
|
|
|
|
for (var pattern in textContains) {
|
|
if (!messageText.contains(pattern)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// A description of an error that is expected to be reported.
|
|
class ExpectedError {
|
|
/// An empty array of error descriptors used when no errors are expected.
|
|
static List<ExpectedError> NO_ERRORS = <ExpectedError>[];
|
|
|
|
/// The diagnostic code associated with the error.
|
|
final DiagnosticCode code;
|
|
|
|
// A pattern that should be contained in the error's correction message, or
|
|
// `null` if the correction message contents should not be checked.
|
|
final Pattern? correctionContains;
|
|
|
|
/// The offset of the beginning of the error's region.
|
|
final int offset;
|
|
|
|
/// The offset of the beginning of the error's region.
|
|
final int length;
|
|
|
|
/// The message text of the error or `null` if the message should not be checked.
|
|
final String? message;
|
|
|
|
/// A list of patterns that should be contained in the error message; empty if
|
|
/// the message contents should not be checked.
|
|
final List<Pattern> messageContains;
|
|
|
|
/// The list of context messages that are expected to be associated with the
|
|
/// error.
|
|
final List<ExpectedContextMessage> expectedContextMessages;
|
|
|
|
/// Initialize a newly created error description.
|
|
ExpectedError(
|
|
this.code,
|
|
this.offset,
|
|
this.length, {
|
|
this.correctionContains,
|
|
this.message,
|
|
this.messageContains = const [],
|
|
this.expectedContextMessages = const <ExpectedContextMessage>[],
|
|
});
|
|
|
|
/// Return `true` if the [error] matches this description of what it's
|
|
/// expected to be.
|
|
bool matches(Diagnostic error) {
|
|
if (error.offset != offset ||
|
|
error.length != length ||
|
|
error.diagnosticCode != code) {
|
|
return false;
|
|
}
|
|
if (message != null && error.message != message) {
|
|
return false;
|
|
}
|
|
for (var pattern in messageContains) {
|
|
if (!error.message.contains(pattern)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (correctionContains != null &&
|
|
!(error.correctionMessage ?? '').contains(correctionContains!)) {
|
|
return false;
|
|
}
|
|
List<DiagnosticMessage> contextMessages = error.contextMessages.toList();
|
|
if (contextMessages.length != expectedContextMessages.length) {
|
|
return false;
|
|
}
|
|
for (int i = 0; i < expectedContextMessages.length; i++) {
|
|
if (!expectedContextMessages[i].matches(contextMessages[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// A diagnostic listener that collects all of the diagnostics passed to it for
|
|
/// later examination.
|
|
class GatheringDiagnosticListener implements DiagnosticListener {
|
|
/// A flag indicating whether error ranges are to be compared when comparing
|
|
/// expected and actual diagnostics.
|
|
final bool checkRanges;
|
|
|
|
/// A list containing the diagnostics that were collected.
|
|
final List<Diagnostic> _diagnostics = [];
|
|
|
|
/// A table mapping sources to the line information for the source.
|
|
final Map<Source, LineInfo> _lineInfoMap = {};
|
|
|
|
/// Initialize a newly created diagnostic listener to collect diagnostics.
|
|
GatheringDiagnosticListener({this.checkRanges = true});
|
|
|
|
/// Returns the diagnostics that were collected.
|
|
List<Diagnostic> get diagnostics => _diagnostics;
|
|
|
|
/// Whether at least one diagnostic has been gathered.
|
|
bool get hasDiagnostics => _diagnostics.isNotEmpty;
|
|
|
|
/// Adds the given [diagnostics] to this listener.
|
|
void addAll(List<Diagnostic> diagnostics) {
|
|
for (Diagnostic diagnostic in diagnostics) {
|
|
onDiagnostic(diagnostic);
|
|
}
|
|
}
|
|
|
|
/// Adds all of the diagnostics recorded by the given [listener] to this
|
|
/// listener.
|
|
void addAll2(RecordingDiagnosticListener listener) {
|
|
addAll(listener.diagnostics);
|
|
}
|
|
|
|
/// Assert that the number of errors that have been gathered matches the
|
|
/// number of [expectedErrors] and that they have the expected error codes and
|
|
/// locations. The order in which the errors were gathered is ignored.
|
|
void assertErrors(List<ExpectedError> expectedErrors) {
|
|
//
|
|
// Match actual errors to expected errors.
|
|
//
|
|
List<Diagnostic> unmatchedActual = diagnostics.toList();
|
|
List<ExpectedError> unmatchedExpected = expectedErrors.toList();
|
|
int actualIndex = 0;
|
|
while (actualIndex < unmatchedActual.length) {
|
|
bool matchFound = false;
|
|
int expectedIndex = 0;
|
|
while (expectedIndex < unmatchedExpected.length) {
|
|
if (unmatchedExpected[expectedIndex].matches(
|
|
unmatchedActual[actualIndex],
|
|
)) {
|
|
matchFound = true;
|
|
unmatchedActual.removeAt(actualIndex);
|
|
unmatchedExpected.removeAt(expectedIndex);
|
|
break;
|
|
}
|
|
expectedIndex++;
|
|
}
|
|
if (!matchFound) {
|
|
actualIndex++;
|
|
}
|
|
}
|
|
//
|
|
// Write the results.
|
|
//
|
|
StringBuffer buffer = StringBuffer();
|
|
if (unmatchedExpected.isNotEmpty) {
|
|
buffer.writeln('Expected but did not find:');
|
|
for (ExpectedError expected in unmatchedExpected) {
|
|
buffer.write(' ');
|
|
buffer.write(expected.code);
|
|
buffer.write(' [');
|
|
buffer.write(expected.offset);
|
|
buffer.write(', ');
|
|
buffer.write(expected.length);
|
|
if (expected.message != null) {
|
|
buffer.write(', message: ');
|
|
buffer.write(json.encode(expected.message));
|
|
}
|
|
if (expected.messageContains.isNotEmpty) {
|
|
buffer.write(', messageContains: ');
|
|
buffer.write(
|
|
json.encode([
|
|
for (var pattern in expected.messageContains) pattern.toString(),
|
|
]),
|
|
);
|
|
}
|
|
if (expected.correctionContains != null) {
|
|
buffer.write(', correctionContains: ');
|
|
buffer.write(json.encode(expected.correctionContains.toString()));
|
|
}
|
|
buffer.writeln(']');
|
|
}
|
|
}
|
|
if (unmatchedActual.isNotEmpty) {
|
|
if (buffer.isNotEmpty) {
|
|
buffer.writeln();
|
|
}
|
|
buffer.writeln('Found but did not expect:');
|
|
for (Diagnostic actual in unmatchedActual) {
|
|
buffer.write(' ');
|
|
buffer.write(actual.diagnosticCode);
|
|
buffer.write(' [');
|
|
buffer.write(actual.offset);
|
|
buffer.write(', ');
|
|
buffer.write(actual.length);
|
|
buffer.write(', ');
|
|
buffer.write(json.encode(actual.message));
|
|
if (actual.correctionMessage != null) {
|
|
buffer.write(', ');
|
|
buffer.write(json.encode(actual.correctionMessage));
|
|
}
|
|
buffer.writeln(']');
|
|
}
|
|
}
|
|
if (buffer.isNotEmpty) {
|
|
diagnostics.sort(
|
|
(first, second) => first.offset.compareTo(second.offset),
|
|
);
|
|
buffer.writeln();
|
|
if (diagnostics.isEmpty) {
|
|
buffer.writeln('To accept the current state, expect no errors.');
|
|
} else {
|
|
buffer.writeln('To accept the current state, expect:');
|
|
for (Diagnostic actual in diagnostics) {
|
|
List<DiagnosticMessage> contextMessages = actual.contextMessages;
|
|
buffer.write(' error(');
|
|
buffer.write(actual.diagnosticCode);
|
|
buffer.write(', ');
|
|
buffer.write(actual.offset);
|
|
buffer.write(', ');
|
|
buffer.write(actual.length);
|
|
if (contextMessages.isNotEmpty) {
|
|
buffer.write(', contextMessages: [');
|
|
for (int i = 0; i < contextMessages.length; i++) {
|
|
DiagnosticMessage message = contextMessages[i];
|
|
if (i > 0) {
|
|
buffer.write(', ');
|
|
}
|
|
buffer.write('message(');
|
|
|
|
// Special case for `testFile`, used very often.
|
|
switch (message.filePath) {
|
|
case '/home/test/lib/test.dart':
|
|
buffer.write('testFile');
|
|
case var filePath:
|
|
buffer.write("'$filePath'");
|
|
}
|
|
|
|
buffer.write(', ');
|
|
buffer.write(message.offset);
|
|
buffer.write(', ');
|
|
buffer.write(message.length);
|
|
buffer.write(')');
|
|
}
|
|
buffer.write(']');
|
|
}
|
|
buffer.writeln('),');
|
|
}
|
|
}
|
|
fail(buffer.toString());
|
|
}
|
|
}
|
|
|
|
/// Asserts that the number of diagnostics that have been gathered matches the
|
|
/// number of [expectedCodes] and that they have the expected diagnostic
|
|
/// codes.
|
|
///
|
|
/// The order in which the diagnostics were gathered is ignored.
|
|
void assertErrorsWithCodes([
|
|
List<DiagnosticCode> expectedCodes = const <DiagnosticCode>[],
|
|
]) {
|
|
StringBuffer buffer = StringBuffer();
|
|
// Compute the expected number of each type of diagnostic.
|
|
Map<DiagnosticCode, int> expectedCounts = <DiagnosticCode, int>{};
|
|
for (DiagnosticCode code in expectedCodes) {
|
|
var count = expectedCounts[code];
|
|
if (count == null) {
|
|
count = 1;
|
|
} else {
|
|
count = count + 1;
|
|
}
|
|
expectedCounts[code] = count;
|
|
}
|
|
//
|
|
// Compute the actual number of each type of error.
|
|
//
|
|
Map<DiagnosticCode, List<Diagnostic>> diagnosticsByCode =
|
|
<DiagnosticCode, List<Diagnostic>>{};
|
|
for (Diagnostic diagnostic in _diagnostics) {
|
|
diagnosticsByCode
|
|
.putIfAbsent(diagnostic.diagnosticCode, () => <Diagnostic>[])
|
|
.add(diagnostic);
|
|
}
|
|
//
|
|
// Compare the expected and actual number of each type of error.
|
|
//
|
|
expectedCounts.forEach((DiagnosticCode code, int expectedCount) {
|
|
int actualCount;
|
|
var list = diagnosticsByCode.remove(code);
|
|
if (list == null) {
|
|
actualCount = 0;
|
|
} else {
|
|
actualCount = list.length;
|
|
}
|
|
if (actualCount != expectedCount) {
|
|
if (buffer.length == 0) {
|
|
buffer.write("Expected ");
|
|
} else {
|
|
buffer.write("; ");
|
|
}
|
|
buffer.write(expectedCount);
|
|
buffer.write(" errors of type ");
|
|
buffer.write(code.uniqueName);
|
|
buffer.write(", found ");
|
|
buffer.write(actualCount);
|
|
}
|
|
});
|
|
//
|
|
// Check that there are no more errors in the actual-errors map,
|
|
// otherwise record message.
|
|
//
|
|
diagnosticsByCode.forEach((
|
|
DiagnosticCode code,
|
|
List<Diagnostic> actualDiagnostics,
|
|
) {
|
|
int actualCount = actualDiagnostics.length;
|
|
if (buffer.length == 0) {
|
|
buffer.write("Expected ");
|
|
} else {
|
|
buffer.write("; ");
|
|
}
|
|
buffer.write("0 errors of type ");
|
|
buffer.write(code.uniqueName);
|
|
buffer.write(", found ");
|
|
buffer.write(actualCount);
|
|
buffer.write(" (");
|
|
for (int i = 0; i < actualDiagnostics.length; i++) {
|
|
Diagnostic diagnostic = actualDiagnostics[i];
|
|
if (i > 0) {
|
|
buffer.write(", ");
|
|
}
|
|
buffer.write(diagnostic.offset);
|
|
}
|
|
buffer.write(")");
|
|
});
|
|
if (buffer.length > 0) {
|
|
fail(buffer.toString());
|
|
}
|
|
}
|
|
|
|
/// Assert that the number of errors that have been gathered matches the
|
|
/// number of [expectedSeverities] and that there are the same number of
|
|
/// errors and warnings as specified by the argument. The order in which the
|
|
/// errors were gathered is ignored.
|
|
void assertErrorsWithSeverities(List<DiagnosticSeverity> expectedSeverities) {
|
|
int expectedErrorCount = 0;
|
|
int expectedWarningCount = 0;
|
|
for (DiagnosticSeverity severity in expectedSeverities) {
|
|
if (severity == DiagnosticSeverity.ERROR) {
|
|
expectedErrorCount++;
|
|
} else {
|
|
expectedWarningCount++;
|
|
}
|
|
}
|
|
int actualErrorCount = 0;
|
|
int actualWarningCount = 0;
|
|
for (Diagnostic diagnostic in _diagnostics) {
|
|
if (diagnostic.diagnosticCode.severity == DiagnosticSeverity.ERROR) {
|
|
actualErrorCount++;
|
|
} else {
|
|
actualWarningCount++;
|
|
}
|
|
}
|
|
if (expectedErrorCount != actualErrorCount ||
|
|
expectedWarningCount != actualWarningCount) {
|
|
fail(
|
|
"Expected $expectedErrorCount errors "
|
|
"and $expectedWarningCount warnings, "
|
|
"found $actualErrorCount errors "
|
|
"and $actualWarningCount warnings",
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Assert that no errors have been gathered.
|
|
void assertNoErrors() {
|
|
assertErrors(ExpectedError.NO_ERRORS);
|
|
}
|
|
|
|
/// Return the line information associated with the given [source], or `null`
|
|
/// if no line information has been associated with the source.
|
|
LineInfo? getLineInfo(Source source) => _lineInfoMap[source];
|
|
|
|
@override
|
|
void onDiagnostic(Diagnostic diagnostic) {
|
|
_diagnostics.add(diagnostic);
|
|
}
|
|
|
|
/// Set the line information associated with the given [source] to [lineInfo].
|
|
void setLineInfo(Source source, LineInfo lineInfo) {
|
|
_lineInfoMap[source] = lineInfo;
|
|
}
|
|
}
|
|
|
|
/// Instances of the class [TestInstrumentor] implement an instrumentation
|
|
/// service that can be used by tests.
|
|
class TestInstrumentor extends NoopInstrumentationService {
|
|
/// All logged messages.
|
|
List<String> log = [];
|
|
|
|
@override
|
|
void logError(String message) {
|
|
log.add("error: $message");
|
|
}
|
|
|
|
@override
|
|
void logException(
|
|
dynamic exception, [
|
|
StackTrace? stackTrace,
|
|
List<InstrumentationServiceAttachment>? attachments,
|
|
]) {
|
|
log.add("error: $exception $stackTrace");
|
|
}
|
|
|
|
@override
|
|
void logInfo(String message, [dynamic exception]) {
|
|
log.add("info: $message");
|
|
}
|
|
}
|
|
|
|
class TestSource extends Source {
|
|
final String _name;
|
|
String _contents;
|
|
bool exists2 = true;
|
|
|
|
/// A flag indicating whether an exception should be generated when an attempt
|
|
/// is made to access the contents of this source.
|
|
bool generateExceptionOnRead = false;
|
|
|
|
/// The number of times that the contents of this source have been requested.
|
|
int readCount = 0;
|
|
|
|
TestSource([this._name = '/test.dart', this._contents = '']);
|
|
|
|
@override
|
|
TimestampedData<String> get contents {
|
|
readCount++;
|
|
if (generateExceptionOnRead) {
|
|
String msg = "I/O Exception while getting the contents of $_name";
|
|
throw Exception(msg);
|
|
}
|
|
return TimestampedData<String>(0, _contents);
|
|
}
|
|
|
|
@override
|
|
String get fullName {
|
|
return _name;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => 0;
|
|
|
|
@override
|
|
String get shortName {
|
|
return _name;
|
|
}
|
|
|
|
@override
|
|
Uri get uri => Uri.file(_name);
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (other is TestSource) {
|
|
return other._name == _name;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
bool exists() => exists2;
|
|
|
|
Source resolve(String uri) {
|
|
throw UnsupportedError('resolve');
|
|
}
|
|
|
|
void setContents(String value) {
|
|
generateExceptionOnRead = false;
|
|
_contents = value;
|
|
}
|
|
|
|
@override
|
|
String toString() => _name;
|
|
}
|
|
|
|
class TestSourceWithUri extends TestSource {
|
|
@override
|
|
final Uri uri;
|
|
|
|
TestSourceWithUri(String path, this.uri, [String content = ''])
|
|
: super(path, content);
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (other is TestSource) {
|
|
return other.uri == uri;
|
|
}
|
|
return false;
|
|
}
|
|
}
|