Add support for context messages to static error tests.

Currently only CFE ("Fasta") tests have their context message output
parsed. It should be easy to extend that to dart2js and DDC if that's
useful. Analyzer might be more work.

This also adds support to the test updater for inserting context
messages when updating tests. By default, that flag is off, so the
existing behavior is preserved where context messages are ignnored. If
you want them, pass "-c" when updating a test.

When validating test output, if the test file contains context messages,
then they are validated. Otherwise, any context messages in the CFE
output are ignored. This way existing tests still pass.


Change StaticError to represent a single error for a single front end.

Before, the data model collapsed errors for different front-ends at the
same location into a single StaticError object which tracked different
messages for each front end. The idea was to move towards a world where
they really are the "same" error with eventually the same message.

But this adds a lot of complexity with things like merging errors and
doesn't reflect the reality that each error from each front end is
basically its own thing. Also, critically, it makes it much harder to
attach context messages to a specific front end's error object.

This changes it so that an instance of StaticError represents a single
error for a single front end. The test file syntax is unchanged and the
updated tool behaves the same. In a static error test, multiple
expectations can still share the same "//   ^^^" marker line. They are
just expanded to multiple StaticError objects at parse time.

This eliminates all of the complexity around merging and simplifying
errors.

Change-Id: Ida1736bfcde436fc2d1ce2963d91fa9cb154afa8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/193281
Commit-Queue: Bob Nystrom <rnystrom@google.com>
Reviewed-by: Paul Berry <paulberry@google.com>
This commit is contained in:
Robert Nystrom 2021-03-30 18:41:21 +00:00 committed by commit-bot@chromium.org
parent 6c30a7d582
commit fdb6ca6d01
8 changed files with 744 additions and 83 deletions

View file

@ -984,8 +984,9 @@ class Dart2jsCompilerCommandOutput extends CompilationCommandOutput
///
/// The test runner only validates the main error message, and not the
/// suggested fixes, so we only parse the first line.
// TODO(rnystrom): Support validating context messages.
static final _errorRegexp =
RegExp(r"^([^:]+):(\d+):(\d+):\nError: (.*)$", multiLine: true);
RegExp(r"^([^:]+):(\d+):(\d+):\n(Error): (.*)$", multiLine: true);
Dart2jsCompilerCommandOutput(
Command command,
@ -1017,8 +1018,9 @@ class DevCompilerCommandOutput extends CommandOutput with _StaticErrorOutput {
///
/// The test runner only validates the main error message, and not the
/// suggested fixes, so we only parse the first line.
// TODO(rnystrom): Support validating context messages.
static final _errorRegexp = RegExp(
r"^org-dartlang-app:/([^:]+):(\d+):(\d+): Error: (.*)$",
r"^org-dartlang-app:/([^:]+):(\d+):(\d+): (Error): (.*)$",
multiLine: true);
DevCompilerCommandOutput(
@ -1252,36 +1254,22 @@ class FastaCommandOutput extends CompilationCommandOutput
static void parseErrors(
String stdout, List<StaticError> errors, List<StaticError> warnings) {
_StaticErrorOutput._parseCfeErrors(
ErrorSource.cfe, _errorRegexp, stdout, errors);
_StaticErrorOutput._parseCfeErrors(
ErrorSource.cfe, _warningRegexp, stdout, warnings);
ErrorSource.cfe, _errorRegexp, stdout, errors, warnings);
}
/// Matches the first line of a Fasta error message. Fasta prints errors to
/// stdout that look like:
/// Matches the first line of a Fasta error, warning, or context message.
/// Fasta prints to stdout like:
///
/// tests/language_2/some_test.dart:7:21: Error: Some message.
/// Try fixing the code to be less bad.
/// var _ = <int>[if (1) 2];
/// ^
///
/// The test runner only validates the main error message, and not the
/// suggested fixes, so we only parse the first line.
static final _errorRegexp =
RegExp(r"^([^:]+):(\d+):(\d+): Error: (.*)$", multiLine: true);
/// Matches the first line of a Fasta warning message. Fasta prints errors to
/// stdout that look like:
///
/// tests/language_2/some_test.dart:7:21: Warning: Some message.
/// Try fixing the code to be less bad.
/// var _ = <int>[if (1) 2];
/// ^
///
/// The test runner only validates the main error message, and not the
/// suggested fixes, so we only parse the first line.
static final _warningRegexp =
RegExp(r"^([^:]+):(\d+):(\d+): Warning: (.*)$", multiLine: true);
/// The test runner only validates the first line of the message, and not the
/// suggested fixes.
static final _errorRegexp = RegExp(
r"^([^:]+):(\d+):(\d+): (Context|Error|Warning): (.*)$",
multiLine: true);
FastaCommandOutput(
Command command,
@ -1310,12 +1298,35 @@ mixin _StaticErrorOutput on CommandOutput {
/// Parses compile errors reported by CFE using the given [regExp] and adds
/// them to [errors] as coming from [errorSource].
static void _parseCfeErrors(ErrorSource errorSource, RegExp regExp,
String stdout, List<StaticError> errors) {
String stdout, List<StaticError> errors,
[List<StaticError> warnings]) {
StaticError previousError;
for (var match in regExp.allMatches(stdout)) {
var line = int.parse(match.group(2));
var column = int.parse(match.group(3));
var message = match.group(4);
errors.add(StaticError(errorSource, message, line: line, column: column));
var severity = match.group(4);
var message = match.group(5);
var error = StaticError(
severity == "Context" ? ErrorSource.context : errorSource, message,
line: line, column: column);
if (severity == "Context") {
// Attach context messages to the preceding error/warning.
if (previousError == null) {
DebugLogger.error("Got context message in CFE output before "
"error to attach it to.");
} else {
previousError.contextMessages.add(error);
}
} else {
if (severity == "Error") {
errors.add(error);
} else {
warnings.add(error);
}
previousError = error;
}
}
}

View file

@ -11,6 +11,9 @@ class ErrorSource {
static const cfe = ErrorSource._("CFE");
static const web = ErrorSource._("web");
/// Pseudo-front end for context messages.
static const context = ErrorSource._("context");
/// All of the supported front ends.
///
/// The order is significant here. In static error tests, error expectations
@ -24,6 +27,8 @@ class ErrorSource {
if (source.marker == name) return source;
}
if (name == "context") return context;
return null;
}
@ -113,6 +118,9 @@ class StaticError implements Comparable<StaticError> {
///
/// * Otherwise, any remaining expected errors are considered missing
/// errors and remaining actual errors are considered unexpected.
///
/// Also describes any mismatches between the context messages in the expected
/// and actual errors.
static String validateExpectations(Iterable<StaticError> expectedErrors,
Iterable<StaticError> actualErrors) {
var expected = expectedErrors.toList();
@ -122,16 +130,20 @@ class StaticError implements Comparable<StaticError> {
expected.sort();
actual.sort();
// Discard matched errors.
var buffer = StringBuffer();
// Pair up errors by location and message.
for (var i = 0; i < expected.length; i++) {
var matchedExpected = false;
for (var j = 0; j < actual.length; j++) {
if (actual[j] == null) continue;
if (expected[i]._matchLocation(actual[j]) == null &&
(expected[i].message == actual[j].message ||
!expected[i].isSpecified)) {
if (expected[i]._matchMessage(actual[j]) &&
expected[i]._matchLocation(actual[j])) {
// Report any mismatches in the context messages.
expected[i]._validateContext(actual[j], buffer);
actual[j] = null;
matchedExpected = true;
@ -147,12 +159,13 @@ class StaticError implements Comparable<StaticError> {
expected.removeWhere((error) => error == null);
actual.removeWhere((error) => error == null);
// If everything matched, we're done.
if (expected.isEmpty && actual.isEmpty) return null;
// If every error was paired up, and the contexts matched, we're done.
if (expected.isEmpty && actual.isEmpty && buffer.isEmpty) return null;
var buffer = StringBuffer();
void fail(StaticError error, String label, String contextLabel,
[String secondary]) {
if (error.isContext) label = contextLabel;
void fail(StaticError error, String label, [String secondary]) {
if (error.isSpecified) {
buffer.writeln("- $label ${error.location}: ${error.message}");
} else {
@ -172,8 +185,10 @@ class StaticError implements Comparable<StaticError> {
if (actual[j] == null) continue;
if (expected[i].message == actual[j].message) {
fail(expected[i], "Wrong error location",
expected[i]._matchLocation(actual[j]));
fail(expected[i], "Wrong error location", "Wrong context location",
expected[i]._locationError(actual[j]));
// Report any mismatches in the context messages.
expected[i]._validateContext(actual[j], buffer);
// Only report this mismatch once.
expected[i] = null;
@ -189,9 +204,11 @@ class StaticError implements Comparable<StaticError> {
for (var j = 0; j < actual.length; j++) {
if (actual[j] == null) continue;
if (expected[i]._matchLocation(actual[j]) == null) {
fail(actual[j], "Wrong message at",
if (expected[i]._matchLocation(actual[j])) {
fail(actual[j], "Wrong message at", "Wrong context message at",
"Expected: ${expected[i].message}");
// Report any mismatches in the context messages.
expected[i]._validateContext(actual[j], buffer);
// Only report this mismatch once.
expected[i] = null;
@ -204,13 +221,14 @@ class StaticError implements Comparable<StaticError> {
// Any remaining expected errors are missing.
for (var i = 0; i < expected.length; i++) {
if (expected[i] == null) continue;
fail(expected[i], "Missing expected error at");
fail(expected[i], "Missing expected error at",
"Missing expected context message at");
}
// Any remaining actual errors are unexpected.
for (var j = 0; j < actual.length; j++) {
if (actual[j] == null) continue;
fail(actual[j], "Unexpected error at");
fail(actual[j], "Unexpected error at", "Unexpected context message at");
}
return buffer.toString().trimRight();
@ -232,6 +250,9 @@ class StaticError implements Comparable<StaticError> {
final String message;
/// Additional context messages associated with this error.
final List<StaticError> contextMessages = [];
/// The zero-based numbers of the lines in the [TestFile] containing comments
/// that were parsed to produce this error.
///
@ -271,6 +292,9 @@ class StaticError implements Comparable<StaticError> {
/// met.
bool get isSpecified => message != _unspecified;
/// Whether this is a context message instead of an error.
bool get isContext => source == ErrorSource.context;
/// Whether this error is only considered a warning on all front ends that
/// report it.
bool get isWarning {
@ -290,7 +314,21 @@ class StaticError implements Comparable<StaticError> {
throw FallThroughError();
}
String toString() => "[${source.marker} $location] $message";
String toString() {
var buffer = StringBuffer("StaticError(");
buffer.write("line: $line, column: $column");
if (length != null) buffer.write(", length: $length");
buffer.write(", message: '$message'");
if (contextMessages.isNotEmpty) {
buffer.write(", context: [ ");
buffer.writeAll(contextMessages, ", ");
buffer.write(" ]");
}
buffer.write(")");
return buffer.toString();
}
/// Orders errors primarily by location, then by other fields if needed.
@override
@ -311,7 +349,20 @@ class StaticError implements Comparable<StaticError> {
}
@override
bool operator ==(other) => other is StaticError && compareTo(other) == 0;
bool operator ==(other) {
if (other is StaticError) {
if (compareTo(other) != 0) return false;
if (contextMessages.length != other.contextMessages.length) return false;
for (var i = 0; i < contextMessages.length; i++) {
if (contextMessages[i] != other.contextMessages[i]) return false;
}
return true;
}
return false;
}
@override
int get hashCode =>
@ -321,11 +372,31 @@ class StaticError implements Comparable<StaticError> {
11 * source.hashCode +
13 * message.hashCode;
/// Returns a string describing how this error's expected location differs
/// from [actual], or `null` if [actual]'s location matches this one.
/// Returns true if [actual]'s message matches this one.
///
/// Takes unspecified errors into account.
bool _matchMessage(StaticError actual) {
return !isSpecified || message == actual.message;
}
/// Returns true if [actual]'s location matches this one.
///
/// Takes into account unspecified errors and errors without lengths.
String _matchLocation(StaticError actual) {
bool _matchLocation(StaticError actual) {
if (line != actual.line) return false;
// Ignore column and length for unspecified errors.
if (isSpecified) {
if (column != actual.column) return false;
if (actual.length != null && length != actual.length) return false;
}
return true;
}
/// Returns a string describing how this error's expected location differs
/// from [actual].
String _locationError(StaticError actual) {
var expectedMismatches = <String>[];
var actualMismatches = <String>[];
@ -347,15 +418,28 @@ class StaticError implements Comparable<StaticError> {
}
}
if (expectedMismatches.isEmpty) {
// Everything matches.
return null;
}
// Should only call this when the locations don't match.
assert(expectedMismatches.isNotEmpty);
var expectedList = expectedMismatches.join(", ");
var actualList = actualMismatches.join(", ");
return "Expected $expectedList but was $actualList.";
}
/// Validates that this expected error's context messages match [actual]'s.
///
/// Writes any mismatch errors to [buffer].
void _validateContext(StaticError actual, StringBuffer buffer) {
// If the expected error has no context, then ignore actual context
// messages.
if (contextMessages.isEmpty) return;
var result = validateExpectations(contextMessages, actual.contextMessages);
if (result != null) {
buffer.writeln(result);
buffer.writeln();
}
}
}
class _ErrorExpectationParser {
@ -382,7 +466,10 @@ class _ErrorExpectationParser {
RegExp(r"^\s*//\s*\[\s*error line\s+(\d+)\s*,\s*column\s+(\d+)\s*\]\s*$");
/// Matches the beginning of an error message, like `// [analyzer]`.
static final _errorMessageRegExp = RegExp(r"^\s*// \[(\w+)\]\s*(.*)");
///
/// May have an optional number like `// [cfe 32]`.
static final _errorMessageRegExp =
RegExp(r"^\s*// \[(\w+)(\s+\d+)?\]\s*(.*)");
/// An analyzer error code is a dotted identifier or the magic string
/// "unspecified".
@ -394,6 +481,17 @@ class _ErrorExpectationParser {
final List<String> _lines;
final List<StaticError> _errors = [];
/// The parsed context messages.
///
/// Once parsing is done, these are added to the errors that own them.
final List<StaticError> _contextMessages = [];
/// For errors that have a number associated with them, tracks that number.
///
/// These are used after parsing to attach context messages to their errors.
final Map<StaticError, int> _errorNumbers = {};
int _currentLine = 0;
// One-based index of the last line that wasn't part of an error expectation.
@ -402,6 +500,7 @@ class _ErrorExpectationParser {
_ErrorExpectationParser(String source) : _lines = source.split("\n");
List<StaticError> _parse() {
// Read all the lines.
while (_canPeek(0)) {
var sourceLine = _peek(0);
@ -441,6 +540,7 @@ class _ErrorExpectationParser {
_advance();
}
_attachContext();
return _errors;
}
@ -454,11 +554,16 @@ class _ErrorExpectationParser {
var match = _errorMessageRegExp.firstMatch(_peek(1));
if (match == null) break;
var number = match.group(2) != null ? int.parse(match.group(2)) : null;
var sourceName = match.group(1);
var source = ErrorSource.find(sourceName);
if (source == null) _fail("Unknown front end '[$sourceName]'.");
if (source == ErrorSource.context && number == null) {
_fail("Context messages must have an error number.");
}
var message = match.group(2);
var message = match.group(3);
_advance();
var sourceLines = {locationLine, _currentLine};
@ -498,11 +603,32 @@ class _ErrorExpectationParser {
errorLength = null;
}
_errors.add(StaticError(source, message,
var error = StaticError(source, message,
line: line,
column: column,
length: errorLength,
sourceLines: sourceLines));
sourceLines: sourceLines);
if (number != null) {
// Make sure two errors don't claim the same number.
if (source != ErrorSource.context) {
var existingError = _errors.firstWhere(
(error) => _errorNumbers[error] == number,
orElse: () => null);
if (existingError != null) {
_fail("Already have an error with number $number.");
}
}
_errorNumbers[error] = number;
}
if (source == ErrorSource.context) {
_contextMessages.add(error);
} else {
_errors.add(error);
}
parsedError = true;
}
@ -511,6 +637,38 @@ class _ErrorExpectationParser {
}
}
/// Attach context messages to their errors and validate that everything lines
/// up.
void _attachContext() {
for (var contextMessage in _contextMessages) {
var number = _errorNumbers[contextMessage];
var error = _errors.firstWhere((error) => _errorNumbers[error] == number,
orElse: () => null);
if (error == null) {
throw FormatException("No error with number $number for context "
"message '${contextMessage.message}'.");
}
error.contextMessages.add(contextMessage);
}
// Make sure every numbered error does have some context, otherwise the
// number is pointless.
for (var error in _errors) {
var number = _errorNumbers[error];
if (number == null) continue;
var context = _contextMessages.firstWhere(
(context) => _errorNumbers[context] == number,
orElse: () => null);
if (context == null) {
throw FormatException("Missing context for numbered error $number "
"'${error.message}'.");
}
}
}
bool _canPeek(int offset) => _currentLine + offset < _lines.length;
void _advance() {

View file

@ -13,9 +13,10 @@ final _lineCommentRegExp = RegExp(r"^\s*//");
/// for the given [errors].
///
/// If [remove] is not `null`, then only removes existing errors for the given
/// sources.
/// sources. If [includeContext] is `true`, then includes context messages in
/// the output. Otherwise discards them.
String updateErrorExpectations(String source, List<StaticError> errors,
{Set<ErrorSource> remove}) {
{Set<ErrorSource> remove, bool includeContext = false}) {
remove ??= {};
// Split the existing errors into kept and deleted lists.
@ -39,15 +40,20 @@ String updateErrorExpectations(String source, List<StaticError> errors,
// Remove all existing marker comments in the file, even for errors we are
// preserving. We will regenerate marker comments for those errors too so
// they can properly share location comments with new errors if needed.
void removeLine(int line) {
if (lines[line] == null) return;
indentation[line] = _countIndentation(lines[line]);
// Null the line instead of removing it so that line numbers in the
// reported errors are still correct.
lines[line] = null;
}
for (var error in existingErrors) {
for (var line in error.sourceLines) {
if (lines[line] == null) continue;
indentation[line] = _countIndentation(lines[line]);
// Null the line instead of removing it so that line numbers in the
// reported errors are still correct.
lines[line] = null;
error.sourceLines.forEach(removeLine);
for (var contextMessage in error.contextMessages) {
contextMessage.sourceLines.forEach(removeLine);
}
}
@ -59,6 +65,14 @@ String updateErrorExpectations(String source, List<StaticError> errors,
for (var error in errors) {
// -1 to translate from one-based to zero-based index.
errorMap.putIfAbsent(error.line - 1, () => []).add(error);
// Flatten out and include context messages.
if (includeContext) {
for (var context in error.contextMessages) {
// -1 to translate from one-based to zero-based index.
errorMap.putIfAbsent(context.line - 1, () => []).add(context);
}
}
}
// If there are multiple errors on the same line, order them
@ -67,6 +81,8 @@ String updateErrorExpectations(String source, List<StaticError> errors,
errorList.sort();
}
var errorNumbers = _numberErrors(errors);
// Rebuild the source file a line at a time.
var previousIndent = 0;
var codeLine = 1;
@ -131,7 +147,12 @@ String updateErrorExpectations(String source, List<StaticError> errors,
previousLength = error.length;
var errorLines = error.message.split("\n");
result.add("$comment [${error.source.marker}] ${errorLines[0]}");
var line = "$comment [${error.source.marker}";
if (includeContext && errorNumbers.containsKey(error)) {
line += " ${errorNumbers[error]}";
}
line += "] ${errorLines[0]}";
result.add(line);
for (var errorLine in errorLines.skip(1)) {
result.add("$comment $errorLine");
}
@ -150,6 +171,25 @@ String updateErrorExpectations(String source, List<StaticError> errors,
return result.join("\n");
}
/// Assigns unique numbers to all [errors] that have context messages, as well
/// as their context messages.
Map<StaticError, int> _numberErrors(List<StaticError> errors) {
var result = <StaticError, int>{};
var number = 1;
for (var error in errors) {
if (error.contextMessages.isEmpty) continue;
result[error] = number;
for (var context in error.contextMessages) {
result[context] = number;
}
number++;
}
return result;
}
/// Returns the number of characters of leading spaces in [line].
int _countIndentation(String line) {
var match = _indentationRegExp.firstMatch(line);

View file

@ -12,7 +12,6 @@ void main() {
testProperties();
testIsWarning();
testCompareTo();
// testDescribeDifferences();
testValidate();
}
@ -223,6 +222,105 @@ void testValidate() {
makeError(line: 1, column: 2, length: 3, webError: "Web 2."),
makeError(line: 1, column: 3, length: 3, webError: "Web 3."),
], null);
// If expectation has context, actual must match it.
expectValidate([
makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
]),
], [
makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
]),
], null);
// Actual context is different.
expectValidate([
makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
]),
], [
makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
makeError(line: 4, column: 5, length: 6, contextError: "Context Z."),
]),
], """
- Wrong context message at line 4, column 5, length 6: Context Z.
Expected: Context A.""");
// Missing some actual context.
expectValidate([
makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
]),
], [
makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
]),
], """
- Missing expected context message at line 4, column 5, length 6: Context A.""");
// Missing all actual context.
expectValidate([
makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
]),
], [
makeError(line: 1, column: 2, length: 3, cfeError: "Error."),
], """
- Missing expected context message at line 4, column 5, length 6: Context A.
- Missing expected context message at line 7, column 8, length 9: Context B.""");
// Unexpected extra actual context.
expectValidate([
makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
]),
], [
makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
]),
], """
- Unexpected context message at line 7, column 8, length 9: Context B.""");
// Actual context owned by wrong error.
// TODO(rnystrom): This error is pretty confusing. Ideally we would detect
// this case specifically and give better guidance.
expectValidate([
makeError(line: 1, column: 2, length: 3, cfeError: "Error A.", context: [
makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
makeError(line: 33, column: 5, length: 6, contextError: "Context."),
]),
makeError(line: 10, column: 2, length: 3, cfeError: "Error B.", context: [
makeError(line: 11, column: 5, length: 6, contextError: "Context B."),
]),
], [
makeError(line: 1, column: 2, length: 3, cfeError: "Error A.", context: [
makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
]),
makeError(line: 10, column: 2, length: 3, cfeError: "Error B.", context: [
makeError(line: 11, column: 5, length: 6, contextError: "Context B."),
makeError(line: 33, column: 5, length: 6, contextError: "Context."),
]),
], """
- Missing expected context message at line 33, column 5, length 6: Context.
- Unexpected context message at line 33, column 5, length 6: Context.""");
// If expectation has no context at all, then ignore actual context.
expectValidate([
makeError(line: 1, column: 2, length: 3, cfeError: "Error."),
], [
makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
]),
], null);
}
void expectValidate(List<StaticError> expected, List<StaticError> actual,

View file

@ -28,6 +28,7 @@ void main() {
testParseMultitest();
testParseErrorFlags();
testParseErrorExpectations();
testParseContextMessages();
testIsRuntimeTest();
testName();
testMultitest();
@ -556,6 +557,145 @@ int j = "s";
]);
}
void testParseContextMessages() {
// Multiple messages.
expectParseErrorExpectations("""
var string = "str";
/\/ ^^^^^^
/\/ [context 1] Analyzer context before.
/\/ [context 2] CFE context before.
int j = string;
/\/ ^^^^^^
/\/ [analyzer 1] Error.BAD
/\/ [cfe 2] Error message.
var string = "str";
/\/ ^^^
/\/ [context 2] CFE context after.
var string = "str";
/\/ ^^^
/\/ [context 1] Analyzer context after.
""", [
makeError(
line: 6,
column: 9,
length: 6,
analyzerError: "Error.BAD",
context: [
makeError(
line: 1,
column: 5,
length: 6,
analyzerError: "Analyzer context before."),
makeError(
line: 15,
column: 15,
length: 3,
analyzerError: "Analyzer context after.")
]),
makeError(
line: 6,
column: 9,
length: 6,
cfeError: "Error message.",
context: [
makeError(
line: 1,
column: 5,
length: 6,
analyzerError: "CFE context before."),
makeError(
line: 11,
column: 15,
length: 3,
analyzerError: "CFE context after.")
]),
]);
// Context before error.
expectParseErrorExpectations("""
var string = "str";
/\/ ^^^^^^
/\/ [context 1] Context.
int j = string;
/\/ ^^^^^^
/\/ [analyzer 1] Error.BAD
""", [
makeError(
line: 5,
column: 9,
length: 6,
analyzerError: "Error.BAD",
context: [
makeError(line: 1, column: 5, length: 6, analyzerError: "Context.")
]),
]);
// Context after error.
expectParseErrorExpectations("""
int j = string;
/\/ ^^^^^^
/\/ [analyzer 1] Error.BAD
var string = "str";
/\/ ^^^^^^
/\/ [context 1] Context.
""", [
makeError(
line: 1,
column: 9,
length: 6,
analyzerError: "Error.BAD",
context: [
makeError(line: 5, column: 5, length: 6, analyzerError: "Context.")
]),
]);
// Context must have a number.
expectFormatError("""
int i = "s";
/\/ ^^^
/\/ [context] No number.
int i = "s";
/\/ ^^^
/\/ [cfe 1] Error.
""");
// Context number must match an error.
expectFormatError("""
int i = "s";
/\/ ^^^
/\/ [context 2] Wrong number.
int i = "s";
/\/ ^^^
/\/ [cfe 1] Error.
""");
// Two errors with same number.
expectFormatError("""
int i = "s";
/\/ ^^^
/\/ [context 1] Context.
int i = "s";
/\/ ^^^
/\/ [cfe 1] Error.
/\/ [analyzer 1] Error.CODE
""");
// Numbered error with no context.
expectFormatError("""
int i = "s";
/\/ ^^^
/\/ [cfe 1] Error.
""");
}
void testIsRuntimeTest() {
// No static errors at all.
var file = parseTestFile("");

View file

@ -403,9 +403,192 @@ x
// [error line 1, column 1, length 0]
// [cfe] Foo""");
contextMessages();
regression();
}
void contextMessages() {
// Inserts context messages.
expectUpdate(
"""
int i = "bad";
int another = "wrong";
int third = "boo";
""",
errors: [
makeError(
line: 3,
column: 15,
length: 7,
analyzerError: "some.error",
context: [
makeError(
line: 1,
column: 9,
length: 5,
contextError: "Analyzer context."),
makeError(
line: 5,
column: 13,
length: 5,
contextError: "More context."),
]),
makeError(
line: 3,
column: 15,
length: 7,
cfeError: "CFE error.",
context: [
makeError(
line: 1, column: 9, length: 5, contextError: "CFE context."),
]),
],
remove: {ErrorSource.analyzer},
includeContext: true,
expected: """
int i = "bad";
/\/ ^^^^^
/\/ [context 1] Analyzer context.
/\/ [context 2] CFE context.
int another = "wrong";
/\/ ^^^^^^^
/\/ [analyzer 1] some.error
/\/ [cfe 2] CFE error.
int third = "boo";
/\/ ^^^^^
/\/ [context 1] More context.
""");
// Removes context messages for removed errors.
expectUpdate(
"""
int i = "bad";
/\/ ^^^^^
/\/ [context 1] Analyzer context.
/\/ [context 2] CFE context.
int another = "wrong";
/\/ ^^^^^^^
/\/ [analyzer 1] some.error
/\/ [cfe 2] CFE error.
int third = "boo";
/\/ ^^^^^
/\/ [context 1] More context.
""",
remove: {ErrorSource.analyzer},
includeContext: true,
expected: """
int i = "bad";
/\/ ^^^^^
/\/ [context 1] CFE context.
int another = "wrong";
/\/ ^^^^^^^
/\/ [cfe 1] CFE error.
int third = "boo";
""");
// Discards context messages when not told to include them.
expectUpdate(
"""
int i = "bad";
int another = "wrong";
int third = "boo";
""",
errors: [
makeError(
line: 3,
column: 15,
length: 7,
analyzerError: "some.error",
context: [
makeError(
line: 1,
column: 9,
length: 5,
contextError: "Analyzer context."),
makeError(
line: 5,
column: 13,
length: 5,
contextError: "More context."),
]),
makeError(
line: 3,
column: 15,
length: 7,
cfeError: "CFE error.",
context: [
makeError(
line: 1, column: 9, length: 5, contextError: "CFE context."),
]),
],
includeContext: false,
expected: """
int i = "bad";
int another = "wrong";
/\/ ^^^^^^^
/\/ [analyzer] some.error
/\/ [cfe] CFE error.
int third = "boo";
""");
// Discards existing context messages when not told to include them.
expectUpdate(
"""
int i = "bad";
/\/ ^^^^^
/\/ [context 1] CFE context.
int another = "wrong";
/\/ ^^^^^^^
/\/ [cfe 1] CFE error.
int third = "boo";
""",
errors: [
makeError(
line: 5,
column: 15,
length: 7,
analyzerError: "some.error",
context: [
makeError(
line: 1,
column: 9,
length: 5,
contextError: "Analyzer context."),
makeError(
line: 7,
column: 13,
length: 5,
contextError: "More context."),
]),
],
remove: {ErrorSource.analyzer},
includeContext: false,
expected: """
int i = "bad";
int another = "wrong";
/\/ ^^^^^^^
/\/ [analyzer] some.error
/\/ [cfe] CFE error.
int third = "boo";
""");
}
void regression() {
// https://github.com/dart-lang/sdk/issues/37990.
expectUpdate("""
@ -462,11 +645,16 @@ class A {
}
void expectUpdate(String original,
{List<StaticError> errors, Set<ErrorSource> remove, String expected}) {
{List<StaticError> errors,
Set<ErrorSource> remove,
bool includeContext,
String expected}) {
errors ??= const [];
remove ??= ErrorSource.all.toSet();
includeContext ??= false;
var actual = updateErrorExpectations(original, errors, remove: remove);
var actual = updateErrorExpectations(original, errors,
remove: remove, includeContext: includeContext);
if (actual != expected) {
// Not using Expect.equals() because the diffs it shows aren't helpful for
// strings this large.

View file

@ -28,27 +28,44 @@ StandardTestSuite makeTestSuite(TestConfiguration configuration,
/// Creates a [StaticError].
///
/// Only one of [analyzerError], [cfeError], or [webError] may be passed.
/// Only one of [analyzerError], [cfeError], [webError], or [contextError] may
/// be passed.
StaticError makeError(
{int line = 1,
int column = 2,
int length,
String analyzerError,
String cfeError,
String webError}) {
String webError,
String contextError,
List<StaticError> context}) {
ErrorSource source;
String message;
if (analyzerError != null) {
assert(cfeError == null && webError == null);
return StaticError(ErrorSource.analyzer, analyzerError,
line: line, column: column, length: length);
assert(cfeError == null);
assert(webError == null);
assert(contextError == null);
source = ErrorSource.analyzer;
message = analyzerError;
} else if (cfeError != null) {
assert(webError == null);
return StaticError(ErrorSource.cfe, cfeError,
line: line, column: column, length: length);
assert(contextError == null);
source = ErrorSource.cfe;
message = cfeError;
} else if (webError != null) {
assert(contextError == null);
source = ErrorSource.web;
message = webError;
} else {
assert(webError != null);
return StaticError(ErrorSource.web, webError,
line: line, column: column, length: length);
assert(contextError != null);
source = ErrorSource.context;
message = contextError;
}
var error =
StaticError(source, message, line: line, column: column, length: length);
if (context != null) error.contextMessages.addAll(context);
return error;
}
class _MockTestSuite extends StandardTestSuite {

View file

@ -37,6 +37,8 @@ Future<void> main(List<String> args) async {
abbr: "n",
help: "Print result but do not overwrite any files.",
negatable: false);
parser.addFlag("context",
abbr: "c", help: "Include context messages in output.");
parser.addSeparator("What operations to perform:");
parser.addFlag("remove-all",
@ -61,8 +63,6 @@ Future<void> main(List<String> args) async {
help: "Update error expectations for given front ends.",
allowed: sources);
parser.addSeparator("Other flags:");
var results = parser.parse(args);
if (results["help"] as bool) {
@ -76,6 +76,8 @@ Future<void> main(List<String> args) async {
var dryRun = results["dry-run"] as bool;
var includeContext = results["context"] as bool;
var removeSources = <ErrorSource>{};
var insertSources = <ErrorSource>{};
@ -132,7 +134,10 @@ Future<void> main(List<String> args) async {
if (entry is pkg_file.File) {
await _processFile(entry,
dryRun: dryRun, remove: removeSources, insert: insertSources);
dryRun: dryRun,
includeContext: includeContext,
remove: removeSources,
insert: insertSources);
}
}
}
@ -146,7 +151,10 @@ void _usageError(ArgParser parser, String message) {
}
Future<void> _processFile(File file,
{bool dryRun, Set<ErrorSource> remove, Set<ErrorSource> insert}) async {
{bool dryRun,
bool includeContext,
Set<ErrorSource> remove,
Set<ErrorSource> insert}) async {
stdout.write("${file.path}...");
var source = file.readAsStringSync();
var testFile = TestFile.parse(Path("."), file.absolute.path, source);
@ -203,7 +211,8 @@ Future<void> _processFile(File file,
}
}
var result = updateErrorExpectations(source, errors, remove: remove);
var result = updateErrorExpectations(source, errors,
remove: remove, includeContext: includeContext);
stdout.writeln("\r${file.path} (Updated with ${errors.length} errors)");