blob: 54b7c0109f0d1054235f0c8680da59c069462ff6 [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: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';
/// 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 prefixes used to reference those declarations.
final Map<Element, Set<String>> movingReferences = {};
/// A map from the elements referenced by the declarations that are staying to
/// the set of prefixes used to reference those declarations.
final Map<Element, Set<String>> stayingReferences = {};
/// Analyze 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 [range].
ImportAnalyzer(this.result, String path, List<SourceRange> ranges) {
for (var unit in result.units) {
var finder = _ReferenceFinder(
_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 [range].
_ElementRecorder(this.analyzer, this.ranges);
/// Record that the [element] is declared in the library.
void recordDeclaration(Element? declaredElement) {
if (declaredElement != null) {
if (_isBeingMoved(declaredElement.nameOffset)) {
analyzer.movingDeclarations.add(declaredElement);
} else {
analyzer.stayingDeclarations.add(declaredElement);
}
}
}
/// Record that the [element] is referenced in the library at the
/// [referenceOffset]. The [prefix] is the prefix used to reference the
/// element, or an empty string if no prefix was used.
void recordReference(
Element referencedElement, int referenceOffset, String prefix) {
if (referencedElement is PropertyAccessorElement &&
referencedElement.isSynthetic) {
referencedElement = referencedElement.variable;
}
if (_isBeingMoved(referenceOffset)) {
var prefixes =
analyzer.movingReferences.putIfAbsent(referencedElement, () => {});
prefixes.add(prefix);
} else {
var prefixes =
analyzer.stayingReferences.putIfAbsent(referencedElement, () => {});
prefixes.add(prefix);
}
}
// 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;
/// Initialize a newly created finder to send information to the [recorder].
_ReferenceFinder(this.recorder);
@override
void visitAssignmentExpression(AssignmentExpression node) {
var writeElement = node.writeElement;
if (writeElement != null) {
recorder.recordReference(writeElement, node.offset,
_getPrefixFromExpression(node.leftHandSide));
}
super.visitAssignmentExpression(node);
}
@override
void visitClassDeclaration(ClassDeclaration node) {
recorder.recordDeclaration(node.declaredElement);
super.visitClassDeclaration(node);
}
@override
void visitClassTypeAlias(ClassTypeAlias node) {
recorder.recordDeclaration(node.declaredElement);
super.visitClassTypeAlias(node);
}
@override
void visitEnumDeclaration(EnumDeclaration node) {
recorder.recordDeclaration(node.declaredElement);
super.visitEnumDeclaration(node);
}
@override
void visitExtensionDeclaration(ExtensionDeclaration node) {
var extensionElement = node.declaredElement;
if (extensionElement != null) {
recorder.recordDeclaration(extensionElement);
for (var accessor in extensionElement.accessors) {
if (!accessor.isStatic && !accessor.isSynthetic) {
recorder.recordDeclaration(accessor);
}
}
for (var field in extensionElement.fields) {
if (!field.isStatic && !field.isSynthetic) {
recorder.recordDeclaration(field);
}
}
for (var method in extensionElement.methods) {
if (!method.isStatic) {
recorder.recordDeclaration(method);
}
}
}
super.visitExtensionDeclaration(node);
}
@override
void visitFunctionDeclaration(FunctionDeclaration node) {
recorder.recordDeclaration(node.declaredElement);
super.visitFunctionDeclaration(node);
}
@override
void visitFunctionTypeAlias(FunctionTypeAlias node) {
recorder.recordDeclaration(node.declaredElement);
super.visitFunctionTypeAlias(node);
}
@override
void visitGenericTypeAlias(GenericTypeAlias node) {
recorder.recordDeclaration(node.declaredElement);
super.visitGenericTypeAlias(node);
}
@override
void visitMixinDeclaration(MixinDeclaration node) {
recorder.recordDeclaration(node.declaredElement);
super.visitMixinDeclaration(node);
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
var element = node.staticElement;
if (element is ExecutableElement &&
element.enclosingElement is ExtensionElement &&
!element.isStatic) {
element = element.enclosingElement;
}
if (element != null && element.isInterestingReference) {
recorder.recordReference(
element, node.offset, _getPrefixForIdentifier(node));
}
super.visitSimpleIdentifier(node);
}
@override
void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
for (var variable in node.variables.variables) {
recorder.recordDeclaration(variable.declaredElement);
}
super.visitTopLevelVariableDeclaration(node);
}
/// Return the prefix used to reference the [node].
String _getPrefixForIdentifier(SimpleIdentifier node) {
return _getPrefixFromExpression(node.parent);
}
/// Return the prefix used to reference the [node].
String _getPrefixFromExpression(AstNode? node) {
if (node is PrefixedIdentifier) {
var prefix = node.prefix;
if (prefix.staticElement is PrefixElement) {
return prefix.name;
}
} else if (node is PropertyAccess) {
// TODO(brianwilkerson) Remove this branch after all prefixes are
// rewritten as a `PrefixedIdentifier`.
var propertyName = node.propertyName;
if (propertyName.staticElement is PrefixElement) {
return propertyName.name;
}
}
return '';
}
}
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 => enclosingElement is CompilationUnitElement;
}