| // Copyright (c) 2017, 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 'dart:collection'; |
| |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/token.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_common.dart' hide Element; |
| import 'package:analyzer_plugin/src/utilities/completion/completion_target.dart'; |
| import 'package:analyzer_plugin/src/utilities/completion/suggestion_builder.dart'; |
| import 'package:analyzer_plugin/src/utilities/visitors/local_declaration_visitor.dart'; |
| import 'package:analyzer_plugin/utilities/completion/completion_core.dart'; |
| import 'package:analyzer_plugin/utilities/completion/relevance.dart'; |
| import 'package:analyzer_plugin/utilities/completion/suggestion_builder.dart'; |
| |
| /// A completion contributor that will generate suggestions for instance |
| /// invocations and accesses. |
| class TypeMemberContributor implements CompletionContributor { |
| /// Plugin contributors should primarily overload this function. |
| /// Should more parameters be needed for autocompletion needs, the |
| /// overloaded function should define those parameters and |
| /// call on `computeSuggestionsWithEntryPoint`. |
| @override |
| Future<void> computeSuggestions( |
| DartCompletionRequest request, CompletionCollector collector) async { |
| var containingLibrary = request.result.libraryElement; |
| |
| // Recompute the target since resolution may have changed it |
| var expression = _computeDotTarget(request.result.unit, request.offset); |
| if (expression == null || expression.isSynthetic) { |
| return; |
| } |
| _computeSuggestions(request, collector, containingLibrary, expression); |
| } |
| |
| /// Clients should not overload this function. |
| Future<void> computeSuggestionsWithEntryPoint(DartCompletionRequest request, |
| CompletionCollector collector, AstNode entryPoint) async { |
| var containingLibrary = request.result.libraryElement; |
| |
| // Recompute the target since resolution may have changed it |
| var expression = _computeDotTarget(entryPoint, request.offset); |
| if (expression == null || expression.isSynthetic) { |
| return; |
| } |
| _computeSuggestions(request, collector, containingLibrary, expression); |
| } |
| |
| /// Update the completion [target] and [dotTarget] based on the given [unit]. |
| Expression? _computeDotTarget(AstNode entryPoint, int offset) { |
| var target = CompletionTarget.forOffset(entryPoint, offset); |
| return target.dotTarget; |
| } |
| |
| void _computeSuggestions( |
| DartCompletionRequest request, |
| CompletionCollector collector, |
| LibraryElement containingLibrary, |
| Expression expression) { |
| if (expression is Identifier) { |
| var element = expression.staticElement; |
| if (element is ClassElement) { |
| // Suggestions provided by StaticMemberContributor |
| return; |
| } |
| if (element is PrefixElement) { |
| // Suggestions provided by LibraryMemberContributor |
| return; |
| } |
| } |
| |
| // Determine the target expression's type |
| var type = expression.staticType; |
| if (type == null || type.isDynamic) { |
| // If the expression does not provide a good type |
| // then attempt to get a better type from the element |
| if (expression is Identifier) { |
| var elem = expression.staticElement; |
| if (elem is FunctionTypedElement) { |
| type = elem.returnType; |
| } else if (elem is ParameterElement) { |
| type = elem.type; |
| } else if (elem is LocalVariableElement) { |
| type = elem.type; |
| } |
| if ((type == null || type.isDynamic) && |
| expression is SimpleIdentifier) { |
| // If the element does not provide a good type |
| // then attempt to get a better type from a local declaration |
| var visitor = _LocalBestTypeVisitor(expression.name, request.offset); |
| if (visitor.visit(expression) && visitor.typeFound != null) { |
| type = visitor.typeFound; |
| } |
| } |
| } |
| } |
| String? containingMethodName; |
| if (expression is SuperExpression && type is InterfaceType) { |
| // Suggest members from superclass if target is "super" |
| type = type.superclass; |
| // Determine the name of the containing method because |
| // the most likely completion is a super expression with same name |
| var containingMethod = |
| expression.thisOrAncestorOfType<MethodDeclaration>(); |
| var id = containingMethod?.name2; |
| if (id != null) { |
| containingMethodName = id.lexeme; |
| } |
| } |
| if (type == null || type.isDynamic) { |
| // Suggest members from object if target is "dynamic" |
| type = request.result.typeProvider.objectType; |
| } |
| |
| // Build the suggestions |
| if (type is InterfaceType) { |
| var builder = _SuggestionBuilder( |
| request.resourceProvider, collector, containingLibrary); |
| builder.buildSuggestions(type, containingMethodName); |
| } |
| } |
| } |
| |
| /// An [AstVisitor] which looks for a declaration with the given name |
| /// and if found, tries to determine a type for that declaration. |
| class _LocalBestTypeVisitor extends LocalDeclarationVisitor { |
| /// The name for the declaration to be found. |
| final String targetName; |
| |
| /// The best type for the found declaration, |
| /// or `null` if no declaration found or failed to determine a type. |
| DartType? typeFound; |
| |
| /// Construct a new instance to search for a declaration |
| _LocalBestTypeVisitor(this.targetName, int offset) : super(offset); |
| |
| @override |
| void declaredClass(ClassDeclaration declaration) { |
| if (declaration.name2.lexeme == targetName) { |
| // no type |
| finished(); |
| } |
| } |
| |
| @override |
| void declaredClassTypeAlias(ClassTypeAlias declaration) { |
| if (declaration.name2.lexeme == targetName) { |
| // no type |
| finished(); |
| } |
| } |
| |
| @override |
| void declaredExtension(ExtensionDeclaration declaration) {} |
| |
| @override |
| void declaredField(FieldDeclaration fieldDecl, VariableDeclaration varDecl) { |
| if (varDecl.name2.lexeme == targetName) { |
| // Type provided by the element in computeFull above |
| finished(); |
| } |
| } |
| |
| @override |
| void declaredFunction(FunctionDeclaration declaration) { |
| if (declaration.name2.lexeme == targetName) { |
| var typeName = declaration.returnType; |
| if (typeName != null) { |
| typeFound = typeName.type; |
| } |
| finished(); |
| } |
| } |
| |
| @override |
| void declaredFunctionTypeAlias(FunctionTypeAlias declaration) { |
| if (declaration.name2.lexeme == targetName) { |
| var typeName = declaration.returnType; |
| if (typeName != null) { |
| typeFound = typeName.type; |
| } |
| finished(); |
| } |
| } |
| |
| @override |
| void declaredGenericTypeAlias(GenericTypeAlias declaration) { |
| if (declaration.name2.lexeme == targetName) { |
| var typeName = declaration.functionType?.returnType; |
| if (typeName != null) { |
| typeFound = typeName.type; |
| } |
| finished(); |
| } |
| } |
| |
| @override |
| void declaredLabel(Label label, bool isCaseLabel) { |
| if (label.label.name == targetName) { |
| // no type |
| finished(); |
| } |
| } |
| |
| @override |
| void declaredLocalVar( |
| Token name, |
| TypeAnnotation? type, |
| LocalVariableElement declaredElement, |
| ) { |
| if (name.lexeme == targetName) { |
| typeFound = declaredElement.type; |
| finished(); |
| } |
| } |
| |
| @override |
| void declaredMethod(MethodDeclaration declaration) { |
| if (declaration.name2.lexeme == targetName) { |
| var typeName = declaration.returnType; |
| if (typeName != null) { |
| typeFound = typeName.type; |
| } |
| finished(); |
| } |
| } |
| |
| @override |
| void declaredParam(Token name, Element? element, TypeAnnotation? type) { |
| if (name.lexeme == targetName) { |
| // Type provided by the element in computeFull above |
| finished(); |
| } |
| } |
| |
| @override |
| void declaredTopLevelVar( |
| VariableDeclarationList varList, VariableDeclaration varDecl) { |
| if (varDecl.name2.lexeme == targetName) { |
| // Type provided by the element in computeFull above |
| finished(); |
| } |
| } |
| } |
| |
| /// This class provides suggestions based upon the visible instance members in |
| /// an interface type. |
| class _SuggestionBuilder { |
| /// Enumerated value indicating that we have not generated any completions for |
| /// a given identifier yet. |
| static const int _COMPLETION_TYPE_NONE = 0; |
| |
| /// Enumerated value indicating that we have generated a completion for a |
| /// getter. |
| static const int _COMPLETION_TYPE_GETTER = 1; |
| |
| /// Enumerated value indicating that we have generated a completion for a |
| /// setter. |
| static const int _COMPLETION_TYPE_SETTER = 2; |
| |
| /// Enumerated value indicating that we have generated a completion for a |
| /// field, a method, or a getter/setter pair. |
| static const int _COMPLETION_TYPE_FIELD_OR_METHOD_OR_GETSET = 3; |
| |
| /// The resource provider used to access the file system. |
| final ResourceProvider resourceProvider; |
| |
| /// The collector being used to collect completion suggestions. |
| final CompletionCollector collector; |
| |
| /// The library containing the unit in which the completion is requested. |
| final LibraryElement containingLibrary; |
| |
| /// Map indicating, for each possible completion identifier, whether we have |
| /// already generated completions for a getter, setter, or both. The "both" |
| /// case also handles the case where have generated a completion for a method |
| /// or a field. |
| /// |
| /// Note: the enumerated values stored in this map are intended to be bitwise |
| /// compared. |
| final Map<String, int> _completionTypesGenerated = HashMap<String, int>(); |
| |
| /// Map from completion identifier to completion suggestion |
| final Map<String, CompletionSuggestion> _suggestionMap = |
| <String, CompletionSuggestion>{}; |
| |
| /// The builder used to build suggestions. |
| final SuggestionBuilder builder; |
| |
| _SuggestionBuilder( |
| this.resourceProvider, this.collector, this.containingLibrary) |
| : builder = SuggestionBuilderImpl(resourceProvider); |
| |
| Iterable<CompletionSuggestion> get suggestions => _suggestionMap.values; |
| |
| /// Create completion suggestions for 'dot' completions on the given [type]. |
| /// If the 'dot' completion is a super expression, then [containingMethodName] |
| /// is the name of the method in which the completion is requested. |
| void buildSuggestions(InterfaceType type, String? containingMethodName) { |
| // Visit all of the types in the class hierarchy, collecting possible |
| // completions. If multiple elements are found that complete to the same |
| // identifier, addSuggestion will discard all but the first (with a few |
| // exceptions to handle getter/setter pairs). |
| var types = _getTypeOrdering(type); |
| for (var targetType in types) { |
| for (var method in targetType.methods) { |
| // Exclude static methods when completion on an instance |
| if (!method.isStatic) { |
| // Boost the relevance of a super expression |
| // calling a method of the same name as the containing method |
| _addSuggestion(method, |
| relevance: method.name == containingMethodName |
| ? DART_RELEVANCE_HIGH |
| : null); |
| } |
| } |
| for (var propertyAccessor in targetType.accessors) { |
| if (!propertyAccessor.isStatic) { |
| if (propertyAccessor.isSynthetic) { |
| // Avoid visiting a field twice |
| if (propertyAccessor.isGetter) { |
| _addSuggestion(propertyAccessor.variable); |
| } |
| } else { |
| _addSuggestion(propertyAccessor); |
| } |
| } |
| } |
| } |
| for (var suggestion in suggestions) { |
| collector.addSuggestion(suggestion); |
| } |
| } |
| |
| /// Add a suggestion based upon the given element, provided that it is not |
| /// shadowed by a previously added suggestion. |
| void _addSuggestion(Element element, {int? relevance}) { |
| if (element.isPrivate) { |
| if (element.library != containingLibrary) { |
| // Do not suggest private members for imported libraries |
| return; |
| } |
| } |
| var identifier = element.displayName; |
| |
| if (relevance == null) { |
| // Decrease relevance of suggestions starting with $ |
| // https://github.com/dart-lang/sdk/issues/27303 |
| if (identifier.startsWith(r'$')) { |
| relevance = DART_RELEVANCE_LOW; |
| } else { |
| relevance = DART_RELEVANCE_DEFAULT; |
| } |
| } |
| |
| var alreadyGenerated = _completionTypesGenerated.putIfAbsent( |
| identifier, () => _COMPLETION_TYPE_NONE); |
| if (element is MethodElement) { |
| // Anything shadows a method. |
| if (alreadyGenerated != _COMPLETION_TYPE_NONE) { |
| return; |
| } |
| _completionTypesGenerated[identifier] = |
| _COMPLETION_TYPE_FIELD_OR_METHOD_OR_GETSET; |
| } else if (element is PropertyAccessorElement) { |
| if (element.isGetter) { |
| // Getters, fields, and methods shadow a getter. |
| if ((alreadyGenerated & _COMPLETION_TYPE_GETTER) != 0) { |
| return; |
| } |
| _completionTypesGenerated[identifier] = |
| _completionTypesGenerated[identifier]! | _COMPLETION_TYPE_GETTER; |
| } else { |
| // Setters, fields, and methods shadow a setter. |
| if ((alreadyGenerated & _COMPLETION_TYPE_SETTER) != 0) { |
| return; |
| } |
| _completionTypesGenerated[identifier] = |
| _completionTypesGenerated[identifier]! | _COMPLETION_TYPE_SETTER; |
| } |
| } else if (element is FieldElement) { |
| // Fields and methods shadow a field. A getter/setter pair shadows a |
| // field, but a getter or setter by itself doesn't. |
| if (alreadyGenerated == _COMPLETION_TYPE_FIELD_OR_METHOD_OR_GETSET) { |
| return; |
| } |
| _completionTypesGenerated[identifier] = |
| _COMPLETION_TYPE_FIELD_OR_METHOD_OR_GETSET; |
| } else { |
| // Unexpected element type; skip it. |
| assert(false); |
| return; |
| } |
| var suggestion = builder.forElement(element, relevance: relevance); |
| if (suggestion != null) { |
| _suggestionMap[suggestion.completion] = suggestion; |
| } |
| } |
| |
| /// Get a list of [InterfaceType]s that should be searched to find the |
| /// possible completions for an object having type [type]. |
| List<InterfaceType> _getTypeOrdering(InterfaceType type) { |
| // Candidate completions can come from [type] as well as any types above it |
| // in the class hierarchy (including mixins, superclasses, and interfaces). |
| // If a given completion identifier shows up in multiple types, we should |
| // use the element that is nearest in the superclass chain, so we will |
| // visit [type] first, then its mixins, then its superclass, then its |
| // superclass's mixins, etc., and only afterwards visit interfaces. |
| // |
| // We short-circuit loops in the class hierarchy by keeping track of the |
| // classes seen (not the interfaces) so that we won't be fooled by nonsense |
| // like "class C<T> extends C<List<T>> {}" |
| var result = <InterfaceType>[]; |
| final classesSeen = <InterfaceElement>{}; |
| var typesToVisit = <InterfaceType>[type]; |
| while (typesToVisit.isNotEmpty) { |
| var nextType = typesToVisit.removeLast(); |
| if (!classesSeen.add(nextType.element2)) { |
| // Class had already been seen, so ignore this type. |
| continue; |
| } |
| result.add(nextType); |
| // typesToVisit is a stack, so push on the interfaces first, then the |
| // superclass, then the mixins. This will ensure that they are visited |
| // in the reverse order. |
| typesToVisit.addAll(nextType.interfaces); |
| if (nextType.superclass != null) { |
| typesToVisit.add(nextType.superclass!); |
| } |
| typesToVisit.addAll(nextType.mixins); |
| } |
| return result; |
| } |
| } |