blob: 1f07ca189a4c2a973c0f3a8ffc5c540114071b5d [file] [edit]
// Copyright (c) 2021, 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:_fe_analyzer_shared/src/base/syntactic_entity.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/ast/element_locator.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/utilities/extensions/element.dart';
/// Return the [Element] of the given [node], or `null` if [node] is `null` or
/// does not have an element.
Element? getElementOfNode(AstNode? node) {
if (node == null) {
return null;
}
if (node is NameWithTypeParameters) {
node = node.parent2;
}
if (node is DottedName) {
node = node.parent2;
}
if (node is StringLiteral && node.parent2 is UriBasedDirective) {
return null;
}
Element? element;
switch (node) {
case ImportDirective():
return MockLibraryImportElement(node.libraryImport!);
case ImportPrefixReference():
element = node.element;
case ConstructorDeclaration():
return node.declaredFragment?.element;
case PrimaryConstructorDeclaration():
return node.declaredFragment?.element;
default:
element = ElementLocatorV2.locate(node);
}
if (node is SimpleIdentifier && element is PrefixElement) {
var parent = node.parent2;
if (parent is ImportDirective) {
element = MockLibraryImportElement(parent.libraryImport!);
} else {
element = _getImportElementInfo(node);
}
} else if (node is ImportPrefixReference && element is PrefixElement) {
element = _getImportElementInfoFromReference(node);
}
return element;
}
/// Return the [Element] of the given V1 [node], or `null` if [node] is `null`
/// or does not have an element.
@ToBeDeprecated('Use getElementOfNode2 instead')
Element? getElementOfNodeV1(AstNode? node) {
if (node == null) {
return null;
}
if (node is NameWithTypeParameters) {
node = node.parent;
}
if (node is DottedName) {
node = node.parent;
}
if (node is StringLiteral && node.parent is UriBasedDirective) {
return null;
}
Element? element;
switch (node) {
case ImportDirective():
return MockLibraryImportElement(node.libraryImport!);
case ImportPrefixReference():
element = node.element;
case ConstructorDeclaration():
return node.declaredFragment?.element;
case PrimaryConstructorDeclaration():
return node.declaredFragment?.element;
default:
element = ElementLocator.locate(node);
}
if (node is SimpleIdentifier && element is PrefixElement) {
var parent = node.parent;
if (parent is ImportDirective) {
element = MockLibraryImportElement(parent.libraryImport!);
} else {
element = _getImportElementInfoV1(node);
}
} else if (node is ImportPrefixReference && element is PrefixElement) {
element = _getImportElementInfoFromReferenceV1(node);
}
return element;
}
/// If the given [constructor] is a synthetic constructor created for a
/// [ClassTypeAlias], return the actual constructor of a [ClassDeclaration]
/// which is invoked. Return `null` if a redirection cycle is detected.
ConstructorElement? _getActualConstructorElement(
ConstructorElement? constructor,
) {
var seenConstructors = <ConstructorElement?>{};
while (constructor is ConstructorElementImpl &&
constructor.isOriginMixinApplication) {
var enclosing = constructor.enclosingElement;
if (enclosing is ClassElementImpl && enclosing.isMixinApplication) {
var superInvocation = constructor.constantInitializers
.whereType<SuperConstructorInvocation>()
.singleOrNull;
if (superInvocation != null) {
constructor = superInvocation.element;
}
} else {
break;
}
// fail if a cycle is detected
if (!seenConstructors.add(constructor)) {
return null;
}
}
return constructor;
}
/// Returns the [MockLibraryImportElement] that is referenced by [prefixNode]
/// with a [PrefixElement], maybe `null`.
MockLibraryImportElement? _getImportElementInfo(SimpleIdentifier prefixNode) {
// prepare environment
var parent = prefixNode.parent2;
var unit = prefixNode.thisOrAncestorOfType2<CompilationUnitImpl>();
var libraryFragment = unit?.declaredFragment;
if (libraryFragment == null) {
return null;
}
// prepare used element
Element? usedElement;
if (parent case PrefixedIdentifier prefixed) {
if (prefixed.prefix == prefixNode) {
usedElement = prefixed.element;
}
} else if (parent case MethodInvocation invocation) {
if (invocation.target2 == prefixNode) {
usedElement = invocation.methodName.element;
}
}
// we need used Element
if (usedElement == null) {
return null;
}
// find ImportElement
var prefix = prefixNode.name;
var importElementsMap = <LibraryImport, Set<Element>>{};
return _getMockImportElement(
libraryFragment,
prefix,
usedElement,
importElementsMap,
);
}
/// Returns the [MockLibraryImportElement] that is referenced by [prefixNode]
/// with a [PrefixElement], maybe `null`.
MockLibraryImportElement? _getImportElementInfoFromReference(
ImportPrefixReference prefixNode,
) {
// prepare environment
var unit = prefixNode.thisOrAncestorOfType2<CompilationUnitImpl>();
var libraryFragment = unit?.declaredFragment;
if (libraryFragment == null) {
return null;
}
// prepare used element
Element? usedElement;
var parent = prefixNode.parent2;
if (parent is ExtensionOverride) {
usedElement = parent.element;
} else if (parent is NamedType) {
usedElement = parent.element;
} else if (parent is ImportPrefixedAssignmentTarget) {
usedElement = switch (parent.write) {
InvalidNamedWriteResolution(:var recoveryElement) => recoveryElement,
_ => parent.write?.element ?? parent.read?.elementOrRecovery,
};
}
if (usedElement == null) {
return null;
}
// find ImportElement
var prefix = prefixNode.name.lexeme;
var importElementsMap = <LibraryImport, Set<Element>>{};
return _getMockImportElement(
libraryFragment,
prefix,
usedElement,
importElementsMap,
);
}
/// Returns the [MockLibraryImportElement] that is referenced by the V1
/// [prefixNode] with a [PrefixElement], maybe `null`.
@ToBeDeprecated('Use _getImportElementInfoFromReference instead')
MockLibraryImportElement? _getImportElementInfoFromReferenceV1(
ImportPrefixReference prefixNode,
) {
// prepare environment
var unit = prefixNode.thisOrAncestorOfType<CompilationUnitImpl>();
var libraryFragment = unit?.declaredFragment;
if (libraryFragment == null) {
return null;
}
// prepare used element
Element? usedElement;
var parent = prefixNode.parent;
if (parent is ExtensionOverride) {
usedElement = parent.element;
} else if (parent is NamedType) {
usedElement = parent.element;
}
if (usedElement == null) {
return null;
}
// find ImportElement
var prefix = prefixNode.name.lexeme;
var importElementsMap = <LibraryImport, Set<Element>>{};
return _getMockImportElement(
libraryFragment,
prefix,
usedElement,
importElementsMap,
);
}
/// Returns the [MockLibraryImportElement] that is referenced by the V1
/// [prefixNode] with a [PrefixElement], maybe `null`.
@ToBeDeprecated('Use _getImportElementInfo2 instead')
MockLibraryImportElement? _getImportElementInfoV1(SimpleIdentifier prefixNode) {
// prepare environment
var parent = prefixNode.parent;
var unit = prefixNode.thisOrAncestorOfType<CompilationUnitImpl>();
var libraryFragment = unit?.declaredFragment;
if (libraryFragment == null) {
return null;
}
// prepare used element
Element? usedElement;
if (parent case PrefixedIdentifier prefixed) {
if (prefixed.prefix == prefixNode) {
usedElement = prefixed.element;
}
} else if (parent case MethodInvocation invocation) {
if (invocation.target == prefixNode) {
usedElement = invocation.methodName.element;
}
}
// we need used Element
if (usedElement == null) {
return null;
}
// find ImportElement
var prefix = prefixNode.name;
var importElementsMap = <LibraryImport, Set<Element>>{};
return _getMockImportElement(
libraryFragment,
prefix,
usedElement,
importElementsMap,
);
}
/// Returns the [MockLibraryImportElement] that declared [prefix] and imports
/// [element].
///
/// [libraryFragment] - the [LibraryFragmentImpl] where reference is.
/// [prefix] - the import prefix, maybe `null`.
/// [element] - the referenced element.
/// [importElementsMap] - the cache of [Element]s imported by [LibraryImport]s.
MockLibraryImportElement? _getMockImportElement(
LibraryFragmentImpl libraryFragment,
String prefix,
Element element,
Map<LibraryImport, Set<Element>> importElementsMap,
) {
if (element.enclosingElement is! LibraryElement) {
return null;
}
var usedLibrary = element.library;
// find ImportElement that imports used library with used prefix
List<LibraryImport>? candidates;
var libraryImports = libraryFragment.withEnclosing2
.expand((fragment) => fragment.libraryImports)
.toList();
for (var importElement in libraryImports) {
// required library
if (importElement.importedLibrary != usedLibrary) {
continue;
}
// required prefix
var prefixElement = importElement.prefix?.element;
if (prefixElement == null) {
continue;
}
if (prefix != prefixElement.name) {
continue;
}
// no combinators => only possible candidate
if (importElement.combinators.isEmpty) {
return MockLibraryImportElement(importElement);
}
// OK, we have candidate
candidates ??= [];
candidates.add(importElement);
}
// no candidates, probably element is defined in this library
if (candidates == null) {
return null;
}
// one candidate
if (candidates.length == 1) {
return MockLibraryImportElement(candidates[0]);
}
// ensure that each ImportElement has set of elements
for (var importElement in candidates) {
if (importElementsMap.containsKey(importElement)) {
continue;
}
var namespace = importElement.namespace;
var elements = namespace.definedNames2.values.toSet();
importElementsMap[importElement] = elements;
}
// use import namespace to choose correct one
for (var entry in importElementsMap.entries) {
var importElement = entry.key;
var elements = entry.value;
if (elements.contains(element)) {
return MockLibraryImportElement(importElement);
}
}
// not found
return null;
}
class MatchInfo {
final int offset;
final int length;
final MatchKind matchKind;
MatchInfo(this.offset, this.length, this.matchKind);
}
/// Instances of the enum [MatchKind] represent the kind of reference that was
/// found when a match represents a reference to an element.
class MatchKind {
/// A declaration of an element.
static const MatchKind DECLARATION = MatchKind('DECLARATION');
/// A reference to an element in which it is being read.
static const MatchKind READ = MatchKind('READ');
/// A reference to an element in which it is being both read and written.
static const MatchKind READ_WRITE = MatchKind('READ_WRITE');
/// A reference to an element in which it is being written.
static const MatchKind WRITE = MatchKind('WRITE');
/// A reference to an element in which it is being invoked.
static const MatchKind INVOCATION = MatchKind('INVOCATION');
/// An invocation of an enum constructor from an enum constant without
/// arguments.
static const MatchKind INVOCATION_BY_ENUM_CONSTANT_WITHOUT_ARGUMENTS =
MatchKind('INVOCATION_BY_ENUM_CONSTANT_WITHOUT_ARGUMENTS');
/// A reference to an element in which it is referenced.
static const MatchKind REFERENCE = MatchKind('REFERENCE');
/// A tear-off reference to a constructor.
static const MatchKind REFERENCE_BY_CONSTRUCTOR_TEAR_OFF = MatchKind(
'REFERENCE_BY_CONSTRUCTOR_TEAR_OFF',
);
final String name;
const MatchKind(this.name);
@override
String toString() => name;
}
class ReferencesCollector extends RecursiveAstVisitor2<void> {
final Element element;
final List<MatchInfo> references = [];
ReferencesCollector(this.element);
@override
void visitAssignmentExpression(AssignmentExpression node) {
var writeElement = node.writeElement;
if (writeElement is PropertyAccessorElement) {
var kind = MatchKind.WRITE;
if (writeElement.variable == element || writeElement == element) {
if (node.leftHandSide2 is SimpleIdentifier) {
references.add(
MatchInfo(
node.leftHandSide2.offset,
node.leftHandSide2.length,
kind,
),
);
} else if (node.leftHandSide2 is PrefixedIdentifier) {
var prefixIdentifier = node.leftHandSide2 as PrefixedIdentifier;
references.add(
MatchInfo(
prefixIdentifier.identifier.offset,
prefixIdentifier.identifier.length,
kind,
),
);
} else if (node.leftHandSide2 is PropertyAccess) {
var accessor = node.leftHandSide2 as PropertyAccess;
references.add(
MatchInfo(accessor.propertyName.offset, accessor.length, kind),
);
}
}
}
var readElement = node.readElement;
if (readElement is PropertyAccessorElement) {
if (readElement.variable == element) {
references.add(
MatchInfo(
node.rightHandSide2.offset,
node.rightHandSide2.length,
MatchKind.READ,
),
);
}
}
}
@override
void visitCascadeMethodInvocation(CascadeMethodInvocation node) {
_visitNamedFunctionInvocation(node);
}
@override
visitCommentReference(CommentReference node) {
var expression = node.expression2;
if (expression is Identifier) {
var element = expression.element;
if (element is ConstructorElement) {
if (expression is PrefixedIdentifier) {
var offset = expression.prefix.end;
var length = expression.end - offset;
references.add(MatchInfo(offset, length, MatchKind.REFERENCE));
return;
} else {
var offset = expression.end;
references.add(MatchInfo(offset, 0, MatchKind.REFERENCE));
return;
}
}
} else if (expression is PropertyAccess) {
// Nothing to do?
} else {
throw UnimplementedError(
'Unhandled CommentReference expression type: '
'${expression.runtimeType}',
);
}
}
@override
void visitCompoundAssignment(CompoundAssignment node) {
var target = node.target;
// Import-prefixed targets are recorded by their own visitor.
if (target is NamedAssignmentTarget &&
target is! ImportPrefixedAssignmentTarget) {
_recordNamedReadWriteTarget(target);
}
super.visitCompoundAssignment(node);
}
@override
visitConstructorDeclaration(covariant ConstructorDeclarationImpl node) {
var constructorElement = node.declaredFragment?.element;
if (node.typeName2 case var typeName?) {
if (constructorElement?.enclosingElement == element &&
typeName.lexeme == element.name) {
references.add(
MatchInfo(typeName.offset, typeName.length, MatchKind.REFERENCE),
);
}
}
if (constructorElement == element) {
if (node.name case var name?) {
var offset = node.period?.offset ?? name.offset;
var length = name.end - offset;
references.add(MatchInfo(offset, length, MatchKind.DECLARATION));
} else {
var end = (node.typeName2 ?? node.newKeyword)!.end;
references.add(MatchInfo(end, 0, MatchKind.DECLARATION));
}
}
super.visitConstructorDeclaration(node);
}
@override
void visitConstructorInvocation(ConstructorInvocation node) {
node.constructorReference.accept2(this);
node.argumentList.accept2(this);
}
@override
void visitConstructorReference2(ConstructorReference2 node) {
var e = node.element?.baseElement;
e = _getActualConstructorElement(e);
if (e == element) {
if (node.selector case var selector?) {
var offset = selector.period.offset;
var length = selector.name2.end - offset;
references.add(
MatchInfo(offset, length, _constructorReferenceKind(node)),
);
} else {
references.add(
MatchInfo(node.typeReference.end, 0, _constructorReferenceKind(node)),
);
}
} else if (e != null && e.enclosingElement == element) {
var name = node.typeReference.name;
references.add(MatchInfo(name.offset, name.length, MatchKind.REFERENCE));
}
node.typeReference.typeArguments?.accept2(this);
}
@override
void visitConstructorTearOff(ConstructorTearOff node) {
var e = node.element?.baseElement;
e = _getActualConstructorElement(e);
if (e == element) {
references.add(
MatchInfo(
node.selector.period.offset,
node.selector.end - node.selector.period.offset,
MatchKind.REFERENCE_BY_CONSTRUCTOR_TEAR_OFF,
),
);
}
node.visitChildren2(this);
}
@override
void visitConstructorTypeReference(ConstructorTypeReference node) {
if (node.element == element) {
references.add(
MatchInfo(node.name.offset, node.name.length, MatchKind.REFERENCE),
);
}
node.typeArguments?.accept2(this);
}
@override
void visitDirectAssignment(DirectAssignment node) {
var target = node.target;
// Import-prefixed targets are recorded by their own visitor.
if (target is ImportPrefixedAssignmentTarget) {
super.visitDirectAssignment(node);
return;
}
switch (target.write) {
case WriteResolution(element: PropertyAccessorElement writeElement):
if (_matches(writeElement)) {
var entity = switch (target) {
PropertyAssignmentTarget() => target.name,
_ => target,
};
references.add(
MatchInfo(entity.offset, entity.length, MatchKind.WRITE),
);
}
case InvalidNamedWriteResolution(:var recoveryElement):
if (_matches(recoveryElement)) {
references.add(
MatchInfo(target.offset, target.length, MatchKind.REFERENCE),
);
}
default:
}
super.visitDirectAssignment(node);
}
@override
void visitEnumConstantDeclaration(EnumConstantDeclaration node) {
var constructorElement = node.constructorElement;
if (constructorElement != null && constructorElement == element) {
int offset;
int length;
var constructorSelector = node.arguments?.constructorSelector;
if (constructorSelector != null) {
offset = constructorSelector.period.offset;
length = constructorSelector.name2.end - offset;
} else {
offset = node.name.end;
length = 0;
}
var kind = node.arguments == null
? MatchKind.INVOCATION_BY_ENUM_CONSTANT_WITHOUT_ARGUMENTS
: MatchKind.INVOCATION;
references.add(MatchInfo(offset, length, kind));
}
}
@override
void visitIfNullAssignment(IfNullAssignment node) {
var target = node.target;
if (target is NamedAssignmentTarget &&
target is! ImportPrefixedAssignmentTarget) {
_recordNamedReadWriteTarget(target);
}
super.visitIfNullAssignment(node);
}
@override
void visitImportPrefixedAssignmentTarget(
ImportPrefixedAssignmentTarget node,
) {
var readMatches = _matches(node.read?.element);
var writeMatches = _matches(node.write?.element);
var kind = switch ((readMatches, writeMatches)) {
(true, true) => MatchKind.READ_WRITE,
(true, false) => MatchKind.READ,
(false, true) => MatchKind.WRITE,
(false, false) => null,
};
if (node.write case InvalidNamedWriteResolution(
:var recoveryElement,
) when kind == null && _matches(recoveryElement)) {
kind = MatchKind.REFERENCE;
}
if (kind != null) {
references.add(MatchInfo(node.name.offset, node.name.length, kind));
}
node.importPrefix.accept2(this);
}
@override
void visitImportPrefixedFunctionInvocation(
ImportPrefixedFunctionInvocation node,
) {
_visitNamedFunctionInvocation(node);
}
@override
void visitImportPrefixedNameExpression(ImportPrefixedNameExpression node) {
_recordNamedRead(node.name, node.resolution);
super.visitImportPrefixedNameExpression(node);
}
@override
void visitNamedType(NamedType node) {
if (node.element == element) {
references.add(
MatchInfo(node.name.offset, node.name.length, MatchKind.REFERENCE),
);
}
node.importPrefix?.accept2(this);
node.typeArguments?.accept2(this);
}
@override
visitPrimaryConstructorDeclaration(
covariant PrimaryConstructorDeclarationImpl node,
) {
if (node.declaredFragment?.element == element) {
if (node.constructorName case var constructorName?) {
var offset = constructorName.period.offset;
var length = constructorName.name.end - offset;
references.add(MatchInfo(offset, length, MatchKind.DECLARATION));
} else {
var end = node.typeName.end;
references.add(MatchInfo(end, 0, MatchKind.DECLARATION));
}
}
super.visitPrimaryConstructorDeclaration(node);
}
@override
void visitReceiverMethodInvocation(ReceiverMethodInvocation node) {
_visitNamedFunctionInvocation(node);
}
@override
void visitReceiverPropertyExtraction(ReceiverPropertyExtraction node) {
_recordNamedRead(node.name, node.resolution);
super.visitReceiverPropertyExtraction(node);
}
@override
void visitRedirectingConstructorInvocation(
RedirectingConstructorInvocation node,
) {
var e = node.element;
if (e == element) {
if (node.constructorSelector case var selector?) {
int offset = selector.period.offset;
int length = selector.name2.end - offset;
references.add(MatchInfo(offset, length, MatchKind.INVOCATION));
} else {
int offset = node.thisKeyword.end;
references.add(MatchInfo(offset, 0, MatchKind.INVOCATION));
}
}
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
if (node.inDeclarationContext()) {
return;
}
var e = node.element;
if (e == element) {
references.add(MatchInfo(node.offset, node.length, MatchKind.REFERENCE));
} else if (e is GetterElement && e.variable == element) {
bool inGetterContext = node.inGetterContext();
bool inSetterContext = node.inSetterContext();
MatchKind kind;
if (inGetterContext && inSetterContext) {
kind = MatchKind.READ_WRITE;
} else if (inGetterContext) {
kind = MatchKind.READ;
} else {
kind = MatchKind.WRITE;
}
references.add(MatchInfo(node.offset, node.length, kind));
}
}
@override
void visitStaticQualifier(StaticQualifier node) {
if (node.element == element) {
references.add(
MatchInfo(node.name.offset, node.name.length, MatchKind.REFERENCE),
);
}
node.importPrefix?.accept2(this);
}
@override
void visitSuperConstructorInvocation(SuperConstructorInvocation node) {
var e = node.element;
if (e == element) {
if (node.constructorSelector case var selector?) {
int offset = selector.period.offset;
int length = selector.name2.end - offset;
references.add(MatchInfo(offset, length, MatchKind.INVOCATION));
} else {
int offset = node.superKeyword.end;
references.add(MatchInfo(offset, 0, MatchKind.INVOCATION));
}
}
}
@override
void visitUnqualifiedFunctionInvocation(UnqualifiedFunctionInvocation node) {
_visitNamedFunctionInvocation(node);
}
@override
void visitUnqualifiedNameExpression(UnqualifiedNameExpression node) {
_recordNamedRead(node.name, node.resolution);
}
MatchKind _constructorReferenceKind(ConstructorReference2 node) {
return switch (node.parent2) {
ConstructorInvocation() => MatchKind.INVOCATION,
ConstructorDeclaration() => MatchKind.REFERENCE,
_ => throw StateError(
'Unexpected ConstructorReference2 parent: ${node.parent2.runtimeType}',
),
};
}
bool _matches(Element? candidate) =>
candidate == element ||
candidate is PropertyAccessorElement && candidate.variable == element;
void _recordNamedRead(
SyntacticEntity entity,
NamedReadResolution? resolution,
) {
if (resolution case InvalidNamedReadResolution(:var recoveryElement)) {
if (_matches(recoveryElement)) {
references.add(
MatchInfo(entity.offset, entity.length, MatchKind.REFERENCE),
);
}
return;
}
var readElement = resolution?.element;
if (readElement == element) {
references.add(
MatchInfo(entity.offset, entity.length, MatchKind.REFERENCE),
);
} else if (readElement is GetterElement &&
readElement.variable == element) {
references.add(MatchInfo(entity.offset, entity.length, MatchKind.READ));
}
}
void _recordNamedReadWriteTarget(NamedAssignmentTarget target) {
var readMatches = _matches(target.read?.element);
var writeMatches = _matches(target.write?.element);
var kind = switch ((readMatches, writeMatches)) {
(true, true) => MatchKind.READ_WRITE,
(true, false) => MatchKind.READ,
(false, true) => MatchKind.WRITE,
(false, false) => null,
};
if (kind != null) {
references.add(MatchInfo(target.name.offset, target.name.length, kind));
}
}
void _visitNamedFunctionInvocation(NamedFunctionInvocation node) {
var invokedElement = ElementLocatorV2.locate(node)?.baseElement;
if (invokedElement == element) {
references.add(
MatchInfo(node.name.offset, node.name.length, MatchKind.REFERENCE),
);
}
node.visitChildren2(this);
}
}