blob: ba05ff55fe55aa81ef59fa2569ac6f1405c7775a [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 '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:test/test.dart';
import '../src/dart/resolution/context_collection_resolution.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);
var elementAnnotation = node.elementAnnotation;
if (elementAnnotation == null) {
if (_knownExceptions == null || !_knownExceptions!.contains(node)) {
_unresolvedNodes.add(node);
}
}
}
@override
void visitBinaryExpression(BinaryExpression node) {
node.visitChildren(this);
if (!node.operator.isUserDefinableOperator) {
return;
}
var 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);
var prefix = node.prefix;
if (prefix == null) {
return;
}
_checkResolved(
prefix, prefix.staticElement, (node) => node is PrefixElement);
}
@override
void visitIndexExpression(IndexExpression node) {
node.visitChildren(this);
var targetType = node.realTarget.staticType;
if (targetType == null || targetType.isDynamic) {
return;
}
var parent = node.parent;
if (parent is AssignmentExpression && parent.leftHandSide == node) {
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;
}
var 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);
var 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;
}
var 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);
var targetType = target.staticType;
if (targetType == null || targetType.isDynamic) {
return;
}
var parent = node.parent;
if (parent is AssignmentExpression && parent.leftHandSide == node) {
return;
}
node.propertyName.accept(this);
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
if (node.name == "void") {
return;
}
var staticType = node.staticType;
if (staticType != null &&
staticType.isDynamic &&
node.staticElement == null) {
return;
}
var parent = node.parent;
if (parent is AssignmentExpression && parent.leftHandSide == node) {
return;
}
if (parent is MethodInvocation) {
MethodInvocation invocation = parent;
if (identical(invocation.methodName, node)) {
var target = invocation.realTarget;
var targetType = target == null ? null : target.staticType;
if (targetType == null || targetType.isDynamic) {
return;
}
}
}
_checkResolved(node, node.staticElement, null);
}
void _checkResolved(
AstNode node,
Element? element,
bool Function(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(")");
}
}
}
/// Shared infrastructure for [StaticTypeAnalyzer2Test].
class StaticTypeAnalyzer2TestShared extends PubPackageResolutionTest
with WithoutNullSafetyMixin {
// TODO(https://github.com/dart-lang/sdk/issues/44666): Use null safety in
// test cases.
/// Looks up the identifier with [name] and validates that its type type
/// stringifies to [type] and that its generics match the given stringified
/// output.
FunctionType expectFunctionType(String name, String type,
{String typeParams = '[]',
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(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.
FunctionType 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);
var declaration = identifier.thisOrAncestorOfType<VariableDeclaration>()!;
var 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);
}
}
FunctionType _getFunctionTypedElementType(SimpleIdentifier identifier) {
var element = identifier.staticElement;
if (element is ExecutableElement) {
return element.type;
} else if (element is VariableElement) {
return element.type as FunctionType;
} else {
fail('Unexpected element: (${element.runtimeType}) $element');
}
}
}