blob: b4a176fedf2bf9d42a456c9e5204ffbc4f0eeee5 [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/dart/abstract_producer.dart';
import 'package:analysis_server/src/services/correction/fix.dart';
import 'package:analysis_server/src/services/correction/levenshtein.dart';
import 'package:analysis_server/src/services/correction/util.dart';
import 'package:analysis_server/src/services/search/hierarchy.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
/// A predicate is a one-argument function that returns a boolean value.
typedef _ElementPredicate = bool Function(Element argument);
class ChangeTo extends CorrectionProducer {
/// The kind of elements that should be proposed.
final _ReplacementKind _kind;
/// The name to which the undefined name will be changed.
String _proposedName;
/// Initialize a newly created instance that will propose elements of the
/// given [_kind].
ChangeTo(this._kind);
@override
List<Object> get fixArguments => [_proposedName];
@override
FixKind get fixKind => DartFixKind.CHANGE_TO;
@override
Future<void> compute(ChangeBuilder builder) async {
// TODO(brianwilkerson) Unify these separate methods as much as is
// reasonably possible.
// TODO(brianwilkerson) Consider proposing all of the names within a
// reasonable distance, rather than just the first near match we find.
if (_kind == _ReplacementKind.annotation) {
await _proposeAnnotation(builder);
} else if (_kind == _ReplacementKind.classOrMixin) {
await _proposeClassOrMixin(builder, node);
} else if (_kind == _ReplacementKind.function) {
await _proposeFunction(builder);
} else if (_kind == _ReplacementKind.getterOrSetter) {
await _proposeGetterOrSetter(builder);
} else if (_kind == _ReplacementKind.method) {
await _proposeMethod(builder);
}
}
Future<void> _proposeAnnotation(ChangeBuilder builder) async {
var node = this.node;
if (node is Annotation) {
var name = node.name;
if (name != null && name.staticElement == null) {
if (node.arguments != null) {
await _proposeClassOrMixin(builder, name);
}
}
}
}
Future<void> _proposeClassOrMixin(ChangeBuilder builder, AstNode node) async {
// Prepare the optional import prefix name.
String prefixName;
if (node is PrefixedIdentifier &&
node.parent is TypeName &&
node.prefix.staticElement is PrefixElement) {
prefixName = (node as PrefixedIdentifier).prefix.name;
node = (node as PrefixedIdentifier).identifier;
}
// Process if looks like a type.
if (mightBeTypeIdentifier(node)) {
// Prepare for selecting the closest element.
var name = (node as SimpleIdentifier).name;
var finder = _ClosestElementFinder(
name, (Element element) => element is ClassElement);
// Check elements of this library.
if (prefixName == null) {
for (var unit in resolvedResult.libraryElement.units) {
finder._updateList(unit.types);
}
}
// Check elements from imports.
for (var importElement in resolvedResult.libraryElement.imports) {
if (importElement.prefix?.name == prefixName) {
var namespace = getImportNamespace(importElement);
finder._updateList(namespace.values);
}
}
// If we have a close enough element, suggest to use it.
if (finder._element != null) {
_proposedName = finder._element.name;
if (_proposedName != null) {
await builder.addDartFileEdit(file, (builder) {
builder.addSimpleReplacement(range.node(node), _proposedName);
});
}
}
}
}
Future<void> _proposeClassOrMixinMember(ChangeBuilder builder,
Expression target, _ElementPredicate predicate) async {
if (node is SimpleIdentifier) {
var name = (node as SimpleIdentifier).name;
var finder = _ClosestElementFinder(name, predicate);
// unqualified invocation
if (target == null) {
var clazz = node.thisOrAncestorOfType<ClassDeclaration>();
if (clazz != null) {
var classElement = clazz.declaredElement;
_updateFinderWithClassMembers(finder, classElement);
}
} else if (target is ExtensionOverride) {
_updateFinderWithExtensionMembers(finder, target.staticElement);
} else if (target is Identifier &&
target.staticElement is ExtensionElement) {
_updateFinderWithExtensionMembers(finder, target.staticElement);
} else {
var classElement = getTargetClassElement(target);
if (classElement != null) {
_updateFinderWithClassMembers(finder, classElement);
}
}
// if we have close enough element, suggest to use it
if (finder._element != null) {
_proposedName = finder._element.displayName;
await builder.addDartFileEdit(file, (builder) {
builder.addSimpleReplacement(range.node(node), _proposedName);
});
}
}
}
Future<void> _proposeFunction(ChangeBuilder builder) async {
var node = this.node;
if (node is SimpleIdentifier) {
// Prepare the optional import prefix name.
String prefixName;
{
var invocation = node.parent;
if (invocation is MethodInvocation && invocation.methodName == node) {
var target = invocation.target;
if (target is SimpleIdentifier &&
target.staticElement is PrefixElement) {
prefixName = target.name;
}
}
}
// Prepare for selecting the closest element.
var finder = _ClosestElementFinder(
node.name, (Element element) => element is FunctionElement);
// Check to this library units.
if (prefixName == null) {
for (var unit in resolvedResult.libraryElement.units) {
finder._updateList(unit.functions);
}
}
// Check unprefixed imports.
for (var importElement in resolvedResult.libraryElement.imports) {
if (importElement.prefix?.name == prefixName) {
var namespace = getImportNamespace(importElement);
finder._updateList(namespace.values);
}
}
// If we have a close enough element, suggest to use it.
if (finder._element != null) {
_proposedName = finder._element.name;
await builder.addDartFileEdit(file, (builder) {
builder.addSimpleReplacement(range.node(node), _proposedName);
});
}
}
}
Future<void> _proposeGetterOrSetter(ChangeBuilder builder) async {
var node = this.node;
if (node is SimpleIdentifier) {
// prepare target
Expression target;
if (node.parent is PrefixedIdentifier) {
target = (node.parent as PrefixedIdentifier).prefix;
} else if (node.parent is PropertyAccess) {
target = (node.parent as PropertyAccess).target;
}
// find getter
if (node.inGetterContext()) {
await _proposeClassOrMixinMember(builder, target, (Element element) {
return element is PropertyAccessorElement && element.isGetter ||
element is FieldElement && element.getter != null;
});
}
// find setter
if (node.inSetterContext()) {
await _proposeClassOrMixinMember(builder, target, (Element element) {
return element is PropertyAccessorElement && element.isSetter ||
element is FieldElement && element.setter != null;
});
}
}
}
Future<void> _proposeMethod(ChangeBuilder builder) async {
if (node.parent is MethodInvocation) {
var invocation = node.parent as MethodInvocation;
await _proposeClassOrMixinMember(builder, invocation.realTarget,
(Element element) => element is MethodElement && !element.isOperator);
}
}
void _updateFinderWithClassMembers(
_ClosestElementFinder finder, ClassElement clazz) {
if (clazz != null) {
var members = getMembers(clazz);
finder._updateList(members);
}
}
void _updateFinderWithExtensionMembers(
_ClosestElementFinder finder, ExtensionElement element) {
if (element != null) {
finder._updateList(getExtensionMembers(element));
}
}
/// Return an instance of this class that will propose classes and mixins.
/// Used as a tear-off in `FixProcessor`.
static ChangeTo annotation() => ChangeTo(_ReplacementKind.annotation);
/// Return an instance of this class that will propose classes and mixins.
/// Used as a tear-off in `FixProcessor`.
static ChangeTo classOrMixin() => ChangeTo(_ReplacementKind.classOrMixin);
/// Return an instance of this class that will propose functions. Used as a
/// tear-off in `FixProcessor`.
static ChangeTo function() => ChangeTo(_ReplacementKind.function);
/// Return an instance of this class that will propose getters and setters.
/// Used as a tear-off in `FixProcessor`.
static ChangeTo getterOrSetter() => ChangeTo(_ReplacementKind.getterOrSetter);
/// Return an instance of this class that will propose methods. Used as a
/// tear-off in `FixProcessor`.
static ChangeTo method() => ChangeTo(_ReplacementKind.method);
}
/// Helper for finding [Element] with name closest to the given.
class _ClosestElementFinder {
/// The maximum Levenshtein distance between the existing name and a possible
/// replacement before the replacement is deemed to not be worth offering.
static const _maxDistance = 3;
/// The name to be replaced.
final String _targetName;
/// A function used to filter the possible elements to those of the right
/// kind.
final _ElementPredicate _predicate;
int _distance = _maxDistance;
Element _element;
_ClosestElementFinder(this._targetName, this._predicate);
void _update(Element element) {
if (_predicate(element)) {
var memberDistance = levenshtein(element.name, _targetName, _distance);
if (memberDistance < _distance) {
_element = element;
_distance = memberDistance;
}
}
}
void _updateList(Iterable<Element> elements) {
for (var element in elements) {
_update(element);
}
}
}
/// A representation of the kind of element that should be suggested.
enum _ReplacementKind {
annotation,
classOrMixin,
function,
getterOrSetter,
method
}