| // Copyright (c) 2019, 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:io'; | |
| import 'package:path/path.dart' as p; | |
| import 'test_file.dart'; | |
| /// A front end that can report static errors. | |
| class ErrorSource { | |
| static const analyzer = ErrorSource._("analyzer"); | |
| static const cfe = ErrorSource._("CFE", marker: "cfe"); | |
| static const specParser = ErrorSource._("spec_parser"); | |
| 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 | |
| /// must be in this order for consistency. | |
| static const all = [analyzer, cfe, specParser, web]; | |
| /// Gets the source whose lowercase name is [name] or `null` if no source | |
| /// with that name could be found. | |
| static ErrorSource? find(String name) { | |
| for (var source in all) { | |
| if (source.marker == name) return source; | |
| } | |
| if (name == "context") return context; | |
| return null; | |
| } | |
| /// A user readable name for the error source. | |
| final String name; | |
| /// The string used to mark errors from this source in test files. | |
| /// | |
| /// Always lower-case. | |
| final String marker; | |
| /// Creates error source. | |
| /// | |
| /// If [name] is not all lower-case, then a [marker] must be passed with | |
| /// an all lower-case name. If `name` is lower-case, `marker` can be omitted | |
| /// and then defaults to `name`. | |
| const ErrorSource._(this.name, {String? marker}) : marker = marker ?? name; | |
| } | |
| /// Describes a single static error reported by a single front end at a specific | |
| /// location. | |
| /// | |
| /// These can be parsed from comments in [TestFile]s, in which case they | |
| /// represent *expected* errors. If a test contains any of these, then it is a | |
| /// "static error test" and exists to validate that a conforming front end | |
| /// produces the expected compile-time errors. This same class is also used for | |
| /// *reported* errors when parsing the output of a front end. | |
| /// | |
| /// For analyzer errors, the error "message" is actually the constant name for | |
| /// the error code, like "CompileTimeErrorCode.WRONG_TYPE". | |
| class StaticError implements Comparable<StaticError> { | |
| static const _unspecified = "unspecified"; | |
| /// The error codes for all of the analyzer errors that are non-fatal | |
| /// warnings. | |
| /// | |
| /// We can't rely on the type ("STATIC_WARNING", etc.) because for historical | |
| /// reasons the "warning" types contain a large number of actual compile | |
| /// errors. | |
| // TODO(rnystrom): This list was generated on 2020/07/24 based on the list | |
| // of error codes in sdk/pkg/analyzer/lib/error/error.dart. Is there a more | |
| // systematic way to handle this? | |
| static const _analyzerWarningCodes = { | |
| "STATIC_WARNING.ANALYSIS_OPTION_DEPRECATED", | |
| "STATIC_WARNING.INCLUDE_FILE_NOT_FOUND", | |
| "STATIC_WARNING.INCLUDED_FILE_WARNING", | |
| "STATIC_WARNING.INVALID_OPTION", | |
| "STATIC_WARNING.INVALID_SECTION_FORMAT", | |
| "STATIC_WARNING.SPEC_MODE_REMOVED", | |
| "STATIC_WARNING.UNREACHABLE_SWITCH_CASE", | |
| "STATIC_WARNING.UNREACHABLE_SWITCH_DEFAULT", | |
| "STATIC_WARNING.UNRECOGNIZED_ERROR_CODE", | |
| "STATIC_WARNING.UNSUPPORTED_OPTION_WITH_LEGAL_VALUE", | |
| "STATIC_WARNING.UNSUPPORTED_OPTION_WITH_LEGAL_VALUES", | |
| "STATIC_WARNING.UNSUPPORTED_OPTION_WITHOUT_VALUES", | |
| "STATIC_WARNING.UNSUPPORTED_VALUE", | |
| "STATIC_WARNING.CAMERA_PERMISSIONS_INCOMPATIBLE", | |
| "STATIC_WARNING.NO_TOUCHSCREEN_FEATURE", | |
| "STATIC_WARNING.NON_RESIZABLE_ACTIVITY", | |
| "STATIC_WARNING.PERMISSION_IMPLIES_UNSUPPORTED_HARDWARE", | |
| "STATIC_WARNING.SETTING_ORIENTATION_ON_ACTIVITY", | |
| "STATIC_WARNING.UNSUPPORTED_CHROME_OS_FEATURE", | |
| "STATIC_WARNING.UNSUPPORTED_CHROME_OS_HARDWARE", | |
| "STATIC_WARNING.DEAD_NULL_AWARE_EXPRESSION", | |
| "STATIC_WARNING.INVALID_NULL_AWARE_OPERATOR", | |
| "STATIC_WARNING.INVALID_OVERRIDE_DIFFERENT_DEFAULT_VALUES_NAMED", | |
| "STATIC_WARNING.INVALID_OVERRIDE_DIFFERENT_DEFAULT_VALUES_POSITIONAL", | |
| "STATIC_WARNING.MISSING_ENUM_CONSTANT_IN_SWITCH", | |
| "STATIC_WARNING.UNNECESSARY_NON_NULL_ASSERTION", | |
| "STATIC_WARNING.TOP_LEVEL_INSTANCE_GETTER", | |
| "STATIC_WARNING.TOP_LEVEL_INSTANCE_METHOD", | |
| }; | |
| /// Parses the set of static error expectations defined in the Dart source | |
| /// file [source]. | |
| static List<StaticError> parseExpectations({ | |
| required String path, | |
| required String source, | |
| }) { | |
| var parsed = parseExpectationsUnattached(path: path, source: source); | |
| return attachContextExpectations([parsed], rootPath: path); | |
| } | |
| /// Parses static error expectations without attaching context messages. | |
| /// | |
| /// This is used by [TestFile] to parse a root test and its helper files | |
| /// before resolving cross-file context relationships. | |
| static ParsedStaticErrorExpectations parseExpectationsUnattached({ | |
| required String path, | |
| required String source, | |
| }) => _ErrorExpectationParser(path: path, source: source)._parse(); | |
| /// Normalizes [path] the same way [StaticError] stores paths, so callers can | |
| /// compare raw file paths with [path], [contextPaths], and [contextOwnerPath]. | |
| static String normalizePath(String path) => | |
| p.relative(path, from: Directory.current.path); | |
| /// Attaches parsed context messages to their owning errors. | |
| static List<StaticError> attachContextExpectations( | |
| Iterable<ParsedStaticErrorExpectations> parsedFiles, { | |
| required String rootPath, | |
| }) { | |
| var parsedByPath = {for (var parsed in parsedFiles) parsed.path: parsed}; | |
| // Go through the errors in each file and attach their contexts. | |
| var root = normalizePath(rootPath); | |
| var errors = <StaticError>[]; | |
| var attachedContexts = Set<StaticError>.identity(); | |
| for (var parsed in parsedFiles) { | |
| errors.addAll(parsed.errors); | |
| for (var error in parsed.errors) { | |
| var number = parsed.numberOf(error); | |
| if (number == null) continue; | |
| var hasContext = false; | |
| var expectedContextPaths = [error.path, ...error.contextPaths]; | |
| for (var contextPath in expectedContextPaths) { | |
| var contextFile = parsedByPath[contextPath]; | |
| if (contextFile == null) { | |
| throw FormatException( | |
| "Context file $contextPath was not parsed for numbered " | |
| "error $number '${error.message}'.", | |
| ); | |
| } | |
| var contexts = contextFile.contextMessages.where((context) { | |
| return contextFile.numberOf(context) == number && | |
| context.contextOwnerPath == error.path; | |
| }).toList(); | |
| if (contextPath != error.path && contexts.isEmpty) { | |
| throw FormatException( | |
| "No context with number $number in " | |
| "$contextPath for '${error.message}'.", | |
| ); | |
| } | |
| for (var context in contexts) { | |
| error.contextMessages.add(context); | |
| attachedContexts.add(context); | |
| hasContext = true; | |
| } | |
| } | |
| if (!hasContext) { | |
| throw FormatException( | |
| "Missing context for numbered error $number " | |
| "'${error.message}'.", | |
| ); | |
| } | |
| } | |
| } | |
| // Make sure every context ended up associated with an error. | |
| for (var parsed in parsedFiles) { | |
| for (var context in parsed.contextMessages) { | |
| if (attachedContexts.contains(context)) continue; | |
| var ownerPath = context.contextOwnerPath; | |
| if (ownerPath == root || parsedByPath.containsKey(ownerPath)) { | |
| var number = parsed.numberOf(context); | |
| throw FormatException( | |
| "No error with number $number for context " | |
| "message '${context.message}'.", | |
| ); | |
| } | |
| } | |
| } | |
| return errors; | |
| } | |
| /// Determines whether all [actualErrors] match the given [expectedErrors]. | |
| /// | |
| /// If they match, returns `null`. Otherwise returns a string describing the | |
| /// mismatches. This is a human-friendly explanation of the difference | |
| /// between the two sets of errors, while also being simple to implement. | |
| /// An expected error that is completely identical to an actual error is | |
| /// treated as a match. Everything else is a failure. | |
| /// | |
| /// It has a few heuristics to try to determine what the discrepancies mean, | |
| /// which it applies in order: | |
| /// | |
| /// * If it sees an actual errors with the same message but different | |
| /// location as expected ones, it considers those to be the same error | |
| /// but with the wrong location. | |
| /// | |
| /// * If it sees an actual errors at the same location as expected ones, | |
| /// it considers those to be wrong messages. | |
| /// | |
| /// * 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 = <StaticError?>[...expectedErrors]; | |
| var actual = <StaticError?>[...actualErrors]; | |
| // Put them in a deterministic order. | |
| expected.sort(); | |
| actual.sort(); | |
| 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]!._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; | |
| // If the expected error is unspecified, keep going so that it can | |
| // consume multiple errors on the same line. | |
| if (expected[i]!.isSpecified) break; | |
| } | |
| } | |
| if (matchedExpected) expected[i] = null; | |
| } | |
| expected.removeWhere((error) => error == null); | |
| actual.removeWhere((error) => error == null); | |
| // If every error was paired up, and the contexts matched, we're done. | |
| if (expected.isEmpty && actual.isEmpty && buffer.isEmpty) return null; | |
| void fail( | |
| StaticError error, | |
| String label, | |
| String contextLabel, [ | |
| String? secondary, | |
| ]) { | |
| if (error.isContext) label = contextLabel; | |
| if (error.isSpecified) { | |
| buffer.writeln("- $label ${error.location}: ${error.message}"); | |
| } else { | |
| label = label.replaceAll("error", "unspecified error"); | |
| buffer.writeln("- $label ${error.location}."); | |
| } | |
| if (secondary != null) buffer.writeln(" $secondary"); | |
| buffer.writeln(); | |
| } | |
| // Look for matching messages, which means a wrong location. | |
| for (var i = 0; i < expected.length; i++) { | |
| if (expected[i] == null) continue; | |
| for (var j = 0; j < actual.length; j++) { | |
| if (actual[j] == null) continue; | |
| if (expected[i]!.message == actual[j]!.message) { | |
| 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; | |
| actual[j] = null; | |
| break; | |
| } | |
| } | |
| } | |
| // Look for matching locations, which means a wrong message. | |
| for (var i = 0; i < expected.length; i++) { | |
| if (expected[i] == null) continue; | |
| for (var j = 0; j < actual.length; j++) { | |
| if (actual[j] == null) continue; | |
| 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; | |
| actual[j] = null; | |
| break; | |
| } | |
| } | |
| } | |
| // 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", | |
| "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", "Unexpected context message at"); | |
| } | |
| return buffer.toString().trimRight(); | |
| } | |
| /// The path of the file containing the error. | |
| final String path; | |
| /// The one-based line number of the beginning of the error's location. | |
| final int line; | |
| /// The one-based column number of the beginning of the error's location. | |
| final int column; | |
| /// The number of characters in the error location. | |
| /// | |
| /// `0` means no length was reported. The CFE only reports error location, | |
| /// but not length. | |
| final int length; | |
| /// The front end this error is for. | |
| final ErrorSource source; | |
| final String message; | |
| /// Additional context messages associated with this error. | |
| final List<StaticError> contextMessages = []; | |
| /// Additional files to search for context messages owned by this error. | |
| final List<String> contextPaths; | |
| /// The path of the error that owns this context message. | |
| /// | |
| /// For same-file context messages this defaults to [path]. For cross-file | |
| /// context messages this comes from the `for` clause. | |
| final String contextOwnerPath; | |
| /// The zero-based numbers of the lines in the [TestFile] containing comments | |
| /// that were parsed to produce this error. | |
| /// | |
| /// This includes a line for the location comment, as well as lines for the | |
| /// error message. Note that lines may not be contiguous and multiple errors | |
| /// may share the same line number for a shared location marker. | |
| final Set<int> sourceLines; | |
| /// Creates a new StaticError at the given location with the given expected | |
| /// error code and message. | |
| /// | |
| /// In order to make it easier to incrementally add error tests before a | |
| /// feature is fully implemented or specified, an error expectation can be in | |
| /// an "unspecified" state for either or both platforms by having the error | |
| /// code or message be the special string "unspecified". When an unspecified | |
| /// error is tested, a front end is expected to report *some* error on that | |
| /// error's line, but it can be any location, error code, or message. | |
| StaticError( | |
| this.source, | |
| this.message, { | |
| required String path, | |
| required this.line, | |
| required this.column, | |
| this.length = 0, | |
| Set<int>? sourceLines, | |
| Iterable<String> contextPaths = const [], | |
| String? contextOwnerPath, | |
| }) : path = normalizePath(path), | |
| contextPaths = [for (var path in contextPaths) normalizePath(path)], | |
| contextOwnerPath = normalizePath(contextOwnerPath ?? path), | |
| sourceLines = {...?sourceLines}; | |
| /// A textual description of this error's location. | |
| String get location { | |
| var result = "$path line $line, column $column"; | |
| if (length > 0) result += ", length $length"; | |
| return result; | |
| } | |
| /// True if this error has a specific expected message and location. | |
| /// | |
| /// Otherwise, it is an "unspecified error", which means that as long as some | |
| /// actual error is reported on this error's line, then the expectation is | |
| /// 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 { | |
| switch (source) { | |
| case ErrorSource.analyzer: | |
| return _analyzerWarningCodes.contains(message); | |
| case ErrorSource.cfe: | |
| // TODO(42787): Once CFE starts reporting warnings, encode that in the | |
| // message somehow and then look for it here. | |
| return false; | |
| case ErrorSource.web: | |
| // TODO(rnystrom): If the web compilers report warnings, encode that in | |
| // the message somehow and then look for it here. | |
| return false; | |
| case ErrorSource.specParser: | |
| // The spec parser does not report warnings. | |
| return false; | |
| default: | |
| break; | |
| } | |
| throw UnsupportedError("ErrorSource ${source.name}"); | |
| } | |
| @override | |
| String toString() { | |
| var buffer = StringBuffer("StaticError("); | |
| buffer.write("line: $line, column: $column"); | |
| if (length > 0) 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 | |
| int compareTo(StaticError other) { | |
| if (line != other.line) return line.compareTo(other.line); | |
| if (column != other.column) return column.compareTo(other.column); | |
| // Sort no length after all other lengths. | |
| if (length == 0 && other.length > 0) return 1; | |
| if (length > 0 && other.length == 0) return -1; | |
| if (length != other.length) return length.compareTo(other.length); | |
| if (source != other.source) { | |
| return source.marker.compareTo(other.source.marker); | |
| } | |
| if (message != other.message) return message.compareTo(other.message); | |
| return path.compareTo(other.path); | |
| } | |
| @override | |
| 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 => Object.hash(line, column, length, source, message, path); | |
| /// 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. | |
| bool _matchLocation(StaticError actual) { | |
| if (path != actual.path) return false; | |
| if (line != actual.line) return false; | |
| // Ignore column and length for unspecified errors. | |
| if (isSpecified) { | |
| if (column != actual.column) return false; | |
| if (actual.length > 0 && length > 0 && 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>[]; | |
| if (line != actual.line) { | |
| expectedMismatches.add("line $line"); | |
| actualMismatches.add("line ${actual.line}"); | |
| } | |
| // Ignore column and length for unspecified errors. | |
| if (isSpecified) { | |
| if (column != actual.column) { | |
| expectedMismatches.add("column $column"); | |
| actualMismatches.add("column ${actual.column}"); | |
| } | |
| if (actual.length > 0 && length != actual.length) { | |
| expectedMismatches.add("length $length"); | |
| actualMismatches.add("length ${actual.length}"); | |
| } | |
| } | |
| // 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(); | |
| } | |
| } | |
| } | |
| /// Static error markers parsed from one file before context attachment. | |
| class ParsedStaticErrorExpectations { | |
| final String path; | |
| final List<StaticError> errors; | |
| final List<StaticError> contextMessages; | |
| final Map<StaticError, int> _numbers; | |
| ParsedStaticErrorExpectations._({ | |
| required String path, | |
| required this.errors, | |
| required this.contextMessages, | |
| required this._numbers, | |
| }) : path = StaticError.normalizePath(path); | |
| int? numberOf(StaticError error) => _numbers[error]; | |
| } | |
| String _resolveMarkerPath(String containingPath, String markerPath) { | |
| return p.normalize(p.join(p.dirname(containingPath), markerPath)); | |
| } | |
| class _ErrorMarker { | |
| final ErrorSource source; | |
| final int? number; | |
| final List<String> seePaths; | |
| final String? forPath; | |
| _ErrorMarker({ | |
| required this.source, | |
| required this.number, | |
| required this.seePaths, | |
| required this.forPath, | |
| }); | |
| } | |
| class _ErrorExpectationParser { | |
| /// Marks the location of an expected error, like so: | |
| /// | |
| /// int i = "s"; | |
| /// // ^^^ | |
| /// | |
| /// We look for a line that starts with a line comment followed by spaces and | |
| /// carets. Only used on single lines. | |
| static final _caretLocationRegExp = RegExp(r"^\s*//\s*(\^+)\s*$"); | |
| /// Matches an explicit error location with a length, like: | |
| /// | |
| /// // [error line 1, column 17, length 3] | |
| /// | |
| /// or implicitly on the previous line | |
| /// | |
| /// // [error column 17, length 3] | |
| /// | |
| /// or either without a length: | |
| /// | |
| /// // [error line 1, column 17] | |
| /// // [error column 17] | |
| /// | |
| /// Only used on single lines. | |
| static final _explicitLocationRegExp = RegExp( | |
| r"^\s*//\s*\[\s*error (?:line\s+(\d+)\s*,)?\s*column\s+(\d+)\s*(?:,\s*" | |
| r"length\s+(\d+)\s*)?\]\s*$", | |
| ); | |
| /// Matches the beginning of an error message, like `// [analyzer]`. | |
| static final _errorMessageRegExp = RegExp(r"^\s*// \[([^\]]+)\]\s*(.*)"); | |
| /// Matches the content inside the brackets of an error message marker. | |
| /// | |
| /// Examples: | |
| /// - `analyzer` | |
| /// - `cfe 1` | |
| /// - `analyzer 1 see helper.dart` | |
| /// - `context 1 for helper.dart` | |
| /// | |
| /// Captures: | |
| /// 1. The source name (`analyzer`, `cfe`, `web`, `context`). | |
| /// 2. The optional error or context number. | |
| /// 3. The optional clause type (`see` or `for`). | |
| /// 4. The optional path or paths in the clause. | |
| static final _markerBodyRegExp = RegExp( | |
| r"^(\w+)(?:\s+(\d+)(?:\s+(see|for)\s+(.+))?)?$", | |
| ); | |
| /// An analyzer error code is a dotted identifier or the magic string | |
| /// "unspecified". | |
| static final _errorCodeRegExp = RegExp(r"^(?:\w+\.\w+|unspecified)$"); | |
| final String path; | |
| 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. | |
| /// | |
| /// Note: if the same context message appears multiple times at the same | |
| /// location, there will be distinct (non-identical) StaticError instances | |
| /// that compare equal. We use `Map.identity` to ensure that we can associate | |
| /// each with its own context number. | |
| final Map<StaticError, int> _errorNumbers = Map.identity(); | |
| int _currentLine = 0; | |
| // One-based index of the last line that wasn't part of an error expectation. | |
| int _lastRealLine = -1; | |
| _ErrorExpectationParser({required this.path, required String source}) | |
| : _lines = source.split("\n"); | |
| ParsedStaticErrorExpectations _parse() { | |
| // Read all the lines. | |
| while (_canPeek(0)) { | |
| var sourceLine = _peek(0); | |
| if (_caretLocationRegExp.firstMatch(sourceLine) case var match?) { | |
| if (_lastRealLine == -1) { | |
| _fail("An error expectation must follow some code."); | |
| } | |
| var markerMatch = match[1]!; | |
| _parseErrors( | |
| path: path, | |
| line: _lastRealLine, | |
| column: sourceLine.indexOf("^") + 1, | |
| length: markerMatch.length, | |
| ); | |
| _advance(); | |
| continue; | |
| } | |
| if (_explicitLocationRegExp.firstMatch(sourceLine) case var match?) { | |
| var lineCapture = match[1]; | |
| var columnCapture = match[2]!; | |
| var lengthCapture = match[3]; | |
| _parseErrors( | |
| path: path, | |
| line: lineCapture == null ? _lastRealLine : int.parse(lineCapture), | |
| column: int.parse(columnCapture), | |
| length: lengthCapture == null ? 0 : int.parse(lengthCapture), | |
| ); | |
| _advance(); | |
| continue; | |
| } | |
| _lastRealLine = _currentLine + 1; | |
| _advance(); | |
| } | |
| return ParsedStaticErrorExpectations._( | |
| path: path, | |
| errors: _errors, | |
| contextMessages: _contextMessages, | |
| numbers: _errorNumbers, | |
| ); | |
| } | |
| /// Finishes parsing a series of error expectations after parsing a location. | |
| void _parseErrors({ | |
| required String path, | |
| required int line, | |
| required int column, | |
| int length = 0, | |
| }) { | |
| var locationLine = _currentLine; | |
| var parsedError = false; | |
| // Allow errors for multiple front-ends to share the same location marker. | |
| while (_canPeek(1)) { | |
| var nextLine = _peek(1); | |
| if (_explicitLocationRegExp.hasMatch(nextLine)) break; | |
| var match = _errorMessageRegExp.firstMatch(nextLine); | |
| if (match == null) break; | |
| var marker = _parseMarker(match[1]!); | |
| var number = marker.number; | |
| var source = marker.source; | |
| if (source == ErrorSource.context && number == null) { | |
| _fail("Context messages must have an error number."); | |
| } | |
| if (source == ErrorSource.context && marker.seePaths.isNotEmpty) { | |
| _fail("Context messages cannot have a 'see' clause."); | |
| } | |
| if (source != ErrorSource.context && marker.forPath != null) { | |
| _fail("Only context messages can have a 'for' clause."); | |
| } | |
| if (source != ErrorSource.context && | |
| marker.seePaths.isNotEmpty && | |
| number == null) { | |
| _fail("Only numbered errors can have a 'see' clause."); | |
| } | |
| var message = StringBuffer(match[2]!); | |
| _advance(); | |
| var sourceLines = {locationLine, _currentLine}; | |
| if (source == ErrorSource.analyzer && | |
| !_errorCodeRegExp.hasMatch(message.toString())) { | |
| _fail("An analyzer error expectation should be a dotted identifier."); | |
| } | |
| // Hack: If the error is CFE-only and the length is one, treat it as no | |
| // length. The CFE does not output length information, and when the update | |
| // tool writes a CFE-only error, it implicitly uses a length of one. Thus, | |
| // when we parse back in a length one CFE error, we ignore the length so | |
| // that the error round-trips correctly. | |
| // TODO(rnystrom): Stop doing this when the CFE reports error lengths. | |
| var errorLength = length; | |
| if (errorLength == 1 && source == ErrorSource.cfe) { | |
| errorLength = 0; | |
| } | |
| var error = StaticError( | |
| source, | |
| message.toString(), | |
| path: path, | |
| line: line, | |
| column: column, | |
| length: errorLength, | |
| sourceLines: sourceLines, | |
| contextPaths: marker.seePaths.map( | |
| (path) => _resolveMarkerPath(this.path, path), | |
| ), | |
| contextOwnerPath: marker.forPath == null | |
| ? null | |
| : _resolveMarkerPath(this.path, marker.forPath!), | |
| ); | |
| if (number != null) { | |
| // Make sure two errors don't claim the same number. | |
| if (source != ErrorSource.context) { | |
| var existingError = _errors.any( | |
| (error) => _errorNumbers[error] == number, | |
| ); | |
| if (existingError) { | |
| _fail("Already have an error with number $number."); | |
| } | |
| } | |
| _errorNumbers[error] = number; | |
| } | |
| if (source == ErrorSource.context) { | |
| _contextMessages.add(error); | |
| } else { | |
| _errors.add(error); | |
| } | |
| parsedError = true; | |
| } | |
| if (!parsedError) { | |
| _fail("An error expectation must specify at least one error message."); | |
| } | |
| } | |
| /// Parses the content inside the brackets of an error message marker. | |
| /// | |
| /// See [_markerBodyRegExp] for the expected format and examples. | |
| _ErrorMarker _parseMarker(String body) { | |
| var match = _markerBodyRegExp.firstMatch(body.trim()); | |
| if (match == null) _fail("Invalid error marker '[$body]'."); | |
| var sourceName = match[1]!; | |
| var source = | |
| ErrorSource.find(sourceName) ?? | |
| _fail("Unknown front end '[$sourceName]'."); | |
| var clause = match[3]; | |
| var clauseValue = match[4]; | |
| return _ErrorMarker( | |
| source: source, | |
| number: match[2] == null ? null : int.parse(match[2]!), | |
| seePaths: clause == 'see' | |
| ? clauseValue!.split(',').map((path) => path.trim()).toList() | |
| : const [], | |
| forPath: clause == 'for' ? clauseValue!.trim() : null, | |
| ); | |
| } | |
| bool _canPeek(int offset) => _currentLine + offset < _lines.length; | |
| void _advance() { | |
| _currentLine++; | |
| } | |
| String _peek(int offset) { | |
| var line = _lines[_currentLine + offset]; | |
| // Strip off any multitest marker. | |
| var index = line.indexOf("//#"); | |
| if (index != -1) { | |
| line = line.substring(0, index).trimRight(); | |
| } | |
| return line; | |
| } | |
| Never _fail(String message) { | |
| throw FormatException("Test error on line ${_currentLine + 1}: $message"); | |
| } | |
| } |