blob: dddd43831e645dd3a2e493bd01c5b4c20fd4d8bc [file] [log] [blame]
// Copyright (c) 2016, 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:async';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/dart/element/type_provider.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/context/packages.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/dart/analysis/file_state.dart';
import 'package:analyzer/src/dart/analysis/performance_logger.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/file_system/file_system.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/java_engine.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/generated/source_io.dart';
import 'package:analyzer/src/generated/testing/element_factory.dart';
import 'package:analyzer/src/source/package_map_resolver.dart';
import 'package:analyzer/src/test_utilities/mock_sdk.dart';
import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
import 'package:test/test.dart';
import '../src/dart/resolution/context_collection_resolution.dart';
import 'test_analysis_context.dart';
import 'test_support.dart';
const String _defaultSourceName = "/test.dart";
/// An AST visitor used to verify that all of the nodes in an AST structure that
/// should have been resolved were resolved.
class ResolutionVerifier extends RecursiveAstVisitor<void> {
/// A set containing nodes that are known to not be resolvable and should
/// therefore not cause the test to fail.
final Set<AstNode> _knownExceptions;
/// A list containing all of the AST nodes that were not resolved.
final List<AstNode> _unresolvedNodes = <AstNode>[];
/// A list containing all of the AST nodes that were resolved to an element of
/// the wrong type.
final List<AstNode> _wrongTypedNodes = <AstNode>[];
/// Initialize a newly created verifier to verify that all of the identifiers
/// in the visited AST structures that are expected to have been resolved have
/// an element associated with them. Nodes in the set of [_knownExceptions]
/// are not expected to have been resolved, even if they normally would have
/// been expected to have been resolved.
ResolutionVerifier([this._knownExceptions]);
/// Assert that all of the visited identifiers were resolved.
void assertResolved() {
if (_unresolvedNodes.isNotEmpty || _wrongTypedNodes.isNotEmpty) {
StringBuffer buffer = StringBuffer();
if (_unresolvedNodes.isNotEmpty) {
buffer.write("Failed to resolve ");
buffer.write(_unresolvedNodes.length);
buffer.writeln(" nodes:");
_printNodes(buffer, _unresolvedNodes);
}
if (_wrongTypedNodes.isNotEmpty) {
buffer.write("Resolved ");
buffer.write(_wrongTypedNodes.length);
buffer.writeln(" to the wrong type of element:");
_printNodes(buffer, _wrongTypedNodes);
}
fail(buffer.toString());
}
}
@override
void visitAnnotation(Annotation node) {
node.visitChildren(this);
ElementAnnotation elementAnnotation = node.elementAnnotation;
if (elementAnnotation == null) {
if (_knownExceptions == null || !_knownExceptions.contains(node)) {
_unresolvedNodes.add(node);
}
} else if (elementAnnotation is! ElementAnnotation) {
_wrongTypedNodes.add(node);
}
}
@override
void visitBinaryExpression(BinaryExpression node) {
node.visitChildren(this);
if (!node.operator.isUserDefinableOperator) {
return;
}
DartType operandType = node.leftOperand.staticType;
if (operandType == null || operandType.isDynamic) {
return;
}
_checkResolved(node, node.staticElement, (node) => node is MethodElement);
}
@override
void visitCommentReference(CommentReference node) {}
@override
void visitCompilationUnit(CompilationUnit node) {
node.visitChildren(this);
_checkResolved(
node, node.declaredElement, (node) => node is CompilationUnitElement);
}
@override
void visitExportDirective(ExportDirective node) {
_checkResolved(node, node.element, (node) => node is ExportElement);
}
@override
void visitFunctionDeclaration(FunctionDeclaration node) {
node.visitChildren(this);
if (node.declaredElement is LibraryElement) {
_wrongTypedNodes.add(node);
}
}
@override
void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
node.visitChildren(this);
// TODO(brianwilkerson) If we start resolving function expressions, then
// conditionally check to see whether the node was resolved correctly.
//checkResolved(node, node.getElement(), FunctionElement.class);
}
@override
void visitImportDirective(ImportDirective node) {
// Not sure how to test the combinators given that it isn't an error if the
// names are not defined.
_checkResolved(node, node.element, (node) => node is ImportElement);
SimpleIdentifier prefix = node.prefix;
if (prefix == null) {
return;
}
_checkResolved(
prefix, prefix.staticElement, (node) => node is PrefixElement);
}
@override
void visitIndexExpression(IndexExpression node) {
node.visitChildren(this);
DartType targetType = node.realTarget.staticType;
if (targetType == null || targetType.isDynamic) {
return;
}
_checkResolved(node, node.staticElement, (node) => node is MethodElement);
}
@override
void visitLibraryDirective(LibraryDirective node) {
_checkResolved(node, node.element, (node) => node is LibraryElement);
}
@override
void visitNamedExpression(NamedExpression node) {
node.expression.accept(this);
}
@override
void visitPartDirective(PartDirective node) {
_checkResolved(
node, node.element, (node) => node is CompilationUnitElement);
}
@override
void visitPartOfDirective(PartOfDirective node) {
_checkResolved(node, node.element, (node) => node is LibraryElement);
}
@override
void visitPostfixExpression(PostfixExpression node) {
node.visitChildren(this);
if (!node.operator.isUserDefinableOperator) {
return;
}
DartType operandType = node.operand.staticType;
if (operandType == null || operandType.isDynamic) {
return;
}
_checkResolved(node, node.staticElement, (node) => node is MethodElement);
}
@override
void visitPrefixedIdentifier(PrefixedIdentifier node) {
SimpleIdentifier prefix = node.prefix;
prefix.accept(this);
DartType prefixType = prefix.staticType;
if (prefixType == null || prefixType.isDynamic) {
return;
}
_checkResolved(node, node.staticElement, null);
}
@override
void visitPrefixExpression(PrefixExpression node) {
node.visitChildren(this);
if (!node.operator.isUserDefinableOperator) {
return;
}
DartType operandType = node.operand.staticType;
if (operandType == null || operandType.isDynamic) {
return;
}
_checkResolved(node, node.staticElement, (node) => node is MethodElement);
}
@override
void visitPropertyAccess(PropertyAccess node) {
Expression target = node.realTarget;
target.accept(this);
DartType targetType = target.staticType;
if (targetType == null || targetType.isDynamic) {
return;
}
node.propertyName.accept(this);
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
if (node.name == "void") {
return;
}
if (node.staticType != null &&
node.staticType.isDynamic &&
node.staticElement == null) {
return;
}
AstNode parent = node.parent;
if (parent is MethodInvocation) {
MethodInvocation invocation = parent;
if (identical(invocation.methodName, node)) {
Expression target = invocation.realTarget;
DartType targetType = target == null ? null : target.staticType;
if (targetType == null || targetType.isDynamic) {
return;
}
}
}
_checkResolved(node, node.staticElement, null);
}
void _checkResolved(
AstNode node, Element element, Predicate<Element> predicate) {
if (element == null) {
if (_knownExceptions == null || !_knownExceptions.contains(node)) {
_unresolvedNodes.add(node);
}
} else if (predicate != null) {
if (!predicate(element)) {
_wrongTypedNodes.add(node);
}
}
}
String _getFileName(AstNode node) {
// TODO (jwren) there are two copies of this method, one here and one in
// StaticTypeVerifier, they should be resolved into a single method
if (node != null) {
AstNode root = node.root;
if (root is CompilationUnit) {
CompilationUnit rootCU = root;
if (rootCU.declaredElement != null) {
return rootCU.declaredElement.source.fullName;
} else {
return "<unknown file- CompilationUnit.getElement() returned null>";
}
} else {
return "<unknown file- CompilationUnit.getRoot() is not a CompilationUnit>";
}
}
return "<unknown file- ASTNode is null>";
}
void _printNodes(StringBuffer buffer, List<AstNode> nodes) {
for (AstNode identifier in nodes) {
buffer.write(" ");
buffer.write(identifier.toString());
buffer.write(" (");
buffer.write(_getFileName(identifier));
buffer.write(" : ");
buffer.write(identifier.offset);
buffer.writeln(")");
}
}
}
class ResolverTestCase with ResourceProviderMixin {
/// Specifies if [assertErrors] should check for [HintCode.UNUSED_ELEMENT] and
/// [HintCode.UNUSED_FIELD].
bool enableUnusedElement = false;
/// Specifies if [assertErrors] should check for
/// [HintCode.UNUSED_LOCAL_VARIABLE].
bool enableUnusedLocalVariable = false;
final Map<Source, TestAnalysisResult> analysisResults = {};
final StringBuffer _logBuffer = StringBuffer();
FileContentOverlay fileContentOverlay = FileContentOverlay();
AnalysisDriver driver;
AnalysisOptions get analysisOptions => driver?.analysisOptions;
/// The default [AnalysisOptions] that should be used by [reset].
AnalysisOptions get defaultAnalysisOptions => AnalysisOptionsImpl();
/// Return the list of experiments that are to be enabled for tests in this
/// class.
List<String> get enabledExperiments => null;
/// Return a type provider that can be used to test the results of resolution.
///
/// Throws an [AnalysisException] if `dart:core` cannot be resolved.
TypeProvider get typeProvider {
if (analysisResults.isEmpty) {
fail('typeProvider called before computing an analysis result.');
}
return analysisResults.values.first.typeProvider;
}
/// Return a type system that can be used to test the results of resolution.
TypeSystemImpl get typeSystem {
if (analysisResults.isEmpty) {
fail('typeSystem called before computing an analysis result.');
}
return analysisResults.values.first.typeSystem;
}
/// Add a source file with the given [filePath] in the root of the file
/// system. The file path should be absolute. The file will have the given
/// [contents] set in the content provider. Return the source representing the
/// added file.
Source addNamedSource(String filePath, String contents) {
filePath = convertPath(filePath);
File file = newFile(filePath, content: contents);
Source source = file.createSource();
driver.addFile(filePath);
return source;
}
/// Add a source file named 'test.dart' in the root of the file system. The
/// file will have the given [contents] set in the content provider. Return
/// the source representing the added file.
Source addSource(String contents) =>
addNamedSource(_defaultSourceName, contents);
/// Assert that the number of errors reported against the given
/// [source] matches the number of errors that are given and that they have
/// the expected error codes. The order in which the errors were gathered is
/// ignored.
void assertErrors(Source source,
[List<ErrorCode> expectedErrorCodes = const <ErrorCode>[]]) {
TestAnalysisResult result = analysisResults[source];
expect(result, isNotNull);
GatheringErrorListener errorListener = GatheringErrorListener();
for (AnalysisError error in result.errors) {
expect(error.source, source);
ErrorCode errorCode = error.errorCode;
if (!enableUnusedElement &&
(errorCode == HintCode.UNUSED_ELEMENT ||
errorCode == HintCode.UNUSED_FIELD)) {
continue;
}
if (!enableUnusedLocalVariable &&
(errorCode == HintCode.UNUSED_CATCH_CLAUSE ||
errorCode == HintCode.UNUSED_CATCH_STACK ||
errorCode == HintCode.UNUSED_LOCAL_VARIABLE)) {
continue;
}
errorListener.onError(error);
}
errorListener.assertErrorsWithCodes(expectedErrorCodes);
}
/// Asserts that [code] verifies, but has errors with the given error codes.
///
/// Like [assertErrors], but takes a string of source code.
// TODO(rnystrom): Use this in more tests that have the same structure.
Future<void> assertErrorsInCode(String code, List<ErrorCode> errors,
{bool verify = true, String sourceName = _defaultSourceName}) async {
Source source = addNamedSource(sourceName, code);
await computeAnalysisResult(source);
assertErrors(source, errors);
if (verify) {
this.verify([source]);
}
}
/// Asserts that [code] has errors with the given error codes.
///
/// Like [assertErrors], but takes a string of source code.
Future<void> assertErrorsInUnverifiedCode(
String code, List<ErrorCode> errors) async {
Source source = addSource(code);
await computeAnalysisResult(source);
assertErrors(source, errors);
}
/// Assert that no errors have been reported against the given source.
///
/// @param source the source against which no errors should have been reported
/// @throws AnalysisException if the reported errors could not be computed
/// @throws AssertionFailedError if any errors have been reported
void assertNoErrors(Source source) {
assertErrors(source);
}
/// Asserts that [code] has no errors or warnings.
// TODO(rnystrom): Use this in more tests that have the same structure.
Future<void> assertNoErrorsInCode(String code) async {
Source source = addSource(code);
await computeAnalysisResult(source);
assertNoErrors(source);
verify([source]);
}
Future<TestAnalysisResult> computeAnalysisResult(Source source) async {
TestAnalysisResult analysisResult;
ResolvedUnitResult result = await driver.getResult(source.fullName);
analysisResult = TestAnalysisResult(source, result.unit, result.errors);
analysisResults[source] = analysisResult;
return analysisResult;
}
/// Compute the analysis result to the given [code] in '/test.dart'.
Future<TestAnalysisResult> computeTestAnalysisResult(String code) async {
Source source = addSource(code);
return await computeAnalysisResult(source);
}
/// Create a library element that represents a library named `"test"`
/// containing a single empty compilation unit.
///
/// @return the library element that was created
LibraryElementImpl createDefaultTestLibrary() =>
createTestLibrary(TestAnalysisContext(), "test");
/// Return a source object representing a file with the given [fileName].
Source createNamedSource(String fileName) {
return getFile(fileName).createSource();
}
/// Create a library element that represents a library with the given name
/// containing a single empty compilation unit.
///
/// @param libraryName the name of the library to be created
/// @return the library element that was created
LibraryElementImpl createTestLibrary(
AnalysisContext context, String libraryName,
[List<String> typeNames]) {
String fileName = convertPath("/test/$libraryName.dart");
Source definingCompilationUnitSource = createNamedSource(fileName);
List<CompilationUnitElement> sourcedCompilationUnits;
if (typeNames == null) {
sourcedCompilationUnits = const <CompilationUnitElement>[];
} else {
int count = typeNames.length;
sourcedCompilationUnits = List<CompilationUnitElement>(count);
for (int i = 0; i < count; i++) {
String typeName = typeNames[i];
ClassElementImpl type = ClassElementImpl(typeName, -1);
String fileName = "$typeName.dart";
CompilationUnitElementImpl compilationUnit =
CompilationUnitElementImpl();
compilationUnit.source = createNamedSource(fileName);
compilationUnit.librarySource = definingCompilationUnitSource;
compilationUnit.types = <ClassElement>[type];
sourcedCompilationUnits[i] = compilationUnit;
}
}
CompilationUnitElementImpl compilationUnit = CompilationUnitElementImpl();
compilationUnit.librarySource =
compilationUnit.source = definingCompilationUnitSource;
var featureSet = context.analysisOptions.contextFeatures;
LibraryElementImpl library = LibraryElementImpl(
context,
driver?.currentSession,
libraryName,
-1,
0,
featureSet.isEnabled(Feature.non_nullable));
library.definingCompilationUnit = compilationUnit;
library.parts = sourcedCompilationUnits;
return library;
}
Expression findTopLevelConstantExpression(
CompilationUnit compilationUnit, String name) =>
findTopLevelDeclaration(compilationUnit, name).initializer;
VariableDeclaration findTopLevelDeclaration(
CompilationUnit compilationUnit, String name) {
for (CompilationUnitMember member in compilationUnit.declarations) {
if (member is TopLevelVariableDeclaration) {
for (VariableDeclaration variable in member.variables.variables) {
if (variable.name.name == name) {
return variable;
}
}
}
}
return null;
// Not found
}
/// Re-create the analysis context being used by the test case.
void reset() {
resetWith();
}
/// Re-create the analysis context being used by the test with the either
/// given [options] or [packages].
void resetWith({AnalysisOptions options, List<List<String>> packages}) {
if (options != null && packages != null) {
fail('Only packages or options can be specified.');
}
options ??= defaultAnalysisOptions;
List<String> experiments = enabledExperiments;
if (experiments != null) {
(options as AnalysisOptionsImpl).contextFeatures =
FeatureSet.fromEnableFlags(experiments);
}
DartSdk sdk = MockSdk(
resourceProvider: resourceProvider,
analysisOptions: options,
);
List<UriResolver> resolvers = <UriResolver>[
DartUriResolver(sdk),
ResourceUriResolver(resourceProvider)
];
if (packages != null) {
var packageMap = <String, List<Folder>>{};
packages.forEach((args) {
String name = args[0];
String content = args[1];
File file = newFile('/packages/$name/$name.dart', content: content);
packageMap[name] = <Folder>[file.parent];
});
resolvers.add(PackageMapUriResolver(resourceProvider, packageMap));
}
SourceFactory sourceFactory = SourceFactory(resolvers);
PerformanceLog log = PerformanceLog(_logBuffer);
AnalysisDriverScheduler scheduler = AnalysisDriverScheduler(log);
driver = AnalysisDriver(scheduler, log, resourceProvider, MemoryByteStore(),
fileContentOverlay, null, sourceFactory, options,
packages: Packages.empty);
scheduler.start();
}
Future<CompilationUnit> resolveSource(String sourceText) =>
resolveSource2('/test.dart', sourceText);
Future<CompilationUnit> resolveSource2(
String fileName, String sourceText) async {
Source source = addNamedSource(fileName, sourceText);
TestAnalysisResult analysisResult = await computeAnalysisResult(source);
return analysisResult.unit;
}
Future<Source> resolveSources(List<String> sourceTexts) async {
for (int i = 0; i < sourceTexts.length; i++) {
Source source = addNamedSource('/lib${i + 1}.dart', sourceTexts[i]);
await computeAnalysisResult(source);
// reference the source if this is the last source
if (i + 1 == sourceTexts.length) {
return source;
}
}
return null;
}
Future<void> resolveWithAndWithoutExperimental(
List<String> strSources,
List<ErrorCode> codesWithoutExperimental,
List<ErrorCode> codesWithExperimental) async {
// Setup analysis context as non-experimental
AnalysisOptionsImpl options = AnalysisOptionsImpl();
// options.enableDeferredLoading = false;
resetWith(options: options);
// Analysis and assertions
Source source = await resolveSources(strSources);
await computeAnalysisResult(source);
assertErrors(source, codesWithoutExperimental);
verify([source]);
// Setup analysis context as experimental
reset();
// Analysis and assertions
source = await resolveSources(strSources);
await computeAnalysisResult(source);
assertErrors(source, codesWithExperimental);
verify([source]);
}
Future<void> resolveWithErrors(
List<String> strSources, List<ErrorCode> codes) async {
Source source = await resolveSources(strSources);
assertErrors(source, codes);
verify([source]);
}
void setUp() {
ElementFactory.flushStaticState();
reset();
}
void tearDown() {
AnalysisEngine.instance.clearCaches();
}
/// Verify that all of the identifiers in the compilation units associated
/// with the given [sources] have been resolved.
void verify(List<Source> sources) {
ResolutionVerifier verifier = ResolutionVerifier();
for (Source source in sources) {
TestAnalysisResult result = analysisResults[source];
expect(result, isNotNull);
result.unit.accept(verifier);
}
verifier.assertResolved();
}
}
/// Shared infrastructure for [StaticTypeAnalyzer2Test].
class StaticTypeAnalyzer2TestShared extends PubPackageResolutionTest {
/// Find the expression that starts at the offset of [search] and validate its
/// that its static type matches the given [type].
///
/// If [type] is a string, validates that the expression's static type
/// stringifies to that text. Otherwise, [type] is used directly a [Matcher]
/// to match the type.
void expectExpressionType(String search, type) {
Expression expression = findNode.expression(search);
_expectType(expression.staticType, type);
}
/// Looks up the identifier with [name] and validates that its type type
/// stringifies to [type] and that its generics match the given stringified
/// output.
FunctionTypeImpl expectFunctionType(String name, String type,
{String typeParams = '[]',
String typeArgs = '[]',
String typeFormals = '[]',
String identifierType}) {
identifierType ??= type;
String typeParametersStr(List<TypeParameterElement> elements) {
var elementsStr = elements.map((e) {
return e.getDisplayString(withNullability: false);
}).join(', ');
return '[$elementsStr]';
}
SimpleIdentifier identifier = findNode.simple(name);
var functionType = _getFunctionTypedElementType(identifier);
assertType(functionType, type);
expect(identifier.staticType, isNull);
expect(functionType.typeArguments.toString(), typeArgs);
expect(typeParametersStr(functionType.typeFormals), typeFormals);
return functionType;
}
/// Looks up the identifier with [name] and validates that its element type
/// stringifies to [type] and that its generics match the given stringified
/// output.
FunctionTypeImpl expectFunctionType2(String name, String type) {
var identifier = findNode.simple(name);
var functionType = _getFunctionTypedElementType(identifier);
assertType(functionType, type);
return functionType;
}
/// Looks up the identifier with [name] and validates its static [type].
///
/// If [type] is a string, validates that the identifier's static type
/// stringifies to that text. Otherwise, [type] is used directly a [Matcher]
/// to match the type.
void expectIdentifierType(String name, type) {
SimpleIdentifier identifier = findNode.simple(name);
_expectType(identifier.staticType, type);
}
/// Looks up the initializer for the declaration containing [identifier] and
/// validates its static [type].
///
/// If [type] is a string, validates that the identifier's static type
/// stringifies to that text. Otherwise, [type] is used directly a [Matcher]
/// to match the type.
void expectInitializerType(String name, type) {
SimpleIdentifier identifier = findNode.simple(name);
VariableDeclaration declaration =
identifier.thisOrAncestorOfType<VariableDeclaration>();
Expression initializer = declaration.initializer;
_expectType(initializer.staticType, type);
}
/// Validates that [type] matches [expected].
///
/// If [expected] is a string, validates that the type stringifies to that
/// text. Otherwise, [expected] is used directly a [Matcher] to match the
/// type.
_expectType(DartType type, expected) {
if (expected is String) {
assertType(type, expected);
} else {
expect(type, expected);
}
}
FunctionTypeImpl _getFunctionTypedElementType(SimpleIdentifier identifier) {
var element = identifier.staticElement;
if (element is ExecutableElement) {
return element.type;
} else if (element is VariableElement) {
return element.type;
} else {
fail('Unexpected element: (${element.runtimeType}) $element');
}
}
}
class TestAnalysisResult {
final Source source;
final CompilationUnit unit;
final List<AnalysisError> errors;
TestAnalysisResult(this.source, this.unit, this.errors);
LibraryElement get libraryElement => unit.declaredElement.library;
TypeProvider get typeProvider => libraryElement.typeProvider;
TypeSystemImpl get typeSystem => libraryElement.typeSystem;
}