| Index: pkg/analyzer/test/src/task/strong/strong_test_helper.dart
|
| diff --git a/pkg/analyzer/test/src/task/strong/strong_test_helper.dart b/pkg/analyzer/test/src/task/strong/strong_test_helper.dart
|
| index 83c047773ff09752d838d8458d010ae5df01b01e..885282d55e26d21820db9c06b3adc318f167fddc 100644
|
| --- a/pkg/analyzer/test/src/task/strong/strong_test_helper.dart
|
| +++ b/pkg/analyzer/test/src/task/strong/strong_test_helper.dart
|
| @@ -1,4 +1,5 @@
|
| // Copyright (c) 2015, 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.
|
|
|
| @@ -7,13 +8,13 @@
|
| library analyzer.test.src.task.strong.strong_test_helper;
|
|
|
| import 'package:analyzer/dart/ast/ast.dart';
|
| -import 'package:analyzer/dart/ast/visitor.dart';
|
| import 'package:analyzer/dart/element/element.dart';
|
| import 'package:analyzer/file_system/file_system.dart';
|
| import 'package:analyzer/file_system/memory_file_system.dart';
|
| import 'package:analyzer/src/context/context.dart' show SdkAnalysisContext;
|
| import 'package:analyzer/src/generated/engine.dart';
|
| import 'package:analyzer/src/generated/error.dart';
|
| +import 'package:analyzer/src/generated/scanner.dart';
|
| import 'package:analyzer/src/generated/sdk.dart';
|
| import 'package:analyzer/src/generated/source.dart';
|
| import 'package:analyzer/src/generated/type_system.dart';
|
| @@ -22,7 +23,6 @@ import 'package:logging/logging.dart';
|
| import 'package:source_span/source_span.dart';
|
| import 'package:unittest/unittest.dart';
|
|
|
| -
|
| MemoryResourceProvider files;
|
| bool _checkCalled;
|
|
|
| @@ -92,8 +92,7 @@ void check() {
|
|
|
| // Run the checker on /main.dart.
|
| Source mainSource = uriResolver.resolveAbsolute(new Uri.file('/main.dart'));
|
| - var initialLibrary =
|
| - context.resolveCompilationUnit2(mainSource, mainSource);
|
| + var initialLibrary = context.resolveCompilationUnit2(mainSource, mainSource);
|
|
|
| var collector = new _ErrorCollector();
|
| var checker = new CodeChecker(
|
| @@ -113,16 +112,11 @@ void check() {
|
|
|
| var librarySource = context.getLibrariesContaining(source).single;
|
| var resolved = context.resolveCompilationUnit2(source, librarySource);
|
| - var analyzerErrors = context
|
| - .getErrors(source)
|
| - .errors
|
| - .where((error) =>
|
| - error.errorCode.name.startsWith('STRONG_MODE_INFERRED_TYPE'))
|
| - .toList();
|
| - errors.addAll(analyzerErrors);
|
| + errors.addAll(context.getErrors(source).errors.where((error) =>
|
| + error.errorCode.name.startsWith('STRONG_MODE_INFERRED_TYPE')));
|
| checker.visitCompilationUnit(resolved);
|
|
|
| - new _ExpectedErrorVisitor(errors).validate(resolved);
|
| + _expectErrors(resolved, errors);
|
| }
|
| }
|
| }
|
| @@ -224,9 +218,10 @@ final Map<String, String> _mockSdkSources = {
|
| };
|
|
|
| SourceSpanWithContext createSpanHelper(
|
| - LineInfo lineInfo, int start, int end, Source source, String content) {
|
| + LineInfo lineInfo, int start, Source source, String content,
|
| + {int end}) {
|
| var startLoc = locationForOffset(lineInfo, source.uri, start);
|
| - var endLoc = locationForOffset(lineInfo, source.uri, end);
|
| + var endLoc = locationForOffset(lineInfo, source.uri, end ?? start);
|
|
|
| var lineStart = startLoc.offset - startLoc.column;
|
| // Find the end of the line. This is not exposed directly on LineInfo, but
|
| @@ -238,6 +233,11 @@ SourceSpanWithContext createSpanHelper(
|
| while (lineEnd < content.length &&
|
| lineInfo.getLocation(++lineEnd).lineNumber == lineNum);
|
|
|
| + if (end == null) {
|
| + end = lineEnd;
|
| + endLoc = locationForOffset(lineInfo, source.uri, lineEnd);
|
| + }
|
| +
|
| var text = content.substring(start, end);
|
| var lineText = content.substring(lineStart, lineEnd);
|
| return new SourceSpanWithContext(startLoc, endLoc, text, lineText);
|
| @@ -286,6 +286,7 @@ class MockDartSdk implements DartSdk {
|
| final String sdkVersion = '0';
|
| final AnalysisContext context = new SdkAnalysisContext();
|
| DartUriResolver _resolver;
|
| +
|
| MockDartSdk(Map<String, String> sources, {this.reportMissing}) {
|
| sources.forEach((uriString, contents) {
|
| var uri = Uri.parse(uriString);
|
| @@ -297,11 +298,13 @@ class MockDartSdk implements DartSdk {
|
| _resolver = new DartUriResolver(this);
|
| context.sourceFactory = new SourceFactory([_resolver]);
|
| }
|
| +
|
| DartUriResolver get resolver => _resolver;
|
|
|
| List<SdkLibrary> get sdkLibraries => _libs.values.toList();
|
|
|
| List<String> get uris => _sources.keys.map((uri) => '$uri').toList();
|
| +
|
| Source fromEncoding(UriKind kind, Uri uri) {
|
| if (kind != UriKind.DART_URI) {
|
| throw new UnsupportedError('expected dart: uri kind, got $kind.');
|
| @@ -331,6 +334,7 @@ class MockDartSdk implements DartSdk {
|
|
|
| class TestUriResolver extends ResourceUriResolver {
|
| final MemoryResourceProvider provider;
|
| +
|
| TestUriResolver(provider)
|
| : provider = provider,
|
| super(provider);
|
| @@ -348,6 +352,7 @@ class TestUriResolver extends ResourceUriResolver {
|
| class _ErrorCollector implements AnalysisErrorListener {
|
| List<AnalysisError> errors;
|
| final bool hints;
|
| +
|
| _ErrorCollector({this.hints: true});
|
|
|
| void onError(AnalysisError error) {
|
| @@ -361,26 +366,29 @@ class _ErrorCollector implements AnalysisErrorListener {
|
|
|
| /// Describes an expected message that should be produced by the checker.
|
| class _ErrorExpectation {
|
| + final int offset;
|
| final Level level;
|
| final String typeName;
|
| - _ErrorExpectation(this.level, this.typeName);
|
|
|
| - String toString() => '$level $typeName';
|
| + _ErrorExpectation(this.offset, this.level, this.typeName);
|
| +
|
| + String toString() =>
|
| + '@$offset ${level.toString().toLowerCase()}: [$typeName]';
|
|
|
| - static _ErrorExpectation parse(String descriptor) {
|
| + static _ErrorExpectation parse(int offset, String descriptor) {
|
| descriptor = descriptor.trim();
|
| var tokens = descriptor.split(' ');
|
| - if (tokens.length == 1) return _parse(tokens[0]);
|
| + if (tokens.length == 1) return _parse(offset, tokens[0]);
|
| expect(tokens.length, 4, reason: 'invalid error descriptor');
|
| expect(tokens[1], "should", reason: 'invalid error descriptor');
|
| expect(tokens[2], "be", reason: 'invalid error descriptor');
|
| if (tokens[0] == "pass") return null;
|
| // TODO(leafp) For now, we just use whatever the current expectation is,
|
| // eventually we could do more automated reporting here.
|
| - return _parse(tokens[0]);
|
| + return _parse(offset, tokens[0]);
|
| }
|
|
|
| - static _ErrorExpectation _parse(String descriptor) {
|
| + static _ErrorExpectation _parse(offset, String descriptor) {
|
| var tokens = descriptor.split(':');
|
| expect(tokens.length, 2, reason: 'invalid error descriptor');
|
| var name = tokens[0].toUpperCase();
|
| @@ -392,122 +400,130 @@ class _ErrorExpectation {
|
| reason: 'invalid level in error descriptor: `${tokens[0]}`');
|
| expect(typeName, isNotNull,
|
| reason: 'invalid type in error descriptor: ${tokens[1]}');
|
| - return new _ErrorExpectation(level, typeName);
|
| + return new _ErrorExpectation(offset, level, typeName);
|
| }
|
| -}
|
| -
|
| -class _ExpectedErrorVisitor extends UnifyingAstVisitor {
|
| - final Set<AnalysisError> _actualErrors;
|
| - CompilationUnit _unit;
|
| - String _unitSourceCode;
|
| -
|
| - _ExpectedErrorVisitor(List<AnalysisError> actualErrors)
|
| - : _actualErrors = new Set.from(actualErrors);
|
| -
|
| - validate(CompilationUnit unit) {
|
| - _unit = unit;
|
| - // This reads the file. Only safe because tests use MemoryFileSystem.
|
| - _unitSourceCode = unit.element.source.contents.data;
|
| -
|
| - // Visit the compilation unit.
|
| - unit.accept(this);
|
|
|
| - if (_actualErrors.isNotEmpty) {
|
| - var actualMsgs = _actualErrors.map(_formatActualError).join('\n');
|
| - fail('Unexpected errors reported by checker:\n\n$actualMsgs');
|
| + AnalysisError _removeMatchingActual(List<AnalysisError> actualErrors) {
|
| + for (var actual in actualErrors) {
|
| + if (actual.offset == offset) {
|
| + actualErrors.remove(actual);
|
| + return actual;
|
| + }
|
| }
|
| + return null;
|
| }
|
| +}
|
|
|
| - visitNode(AstNode node) {
|
| - var token = node.beginToken;
|
| - var comment = token.precedingComments;
|
| - // Use error marker found in an immediately preceding comment,
|
| - // and attach it to the outermost expression that starts at that token.
|
| - if (comment != null) {
|
| - while (comment.next != null) {
|
| - comment = comment.next;
|
| - }
|
| - if (comment.end == token.offset && node.parent.beginToken != token) {
|
| - var commentText = '$comment';
|
| - var start = commentText.lastIndexOf('/*');
|
| - var end = commentText.lastIndexOf('*/');
|
| - if (start != -1 &&
|
| - end != -1 &&
|
| - !commentText.startsWith('/*<', start) &&
|
| - !commentText.startsWith('/*=', start)) {
|
| - expect(start, lessThan(end));
|
| - var errors = commentText.substring(start + 2, end).split(',');
|
| - var expectations =
|
| - errors.map(_ErrorExpectation.parse).where((x) => x != null);
|
| -
|
| - for (var e in expectations) {
|
| - _expectError(node, e);
|
| - }
|
| - }
|
| +void _expectErrors(CompilationUnit unit, List<AnalysisError> actualErrors) {
|
| + var expectedErrors = _findExpectedErrors(unit.beginToken);
|
| +
|
| + // Categorize the differences, if any.
|
| + var unreported = <_ErrorExpectation>[];
|
| + var different = <_ErrorExpectation, AnalysisError>{};
|
| +
|
| + for (var expected in expectedErrors) {
|
| + AnalysisError actual = expected._removeMatchingActual(actualErrors);
|
| + if (actual != null) {
|
| + if (_actualErrorLevel(actual) != expected.level ||
|
| + errorCodeName(actual.errorCode) != expected.typeName) {
|
| + different[expected] = actual;
|
| }
|
| + } else {
|
| + unreported.add(expected);
|
| }
|
| - return super.visitNode(node);
|
| }
|
|
|
| - Level _actualErrorLevel(AnalysisError actual) {
|
| - return const <ErrorSeverity, Level>{
|
| - ErrorSeverity.ERROR: Level.SEVERE,
|
| - ErrorSeverity.WARNING: Level.WARNING,
|
| - ErrorSeverity.INFO: Level.INFO
|
| - }[actual.errorCode.errorSeverity];
|
| - }
|
| + // Whatever is left was an unexpected error.
|
| + List<AnalysisError> unexpected = actualErrors;
|
|
|
| - SourceSpan _createSpan(int offset, int len) {
|
| - return createSpanHelper(_unit.lineInfo, offset, offset + len,
|
| - _unit.element.source, _unitSourceCode);
|
| + if (unreported.isNotEmpty || unexpected.isNotEmpty || different.isNotEmpty) {
|
| + _reportFailure(unit, unreported, unexpected, different);
|
| }
|
| +}
|
|
|
| - void _expectError(AstNode node, _ErrorExpectation expected) {
|
| - // See if we can find the expected error in our actual errors
|
| - for (var actual in _actualErrors) {
|
| - if (actual.offset == node.offset && actual.length == node.length) {
|
| - var actualMsg = _formatActualError(actual);
|
| - expect(_actualErrorLevel(actual), expected.level,
|
| - reason: 'expected different error code at:\n\n$actualMsg');
|
| - expect(errorCodeName(actual.errorCode), expected.typeName,
|
| - reason: 'expected different error type at:\n\n$actualMsg');
|
| -
|
| - // We found it. Stop the search.
|
| - _actualErrors.remove(actual);
|
| - return;
|
| - }
|
| - }
|
| +void _reportFailure(
|
| + CompilationUnit unit,
|
| + List<_ErrorExpectation> unreported,
|
| + List<AnalysisError> unexpected,
|
| + Map<_ErrorExpectation, AnalysisError> different) {
|
| +
|
| + // Get the source code. This reads the data again, but it's safe because
|
| + // all tests use memory file system.
|
| + var sourceCode = unit.element.source.contents.data;
|
| +
|
| + String formatActualError(AnalysisError error) {
|
| + int offset = error.offset;
|
| + int length = error.length;
|
| + var span = createSpanHelper(
|
| + unit.lineInfo, offset, unit.element.source, sourceCode,
|
| + end: offset + length);
|
| + var levelName = _actualErrorLevel(error).name.toLowerCase();
|
| + return '@$offset $levelName: [${errorCodeName(error.errorCode)}]\n' +
|
| + span.message(error.message);
|
| + }
|
|
|
| - var span = _createSpan(node.offset, node.length);
|
| - var levelName = expected.level.name.toLowerCase();
|
| - var msg = span.message(expected.typeName, color: _colorOf(levelName));
|
| - fail('expected error was not reported at:\n\n$levelName: $msg');
|
| + String formatExpectedError(_ErrorExpectation error) {
|
| + int offset = error.offset;
|
| + var span = createSpanHelper(
|
| + unit.lineInfo, offset, unit.element.source, sourceCode);
|
| + var levelName = error.level.toString().toLowerCase();
|
| + return '@$offset $levelName: [${error.typeName}]\n' + span.message('');
|
| }
|
|
|
| - String _formatActualError(AnalysisError actual) {
|
| - var span = _createSpan(actual.offset, actual.length);
|
| - var levelName = _actualErrorLevel(actual).name.toLowerCase();
|
| - var msg = span.message(actual.message, color: _colorOf(levelName));
|
| - return '$levelName: [${errorCodeName(actual.errorCode)}] $msg';
|
| + var message = new StringBuffer();
|
| + if (unreported.isNotEmpty) {
|
| + message.writeln('Expected errors that were not reported:');
|
| + unreported.map(formatExpectedError).forEach(message.writeln);
|
| + message.writeln();
|
| }
|
| + if (unexpected.isNotEmpty) {
|
| + message.writeln('Errors that were not expected:');
|
| + unexpected.map(formatActualError).forEach(message.writeln);
|
| + message.writeln();
|
| + }
|
| + if (different.isNotEmpty) {
|
| + message.writeln('Errors that were reported, but different than expected:');
|
| + different.forEach((expected, actual) {
|
| + message.writeln('Expected: ' + formatExpectedError(expected));
|
| + message.writeln('Actual: ' + formatActualError(actual));
|
| + });
|
| + message.writeln();
|
| + }
|
| + fail('Checker errors do not match expected errors:\n\n$message');
|
| +}
|
|
|
| - /// Returns an ANSII color escape sequence corresponding to [levelName].
|
| - ///
|
| - /// Colors are defined for: severe, error, warning, or info.
|
| - /// Returns null if the level name is not recognized.
|
| - String _colorOf(String levelName) {
|
| - const String CYAN_COLOR = '\u001b[36m';
|
| - const String MAGENTA_COLOR = '\u001b[35m';
|
| - const String RED_COLOR = '\u001b[31m';
|
| -
|
| - levelName = levelName.toLowerCase();
|
| - if (levelName == 'shout' || levelName == 'severe' || levelName == 'error') {
|
| - return RED_COLOR;
|
| +List<_ErrorExpectation> _findExpectedErrors(Token beginToken) {
|
| + var expectedErrors = <_ErrorExpectation>[];
|
| +
|
| + // Collect expectations like "severe:STATIC_TYPE_ERROR" from comment tokens.
|
| + for (Token t = beginToken; t.type != TokenType.EOF; t = t.next) {
|
| + for (CommentToken c = t.precedingComments; c != null; c = c.next) {
|
| + if (c.type == TokenType.MULTI_LINE_COMMENT) {
|
| + String value = c.lexeme.substring(2, c.lexeme.length - 2);
|
| + if (value.contains(':')) {
|
| + var offset = c.end;
|
| + if (c.next?.type == TokenType.GENERIC_METHOD_TYPE_LIST) {
|
| + offset += 2;
|
| + }
|
| + for (var expectCode in value.split(',')) {
|
| + var expected = _ErrorExpectation.parse(offset, expectCode);
|
| + if (expected != null) {
|
| + expectedErrors.add(expected);
|
| + }
|
| + }
|
| + }
|
| + }
|
| }
|
| - if (levelName == 'warning') return MAGENTA_COLOR;
|
| - if (levelName == 'info') return CYAN_COLOR;
|
| - return null;
|
| }
|
| + return expectedErrors;
|
| +}
|
| +
|
| +Level _actualErrorLevel(AnalysisError actual) {
|
| + return const <ErrorSeverity, Level>{
|
| + ErrorSeverity.ERROR: Level.SEVERE,
|
| + ErrorSeverity.WARNING: Level.WARNING,
|
| + ErrorSeverity.INFO: Level.INFO
|
| + }[actual.errorCode.errorSeverity];
|
| }
|
|
|
| class _MockSdkSource implements Source {
|
|
|