blob: 19ed5b3ef3cade4fb7611e78fd655b935e0e6523 [file] [log] [blame]
// 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.
// TODO(jmesserly): this file needs to be refactored, it's a port from
// package:dev_compiler's tests
library analyzer.test.src.task.strong.strong_test_helper;
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.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/dart/ast/token.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/error.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:logging/logging.dart';
import 'package:source_span/source_span.dart';
import 'package:unittest/unittest.dart';
import '../../context/mock_sdk.dart';
MemoryResourceProvider files;
bool _checkCalled;
/// Adds a file to check. The file should contain:
///
/// * all expected failures are listed in the source code using comments
/// immediately in front of the AST node that should contain the error.
///
/// * errors are formatted as a token `level:Type`, where `level` is the
/// logging level were the error would be reported at, and `Type` is the
/// concrete subclass of [StaticInfo] that denotes the error.
///
/// For example to check that an assignment produces a type error, you can
/// create a file like:
///
/// addFile('''
/// String x = /*severe:STATIC_TYPE_ERROR*/3;
/// ''');
/// check();
///
/// For a single file, you may also use [checkFile].
void addFile(String content, {String name: '/main.dart'}) {
name = name.replaceFirst('^package:', '/packages/');
files.newFile(name, content);
}
/// Run the checker on a program, staring from '/main.dart', and verifies that
/// errors/warnings/hints match the expected value.
///
/// See [addFile] for more information about how to encode expectations in
/// the file text.
void check() {
_checkCalled = true;
expect(files.getFile('/main.dart').exists, true,
reason: '`/main.dart` is missing');
var uriResolver = new _TestUriResolver(files);
// Enable task model strong mode
var context = AnalysisEngine.instance.createAnalysisContext();
context.analysisOptions.strongMode = true;
(context.analysisOptions as AnalysisOptionsImpl).strongModeHints = true;
context.sourceFactory =
new SourceFactory([new DartUriResolver(new MockSdk()), uriResolver]);
// Run the checker on /main.dart.
Source mainSource = uriResolver.resolveAbsolute(new Uri.file('/main.dart'));
var initialLibrary = context.resolveCompilationUnit2(mainSource, mainSource);
var collector = new _ErrorCollector();
// Extract expectations from the comments in the test files, and
// check that all errors we emit are included in the expected map.
var allLibraries = _reachableLibraries(initialLibrary.element.library);
for (var lib in allLibraries) {
for (var unit in lib.units) {
var errors = <AnalysisError>[];
collector.errors = errors;
var source = unit.source;
if (source.uri.scheme == 'dart') continue;
var librarySource = context.getLibrariesContaining(source).single;
var resolved = context.resolveCompilationUnit2(source, librarySource);
errors.addAll(context.getErrors(source).errors.where((e) =>
e.errorCode != HintCode.UNUSED_LOCAL_VARIABLE &&
// TODO(jmesserly): these are usually intentional dynamic calls.
e.errorCode.name != 'UNDEFINED_METHOD'));
_expectErrors(resolved, errors);
}
}
}
/// Adds a file using [addFile] and calls [check].
void checkFile(String content) {
addFile(content);
check();
}
SourceSpanWithContext _createSpanHelper(
LineInfo lineInfo, int start, Source source, String content,
{int end}) {
var startLoc = _locationForOffset(lineInfo, source.uri, start);
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
// we can find it pretty easily.
// TODO(jmesserly): for now we do the simple linear scan. Ideally we can get
// some help from the LineInfo API.
int lineEnd = endLoc.offset;
int lineNum = lineInfo.getLocation(lineEnd).lineNumber;
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);
}
String _errorCodeName(ErrorCode errorCode) {
var name = errorCode.name;
final prefix = 'STRONG_MODE_';
if (name.startsWith(prefix)) {
return name.substring(prefix.length);
} else {
return name;
}
}
void initStrongModeTests() {
setUp(() {
AnalysisEngine.instance.processRequiredPlugins();
files = new MemoryResourceProvider();
_checkCalled = false;
});
tearDown(() {
// This is a sanity check, in case only addFile is called.
expect(_checkCalled, true, reason: 'must call check() method in test case');
files = null;
});
}
SourceLocation _locationForOffset(LineInfo lineInfo, Uri uri, int offset) {
var loc = lineInfo.getLocation(offset);
return new SourceLocation(offset,
sourceUrl: uri, line: loc.lineNumber - 1, column: loc.columnNumber - 1);
}
/// Returns all libraries transitively imported or exported from [start].
List<LibraryElement> _reachableLibraries(LibraryElement start) {
var results = <LibraryElement>[];
var seen = new Set();
void find(LibraryElement lib) {
if (seen.contains(lib)) return;
seen.add(lib);
results.add(lib);
lib.importedLibraries.forEach(find);
lib.exportedLibraries.forEach(find);
}
find(start);
return results;
}
Level _actualErrorLevel(AnalysisError actual) {
return const <ErrorSeverity, Level>{
ErrorSeverity.ERROR: Level.SEVERE,
ErrorSeverity.WARNING: Level.WARNING,
ErrorSeverity.INFO: Level.INFO
}[actual.errorCode.errorSeverity];
}
void _expectErrors(CompilationUnit unit, List<AnalysisError> actualErrors) {
var expectedErrors = _findExpectedErrors(unit.beginToken);
// Sort both lists: by offset, then level, then name.
actualErrors.sort((x, y) {
int delta = x.offset.compareTo(y.offset);
if (delta != 0) return delta;
delta = x.errorCode.errorSeverity.compareTo(y.errorCode.errorSeverity);
if (delta != 0) return delta;
return _errorCodeName(x.errorCode).compareTo(_errorCodeName(y.errorCode));
});
expectedErrors.sort((x, y) {
int delta = x.offset.compareTo(y.offset);
if (delta != 0) return delta;
delta = x.level.compareTo(y.level);
if (delta != 0) return delta;
return x.typeName.compareTo(y.typeName);
});
// 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);
}
}
// Whatever is left was an unexpected error.
List<AnalysisError> unexpected = actualErrors;
if (unreported.isNotEmpty || unexpected.isNotEmpty || different.isNotEmpty) {
_reportFailure(unit, unreported, unexpected, different);
}
}
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(':')) {
int offset = t.offset;
Token previous = t.previous;
while (previous != null && previous.offset > c.offset) {
offset = previous.offset;
previous = previous.previous;
}
for (var expectCode in value.split(',')) {
var expected = _ErrorExpectation.parse(offset, expectCode);
if (expected != null) {
expectedErrors.add(expected);
}
}
}
}
}
}
return expectedErrors;
}
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);
}
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('');
}
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');
}
class _ErrorCollector implements AnalysisErrorListener {
List<AnalysisError> errors;
final bool hints;
_ErrorCollector({this.hints: true});
void onError(AnalysisError error) {
// Unless DDC hints are requested, filter them out.
var HINT = ErrorSeverity.INFO.ordinal;
if (hints || error.errorCode.errorSeverity.ordinal > HINT) {
errors.add(error);
}
}
}
/// Describes an expected message that should be produced by the checker.
class _ErrorExpectation {
final int offset;
final Level level;
final String typeName;
_ErrorExpectation(this.offset, this.level, this.typeName);
String toString() =>
'@$offset ${level.toString().toLowerCase()}: [$typeName]';
AnalysisError _removeMatchingActual(List<AnalysisError> actualErrors) {
for (var actual in actualErrors) {
if (actual.offset == offset) {
actualErrors.remove(actual);
return actual;
}
}
return null;
}
static _ErrorExpectation parse(int offset, String descriptor) {
descriptor = descriptor.trim();
var tokens = descriptor.split(' ');
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(offset, tokens[0]);
}
static _ErrorExpectation _parse(offset, String descriptor) {
var tokens = descriptor.split(':');
expect(tokens.length, 2, reason: 'invalid error descriptor');
var name = tokens[0].toUpperCase();
var typeName = tokens[1];
var level =
Level.LEVELS.firstWhere((l) => l.name == name, orElse: () => null);
expect(level, isNotNull,
reason: 'invalid level in error descriptor: `${tokens[0]}`');
expect(typeName, isNotNull,
reason: 'invalid type in error descriptor: ${tokens[1]}');
return new _ErrorExpectation(offset, level, typeName);
}
}
class _TestUriResolver extends ResourceUriResolver {
final MemoryResourceProvider provider;
_TestUriResolver(provider)
: provider = provider,
super(provider);
@override
Source resolveAbsolute(Uri uri, [Uri actualUri]) {
if (uri.scheme == 'package') {
return (provider.getResource('/packages/' + uri.path) as File)
.createSource(uri);
}
return super.resolveAbsolute(uri, actualUri);
}
}