blob: f87525e4909c35fd5bddb80b27a505f85ad8e903 [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.
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, PrefixElement;
import 'package:analyzer/dart/element/type.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
/// whose name matches the given [components] and element [kinds] in a library
/// that imports the [importedUris].
ElementMatcher(
{required this.importedUris,
required this.components,
required List<ElementKind> kinds})
: assert(components.isNotEmpty),
validKinds = kinds;
/// 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 a list of element matchers that will match the element that is, or
/// should be, associated with the given [node]. The list will be empty if
/// there are no appropriate matchers for the [node].
static List<ElementMatcher> matchersForNode(AstNode? node) {
if (node == null) {
return const <ElementMatcher>[];
}
var importedUris = _importElementsForNode(node);
if (importedUris == null) {
return const <ElementMatcher>[];
}
var builder = _MatcherBuilder(importedUris);
builder.buildMatchersForNode(node);
return builder.matchers.toList();
}
/// 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.declaredElement?.library;
if (library == null) {
return null;
}
for (var importElement in library.imports2) {
// 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;
}
}
/// A helper class used to build a list of element matchers.
class _MatcherBuilder {
final List<ElementMatcher> matchers = [];
final List<Uri> importedUris;
_MatcherBuilder(this.importedUris);
void buildMatchersForNode(AstNode? node) {
if (node is ArgumentList) {
_buildFromArgumentList(node);
} else if (node is BinaryExpression) {
_buildFromBinaryExpression(node);
} else if (node is ConstructorName) {
_buildFromConstructorName(node);
} else if (node is Literal) {
var parent = node.parent;
if (parent is ArgumentList) {
_buildFromArgumentList(parent);
}
} else if (node is NamedType) {
_buildFromNamedType(node);
} else if (node is PrefixedIdentifier) {
_buildFromPrefixedIdentifier(node);
} else if (node is SimpleIdentifier) {
_buildFromSimpleIdentifier(node);
} else if (node is TypeArgumentList) {
_buildFromTypeArgumentList(node);
}
}
void _addMatcher(
{required List<String> components, required List<ElementKind> kinds}) {
matchers.add(ElementMatcher(
importedUris: importedUris, components: components, kinds: kinds));
}
/// Build a matcher for the element being invoked.
void _buildFromArgumentList(ArgumentList node) {
var parent = node.parent;
if (parent is Annotation) {
_addMatcher(
components: [parent.constructorName?.name ?? '', parent.name.name],
kinds: [ElementKind.constructorKind],
);
// } else if (parent is ExtensionOverride) {
// // TODO(brianwilkerson) Determine whether this branch can be reached.
// _buildFromExtensionOverride(parent);
} else if (parent is FunctionExpressionInvocation) {
_buildFromFunctionExpressionInvocation(parent);
} else if (parent is InstanceCreationExpression) {
_buildFromInstanceCreationExpression(parent);
} else if (parent is MethodInvocation) {
_buildFromMethodInvocation(parent);
} else if (parent is RedirectingConstructorInvocation) {
var grandparent = parent.parent;
if (grandparent is ConstructorDeclaration) {
_addMatcher(
components: [
parent.constructorName?.name ?? '',
grandparent.returnType.name
],
kinds: [ElementKind.constructorKind],
);
}
} else if (parent is SuperConstructorInvocation) {
var superclassName = parent.staticElement?.enclosingElement.name;
if (superclassName != null) {
_addMatcher(
components: [parent.constructorName?.name ?? '', superclassName],
kinds: [ElementKind.constructorKind],
);
}
}
}
/// Build a matcher for the operator being invoked.
void _buildFromBinaryExpression(BinaryExpression node) {
// TODO(brianwilkerson) Implement this method in order to support changes to
// operators.
}
/// Build a matcher for the constructor being referenced.
void _buildFromConstructorName(ConstructorName node) {
// TODO(brianwilkerson) Use the static element, if there is one, in order to
// get a more exact matcher.
// TODO(brianwilkerson) Use 'new' for the name of the unnamed constructor.
var constructorName = node.name?.name ?? ''; // ?? 'new';
var className = node.type.name.simpleName;
_addMatcher(
components: [constructorName, className],
kinds: const [ElementKind.constructorKind],
);
_addMatcher(
components: [className],
kinds: const [ElementKind.classKind],
);
}
/// Build a matcher for the extension.
void _buildFromExtensionOverride(ExtensionOverride node) {
_addMatcher(
components: [node.extensionName.name],
kinds: [ElementKind.extensionKind],
);
}
/// Build a matcher for the function being invoked.
void _buildFromFunctionExpressionInvocation(
FunctionExpressionInvocation node) {
// TODO(brianwilkerson) This case was missed in the original implementation
// and there are no tests for it at this point, but it ought to be supported.
}
/// Build a matcher for the constructor being invoked.
void _buildFromInstanceCreationExpression(InstanceCreationExpression node) {
_buildFromConstructorName(node.constructorName);
}
/// Build a matcher for the method being declared.
void _buildFromMethodDeclaration(MethodDeclaration node) {
_addMatcher(
components: [node.name.name],
kinds: [ElementKind.methodKind],
);
}
/// Build a matcher for the method being invoked.
void _buildFromMethodInvocation(MethodInvocation node) {
// TODO(brianwilkerson) Use the static element, if there is one, in order to
// get a more exact matcher.
// var element = node.methodName.staticElement;
// if (element != null) {
// return _buildFromElement(element);
// }
var methodName = node.methodName;
var targetName = _nameOfTarget(node.realTarget);
if (targetName != null) {
// If there is a target, and we know the type of the target, then we know
// that a method is being invoked.
_addMatcher(
components: [methodName.name, targetName],
kinds: [
ElementKind.constructorKind,
ElementKind.methodKind,
],
);
} else if (node.realTarget != null) {
// If there is a target, but we don't know the type of the target, then
// the target type might be undefined and this might have been either a
// method invocation, an invocation of a function returned by a getter, or
// a constructor invocation prior to the type having been deleted.
_addMatcher(
components: _componentsFromIdentifier(methodName),
kinds: [
ElementKind.constructorKind,
ElementKind.getterKind,
ElementKind.methodKind,
],
);
} else {
// If there is no target, then this might have been either a method
// invocation, a function invocation (of either a function or a function
// returned from a getter), a constructor invocation, or an extension
// override. If it's a constructor, then the change might have been to the
// class rather than an individual constructor.
_addMatcher(
components: _componentsFromIdentifier(methodName),
kinds: [
ElementKind.classKind,
ElementKind.constructorKind,
ElementKind.extensionKind,
ElementKind.functionKind,
ElementKind.getterKind,
ElementKind.methodKind,
],
);
}
}
/// Build a matcher for the type.
void _buildFromNamedType(NamedType node) {
var parent = node.parent;
if (parent is ConstructorName) {
return _buildFromConstructorName(parent);
}
// TODO(brianwilkerson) Use the static element, if there is one, in order to
// get a more exact matcher.
_addMatcher(
components: [node.name.simpleName],
kinds: const [
ElementKind.classKind,
ElementKind.enumKind,
ElementKind.mixinKind,
ElementKind.typedefKind
],
);
// TODO(brianwilkerson) Determine whether we can ever get here as a result
// of having a removed unnamed constructor.
// _addMatcher(
// components: ['', node.name.name],
// kinds: const [ElementKind.constructorKind],
// );
}
/// Build a matcher for the element represented by the prefixed identifier.
void _buildFromPrefixedIdentifier(PrefixedIdentifier node) {
var parent = node.parent;
if (parent is NamedType) {
return _buildFromNamedType(parent);
}
// TODO(brianwilkerson) Use the static element, if there is one, in order to
// get a more exact matcher.
var prefix = node.prefix;
if (prefix.staticElement is PrefixElement) {
var parent = node.parent;
if ((parent is NamedType && parent.parent is! ConstructorName) ||
(parent is PropertyAccess && parent.target == node)) {
_addMatcher(components: [
node.identifier.name
], kinds: const [
ElementKind.classKind,
ElementKind.enumKind,
ElementKind.extensionKind,
ElementKind.mixinKind,
ElementKind.typedefKind
]);
}
_addMatcher(components: [
node.identifier.name
], kinds: const [
// If the old class has been removed then this might have been a
// constructor invocation.
ElementKind.constructorKind,
ElementKind.functionKind, // tear-off
ElementKind.getterKind,
ElementKind.setterKind,
ElementKind.variableKind
]);
}
// It looks like we're accessing a member, so try to figure out the
// name of the type defining the member.
var targetType = node.prefix.staticType;
if (targetType is InterfaceType) {
_addMatcher(
components: [node.identifier.name, targetType.element.name],
kinds: const [
ElementKind.constantKind,
ElementKind.fieldKind,
ElementKind.functionKind, // tear-off
ElementKind.getterKind,
ElementKind.methodKind, // tear-off
ElementKind.setterKind
],
);
}
// It looks like we're accessing a member, but we don't know what kind of
// member, so we include all of the member kinds.
var container = node.prefix.staticElement;
if (container is ClassElement) {
_addMatcher(
components: [node.identifier.name, container.name],
kinds: const [
ElementKind.constantKind,
ElementKind.fieldKind,
ElementKind.functionKind, // tear-off
ElementKind.getterKind,
ElementKind.methodKind, // tear-off
ElementKind.setterKind
],
);
} else if (container is ExtensionElement) {
_addMatcher(
components: [node.identifier.name, container.displayName],
kinds: const [
ElementKind.constantKind,
ElementKind.fieldKind,
ElementKind.functionKind, // tear-off
ElementKind.getterKind,
ElementKind.methodKind, // tear-off
ElementKind.setterKind
],
);
}
}
/// Build a matcher for the property being accessed.
void _buildFromPropertyAccess(PropertyAccess node) {
// TODO(brianwilkerson) Use the static element, if there is one, in order to
// get a more exact matcher.
var propertyName = node.propertyName;
var targetName = _nameOfTarget(node.realTarget);
List<String> components;
if (targetName != null) {
components = [propertyName.name, targetName];
} else {
components = _componentsFromIdentifier(propertyName);
}
_addMatcher(
components: components,
kinds: const [
ElementKind.constantKind,
ElementKind.fieldKind,
ElementKind.functionKind, // tear-off, prefixed
ElementKind.getterKind,
ElementKind.methodKind, // tear-off, prefixed
ElementKind.setterKind
],
);
}
/// Build a matcher for the element referenced by the identifier.
void _buildFromSimpleIdentifier(SimpleIdentifier node) {
// TODO(brianwilkerson) Use the static element, if there is one, in order to
// get a more exact matcher.
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.
_buildFromArgumentList(parent.parent!.parent as ArgumentList);
} else if (parent is NamedType) {
_buildFromNamedType(parent);
} else if (parent is MethodDeclaration && node == parent.name) {
_buildFromMethodDeclaration(parent);
} else if (parent is MethodInvocation &&
node == parent.methodName &&
!_isPrefix(parent.target)) {
_buildFromMethodInvocation(parent);
} else if (parent is PrefixedIdentifier && node == parent.identifier) {
_buildFromPrefixedIdentifier(parent);
} else if (parent is PropertyAccess &&
node == parent.propertyName &&
!_isPrefix(parent.target)) {
_buildFromPropertyAccess(parent);
} else {
// TODO(brianwilkerson) See whether the list of kinds can be specified.
_addMatcher(components: [node.name], kinds: []);
}
}
/// Build a matcher for the element with which the type arguments are
/// associated.
void _buildFromTypeArgumentList(TypeArgumentList node) {
var parent = node.parent;
if (parent is ExtensionOverride) {
_buildFromExtensionOverride(parent);
} else if (parent is FunctionExpressionInvocation) {
_buildFromFunctionExpressionInvocation(parent);
} else if (parent is InstanceCreationExpression) {
_buildFromInstanceCreationExpression(parent);
} else if (parent is MethodInvocation) {
_buildFromMethodInvocation(parent);
}
}
/// 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) {
return [identifier.name, enclosingElement.name];
} else if (enclosingElement is ExtensionElement) {
var name = enclosingElement.name;
if (name != null) {
return [identifier.name, name];
}
}
}
return [identifier.name];
}
/// Return `true` if the [node] is a prefix
static bool _isPrefix(AstNode? node) {
return node is SimpleIdentifier && node.staticElement is PrefixElement;
}
/// 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;
}
}
extension on Identifier {
String get simpleName {
var identifier = this;
if (identifier is PrefixedIdentifier) {
// The prefix isn't part of the name of the class.
return identifier.identifier.name;
}
return name;
}
}