// 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.) library; import 'package:analyzer/diagnostic/diagnostic.dart'; import 'package:analyzer/error/error.dart'; import 'package:analyzer/error/listener.dart'; import 'package:analyzer/source/line_info.dart'; import 'package:analyzer/source/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 NO_ERRORS = []; /// The error code associated with the error. final DiagnosticCode 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 expectedContextMessages; /// Initialize a newly created error description. ExpectedError( this.code, this.offset, this.length, { this.message, this.messageContains, this.expectedContextMessages = const [], }); /// Return `true` if the [diagnostic] matches this description of what the state /// of the [diagnostic] is expected to be. bool matches(Diagnostic diagnostic) { if (diagnostic.offset != offset || diagnostic.length != length || diagnostic.errorCode != code) { return false; } if (message != null && diagnostic.message != message) { return false; } var messageContains = this.messageContains; if (messageContains != null && diagnostic.message.contains(messageContains) != true) { return false; } var contextMessages = diagnostic.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 _errors = []; /// A table mapping sources to the line information for the source. final Map _lineInfoMap = {}; /// Initialize a newly created error listener to collect errors. GatheringErrorListener({this.checkRanges = true}); /// Return the errors that were collected. List 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 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 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 [expectedCodes] and that they have the expected error /// codes. The order in which the errors were gathered is ignored. void assertErrorsWithCodes([ List expectedCodes = const [], ]) { var buffer = StringBuffer(); // // Compute the expected number of each type of error. // var expectedCounts = {}; for (var 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. // var errorsByCode = >{}; for (var error in _errors) { errorsByCode .putIfAbsent(error.errorCode, () => []) .add(error); } // // Compare the expected and actual number of each type of error. // expectedCounts.forEach((DiagnosticCode 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((code, 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 expectedSeverities) { var expectedErrorCount = 0; var expectedWarningCount = 0; for (var severity in expectedSeverities) { if (severity == DiagnosticSeverity.ERROR) { expectedErrorCount++; } else { expectedWarningCount++; } } var actualErrorCount = 0; var actualWarningCount = 0; for (var error in _errors) { if (error.errorCode.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]; /// Returns whether a diagnostic with the given [diagnosticCode] has been /// gathered. bool hasDiagnostic(DiagnosticCode diagnosticCode) { for (var error in _errors) { if (identical(error.errorCode, diagnosticCode)) { return true; } } return false; } @override void onError(Diagnostic error) { _errors.add(error); } /// Set the line information associated with the given [source] to the given /// list of [lineStarts]. void setLineInfo(Source source, List lineStarts) { _lineInfoMap[source] = LineInfo(lineStarts); } }