| // 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. |
| |
| /// Utility methods to compute the value of the features used for code |
| /// completion. |
| library; |
| |
| import 'dart:math' as math; |
| |
| import 'package:analysis_server/src/protocol_server.dart' |
| as protocol |
| show ElementKind; |
| import 'package:analysis_server/src/services/completion/dart/relevance_tables.g.dart'; |
| import 'package:analysis_server/src/utilities/extensions/element.dart'; |
| import 'package:analysis_server/src/utilities/extensions/numeric.dart'; |
| import 'package:analyzer/dart/ast/token.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'package:analyzer/dart/element/type_provider.dart'; |
| import 'package:analyzer/dart/element/type_system.dart'; |
| import 'package:analyzer/src/dart/ast/ast.dart'; |
| import 'package:analyzer/src/dart/element/extensions.dart'; |
| import 'package:analyzer/src/dart/element/inheritance_manager3.dart'; |
| import 'package:analyzer/src/utilities/extensions/object.dart'; |
| import 'package:analyzer_plugin/utilities/range_factory.dart'; |
| |
| const List<String> intNames = ['i', 'j', 'index', 'length']; |
| const List<String> listNames = ['list', 'items']; |
| |
| /// The maximum relevance score a completion can have. |
| const int maximumRelevance = 1000; |
| |
| const List<String> numNames = ['height', 'width']; |
| |
| const List<String> stringNames = [ |
| 'key', |
| 'text', |
| 'url', |
| 'uri', |
| 'name', |
| 'str', |
| 'string', |
| ]; |
| |
| /// Convert a relevance score (assumed to be between `0.0` and `1.0` inclusive) |
| /// to a relevance value between `0` and `1000` ([maximumRelevance]). |
| int toRelevance(double score) { |
| assert(score.between(0.0, 1.0)); |
| return (score * maximumRelevance).truncate(); |
| } |
| |
| /// Return the weighted average of the given values, applying some constant and |
| /// predetermined weights. |
| double weightedAverage({ |
| double contextType = 0.0, |
| double elementKind = 0.0, |
| double hasDeprecated = 0.0, |
| double isConstant = 0.0, |
| double isNoSuchMethod = 0.0, |
| double isNotImported = 0.0, |
| double keyword = 0.0, |
| double startsWithDollar = 0.0, |
| double superMatches = 0.0, |
| double localVariableDistance = 0.0, |
| }) { |
| assert(contextType.between(0.0, 1.0)); |
| assert(elementKind.between(0.0, 1.0)); |
| assert(hasDeprecated.between(-1.0, 0.0)); |
| assert(isConstant.between(0.0, 1.0)); |
| assert(isNoSuchMethod.between(-1.0, 0.0)); |
| assert(isNotImported.between(-1.0, 0.0)); |
| assert(keyword.between(0.0, 1.0)); |
| assert(startsWithDollar.between(-1.0, 0.0)); |
| assert(superMatches.between(0.0, 1.0)); |
| assert(localVariableDistance.between(0.0, 1.0)); |
| var average = _weightedAverage([ |
| contextType, |
| elementKind, |
| hasDeprecated, |
| isConstant, |
| isNoSuchMethod, |
| isNotImported, |
| keyword, |
| startsWithDollar, |
| superMatches, |
| localVariableDistance, |
| ], FeatureComputer.featureWeights); |
| return (average + 1.0) / 2.0; |
| } |
| |
| DartType? _impliedDartTypeWithName(TypeProvider typeProvider, String name) { |
| if (name.isEmpty) { |
| return null; |
| } |
| if (intNames.contains(name)) { |
| return typeProvider.intType; |
| } else if (numNames.contains(name)) { |
| return typeProvider.numType; |
| } else if (listNames.contains(name)) { |
| return typeProvider.listType(typeProvider.dynamicType); |
| } else if (stringNames.contains(name)) { |
| return typeProvider.stringType; |
| } else if (name == 'iterator') { |
| return typeProvider.iterableDynamicType; |
| } else if (name == 'map') { |
| return typeProvider.mapType( |
| typeProvider.dynamicType, |
| typeProvider.dynamicType, |
| ); |
| } |
| return null; |
| } |
| |
| /// Return the weighted average of the given [values], applying the given |
| /// [weights]. The number of weights must be equal to the number of values. |
| double _weightedAverage(List<double> values, List<double> weights) { |
| assert(values.length == weights.length); |
| var totalValue = 0.0; |
| var totalWeight = 0.0; |
| for (var i = 0; i < values.length; i++) { |
| var value = values[i]; |
| var weight = weights[i]; |
| totalWeight += weight; |
| totalValue += value * weight; |
| } |
| return totalValue / totalWeight; |
| } |
| |
| /// An object that computes the values of features. |
| class FeatureComputer { |
| /// The names of features whose values are averaged. |
| static List<String> featureNames = [ |
| 'contextType', |
| 'elementKind', |
| 'hasDeprecated', |
| 'inheritanceDistance', |
| 'isConstant', |
| 'isNoSuchMethod', |
| 'keyword', |
| 'localVariableDistance', |
| 'startsWithDollar', |
| 'superMatches', |
| 'localVariableDistance', |
| ]; |
| |
| /// The values of the weights used to compute an average of feature values. |
| static List<double> featureWeights = defaultFeatureWeights; |
| |
| /// The default values of the weights used to compute an average of feature |
| /// values. |
| static const List<double> defaultFeatureWeights = [ |
| 1.00, // contextType |
| 1.00, // elementKind |
| 0.50, // hasDeprecated |
| 1.00, // isConstant |
| 1.00, // isNoSuchMethod |
| 1.00, // isNotImported |
| 1.00, // keyword |
| 0.50, // startsWithDollar |
| 1.00, // superMatches |
| 1.00, // localVariableDistance |
| ]; |
| |
| /// The type system used to perform operations on types. |
| final TypeSystem typeSystem; |
| |
| /// The type provider used to access types defined by the spec. |
| final TypeProvider typeProvider; |
| |
| /// Initialize a newly created feature computer. |
| FeatureComputer(this.typeSystem, this.typeProvider); |
| |
| /// Return the type imposed when completing at the given [offset], where the |
| /// offset is within the given [node], or `null` if the context does not |
| /// impose any type. |
| DartType? computeContextType(AstNode node, int offset) { |
| var contextType = node.accept(_ContextTypeVisitor(typeProvider, offset)); |
| if (contextType == null || contextType is DynamicType) { |
| return null; |
| } |
| return typeSystem.resolveToBound(contextType); |
| } |
| |
| /// Return the element kind used to compute relevance for the given [element]. |
| /// This differs from the kind returned to the client in that getters and |
| /// setters are always mapped into a different kind: FIELD for getters and |
| /// setters declared in a class or extension, and TOP_LEVEL_VARIABLE for |
| /// top-level getters and setters. |
| protocol.ElementKind computeElementKind(Element2 element) { |
| if (element is LibraryElement2) { |
| return protocol.ElementKind.PREFIX; |
| } else if (element is EnumElement2) { |
| return protocol.ElementKind.ENUM; |
| } else if (element is MixinElement2) { |
| return protocol.ElementKind.MIXIN; |
| } else if (element is ClassElement2) { |
| return protocol.ElementKind.CLASS; |
| } else if (element is FieldElement2 && element.isEnumConstant) { |
| return protocol.ElementKind.ENUM_CONSTANT; |
| } else if (element is PropertyAccessorElement2) { |
| var variable = element.variable3; |
| if (variable == null) { |
| return protocol.ElementKind.UNKNOWN; |
| } |
| element = variable; |
| } |
| var kind = element.kind; |
| if (kind == ElementKind.CONSTRUCTOR) { |
| return protocol.ElementKind.CONSTRUCTOR; |
| } else if (kind == ElementKind.EXTENSION) { |
| return protocol.ElementKind.EXTENSION; |
| } else if (kind == ElementKind.FIELD) { |
| return protocol.ElementKind.FIELD; |
| } else if (kind == ElementKind.FUNCTION) { |
| return protocol.ElementKind.FUNCTION; |
| } else if (kind == ElementKind.FUNCTION_TYPE_ALIAS) { |
| return protocol.ElementKind.FUNCTION_TYPE_ALIAS; |
| } else if (kind == ElementKind.GENERIC_FUNCTION_TYPE) { |
| return protocol.ElementKind.FUNCTION_TYPE_ALIAS; |
| } else if (kind == ElementKind.LABEL) { |
| return protocol.ElementKind.LABEL; |
| } else if (kind == ElementKind.LOCAL_VARIABLE) { |
| return protocol.ElementKind.LOCAL_VARIABLE; |
| } else if (kind == ElementKind.METHOD) { |
| return protocol.ElementKind.METHOD; |
| } else if (kind == ElementKind.PARAMETER) { |
| return protocol.ElementKind.PARAMETER; |
| } else if (kind == ElementKind.PREFIX) { |
| return protocol.ElementKind.PREFIX; |
| } else if (kind == ElementKind.TOP_LEVEL_VARIABLE) { |
| return protocol.ElementKind.TOP_LEVEL_VARIABLE; |
| } else if (kind == ElementKind.TYPE_ALIAS) { |
| return protocol.ElementKind.TYPE_ALIAS; |
| } else if (kind == ElementKind.TYPE_PARAMETER) { |
| return protocol.ElementKind.TYPE_PARAMETER; |
| } |
| return protocol.ElementKind.UNKNOWN; |
| } |
| |
| /// Return the element kind used to compute relevance for the given [element]. |
| /// This differs from the kind returned to the client in that getters and |
| /// setters are always mapped into a different kind: FIELD for getters and |
| /// setters declared in a class or extension, and TOP_LEVEL_VARIABLE for |
| /// top-level getters and setters. |
| protocol.ElementKind computeElementKind2(Element2 element) { |
| if (element is LibraryElement2) { |
| return protocol.ElementKind.PREFIX; |
| } else if (element is EnumElement2) { |
| return protocol.ElementKind.ENUM; |
| } else if (element is MixinElement2) { |
| return protocol.ElementKind.MIXIN; |
| } else if (element is ClassElement2) { |
| return protocol.ElementKind.CLASS; |
| } else if (element is FieldElement2 && element.isEnumConstant) { |
| return protocol.ElementKind.ENUM_CONSTANT; |
| } else if (element is PropertyAccessorElement2) { |
| var variable = element.variable3; |
| if (variable == null) { |
| return protocol.ElementKind.UNKNOWN; |
| } |
| element = variable; |
| } |
| var kind = element.kind; |
| if (kind == ElementKind.CONSTRUCTOR) { |
| return protocol.ElementKind.CONSTRUCTOR; |
| } else if (kind == ElementKind.EXTENSION) { |
| return protocol.ElementKind.EXTENSION; |
| } else if (kind == ElementKind.FIELD) { |
| return protocol.ElementKind.FIELD; |
| } else if (kind == ElementKind.FUNCTION) { |
| return protocol.ElementKind.FUNCTION; |
| } else if (kind == ElementKind.FUNCTION_TYPE_ALIAS) { |
| return protocol.ElementKind.FUNCTION_TYPE_ALIAS; |
| } else if (kind == ElementKind.GENERIC_FUNCTION_TYPE) { |
| return protocol.ElementKind.FUNCTION_TYPE_ALIAS; |
| } else if (kind == ElementKind.LABEL) { |
| return protocol.ElementKind.LABEL; |
| } else if (kind == ElementKind.LOCAL_VARIABLE) { |
| return protocol.ElementKind.LOCAL_VARIABLE; |
| } else if (kind == ElementKind.METHOD) { |
| return protocol.ElementKind.METHOD; |
| } else if (kind == ElementKind.PARAMETER) { |
| return protocol.ElementKind.PARAMETER; |
| } else if (kind == ElementKind.PREFIX) { |
| return protocol.ElementKind.PREFIX; |
| } else if (kind == ElementKind.TOP_LEVEL_VARIABLE) { |
| return protocol.ElementKind.TOP_LEVEL_VARIABLE; |
| } else if (kind == ElementKind.TYPE_ALIAS) { |
| return protocol.ElementKind.TYPE_ALIAS; |
| } else if (kind == ElementKind.TYPE_PARAMETER) { |
| return protocol.ElementKind.TYPE_PARAMETER; |
| } |
| return protocol.ElementKind.UNKNOWN; |
| } |
| |
| /// Return the value of the _context type_ feature for an element with the |
| /// given [elementType] when completing in a location with the given |
| /// [contextType]. |
| double contextTypeFeature(DartType? contextType, DartType? elementType) { |
| if (contextType == null || elementType == null) { |
| // Disable the feature if we don't have both types. |
| return 0.0; |
| } |
| if (elementType == contextType) { |
| // Exact match. |
| return 1.0; |
| } else if (typeSystem.isSubtypeOf(elementType, contextType)) { |
| // Subtype. |
| return 0.40; |
| } else if (typeSystem.isSubtypeOf(contextType, elementType)) { |
| // Supertype. |
| return 0.02; |
| } else { |
| // Unrelated. |
| return 0.13; |
| } |
| } |
| |
| /// Convert a [distance] to a percentage value and return the percentage. If |
| /// the [distance] is negative, return `0.0`. |
| double distanceToPercent(int distance) { |
| if (distance < 0) { |
| return 0.0; |
| } |
| return math.pow(0.9, distance) as double; |
| } |
| |
| /// Return the value of the _element kind_ feature for the [element] when |
| /// completing at the given [completionLocation]. If a [distance] is given it |
| /// will be used to provide finer-grained relevance scores. |
| double elementKindFeature( |
| Element2 element, |
| String? completionLocation, { |
| double? distance, |
| }) { |
| if (completionLocation == null) { |
| return 0.0; |
| } |
| var locationTable = elementKindRelevance[completionLocation]; |
| if (locationTable == null) { |
| return 0.0; |
| } |
| var range = locationTable[computeElementKind2(element)]; |
| if (range == null) { |
| return 0.0; |
| } |
| if (distance == null) { |
| return range.middle; |
| } |
| return range.conditionalProbability(distance); |
| } |
| |
| // Return the value of the _has deprecated_ feature for the given [element]. |
| double hasDeprecatedFeature(Element2 element) { |
| return element.hasOrInheritsDeprecated ? -1.0 : 0.0; |
| } |
| |
| /// Return the inheritance distance between the [subclass] and the |
| /// [superclass]. We define the inheritance distance between two types to be |
| /// zero if the two types are the same and the minimum number of edges that |
| /// must be traversed in the type graph to get from the subtype to the |
| /// supertype if the two types are not the same. Return `-1` if the [subclass] |
| /// is not a subclass of the [superclass]. |
| int inheritanceDistance( |
| InterfaceElement2 subclass, |
| InterfaceElement2 superclass, |
| ) { |
| // This method is only visible for the metrics computation and might be made |
| // private at some future date. |
| return _inheritanceDistance(subclass, superclass, {}); |
| } |
| |
| /// Return the value of the _inheritance distance_ feature for a member |
| /// defined in the [superclass] that is being accessed through an expression |
| /// whose static type is the [subclass]. |
| double inheritanceDistanceFeature( |
| InterfaceElement2 subclass, |
| InterfaceElement2 superclass, |
| ) { |
| var distance = _inheritanceDistance(subclass, superclass, {}); |
| return distanceToPercent(distance); |
| } |
| |
| /// Return the value of the _is constant_ feature for the given [element]. |
| double isConstantFeature(Element2 element) { |
| if (element is ConstructorElement2 && element.isConst) { |
| return 1.0; |
| } else if (element is FieldElement2 && |
| element.isStatic && |
| element.isConst) { |
| return 1.0; |
| } else if (element is TopLevelVariableElement2 && element.isConst) { |
| return 1.0; |
| } else if (element is PropertyAccessorElement2 && element.isSynthetic) { |
| var variable = element.variable3; |
| if (variable != null && variable.isStatic && variable.isConst) { |
| return 1.0; |
| } |
| } |
| return 0.0; |
| } |
| |
| /// Return the value of the _is noSuchMethod_ feature. |
| double isNoSuchMethodFeature( |
| String? containingMethodName, |
| String proposedMemberName, |
| ) { |
| if (proposedMemberName == containingMethodName) { |
| // Don't penalize `noSuchMethod` when completing after `super` in an |
| // override of `noSuchMethod`. |
| return 0.0; |
| } |
| return proposedMemberName == MethodElement2.NO_SUCH_METHOD_METHOD_NAME |
| ? -1.0 |
| : 0.0; |
| } |
| |
| /// Return the feature for the not-yet-imported property. |
| double isNotImportedFeature(bool isNotImported) { |
| return isNotImported ? -1.0 : 0.0; |
| } |
| |
| /// Return the value of the _keyword_ feature for the [keyword] when |
| /// completing at the given [completionLocation]. |
| double keywordFeature(String keyword, String? completionLocation) { |
| if (completionLocation == null) { |
| return 0.0; |
| } |
| var locationTable = keywordRelevance[completionLocation]; |
| if (locationTable == null) { |
| return 0.0; |
| } |
| var range = locationTable[keyword]; |
| if (range == null) { |
| // We sometimes suggest multiple tokens where a keyword is allowed, such |
| // as 'async*'. In those cases a valid keyword is always first followed by |
| // a non-alphabetic character. Try stripping off everything after the |
| // keyword and indexing into the table again. |
| var index = keyword.indexOf(RegExp('[^a-z]')); |
| if (index > 0) { |
| range = locationTable[keyword.substring(0, index)]; |
| } |
| } |
| if (range == null) { |
| return 0.0; |
| } |
| return range.upper; |
| } |
| |
| /// Return the value of the _starts with dollar_ feature. |
| double startsWithDollarFeature(String name) { |
| return name.startsWith('\$') ? -1.0 : 0.0; |
| } |
| |
| /// Return the value of the _super matches_ feature. |
| double superMatchesFeature( |
| String? containingMethodName, |
| String proposedMemberName, |
| ) => |
| containingMethodName == null |
| ? 0.0 |
| : (proposedMemberName == containingMethodName ? 1.0 : 0.0); |
| |
| /// Return the inheritance distance between the [subclass] and the |
| /// [superclass]. The set of [visited] elements is used to guard against |
| /// cycles in the type graph. |
| /// |
| /// This is the implementation of [inheritanceDistance]. |
| int _inheritanceDistance( |
| InterfaceElement2? subclass, |
| InterfaceElement2 superclass, |
| Set<InterfaceElement2> visited, |
| ) { |
| if (subclass == null) { |
| return -1; |
| } else if (subclass == superclass) { |
| return 0; |
| } else if (!visited.add(subclass)) { |
| return -1; |
| } |
| var minDepth = _inheritanceDistance( |
| subclass.supertype?.element3, |
| superclass, |
| visited, |
| ); |
| |
| void visitTypes(List<InterfaceType> types) { |
| for (var type in types) { |
| var depth = _inheritanceDistance(type.element3, superclass, visited); |
| if (minDepth < 0 || (depth >= 0 && depth < minDepth)) { |
| minDepth = depth; |
| } |
| } |
| } |
| |
| if (subclass is MixinElement2) { |
| visitTypes(subclass.superclassConstraints); |
| } |
| visitTypes(subclass.mixins); |
| visitTypes(subclass.interfaces); |
| |
| visited.remove(subclass); |
| if (minDepth < 0) { |
| return minDepth; |
| } |
| return minDepth + 1; |
| } |
| } |
| |
| /// An visitor used to compute the required type of an expression or identifier |
| /// based on its context. The visitor should be initialized with the node whose |
| /// context type is to be computed and the parent of that node should be |
| /// visited. |
| class _ContextTypeVisitor extends SimpleAstVisitor<DartType> { |
| final TypeProvider typeProvider; |
| |
| int offset; |
| |
| _ContextTypeVisitor(this.typeProvider, this.offset); |
| |
| @override |
| DartType? visitAdjacentStrings(AdjacentStrings node) { |
| if (offset == node.offset) { |
| return _visitParent(node); |
| } |
| return typeProvider.stringType; |
| } |
| |
| @override |
| DartType? visitArgumentList(ArgumentList node) { |
| if (range |
| .endStart(node.leftParenthesis, node.rightParenthesis) |
| .contains(offset)) { |
| var parameters = node.functionType?.formalParameters; |
| if (parameters == null) { |
| return null; |
| } |
| |
| var index = 0; |
| |
| DartType? typeOfIndexPositionalParameter() { |
| if (index < parameters.length) { |
| var parameter = parameters[index]; |
| if (parameter.isPositional) { |
| return parameter.type; |
| } |
| } |
| return null; |
| } |
| |
| Expression? previousArgument; |
| for (var argument in node.arguments) { |
| if (argument is NamedExpression) { |
| if (offset <= argument.offset) { |
| return typeOfIndexPositionalParameter(); |
| } |
| if (argument.contains(offset)) { |
| if (offset >= argument.name.end) { |
| return argument.element2?.type; |
| } |
| return null; |
| } |
| } else { |
| if (previousArgument == null || previousArgument.end < offset) { |
| if (offset <= argument.end) { |
| return argument.correspondingParameter?.type; |
| } |
| } |
| previousArgument = argument; |
| index++; |
| } |
| } |
| |
| return typeOfIndexPositionalParameter(); |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitAsExpression(AsExpression node) { |
| if (node.asOperator.end < offset) { |
| return node.expression.staticType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitAssertInitializer(AssertInitializer node) { |
| if (range |
| .endStart( |
| node.leftParenthesis, |
| node.message?.beginToken.previous ?? node.rightParenthesis, |
| ) |
| .contains(offset)) { |
| return typeProvider.boolType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitAssertStatement(AssertStatement node) { |
| if (range |
| .endStart( |
| node.leftParenthesis, |
| node.message?.beginToken.previous ?? node.rightParenthesis, |
| ) |
| .contains(offset)) { |
| return typeProvider.boolType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitAssignmentExpression(AssignmentExpression node) { |
| if (node.operator.end <= offset) { |
| // RHS |
| if (node.operator.type == TokenType.EQ) { |
| return node.writeType; |
| } |
| var method = node.element; |
| if (method != null) { |
| var parameters = method.formalParameters; |
| if (parameters.isNotEmpty) { |
| return parameters[0].type; |
| } |
| } |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitAwaitExpression(AwaitExpression node) { |
| return _visitParent(node); |
| } |
| |
| @override |
| DartType? visitBinaryExpression(BinaryExpression node) { |
| if (node.operator.end <= offset) { |
| return node.rightOperand.correspondingParameter?.type; |
| } |
| return _visitParent(node); |
| } |
| |
| @override |
| DartType? visitCascadeExpression(CascadeExpression node) { |
| if (offset == node.target.offset) { |
| return _visitParent(node); |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitConditionalExpression(ConditionalExpression node) { |
| if (offset <= node.question.offset) { |
| return typeProvider.boolType; |
| } else { |
| return _visitParent(node); |
| } |
| } |
| |
| @override |
| DartType? visitConstructorFieldInitializer(ConstructorFieldInitializer node) { |
| if (node.equals.end <= offset) { |
| var element = node.fieldName.element; |
| if (element is FieldElement2) { |
| return element.type; |
| } |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitConstructorName(ConstructorName node) { |
| return _visitParent(node); |
| } |
| |
| @override |
| DartType? visitConstructorReference(ConstructorReference node) { |
| return _visitParent(node); |
| } |
| |
| @override |
| DartType? visitDefaultFormalParameter(DefaultFormalParameter node) { |
| var separator = node.separator; |
| if (separator != null && separator.end <= offset) { |
| return node.parameter.declaredFragment?.element.type; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitDoStatement(DoStatement node) { |
| if (range |
| .endStart(node.leftParenthesis, node.rightParenthesis) |
| .contains(offset)) { |
| return typeProvider.boolType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitExpressionFunctionBody(ExpressionFunctionBody node) { |
| if (range.endEnd(node.functionDefinition, node).contains(offset)) { |
| var parent = node.parent; |
| if (parent is MethodDeclarationImpl) { |
| return parent.body.bodyContext?.contextType; |
| } else if (parent is FunctionExpressionImpl) { |
| // If the surrounding function has a context type, use it. |
| var functionContextType = parent.body.bodyContext?.contextType; |
| if (functionContextType != null) { |
| return functionContextType; |
| } else if (parent.parent is FunctionDeclaration) { |
| // Don't walk up past the function declaration. |
| return null; |
| } |
| return _visitParent(parent); |
| } |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitFieldDeclaration(FieldDeclaration node) { |
| if (node.fields.contains(offset)) { |
| return node.fields.accept(this); |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) { |
| if (range |
| .startOffsetEndOffset(node.inKeyword.end, node.end) |
| .contains(offset)) { |
| var parent = node.parent; |
| if ((parent is ForStatement && parent.awaitKeyword != null) || |
| (parent is ForElement && parent.awaitKeyword != null)) { |
| return typeProvider.streamDynamicType; |
| } |
| return typeProvider.iterableDynamicType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) { |
| if (range.endEnd(node.inKeyword, node).contains(offset)) { |
| var parent = node.parent; |
| if ((parent is ForStatement && parent.awaitKeyword != null) || |
| (parent is ForElement && parent.awaitKeyword != null)) { |
| return typeProvider.streamDynamicType; |
| } |
| return typeProvider.iterableDynamicType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitForEachPartsWithPattern(ForEachPartsWithPattern node) { |
| if (range |
| .startOffsetEndOffset(node.inKeyword.end, node.end) |
| .contains(offset)) { |
| return typeProvider.iterableDynamicType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitForPartsWithDeclarations(ForPartsWithDeclarations node) { |
| if (range |
| .endStart(node.leftSeparator, node.rightSeparator) |
| .contains(offset)) { |
| return typeProvider.boolType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitForPartsWithExpression(ForPartsWithExpression node) { |
| if (range |
| .endStart(node.leftSeparator, node.rightSeparator) |
| .contains(offset)) { |
| return typeProvider.boolType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitForPartsWithPattern(ForPartsWithPattern node) { |
| if (range |
| .endStart(node.leftSeparator, node.rightSeparator) |
| .contains(offset)) { |
| return typeProvider.boolType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitFunctionExpressionInvocation( |
| FunctionExpressionInvocation node, |
| ) { |
| if (node.function.contains(offset)) { |
| return _visitParent(node); |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitIfElement(IfElement node) { |
| if (range |
| .endStart(node.leftParenthesis, node.rightParenthesis) |
| .contains(offset)) { |
| return typeProvider.boolType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitIfStatement(IfStatement node) { |
| if (range |
| .endStart(node.leftParenthesis, node.rightParenthesis) |
| .contains(offset)) { |
| return typeProvider.boolType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitIndexExpression(IndexExpression node) { |
| if (range.endStart(node.leftBracket, node.rightBracket).contains(offset)) { |
| var formalParameters = node.element?.formalParameters; |
| return formalParameters?.firstOrNull?.type; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitIsExpression(IsExpression node) { |
| if (node.isOperator.end < offset) { |
| return node.expression.staticType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitLabel(Label node) { |
| if (offset == node.offset) { |
| return _visitParent(node); |
| } |
| if (node.colon.end <= offset) { |
| return _visitParent(node); |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitListLiteral(ListLiteral node) { |
| if (range.endStart(node.leftBracket, node.rightBracket).contains(offset)) { |
| var type = node.staticType; |
| // TODO(scheglov): https://github.com/dart-lang/sdk/issues/48965 |
| if (type == null) { |
| throw ''' |
| No type. |
| node: $node |
| parent: ${node.parent} |
| parent2: ${node.parent?.parent} |
| parent3: ${node.parent?.parent?.parent} |
| '''; |
| } |
| return (type as InterfaceType).typeArguments[0]; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitListPattern(ListPattern node) { |
| if (range.endStart(node.leftBracket, node.rightBracket).contains(offset)) { |
| var type = node.requiredType; |
| if (type == null) { |
| throw ''' |
| No required type. |
| node: $node |
| parent: ${node.parent} |
| parent2: ${node.parent?.parent} |
| parent3: ${node.parent?.parent?.parent} |
| '''; |
| } |
| return (type as InterfaceType).typeArguments[0]; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitMapLiteralEntry(MapLiteralEntry node) { |
| var literal = node.thisOrAncestorOfType<SetOrMapLiteral>(); |
| var literalType = literal?.staticType; |
| if (literalType is InterfaceType && literalType.isDartCoreMap) { |
| var typeArguments = literalType.typeArguments; |
| if (offset <= node.separator.offset) { |
| return typeArguments[0]; |
| } else { |
| return typeArguments[1]; |
| } |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitMapPattern(MapPattern node) { |
| if (range.endStart(node.leftBracket, node.rightBracket).contains(offset)) { |
| var type = node.requiredType; |
| if (type is InterfaceType && type.isDartCoreMap) { |
| var typeArguments = type.typeArguments; |
| return typeArguments[0]; |
| } |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitMapPatternEntry(MapPatternEntry node) { |
| var pattern = node.parent.ifTypeOrNull<MapPattern>(); |
| var type = pattern?.requiredType; |
| if (type is InterfaceType && type.isDartCoreMap) { |
| var typeArguments = type.typeArguments; |
| if (offset <= node.separator.offset) { |
| return typeArguments[0]; |
| } else { |
| return typeArguments[1]; |
| } |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitMethodInvocation(MethodInvocation node) { |
| if (offset == node.offset) { |
| return _visitParent(node); |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitNamedExpression(NamedExpression node) { |
| if (offset == node.offset) { |
| return _visitParent(node); |
| } |
| if (node.name.end <= offset) { |
| return _visitParent(node); |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitParenthesizedExpression(ParenthesizedExpression node) { |
| var type = _visitParent(node); |
| |
| // `RecordType := (^)` without any fields. |
| if (type is RecordType) { |
| return type.positionalFields.firstOrNull?.type; |
| } |
| |
| return type; |
| } |
| |
| @override |
| DartType? visitPatternAssignment(PatternAssignment node) { |
| if (offset >= node.equals.end) { |
| return _requiredTypeOfPattern(node.pattern); |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitPatternField(PatternField node) { |
| var parent = node.parent; |
| if (parent is ObjectPattern) { |
| return _visitFieldInObjectPattern(parent, node); |
| } else if (parent is RecordPattern) { |
| return _visitFieldInRecordPattern(parent, node); |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitPatternVariableDeclaration(PatternVariableDeclaration node) { |
| if (offset >= node.equals.end) { |
| return _requiredTypeOfPattern(node.pattern); |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitPostfixExpression(PostfixExpression node) { |
| return node.operand.correspondingParameter?.type; |
| } |
| |
| @override |
| DartType? visitPrefixedIdentifier(PrefixedIdentifier node) { |
| return _visitParent(node); |
| } |
| |
| @override |
| DartType? visitPrefixExpression(PrefixExpression node) { |
| return node.operand.correspondingParameter?.type; |
| } |
| |
| @override |
| DartType? visitPropertyAccess(PropertyAccess node) { |
| return _visitParent(node); |
| } |
| |
| @override |
| DartType? visitRecordLiteral(RecordLiteral node) { |
| var type = node.parent?.accept(this); |
| if (type is! RecordType) { |
| return null; |
| } |
| |
| var index = 0; |
| |
| DartType? typeOfIndexPositionalField() { |
| if (index < type.positionalFields.length) { |
| return type.positionalFields[index].type; |
| } |
| return null; |
| } |
| |
| for (var argument in node.fields) { |
| if (argument is NamedExpression) { |
| if (offset <= argument.offset) { |
| return typeOfIndexPositionalField(); |
| } |
| if (argument.contains(offset)) { |
| if (offset >= argument.name.colon.end) { |
| var name = argument.name.label.name; |
| return type.namedField(name)?.type; |
| } |
| return null; |
| } |
| } else { |
| if (offset <= argument.end) { |
| return typeOfIndexPositionalField(); |
| } |
| index++; |
| } |
| } |
| |
| return typeOfIndexPositionalField(); |
| } |
| |
| @override |
| DartType? visitRecordPattern(RecordPattern node) { |
| if (!range |
| .endStart(node.leftParenthesis, node.rightParenthesis) |
| .contains(offset)) { |
| return null; |
| } |
| var recordType = node.matchedValueType; |
| if (recordType is! RecordType) { |
| return null; |
| } |
| var positionalIndex = _computePositionalIndex(node); |
| var positionalFields = recordType.positionalFields; |
| if (positionalIndex < positionalFields.length) { |
| return positionalFields[positionalIndex].type; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitReturnStatement(ReturnStatement node) { |
| if (node.returnKeyword.end < offset) { |
| var functionBody = node.thisOrAncestorOfType<FunctionBodyImpl>(); |
| if (functionBody != null) { |
| return functionBody.bodyContext?.contextType; |
| } |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitSetOrMapLiteral(SetOrMapLiteral node) { |
| var type = node.staticType; |
| if (type is InterfaceType && |
| range.endStart(node.leftBracket, node.rightBracket).contains(offset) && |
| (type.isDartCoreMap || type.isDartCoreSet)) { |
| return type.typeArguments[0]; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitSimpleIdentifier(SimpleIdentifier node) { |
| return _visitParent(node); |
| } |
| |
| @override |
| DartType? visitSimpleStringLiteral(SimpleStringLiteral node) { |
| // The only completion inside of a String literal would be a directive, |
| // where the context type would not be of value. |
| return null; |
| } |
| |
| @override |
| DartType? visitSpreadElement(SpreadElement node) { |
| if (node.spreadOperator.end <= offset) { |
| var currentNode = node.parent; |
| while (currentNode != null) { |
| if (currentNode is ListLiteral) { |
| return typeProvider.iterableDynamicType; |
| } else if (currentNode is SetOrMapLiteral) { |
| if (currentNode.isSet) { |
| return typeProvider.iterableDynamicType; |
| } |
| return typeProvider.mapType( |
| typeProvider.dynamicType, |
| typeProvider.dynamicType, |
| ); |
| } |
| currentNode = currentNode.parent; |
| } |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitSwitchCase(SwitchCase node) { |
| if (range.endStart(node.keyword, node.colon).contains(offset)) { |
| var parent = node.parent; |
| if (parent is SwitchStatement) { |
| return parent.expression.staticType; |
| } |
| } |
| return super.visitSwitchCase(node); |
| } |
| |
| @override |
| DartType? visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { |
| if (node.variables.contains(offset)) { |
| return node.variables.accept(this); |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitVariableDeclaration(VariableDeclaration node) { |
| var equals = node.equals; |
| if (equals != null && equals.end <= offset) { |
| var parent = node.parent; |
| if (parent is VariableDeclarationList) { |
| return parent.type?.type ?? |
| _impliedDartTypeWithName(typeProvider, node.name.lexeme); |
| } |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitVariableDeclarationList(VariableDeclarationList node) { |
| for (var varDecl in node.variables) { |
| if (varDecl.contains(offset)) { |
| var equals = varDecl.equals; |
| if (equals != null && equals.end <= offset) { |
| return node.type?.type ?? |
| _impliedDartTypeWithName(typeProvider, varDecl.name.lexeme); |
| } |
| } |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitWhenClause(WhenClause node) { |
| return typeProvider.boolType; |
| } |
| |
| @override |
| DartType? visitWhileStatement(WhileStatement node) { |
| if (range |
| .endStart(node.leftParenthesis, node.rightParenthesis) |
| .contains(offset)) { |
| return typeProvider.boolType; |
| } |
| return null; |
| } |
| |
| @override |
| DartType? visitYieldStatement(YieldStatement node) { |
| if (range.endStart(node.yieldKeyword, node.semicolon).contains(offset)) { |
| var functionBody = node.thisOrAncestorOfType<FunctionBodyImpl>(); |
| if (functionBody != null) { |
| return functionBody.bodyContext?.contextType; |
| } |
| } |
| return null; |
| } |
| |
| int _computePositionalIndex(RecordPattern node) { |
| var fields = node.fields; |
| if (fields.isEmpty) { |
| return 0; |
| } |
| var index = 0; |
| for (var field in fields) { |
| var rightToken = field.endToken; |
| if (rightToken.next!.type == TokenType.COMMA) { |
| rightToken = rightToken.next!; |
| } |
| if (offset <= rightToken.offset) { |
| return index; |
| } |
| if (field.name == null) { |
| index++; |
| } |
| } |
| return index; |
| } |
| |
| /// Given a [pattern] that can appear on the left of either a |
| /// `PatternAssignment` or a `PatternVariableDeclaration`, return the context |
| /// type for the right-hand side. |
| DartType? _requiredTypeOfPattern(DartPattern pattern) { |
| // TODO(brianwilkerson): Replace with `patternTypeSchema` (on AST) where |
| // possible. |
| pattern = pattern.unParenthesized; |
| Element2? element; |
| if (pattern is AssignedVariablePattern) { |
| element = pattern.element2; |
| } else if (pattern is DeclaredVariablePattern) { |
| element = pattern.declaredElement2; |
| // } else if (pattern is RecordPattern) { |
| // pattern.fields.map((e) => _requiredTypeOfPattern(e.pattern)).toList(); |
| } else if (pattern is ListPattern) { |
| return pattern.requiredType; |
| } |
| if (element is VariableElement2) { |
| return element.type; |
| } |
| return null; |
| } |
| |
| DartType? _visitFieldInObjectPattern( |
| ObjectPattern parent, |
| PatternField field, |
| ) { |
| var fieldName = field.name; |
| if (fieldName == null || offset < fieldName.end) { |
| return null; |
| } |
| var name = fieldName.name?.lexeme; |
| if (name == null) { |
| return null; |
| } |
| var type = parent.type.type; |
| if (type is! InterfaceType) { |
| return null; |
| } |
| var declaredElement = field.element2?.library2; |
| var uri = declaredElement?.uri; |
| var manager = InheritanceManager3(); |
| var member = manager.getMember3(type, Name(uri, name)); |
| if (member is GetterElement) { |
| return member.returnType; |
| } else if (member is MethodElement2) { |
| return member.returnType; |
| } |
| return null; |
| } |
| |
| DartType? _visitFieldInRecordPattern( |
| RecordPattern parent, |
| PatternField field, |
| ) { |
| var recordType = parent.matchedValueType; |
| if (recordType is! RecordType) { |
| return null; |
| } |
| var fieldName = field.name; |
| if (fieldName == null) { |
| // Completing a positional field. |
| var fields = parent.fields; |
| var index = fields.indexOf(field); |
| int fieldIndex = 0; // The index of the positional field being matched. |
| for (int i = 0; i < index; i++) { |
| if (fields[i].name == null) { |
| fieldIndex++; |
| } |
| } |
| var positionalFields = recordType.positionalFields; |
| if (fieldIndex < positionalFields.length) { |
| return positionalFields[fieldIndex].type; |
| } |
| return null; |
| } |
| // Completing a named field. |
| if (offset < fieldName.end) { |
| // Completing before the end of the colon means we're not in the field's |
| // value, so there is no context type. |
| return null; |
| } |
| var name = fieldName.name?.lexeme; |
| if (name == null) { |
| return null; |
| } |
| var namedFields = recordType.namedFields; |
| for (var field in namedFields) { |
| if (field.name == name) { |
| return field.type; |
| } |
| } |
| return null; |
| } |
| |
| /// Returns the result of visiting the parent of the [node]. |
| DartType? _visitParent(AstNode node) { |
| var parent = node.parent; |
| if (parent == null) { |
| return null; |
| } |
| return parent.accept(this); |
| } |
| } |
| |
| /// Some useful extensions on [AstNode] for this computer. |
| extension on AstNode { |
| bool contains(int o) => offset <= o && o <= end; |
| } |
| |
| /// Some useful extensions on [ArgumentList] for this computer. |
| extension on ArgumentList { |
| /// Return the [FunctionType], if there is one, for this [ArgumentList]. |
| FunctionType? get functionType { |
| var parent = this.parent; |
| if (parent is InstanceCreationExpression) { |
| return parent.constructorName.element?.type; |
| } else if (parent is MethodInvocation) { |
| var type = parent.staticInvokeType; |
| if (type is FunctionType) { |
| return type; |
| } |
| } else if (parent is FunctionExpressionInvocation) { |
| var type = parent.staticInvokeType; |
| if (type is FunctionType) { |
| return type; |
| } |
| } |
| return null; |
| } |
| } |