blob: 21e053691b2a3f9363a8bf1b47e119ac9ef9702f [file] [log] [blame]
// Copyright (c) 2022, 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/utilities/extensions/object.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:collection/collection.dart';
/// A utility class used to analyze a library from which some set of
/// declarations are being moved in order to compute the set of changes needed
/// in order for the imports to be correct in both the library from which the
/// declarations will be removed and the library to which the declarations will
/// be added.
class ImportAnalyzer {
/// The result of resolving the library containing the declarations to be
/// moved.
final ResolvedLibraryResult result;
/// The elements for the declarations to be moved.
final Set<Element> movingDeclarations = {};
/// The elements for the declarations that are staying.
final Set<Element> stayingDeclarations = {};
/// A map from the elements referenced by the declarations to be moved to the
/// set of imports used to reference those declarations.
final Map<Element, Set<LibraryImport>> movingReferences = {};
/// A map from the elements referenced by the declarations that are staying to
/// the set of imports used to reference those declarations.
final Map<Element, Set<LibraryImport>> stayingReferences = {};
/// Analyzes the given library [result] to find the declarations and
/// references being moved and that are staying.
///
/// The declarations being moved are in the file at the given [path] in the
/// given [ranges].
ImportAnalyzer(this.result, String path, List<SourceRange> ranges) {
for (var unit in result.units) {
var finder = _ReferenceFinder(
unit,
_ElementRecorder(this, path == unit.path ? ranges : []),
);
unit.unit.accept(finder);
}
// Remove references that will be within the same file.
for (var element in movingDeclarations) {
movingReferences.remove(element);
}
for (var element in stayingDeclarations) {
stayingReferences.remove(element);
}
}
/// Return `true` if there are any references in the code that's being moved
/// to any of the declarations that are staying. If there are, then the
/// library to which the declarations are being moved needs to have an import
/// for the library from which they are being moved.
bool get hasMovingReferenceToStayingDeclaration {
for (var declaration in stayingDeclarations) {
if (movingReferences.containsKey(declaration)) {
return true;
}
}
return false;
}
/// Return `true` if there are any references in the code that's staying to
/// any of the declarations that are being moved. If there are, then the
/// library from which the declarations are being moved needs to have an
/// import for the library to which they are being moved.
bool get hasStayingReferenceToMovingDeclaration {
for (var declaration in movingDeclarations) {
if (stayingReferences.containsKey(declaration)) {
return true;
}
}
return false;
}
}
class _ElementRecorder {
/// The import analyzer to which declaration and reference information will be
/// sent.
final ImportAnalyzer analyzer;
/// The range of characters being moved, or `null` if the code being moved is
/// in a different compilation unit that the one currently being visited.
final List<SourceRange> ranges;
/// Initialize a newly created recorder to use the [analyzer] to record
/// declarations of and references to elements, based on whether the reference
/// is within the [ranges].
_ElementRecorder(this.analyzer, this.ranges);
/// Record that the [declaredElement] is declared in the library.
///
/// The [offset] is the decision offset, used to decide whether the element
/// is being moved. Not necessary the offset of the name. For example to
/// a [MethodDeclaration] it would be the offset of the enclosing
/// [ClassDeclaration] because we move the whole top-level declarations.
void recordDeclaration(int offset, Element? declaredElement) {
if (declaredElement == null) {
return;
}
if (_isBeingMoved(offset)) {
analyzer.movingDeclarations.add(declaredElement);
} else {
analyzer.stayingDeclarations.add(declaredElement);
}
}
/// Record that [referencedElement] is referenced in the library at the
/// [referenceOffset]. [import] is the specific import used to reference the
/// including any prefix, show, hide.
void recordReference(
Element referencedElement,
int referenceOffset,
LibraryImport? import,
) {
if (referencedElement is PropertyAccessorElement) {
if (referencedElement.isSynthetic) {
var variable = referencedElement.variable;
if (variable == null) {
return;
}
referencedElement = variable;
}
}
if (_isBeingMoved(referenceOffset)) {
var imports = analyzer.movingReferences.putIfAbsent(
referencedElement,
() => {},
);
if (import != null) {
imports.add(import);
}
} else {
var imports = analyzer.stayingReferences.putIfAbsent(
referencedElement,
() => {},
);
if (import != null) {
imports.add(import);
}
}
}
// Return `true` if the code at the [offset] is being moved to a different
// file.
bool _isBeingMoved(int offset) {
for (var range in ranges) {
if (range.contains(offset)) {
return true;
}
}
return false;
}
}
class _ReferenceFinder extends RecursiveAstVisitor<void> {
/// The import analyzer to which declaration and reference information will be
/// sent.
final _ElementRecorder recorder;
/// The unit being searched for references.
final ResolvedUnitResult unit;
/// A mapping of prefixes to the imports with those prefixes. An
/// empty string is used for unprefixed imports.
///
/// Library imports are ordered the same as they appear in the source file
/// (since this is a `LinkedHashSet`).
final _importsByPrefix = <String, Set<LibraryImport>>{};
/// Initialize a newly created finder to send information to the [recorder].
_ReferenceFinder(this.unit, this.recorder) {
for (var import in unit.libraryElement2.firstFragment.libraryImports2) {
_importsByPrefix
.putIfAbsent(import.prefix2?.element.name ?? '', () => {})
.add(import);
}
}
@override
void visitAssignmentExpression(AssignmentExpression node) {
_recordReference(node.writeElement, node, node.leftHandSide);
_recordReference(node.readElement, node, node.leftHandSide);
super.visitAssignmentExpression(node);
}
@override
void visitBinaryExpression(BinaryExpression node) {
_recordReference(node.element, node, node.parent);
super.visitBinaryExpression(node);
}
@override
void visitClassDeclaration(ClassDeclaration node) {
recorder.recordDeclaration(node.offset, node.declaredFragment?.element);
super.visitClassDeclaration(node);
}
@override
void visitClassTypeAlias(ClassTypeAlias node) {
recorder.recordDeclaration(node.offset, node.declaredFragment?.element);
super.visitClassTypeAlias(node);
}
@override
void visitEnumDeclaration(EnumDeclaration node) {
recorder.recordDeclaration(node.offset, node.declaredFragment?.element);
super.visitEnumDeclaration(node);
}
@override
void visitExtensionDeclaration(ExtensionDeclaration node) {
var extensionElement = node.declaredFragment?.element;
if (extensionElement != null) {
recorder.recordDeclaration(node.offset, extensionElement);
for (var getter in extensionElement.getters) {
if (!getter.isStatic && !getter.isSynthetic) {
recorder.recordDeclaration(node.offset, getter);
}
}
for (var setter in extensionElement.setters) {
if (!setter.isStatic && !setter.isSynthetic) {
recorder.recordDeclaration(node.offset, setter);
}
}
for (var field in extensionElement.fields) {
if (!field.isStatic && !field.isSynthetic) {
recorder.recordDeclaration(node.offset, field);
}
}
for (var method in extensionElement.methods) {
if (!method.isStatic) {
recorder.recordDeclaration(node.offset, method);
}
}
}
super.visitExtensionDeclaration(node);
}
@override
void visitFunctionDeclaration(FunctionDeclaration node) {
recorder.recordDeclaration(node.offset, node.declaredFragment?.element);
super.visitFunctionDeclaration(node);
}
@override
void visitFunctionTypeAlias(FunctionTypeAlias node) {
recorder.recordDeclaration(node.offset, node.declaredFragment?.element);
super.visitFunctionTypeAlias(node);
}
@override
void visitGenericTypeAlias(GenericTypeAlias node) {
recorder.recordDeclaration(node.offset, node.declaredFragment?.element);
super.visitGenericTypeAlias(node);
}
@override
void visitMixinDeclaration(MixinDeclaration node) {
recorder.recordDeclaration(node.offset, node.declaredFragment?.element);
super.visitMixinDeclaration(node);
}
@override
void visitNamedType(NamedType node) {
_recordReference(node.element, node, node);
super.visitNamedType(node);
}
@override
void visitPostfixExpression(PostfixExpression node) {
_recordReference(node.writeElement, node, node.operand);
_recordReference(node.readElement, node, node.operand);
super.visitPostfixExpression(node);
}
@override
void visitPrefixExpression(PrefixExpression node) {
_recordReference(node.writeElement, node, node.operand);
_recordReference(node.readElement, node, node.operand);
super.visitPrefixExpression(node);
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
_recordReference(node.element, node, node.parent);
super.visitSimpleIdentifier(node);
}
@override
void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
for (var variable in node.variables.variables) {
recorder.recordDeclaration(
node.offset,
variable.declaredFragment?.element,
);
}
super.visitTopLevelVariableDeclaration(node);
}
/// Finds the [LibraryImport] that is used to import [element] for use
/// in [node].
LibraryImport? _getImportForElement(AstNode? node, Element element) {
var prefix = _getPrefixFromExpression(node)?.name;
var lookupName = () {
var name = element.name;
if (name == null) {
return null;
}
if (element is SetterElement) {
return '$name=';
}
return name;
}();
// We cannot locate imports for unnamed elements.
if (lookupName == null) {
return null;
}
var import =
_importsByPrefix[prefix ?? '']?.where((import) {
// Check if this import is providing our element with the correct
// prefix/name.
var exportedElement =
prefix != null
? import.namespace.getPrefixed2(prefix, lookupName)
: import.namespace.get2(lookupName);
return exportedElement == element;
}).firstOrNull;
// Extensions can be used without a prefix, so we can use any import that
// brings in the extension.
if (import == null && prefix == null && element is ExtensionElement) {
import =
_importsByPrefix.values.flattenedToList
.where(
(import) =>
// Because we don't know what prefix we're looking for (any is
// allowed), use the imports own prefix when checking for the
// element.
import.namespace.getPrefixed2(
import.prefix2?.element.name ?? '',
lookupName,
) ==
element,
)
.firstOrNull;
}
return import;
}
/// Return the prefix used in [node].
PrefixElement? _getPrefixFromExpression(AstNode? node) {
if (node is PrefixedIdentifier) {
var prefix = node.prefix;
var element = prefix.element;
if (element is PrefixElement) {
return element;
}
} else if (node is PropertyAccess) {
var target = node.target;
if (target is PrefixedIdentifier) {
var element = target.prefix.element;
if (element is PrefixElement) {
return element;
}
}
} else if (node is MethodInvocation) {
var target = node.target;
if (target is SimpleIdentifier) {
var element = target.element;
if (element is PrefixElement) {
return element;
}
}
} else if (node is NamedType) {
return node.importPrefix?.element.ifTypeOrNull();
}
return null;
}
/// Records a reference to [element] (if not null) at the offset of [node],
/// extracting any prefix from [prefixNode].
void _recordReference(Element? element, AstNode node, AstNode? prefixNode) {
if (element is ExecutableElement &&
element.enclosingElement is ExtensionElement &&
!element.isStatic) {
element = element.enclosingElement;
}
if (element == null) {
return;
}
if (!element.isInterestingReference) {
return;
}
var import = _getImportForElement(prefixNode, element);
recorder.recordReference(element, node.offset, import);
}
}
extension on Element {
/// Return `true` if this element reference is an interesting reference from
/// the perspective of determining which imports need to be added.
bool get isInterestingReference {
return this is! PrefixElement && enclosingElement is LibraryElement;
}
}