| // 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'; |
| import 'package:collection/collection.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 classes and mixins. |
| ChangeTo.annotation() : _kind = _ReplacementKind.annotation; |
| |
| /// Initialize a newly created instance that will propose classes and mixins. |
| ChangeTo.classOrMixin() : _kind = _ReplacementKind.classOrMixin; |
| |
| /// Initialize a newly created instance that will propose formal parameters. |
| ChangeTo.formalParameter() : _kind = _ReplacementKind.formalParameter; |
| |
| /// Initialize a newly created instance that will propose functions. |
| ChangeTo.function() : _kind = _ReplacementKind.function; |
| |
| /// Initialize a newly created instance that will propose getters and setters. |
| ChangeTo.getterOrSetter() : _kind = _ReplacementKind.getterOrSetter; |
| |
| /// Initialize a newly created instance that will propose methods. |
| ChangeTo.method() : _kind = _ReplacementKind.method; |
| |
| @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.formalParameter) { |
| await _proposeFormalParameter(builder); |
| } else if (_kind == _ReplacementKind.function) { |
| await _proposeFunction(builder); |
| } else if (_kind == _ReplacementKind.getterOrSetter) { |
| await _proposeGetterOrSetter(builder); |
| } else if (_kind == _ReplacementKind.method) { |
| await _proposeMethod(builder); |
| } |
| } |
| |
| Iterable<ParameterElement> _formalParameterSuggestions( |
| FunctionTypedElement element, |
| Iterable<FormalParameter> formalParameters) { |
| return element.parameters.where((superParam) => |
| superParam.isNamed && |
| !formalParameters |
| .any((param) => superParam.name == param.identifier?.name)); |
| } |
| |
| Future<void> _proposeAnnotation(ChangeBuilder builder) async { |
| final node = this.node; |
| if (node is Annotation) { |
| var name = node.name; |
| if (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 NamedType && |
| node.prefix.staticElement is PrefixElement) { |
| prefixName = node.prefix.name; |
| node = node.identifier; |
| } |
| // Process if looks like a type. |
| var name = nameOfType(node); |
| if (name != null) { |
| // Prepare for selecting the closest element. |
| 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.classes); |
| } |
| } |
| // 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. |
| await _suggest(builder, node, finder._element?.name); |
| } |
| } |
| |
| Future<void> _proposeClassOrMixinMember(ChangeBuilder builder, |
| Expression? target, _ElementPredicate predicate) async { |
| final node = this.node; |
| var targetIdentifierElement = |
| target is Identifier ? target.staticElement : null; |
| if (node is SimpleIdentifier) { |
| var finder = _ClosestElementFinder(node.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 (targetIdentifierElement is ExtensionElement) { |
| _updateFinderWithExtensionMembers(finder, targetIdentifierElement); |
| } else { |
| var classElement = getTargetClassElement(target); |
| if (classElement != null) { |
| _updateFinderWithClassMembers(finder, classElement); |
| } |
| } |
| // if we have close enough element, suggest to use it |
| await _suggest(builder, node, finder._element?.displayName); |
| } |
| } |
| |
| Future<void> _proposeFormalParameter(ChangeBuilder builder) async { |
| var parent = node.parent; |
| if (parent is! SuperFormalParameter) return; |
| |
| var constructorDeclaration = |
| parent.thisOrAncestorOfType<ConstructorDeclaration>(); |
| if (constructorDeclaration == null) return; |
| |
| var formalParameters = constructorDeclaration.parameters.parameters |
| .whereType<DefaultFormalParameter>(); |
| |
| var finder = |
| _ClosestElementFinder(parent.identifier.name, (Element e) => true); |
| |
| var superInvocation = constructorDeclaration.initializers.lastOrNull; |
| |
| if (superInvocation is SuperConstructorInvocation) { |
| var staticElement = superInvocation.staticElement; |
| if (staticElement == null) return; |
| |
| var list = _formalParameterSuggestions(staticElement, formalParameters); |
| finder._updateList(list); |
| } else { |
| var targetClassNode = parent.thisOrAncestorOfType<ClassDeclaration>(); |
| if (targetClassNode == null) return; |
| |
| var targetClassElement = targetClassNode.declaredElement!; |
| var superType = targetClassElement.supertype; |
| if (superType == null) return; |
| |
| for (var constructor in superType.constructors) { |
| if (constructor.name.isEmpty) { |
| var list = _formalParameterSuggestions(constructor, formalParameters); |
| finder._updateList(list); |
| break; |
| } |
| } |
| } |
| |
| // If we have a close enough element, suggest to use it. |
| await _suggest(builder, node, finder._element?.name); |
| } |
| |
| Future<void> _proposeFunction(ChangeBuilder builder) async { |
| final 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. |
| await _suggest(builder, node, finder._element?.name); |
| } |
| } |
| |
| Future<void> _proposeGetterOrSetter(ChangeBuilder builder) async { |
| final node = this.node; |
| if (node is SimpleIdentifier) { |
| // prepare target |
| Expression? target; |
| var parent = node.parent; |
| if (parent is PrefixedIdentifier) { |
| target = parent.prefix; |
| } else if (parent is PropertyAccess) { |
| target = parent.target; |
| } |
| // find getter or setter |
| var wantGetter = node.inGetterContext(); |
| var wantSetter = node.inSetterContext(); |
| await _proposeClassOrMixinMember(builder, target, (Element element) { |
| if (element is PropertyAccessorElement) { |
| return wantGetter && element.isGetter || |
| wantSetter && element.isSetter; |
| } else if (element is FieldElement) { |
| return wantGetter && element.getter != null || |
| wantSetter && element.setter != null; |
| } |
| return false; |
| }); |
| } |
| } |
| |
| Future<void> _proposeMethod(ChangeBuilder builder) async { |
| var parent = node.parent; |
| if (parent is MethodInvocation) { |
| await _proposeClassOrMixinMember(builder, parent.realTarget, |
| (Element element) => element is MethodElement && !element.isOperator); |
| } |
| } |
| |
| Future<void> _suggest( |
| ChangeBuilder builder, AstNode node, String? name) async { |
| if (name != null) { |
| _proposedName = name; |
| await builder.addDartFileEdit(file, (builder) { |
| builder.addSimpleReplacement(range.node(node), _proposedName); |
| }); |
| } |
| } |
| |
| void _updateFinderWithClassMembers( |
| _ClosestElementFinder finder, ClassElement clazz) { |
| var members = getMembers(clazz); |
| finder._updateList(members); |
| } |
| |
| void _updateFinderWithExtensionMembers( |
| _ClosestElementFinder finder, ExtensionElement? element) { |
| if (element != null) { |
| finder._updateList(getExtensionMembers(element)); |
| } |
| } |
| } |
| |
| /// 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 name = element.name; |
| if (name != null) { |
| var memberDistance = levenshtein(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, |
| formalParameter, |
| function, |
| getterOrSetter, |
| method |
| } |