blob: fe8ba62f582c94a9ba834b4f9362cc4d9158c5b2 [file] [log] [blame]
// Copyright (c) 2020, 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.
// @dart = 2.9
import 'package:analysis_server/src/services/correction/fix/data_driven/element_descriptor.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/element_kind.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart'
show ClassElement, ExtensionElement;
import 'package:analyzer/dart/element/type.dart';
import 'package:meta/meta.dart';
/// An object that can be used to determine whether an element is appropriate
/// for a given reference.
class ElementMatcher {
/// The URIs of the libraries that are imported in the library containing the
/// reference.
final List<Uri> importedUris;
/// The components of the element being referenced. The components are ordered
/// from the most local to the most global.
final List<String> components;
/// A list of the kinds of elements that are appropriate for some given
/// location in the code An empty list represents all kinds rather than no
/// kinds.
final List<ElementKind> validKinds;
/// Initialize a newly created matcher representing a reference to an element
/// with the given [name] in a library that imports the [importedUris].
ElementMatcher(
{@required this.importedUris,
@required this.components,
List<ElementKind> kinds})
: assert(components != null && components.isNotEmpty),
validKinds = kinds ?? const [];
/// Return `true` if this matcher matches the given [element].
bool matches(ElementDescriptor element) {
//
// Check that the components in the element's name match the node.
//
// This algorithm is probably too general given that there will currently
// always be either one or two components.
//
var elementComponents = element.components;
var elementComponentCount = elementComponents.length;
var nodeComponentCount = components.length;
if (nodeComponentCount == elementComponentCount) {
// The component counts are the same, so we can just compare the two
// lists.
for (var i = 0; i < nodeComponentCount; i++) {
if (elementComponents[i] != components[i]) {
return false;
}
}
} else if (nodeComponentCount < elementComponentCount) {
// The node has fewer components, which can happen, for example, when we
// can't figure out the class that used to define a field. We treat the
// missing components as wildcards and match the rest.
for (var i = 0; i < nodeComponentCount; i++) {
if (elementComponents[i] != components[i]) {
return false;
}
}
} else {
// The node has more components than the element, which can happen when a
// constructor is implicitly renamed because the class was renamed.
// TODO(brianwilkerson) Figure out whether we want to support this or
// whether we want to require fix data authors to explicitly include the
// change to the constructor. On the one hand it's more work for the
// author, on the other hand it give us more data so we're less likely to
// make apply a fix in invalid circumstances.
if (elementComponents[0] != components[1]) {
return false;
}
}
//
// Check whether the kind of element matches the possible kinds that the
// node might have.
//
if (validKinds.isNotEmpty && !validKinds.contains(element.kind)) {
return false;
}
//
// Check whether the element is in an imported library.
//
var libraryUris = element.libraryUris;
for (var importedUri in importedUris) {
if (libraryUris.contains(importedUri)) {
return true;
}
}
return false;
}
/// Return an element matcher that will match the element that is, or should
/// be, associated with the given [node], or `null` if there is no appropriate
/// matcher for the node.
static ElementMatcher forNode(AstNode node) {
if (node == null) {
return null;
}
var importedUris = _importElementsForNode(node);
if (importedUris == null) {
return null;
}
var components = _componentsForNode(node);
if (components == null) {
return null;
}
return ElementMatcher(
importedUris: importedUris,
components: components,
kinds: _kindsForNode(node));
}
/// Return the components of the path of the element associated with the given
/// [node]. The components are ordered from the most local to the most global.
/// For example, for a constructor this would be the name of the constructor
/// followed by the name of the class in which the constructor is declared
/// (with an empty string for the unnamed constructor).
static List<String> _componentsForNode(AstNode node) {
if (node is SimpleIdentifier) {
var parent = node.parent;
if (parent is Label && parent.parent is NamedExpression) {
// The parent of the named expression is an argument list. Because we
// don't represent parameters as elements, the element we need to match
// against is the invocation containing those arguments.
return _componentsFromParent(parent.parent.parent);
} else if (parent is TypeName && parent.parent is ConstructorName) {
return ['', node.name];
} else if (parent is MethodDeclaration && node == parent.name) {
return [node.name];
} else if ((parent is MethodInvocation && node == parent.methodName) ||
(parent is PrefixedIdentifier && node == parent.identifier) ||
(parent is PropertyAccess && node == parent.propertyName)) {
return _componentsFromParent(node);
}
return _componentsFromIdentifier(node);
} else if (node is PrefixedIdentifier) {
var parent = node.parent;
if (parent is TypeName && parent.parent is ConstructorName) {
return ['', node.identifier.name];
}
return [node.identifier.name];
} else if (node is ConstructorName) {
return [node.name.name];
} else if (node is NamedType) {
return [node.name.name];
} else if (node is TypeArgumentList) {
return _componentsFromParent(node);
} else if (node is ArgumentList) {
return _componentsFromParent(node);
} else if (node?.parent is ArgumentList) {
return _componentsFromParent(node.parent);
}
return null;
}
/// Return the components associated with the [identifier] when there is no
/// contextual information.
static List<String> _componentsFromIdentifier(SimpleIdentifier identifier) {
var element = identifier.staticElement;
if (element == null) {
var parent = identifier.parent;
if (parent is AssignmentExpression && identifier == parent.leftHandSide) {
element = parent.writeElement;
}
}
if (element != null) {
var enclosingElement = element.enclosingElement;
if (enclosingElement is ClassElement ||
enclosingElement is ExtensionElement) {
return [identifier.name, enclosingElement.name];
}
}
return [identifier.name];
}
/// Return the components for the element associated with the given [node] by
/// looking at the parent of the [node].
static List<String> _componentsFromParent(AstNode node) {
var parent = node.parent;
if (parent is ArgumentList) {
parent = parent.parent;
}
if (parent is Annotation) {
return [parent.constructorName?.name ?? '', parent.name.name];
} else if (parent is ExtensionOverride) {
return [parent.extensionName.name];
} else if (parent is InstanceCreationExpression) {
var constructorName = parent.constructorName;
return [constructorName.name?.name ?? '', constructorName.type.name.name];
} else if (parent is MethodInvocation) {
var methodName = parent.methodName;
var targetName = _nameOfTarget(parent.realTarget);
if (targetName != null) {
return [methodName.name, targetName];
}
return _componentsFromIdentifier(methodName);
} else if (parent is PrefixedIdentifier) {
var identifier = parent.identifier;
var targetName = _nameOfTarget(parent.prefix);
if (targetName != null) {
return [identifier.name, targetName];
}
return _componentsFromIdentifier(identifier);
} else if (parent is PropertyAccess) {
var propertyName = parent.propertyName;
var targetName = _nameOfTarget(parent.realTarget);
if (targetName != null) {
return [propertyName.name, targetName];
}
return _componentsFromIdentifier(propertyName);
} else if (parent is RedirectingConstructorInvocation) {
var ancestor = parent.parent;
if (ancestor is ConstructorDeclaration) {
return [parent.constructorName?.name ?? '', ancestor.returnType.name];
}
} else if (parent is SuperConstructorInvocation) {
var ancestor = parent.parent;
if (ancestor is ConstructorDeclaration) {
return [parent.constructorName?.name ?? '', ancestor.returnType.name];
}
}
return null;
}
/// Return the URIs of the imports in the library containing the [node], or
/// `null` if the imports can't be determined.
static List<Uri> _importElementsForNode(AstNode node) {
var root = node.root;
if (root is! CompilationUnit) {
return null;
}
var importedUris = <Uri>[];
var library = (root as CompilationUnit).declaredElement.library;
for (var importElement in library.imports) {
// TODO(brianwilkerson) Filter based on combinators to help avoid making
// invalid suggestions.
var uri = importElement.importedLibrary?.source?.uri;
if (uri != null) {
// The [uri] is `null` if the literal string is not a valid URI.
importedUris.add(uri);
}
}
return importedUris;
}
/// Return the kinds of elements that could reasonably be referenced at the
/// location of the [node]. If [child] is no `null` then the [node] is a
/// parent of the original node.
static List<ElementKind> _kindsForNode(AstNode node, {AstNode child}) {
if (node is ConstructorName) {
return const [ElementKind.constructorKind];
} else if (node is ExtensionOverride) {
return const [ElementKind.extensionKind];
} else if (node is InstanceCreationExpression) {
return const [ElementKind.constructorKind];
} else if (node is Label) {
var argumentList = node.parent.parent;
return _kindsForNode(argumentList.parent, child: argumentList);
} else if (node is MethodInvocation) {
assert(child != null);
if (node.target == child) {
return const [
ElementKind.classKind,
ElementKind.enumKind,
ElementKind.mixinKind
];
} else if (node.realTarget != null) {
return const [ElementKind.constructorKind, ElementKind.methodKind];
}
return const [
ElementKind.classKind,
ElementKind.extensionKind,
ElementKind.functionKind,
ElementKind.methodKind
];
} else if (node is NamedType) {
var parent = node.parent;
if (parent is ConstructorName && parent.name == null) {
return const [ElementKind.classKind, ElementKind.constructorKind];
}
return const [
ElementKind.classKind,
ElementKind.enumKind,
ElementKind.mixinKind,
ElementKind.typedefKind
];
} else if (node is PrefixedIdentifier) {
if (node.prefix == child) {
return const [
ElementKind.classKind,
ElementKind.enumKind,
ElementKind.extensionKind,
ElementKind.mixinKind,
ElementKind.typedefKind
];
}
return const [
ElementKind.fieldKind,
ElementKind.getterKind,
ElementKind.setterKind
];
} else if (node is PropertyAccess) {
return const [ElementKind.getterKind, ElementKind.setterKind];
} else if (node is SimpleIdentifier) {
return _kindsForNode(node.parent, child: node);
}
return null;
}
/// Return the name of the class associated with the given [target].
static String _nameOfTarget(Expression target) {
if (target is SimpleIdentifier) {
var type = target.staticType;
if (type != null) {
if (type is InterfaceType) {
return type.element.name;
} else if (type.isDynamic) {
// The name is likely to be undefined.
return target.name;
}
return null;
}
return target.name;
} else if (target != null) {
var type = target.staticType;
if (type is InterfaceType) {
return type.element.name;
}
return null;
}
return null;
}
}