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 { |