blob: a791cfb8acb255eb535996ad6d4cf78fe22e4ca4 [file] [log] [blame]
// Copyright (c) 2020, 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.
/// These classes were copied from `analyzer`. They should be moved into the
/// `analyzer/lib/src/test_utilities` directory so that they can be shared.
/// (This version has been converted to a more modern style.)
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/generated/source.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 String filePath;
/// 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;
ExpectedContextMessage(this.filePath, this.offset, this.length, {this.text});
/// Return `true` if the [message] matches this description of what the state
/// of the [message] is expected to be.
bool matches(DiagnosticMessage message) {
return message.filePath == filePath &&
message.offset == offset &&
message.length == length &&
(text == null || message.messageText(includeUrl: true) == text);
}
}
/// 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 error code associated with the error.
final ErrorCode code;
/// 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 pattern that should be contained in the error message or `null` if the
/// message contents should not be checked.
final 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.message,
this.messageContains,
this.expectedContextMessages = const <ExpectedContextMessage>[]});
/// Return `true` if the [error] matches this description of what the state
/// of the [error] is expected to be.
bool matches(AnalysisError error) {
if (error.offset != offset ||
error.length != length ||
error.errorCode != code) {
return false;
}
if (message != null && error.message != message) {
return false;
}
final messageContains = this.messageContains;
if (messageContains != null &&
error.message.contains(messageContains) != true) {
return false;
}
var contextMessages = error.contextMessages.toList();
contextMessages.sort((first, second) {
var result = first.filePath.compareTo(second.filePath);
if (result != 0) {
return result;
}
return second.offset - first.offset;
});
if (contextMessages.length != expectedContextMessages.length) {
return false;
}
for (var i = 0; i < expectedContextMessages.length; i++) {
if (!expectedContextMessages[i].matches(contextMessages[i])) {
return false;
}
}
return true;
}
}
/// An error listener that collects all of the errors passed to it for later
/// examination.
class GatheringErrorListener implements AnalysisErrorListener {
/// A flag indicating whether error ranges are to be compared when comparing
/// expected and actual errors.
final bool checkRanges;
/// A list containing the errors that were collected.
final List<AnalysisError> _errors = <AnalysisError>[];
/// A table mapping sources to the line information for the source.
final Map<Source, LineInfo> _lineInfoMap = <Source, LineInfo>{};
/// Initialize a newly created error listener to collect errors.
GatheringErrorListener({this.checkRanges = true});
/// Return the errors that were collected.
List<AnalysisError> get errors => _errors;
/// Return `true` if at least one error has been gathered.
bool get hasErrors => _errors.isNotEmpty;
/// Add the given [errors] to this listener.
void addAll(List<AnalysisError> errors) {
for (var error in errors) {
onError(error);
}
}
/// Add all of the errors recorded by the given [listener] to this listener.
void addAll2(RecordingErrorListener listener) {
addAll(listener.errors);
}
/// 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.
//
var unmatchedActual = errors.toList();
var unmatchedExpected = expectedErrors.toList();
var actualIndex = 0;
while (actualIndex < unmatchedActual.length) {
var matchFound = false;
var 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.
//
var buffer = StringBuffer();
if (unmatchedExpected.isNotEmpty) {
buffer.writeln('Expected but did not find:');
for (var 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(', ');
buffer.write(expected.message);
}
buffer.writeln(']');
}
}
if (unmatchedActual.isNotEmpty) {
if (buffer.isNotEmpty) {
buffer.writeln();
}
buffer.writeln('Found but did not expect:');
for (var actual in unmatchedActual) {
buffer.write(' ');
buffer.write(actual.errorCode);
buffer.write(' [');
buffer.write(actual.offset);
buffer.write(', ');
buffer.write(actual.length);
buffer.write(', ');
buffer.write(actual.message);
buffer.writeln(']');
}
}
if (buffer.isNotEmpty) {
errors.sort((first, second) => first.offset.compareTo(second.offset));
buffer.writeln();
buffer.writeln('To accept the current state, expect:');
for (var actual in errors) {
var contextMessages = actual.contextMessages;
buffer.write(' error(');
buffer.write(actual.errorCode);
buffer.write(', ');
buffer.write(actual.offset);
buffer.write(', ');
buffer.write(actual.length);
if (contextMessages.isNotEmpty) {
buffer.write(', contextMessages: [');
for (var i = 0; i < contextMessages.length; i++) {
var message = contextMessages[i];
if (i > 0) {
buffer.write(', ');
}
buffer.write('message(\'');
buffer.write(message.filePath);
buffer.write('\', ');
buffer.write(message.offset);
buffer.write(', ');
buffer.write(message.length);
buffer.write(')');
}
buffer.write(']');
}
buffer.writeln('),');
}
fail(buffer.toString());
}
}
/// Assert that the number of errors that have been gathered matches the
/// number of [expectedErrorCodes] and that they have the expected error
/// codes. The order in which the errors were gathered is ignored.
void assertErrorsWithCodes(
[List<ErrorCode> expectedErrorCodes = const <ErrorCode>[]]) {
var buffer = StringBuffer();
//
// Compute the expected number of each type of error.
//
var expectedCounts = <ErrorCode, int>{};
for (var code in expectedErrorCodes) {
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.
//
var errorsByCode = <ErrorCode, List<AnalysisError>>{};
for (var error in _errors) {
errorsByCode
.putIfAbsent(error.errorCode, () => <AnalysisError>[])
.add(error);
}
//
// Compare the expected and actual number of each type of error.
//
expectedCounts.forEach((ErrorCode code, int expectedCount) {
int actualCount;
var list = errorsByCode.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.
//
errorsByCode.forEach((ErrorCode code, List<AnalysisError> actualErrors) {
var actualCount = actualErrors.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 (var i = 0; i < actualErrors.length; i++) {
var error = actualErrors[i];
if (i > 0) {
buffer.write(', ');
}
buffer.write(error.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<ErrorSeverity> expectedSeverities) {
var expectedErrorCount = 0;
var expectedWarningCount = 0;
for (var severity in expectedSeverities) {
if (severity == ErrorSeverity.ERROR) {
expectedErrorCount++;
} else {
expectedWarningCount++;
}
}
var actualErrorCount = 0;
var actualWarningCount = 0;
for (var error in _errors) {
if (error.errorCode.errorSeverity == ErrorSeverity.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];
/// Return `true` if an error with the given [errorCode] has been gathered.
bool hasError(ErrorCode errorCode) {
for (var error in _errors) {
if (identical(error.errorCode, errorCode)) {
return true;
}
}
return false;
}
@override
void onError(AnalysisError error) {
_errors.add(error);
}
/// Set the line information associated with the given [source] to the given
/// list of [lineStarts].
void setLineInfo(Source source, List<int> lineStarts) {
_lineInfoMap[source] = LineInfo(lineStarts);
}
}