blob: 966100ef45dde60b37f33a4587baf07f17131388 [file] [log] [blame]
// 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: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/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? getElementOfNode2(AstNode? node) {
if (node == null) {
return null;
}
if (node is SimpleIdentifier && node.parent is LibraryIdentifier) {
node = node.parent;
}
if (node is LibraryIdentifier) {
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.element2;
default:
element = ElementLocator.locate2(node);
}
if (node is SimpleIdentifier && element is PrefixElement) {
var parent = node.parent;
if (parent is ImportDirective) {
element = MockLibraryImportElement(parent.libraryImport!);
} else {
element = _getImportElementInfo2(node);
}
} else if (node is ImportPrefixReference && element is PrefixElement) {
element = _getImportElementInfoFromReference(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 ConstructorElementImpl2 && constructor.isSynthetic) {
var enclosing = constructor.enclosingElement;
if (enclosing is ClassElementImpl2 && 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 [LibraryImportElement] that is referenced by [prefixNode] with a
/// [PrefixElement], maybe `null`.
MockLibraryImportElement? _getImportElementInfo2(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 is PrefixedIdentifier) {
var prefixed = parent;
if (prefixed.prefix == prefixNode) {
usedElement = prefixed.element;
}
} else if (parent is MethodInvocation) {
var invocation = parent;
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 [LibraryImportElement] that is referenced by [prefixNode] with a
/// [PrefixElement], maybe `null`.
MockLibraryImportElement? _getImportElementInfoFromReference(
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.element2;
} else if (parent is NamedType) {
usedElement = parent.element2;
}
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 [LibraryImportElement] 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 [LibraryImportElement]s.
MockLibraryImportElement? _getMockImportElement(
LibraryFragmentImpl libraryFragment,
String prefix,
Element element,
Map<LibraryImport, Set<Element>> importElementsMap,
) {
if (element.enclosingElement is! LibraryElement) {
return null;
}
var usedLibrary = element.library2;
// find ImportElement that imports used library with used prefix
List<LibraryImport>? candidates;
var libraryImports =
libraryFragment.withEnclosing2
.expand((fragment) => fragment.libraryImports2)
.toList();
for (var importElement in libraryImports) {
// required library
if (importElement.importedLibrary2 != usedLibrary) {
continue;
}
// required prefix
var prefixElement = importElement.prefix2?.element;
if (prefixElement == null) {
continue;
}
if (prefix != prefixElement.name3) {
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 GeneralizingAstVisitor<void> {
final Element element;
final List<MatchInfo> references = [];
ReferencesCollector(this.element);
@override
void visitAssignmentExpression(AssignmentExpression node) {
var writeElement = node.writeElement2;
if (writeElement is PropertyAccessorElement) {
var kind = MatchKind.WRITE;
if (writeElement.variable3 == element || writeElement == element) {
if (node.leftHandSide is SimpleIdentifier) {
references.add(
MatchInfo(node.leftHandSide.offset, node.leftHandSide.length, kind),
);
} else if (node.leftHandSide is PrefixedIdentifier) {
var prefixIdentifier = node.leftHandSide as PrefixedIdentifier;
references.add(
MatchInfo(
prefixIdentifier.identifier.offset,
prefixIdentifier.identifier.length,
kind,
),
);
} else if (node.leftHandSide is PropertyAccess) {
var accessor = node.leftHandSide as PropertyAccess;
references.add(
MatchInfo(accessor.propertyName.offset, accessor.length, kind),
);
}
}
}
var readElement = node.readElement2;
if (readElement is PropertyAccessorElement) {
if (readElement.variable3 == element) {
references.add(
MatchInfo(
node.rightHandSide.offset,
node.rightHandSide.length,
MatchKind.READ,
),
);
}
}
}
@override
visitCommentReference(CommentReference node) {
var expression = node.expression;
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
visitConstructorDeclaration(covariant ConstructorDeclarationImpl node) {
var fragment = node.declaredFragment;
if (fragment?.element == element) {
if (fragment!.name2 == 'new') {
references.add(
MatchInfo(
fragment.nameOffset + fragment.nameLength,
0,
MatchKind.DECLARATION,
),
);
} else {
var offset = node.period!.offset;
var length = node.name!.end - offset;
references.add(MatchInfo(offset, length, MatchKind.DECLARATION));
}
}
super.visitConstructorDeclaration(node);
}
@override
void visitConstructorName(ConstructorName node) {
var e = node.element?.baseElement;
e = _getActualConstructorElement(e);
MatchKind kind;
int offset;
int length;
if (e == element) {
if (node.parent is ConstructorReference) {
kind = MatchKind.REFERENCE_BY_CONSTRUCTOR_TEAR_OFF;
} else if (node.parent is InstanceCreationExpression) {
kind = MatchKind.INVOCATION;
} else {
kind = MatchKind.REFERENCE;
}
if (node.name != null) {
offset = node.period!.offset;
length = node.name!.end - offset;
} else {
offset = node.type.end;
length = 0;
}
references.add(MatchInfo(offset, length, kind));
} else if (e != null && e.enclosingElement == element) {
kind = MatchKind.REFERENCE;
offset = node.offset;
length = element.name3?.length ?? 0;
references.add(MatchInfo(offset, length, kind));
}
}
@override
void visitEnumConstantDeclaration(EnumConstantDeclaration node) {
var constructorElement = node.constructorElement2;
if (constructorElement != null && constructorElement == element) {
int offset;
int length;
var constructorSelector = node.arguments?.constructorSelector;
if (constructorSelector != null) {
offset = constructorSelector.period.offset;
length = constructorSelector.name.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 visitNamedType(NamedType node) {
if (node.element2 == element) {
references.add(
MatchInfo(node.name.offset, node.name.length, MatchKind.REFERENCE),
);
}
node.importPrefix?.accept(this);
node.typeArguments?.accept(this);
}
@override
void visitRedirectingConstructorInvocation(
RedirectingConstructorInvocation node,
) {
var e = node.element;
if (e == element) {
if (node.constructorName != null) {
int offset = node.period!.offset;
int length = node.constructorName!.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.variable3 == 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 visitSuperConstructorInvocation(SuperConstructorInvocation node) {
var e = node.element;
if (e == element) {
if (node.constructorName != null) {
int offset = node.period!.offset;
int length = node.constructorName!.end - offset;
references.add(MatchInfo(offset, length, MatchKind.INVOCATION));
} else {
int offset = node.superKeyword.end;
references.add(MatchInfo(offset, 0, MatchKind.INVOCATION));
}
}
}
}