blob: bdf356d63f0766a555f3a0fe32c76c4ec1ab498f [file] [log] [blame]
// Copyright (c) 2015, 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:analysis_server/src/computer/computer_hover.dart';
import 'package:analysis_server/src/protocol_server.dart' as protocol;
import 'package:analysis_server/src/protocol_server.dart'
hide Element, ElementKind;
import 'package:analysis_server/src/protocol_server.dart'
show CompletionSuggestion;
import 'package:analysis_server/src/provisional/completion/dart/completion_dart.dart';
import 'package:analysis_server/src/services/completion/dart/feature_computer.dart';
import 'package:analysis_server/src/services/completion/dart/utilities.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/visitor.dart';
import 'package:analyzer/src/util/comment.dart';
import 'package:meta/meta.dart';
/// Return a suggestion based upon the given element or `null` if a suggestion
/// is not appropriate for the given element.
CompletionSuggestion createSuggestion(
DartCompletionRequest request, Element element,
{String completion,
CompletionSuggestionKind kind,
int relevance = DART_RELEVANCE_DEFAULT,
bool useNewRelevance = false}) {
if (element == null) {
return null;
}
if (element is ExecutableElement && element.isOperator) {
// Do not include operators in suggestions
return null;
}
completion ??= element.displayName;
kind ??= CompletionSuggestionKind.INVOCATION;
var isDeprecated = element.hasDeprecated;
if (!useNewRelevance && isDeprecated) {
relevance = DART_RELEVANCE_LOW;
}
var suggestion = CompletionSuggestion(
kind, relevance, completion, completion.length, 0, isDeprecated, false);
// Attach docs.
var doc = DartUnitHoverComputer.computeDocumentation(
request.dartdocDirectiveInfo, element);
if (doc != null) {
suggestion.docComplete = doc;
suggestion.docSummary = getDartDocSummary(doc);
}
suggestion.element = protocol.convertElement(element);
var enclosingElement = element.enclosingElement;
if (enclosingElement is ClassElement) {
suggestion.declaringType = enclosingElement.displayName;
}
suggestion.returnType = getReturnTypeString(element);
if (element is ExecutableElement && element is! PropertyAccessorElement) {
suggestion.parameterNames = element.parameters
.map((ParameterElement parameter) => parameter.name)
.toList();
suggestion.parameterTypes =
element.parameters.map((ParameterElement parameter) {
var paramType = parameter.type;
// Gracefully degrade if type not resolved yet
return paramType != null
? paramType.getDisplayString(withNullability: false)
: 'var';
}).toList();
var requiredParameters = element.parameters
.where((ParameterElement param) => param.isRequiredPositional);
suggestion.requiredParameterCount = requiredParameters.length;
var namedParameters =
element.parameters.where((ParameterElement param) => param.isNamed);
suggestion.hasNamedParameters = namedParameters.isNotEmpty;
addDefaultArgDetails(
suggestion, element, requiredParameters, namedParameters);
}
return suggestion;
}
/// Common mixin for sharing behavior.
mixin ElementSuggestionBuilder {
/// A collection of completion suggestions.
final List<CompletionSuggestion> suggestions = <CompletionSuggestion>[];
/// A set of existing completions used to prevent duplicate suggestions.
final Set<String> _completions = <String>{};
/// A map of element names to suggestions for synthetic getters and setters.
final Map<String, CompletionSuggestion> _syntheticMap =
<String, CompletionSuggestion>{};
/// Return the library in which the completion is requested.
LibraryElement get containingLibrary;
/// Return the kind of suggestions that should be built.
CompletionSuggestionKind get kind;
/// Return the completion request for which suggestions are being built.
DartCompletionRequest get request;
/// Add a suggestion based upon the given element.
CompletionSuggestion addSuggestion(Element element,
{String prefix,
int relevance = DART_RELEVANCE_DEFAULT,
bool useNewRelevance = false,
String elementCompletion}) {
if (element.isPrivate) {
if (element.library != containingLibrary) {
return null;
}
}
var completion = elementCompletion ?? element.displayName;
if (prefix != null && prefix.isNotEmpty) {
if (completion == null || completion.isEmpty) {
completion = prefix;
} else {
completion = '$prefix.$completion';
}
}
if (completion == null || completion.isEmpty) {
return null;
}
var suggestion = createSuggestion(request, element,
completion: completion,
kind: kind,
relevance: relevance,
useNewRelevance: useNewRelevance);
if (suggestion != null) {
if (element.isSynthetic && element is PropertyAccessorElement) {
String cacheKey;
if (element.isGetter) {
cacheKey = element.name;
}
if (element.isSetter) {
cacheKey = element.name;
cacheKey = cacheKey.substring(0, cacheKey.length - 1);
}
if (cacheKey != null) {
var existingSuggestion = _syntheticMap[cacheKey];
// Pair getter/setter by updating the existing suggestion
if (existingSuggestion != null) {
var getter = element.isGetter ? suggestion : existingSuggestion;
var elemKind = element.enclosingElement is ClassElement
? protocol.ElementKind.FIELD
: protocol.ElementKind.TOP_LEVEL_VARIABLE;
existingSuggestion.element = protocol.Element(
elemKind,
existingSuggestion.element.name,
existingSuggestion.element.flags,
location: getter.element.location,
typeParameters: getter.element.typeParameters,
parameters: null,
returnType: getter.returnType);
return existingSuggestion;
}
// Cache lone getter/setter so that it can be paired
_syntheticMap[cacheKey] = suggestion;
}
}
if (_completions.add(suggestion.completion)) {
suggestions.add(suggestion);
}
}
return suggestion;
}
}
/// This class creates suggestions based on top-level elements.
class LibraryElementSuggestionBuilder extends SimpleElementVisitor<void>
with ElementSuggestionBuilder {
@override
final DartCompletionRequest request;
@override
final CompletionSuggestionKind kind;
final bool typesOnly;
final bool instCreation;
/// Return `true` if the new relevance scores should be produced.
final bool useNewRelevance;
LibraryElementSuggestionBuilder(
this.request, this.kind, this.typesOnly, this.instCreation)
: useNewRelevance = request.useNewRelevance;
@override
LibraryElement get containingLibrary => request.libraryElement;
@override
void visitClassElement(ClassElement element) {
if (instCreation) {
element.visitChildren(this);
} else {
// TODO(brianwilkerson) Determine whether this should be based on features
// (such as the kind of the element) or a constant.
var relevance = useNewRelevance ? 750 : DART_RELEVANCE_DEFAULT;
addSuggestion(element,
relevance: relevance, useNewRelevance: useNewRelevance);
}
}
@override
void visitConstructorElement(ConstructorElement element) {
if (instCreation) {
var classElem = element.enclosingElement;
if (classElem != null) {
var prefix = classElem.name;
if (prefix != null && prefix.isNotEmpty) {
// TODO(brianwilkerson) Determine whether this should be based on features
// (such as the kind of the element) or a constant.
var relevance = useNewRelevance ? 750 : DART_RELEVANCE_DEFAULT;
addSuggestion(element,
prefix: prefix,
relevance: relevance,
useNewRelevance: useNewRelevance);
}
}
}
}
@override
void visitExtensionElement(ExtensionElement element) {
if (!instCreation) {
// TODO(brianwilkerson) Determine whether this should be based on features
// (such as the kind of the element) or a constant.
var relevance = useNewRelevance ? 750 : DART_RELEVANCE_DEFAULT;
addSuggestion(element,
relevance: relevance, useNewRelevance: useNewRelevance);
}
}
@override
void visitFunctionElement(FunctionElement element) {
if (!typesOnly) {
int relevance;
if (useNewRelevance) {
// TODO(brianwilkerson) Determine whether this should be based on
// features (such as the kind of the element) or a constant.
relevance = element.library == containingLibrary ? 800 : 750;
} else {
relevance = element.library == containingLibrary
? DART_RELEVANCE_LOCAL_FUNCTION
: DART_RELEVANCE_DEFAULT;
}
addSuggestion(element,
relevance: relevance, useNewRelevance: useNewRelevance);
}
}
@override
void visitFunctionTypeAliasElement(FunctionTypeAliasElement element) {
if (!instCreation) {
// TODO(brianwilkerson) Determine whether this should be based on features
// (such as the kind of the element) or a constant.
var relevance = useNewRelevance ? 750 : DART_RELEVANCE_DEFAULT;
addSuggestion(element,
relevance: relevance, useNewRelevance: useNewRelevance);
}
}
@override
void visitPropertyAccessorElement(PropertyAccessorElement element) {
if (!typesOnly) {
var variable = element.variable;
int relevance;
if (useNewRelevance) {
// TODO(brianwilkerson) Determine whether this should be based on
// features (such as the kind of the element) or a constant.
relevance = variable.library == containingLibrary ? 800 : 750;
} else {
relevance = variable.library == containingLibrary
? DART_RELEVANCE_LOCAL_TOP_LEVEL_VARIABLE
: DART_RELEVANCE_DEFAULT;
}
addSuggestion(variable,
relevance: relevance, useNewRelevance: useNewRelevance);
}
}
}
/// This class provides suggestions based upon the visible instance members in
/// an interface type.
class MemberSuggestionBuilder {
/// 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 request for which suggestions are being built.
final DartCompletionRequest request;
/// 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>();
/// A map from a completion identifier to a completion suggestion.
final Map<String, CompletionSuggestion> _suggestionMap =
<String, CompletionSuggestion>{};
MemberSuggestionBuilder(this.request);
Iterable<CompletionSuggestion> get suggestions => _suggestionMap.values;
/// Add the given completion [suggestion].
void addCompletionSuggestion(CompletionSuggestion suggestion) {
_suggestionMap[suggestion.completion] = suggestion;
}
/// Add a suggestion for the given [method].
CompletionSuggestion addSuggestionForAccessor(
{@required PropertyAccessorElement accessor,
String containingMethodName,
@required double inheritanceDistance}) {
int oldRelevance() {
if (accessor.hasDeprecated) {
return DART_RELEVANCE_LOW;
}
var identifier = accessor.displayName;
if (identifier != null && identifier.startsWith(r'$')) {
// Decrease relevance of suggestions starting with $
// https://github.com/dart-lang/sdk/issues/27303
return DART_RELEVANCE_LOW;
}
return DART_RELEVANCE_DEFAULT;
}
if (!accessor.isAccessibleIn(request.libraryElement)) {
// Don't suggest private members from imported libraries.
return null;
}
if (accessor.isSynthetic) {
// Avoid visiting a field twice. All fields induce a getter, but only
// non-final fields induce a setter, so we don't add a suggestion for a
// synthetic setter.
if (accessor.isGetter) {
var variable = accessor.variable;
int relevance;
var useNewRelevance = request.useNewRelevance;
if (useNewRelevance) {
var featureComputer = request.featureComputer;
var contextType = featureComputer.contextTypeFeature(
request.contextType, variable.type);
var hasDeprecated = featureComputer.hasDeprecatedFeature(accessor);
var startsWithDollar =
featureComputer.startsWithDollarFeature(accessor.name);
var superMatches = featureComputer.superMatchesFeature(
containingMethodName, accessor.name);
relevance = _computeRelevance(
contextType: contextType,
hasDeprecated: hasDeprecated,
inheritanceDistance: inheritanceDistance,
startsWithDollar: startsWithDollar,
superMatches: superMatches);
} else {
relevance = oldRelevance();
}
return _addSuggestion(variable, relevance, useNewRelevance);
}
} else {
var type =
accessor.isGetter ? accessor.returnType : accessor.parameters[0].type;
int relevance;
var useNewRelevance = request.useNewRelevance;
if (useNewRelevance) {
var featureComputer = request.featureComputer;
var contextType =
featureComputer.contextTypeFeature(request.contextType, type);
var hasDeprecated = featureComputer.hasDeprecatedFeature(accessor);
var startsWithDollar =
featureComputer.startsWithDollarFeature(accessor.name);
var superMatches = featureComputer.superMatchesFeature(
containingMethodName, accessor.name);
relevance = _computeRelevance(
contextType: contextType,
hasDeprecated: hasDeprecated,
inheritanceDistance: inheritanceDistance,
startsWithDollar: startsWithDollar,
superMatches: superMatches);
} else {
relevance = oldRelevance();
}
return _addSuggestion(accessor, relevance, useNewRelevance);
}
return null;
}
/// Add a suggestion for the given [method].
CompletionSuggestion addSuggestionForMethod(
{@required MethodElement method,
String containingMethodName,
CompletionSuggestionKind kind,
@required double inheritanceDistance}) {
int oldRelevance() {
if (method.hasDeprecated) {
return DART_RELEVANCE_LOW;
} else if (method.name == containingMethodName) {
// Boost the relevance of a super expression calling a method of the
// same name as the containing method.
return DART_RELEVANCE_HIGH;
}
var identifier = method.displayName;
if (identifier != null && identifier.startsWith(r'$')) {
// Decrease relevance of suggestions starting with $
// https://github.com/dart-lang/sdk/issues/27303
return DART_RELEVANCE_LOW;
}
return DART_RELEVANCE_DEFAULT;
}
if (!method.isAccessibleIn(request.libraryElement)) {
// Don't suggest private members from imported libraries.
return null;
}
int relevance;
var useNewRelevance = request.useNewRelevance;
if (useNewRelevance) {
var featureComputer = request.featureComputer;
var contextType = featureComputer.contextTypeFeature(
request.contextType, method.returnType);
var hasDeprecated = featureComputer.hasDeprecatedFeature(method);
var startsWithDollar =
featureComputer.startsWithDollarFeature(method.name);
var superMatches = featureComputer.superMatchesFeature(
containingMethodName, method.name);
relevance = _computeRelevance(
contextType: contextType,
hasDeprecated: hasDeprecated,
inheritanceDistance: inheritanceDistance,
startsWithDollar: startsWithDollar,
superMatches: superMatches);
} else {
relevance = oldRelevance();
}
return _addSuggestion(method, relevance, useNewRelevance, kind: kind);
}
/// Add a suggestion for the given [element] with the given [relevance],
/// provided that it is not shadowed by a previously added suggestion.
CompletionSuggestion _addSuggestion(
Element element, int relevance, bool useNewRelevance,
{CompletionSuggestionKind kind}) {
var identifier = element.displayName;
var alreadyGenerated = _completionTypesGenerated.putIfAbsent(
identifier, () => _COMPLETION_TYPE_NONE);
if (element is MethodElement) {
// Anything shadows a method.
if (alreadyGenerated != _COMPLETION_TYPE_NONE) {
return null;
}
_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 null;
}
_completionTypesGenerated[identifier] |= _COMPLETION_TYPE_GETTER;
} else {
// Setters, fields, and methods shadow a setter.
if ((alreadyGenerated & _COMPLETION_TYPE_SETTER) != 0) {
return null;
}
_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 null;
}
_completionTypesGenerated[identifier] =
_COMPLETION_TYPE_FIELD_OR_METHOD_OR_GETSET;
} else {
// Unexpected element type; skip it.
assert(false);
return null;
}
var suggestion = createSuggestion(request, element,
kind: kind, relevance: relevance, useNewRelevance: useNewRelevance);
if (suggestion != null) {
addCompletionSuggestion(suggestion);
}
return suggestion;
}
/// Compute a relevance value from the given feature scores:
/// - [contextType] is higher if the type of the element matches the context
/// type,
/// - [hasDeprecated] is higher if the element is not deprecated,
/// - [inheritanceDistance] is higher if the element is defined closer to the
/// target type,
/// - [startsWithDollar] is higher if the element's name doe _not_ start with
/// a dollar sign, and
/// - [superMatches] is higher if the element is being invoked through `super`
/// and the element's name matches the name of the enclosing method.
int _computeRelevance(
{@required double contextType,
@required double hasDeprecated,
@required double inheritanceDistance,
@required double startsWithDollar,
@required double superMatches}) {
var score = weightedAverage([
contextType,
hasDeprecated,
inheritanceDistance,
startsWithDollar,
superMatches
], [
1.0,
0.5,
1.0,
0.5,
1.0
]);
return toRelevance(score, Relevance.member);
}
}
/// An object used to build a list of suggestions in response to a single
/// completion request.
class SuggestionBuilder {
/// The completion request for which suggestions are being built.
final DartCompletionRequest request;
/// A collection of completion suggestions.
final List<CompletionSuggestion> suggestions = <CompletionSuggestion>[];
/// Initialize a newly created suggestion builder to build suggestions for the
/// given [request].
SuggestionBuilder(this.request);
}