| // Copyright (c) 2014, 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/protocol/protocol_generated.dart' | |
| show HoverInformation; | |
| import 'package:analysis_server/src/computer/computer_documentation.dart'; | |
| import 'package:analyzer/dart/ast/ast.dart'; | |
| import 'package:analyzer/dart/ast/syntactic_entity.dart'; | |
| import 'package:analyzer/dart/element/element.dart'; | |
| import 'package:analyzer/dart/element/type.dart'; | |
| import 'package:analyzer/src/dart/ast/element_locator.dart'; | |
| import 'package:analyzer/src/dart/element/extensions.dart'; | |
| import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart'; | |
| import 'package:path/path.dart' as path; | |
| /// Information about a library to display in a hover. | |
| typedef _LibraryInfo = ({String? libraryName, String? libraryPath})?; | |
| typedef _OffsetLength = ({int offset, int length}); | |
| /// A computer for the hover at the specified offset of a Dart | |
| /// [CompilationUnit]. | |
| class DartUnitHoverComputer { | |
| final CompilationUnit _unit; | |
| final int _offset; | |
| final DartDocumentationComputer _documentationComputer; | |
| new(DartdocDirectiveInfo dartdocInfo, this._unit, this._offset) | |
| : _documentationComputer = DartDocumentationComputer(dartdocInfo); | |
| /// Returns the computed hover, maybe `null`. | |
| HoverInformation? compute() { | |
| var node = _unit.nodeCovering(offset: _offset); | |
| if (node == null) { | |
| return null; | |
| } | |
| var locationEntity = _locationEntity(node, _offset); | |
| node = _targetNode(node); | |
| if (node == null || locationEntity == null) { | |
| return null; | |
| } | |
| if (node is CompilationUnitMember || | |
| node is CatchClauseParameter || | |
| node is EnumConstantDeclaration || | |
| node is Expression || | |
| node is FormalParameter || | |
| node is MethodDeclaration || | |
| node is NamedArgument || | |
| node is NamedType || | |
| node is ConstructorDeclaration || | |
| node is DeclaredIdentifier || | |
| node is RecordLiteralNamedField || | |
| node is VariableDeclaration || | |
| node is VariablePattern || | |
| node is PatternFieldName || | |
| node is PrimaryConstructorBody || | |
| node is PrimaryConstructorDeclaration || | |
| node is PrimaryConstructorName || | |
| node is DartPattern || | |
| (node is LibraryDirective && node.name == null) || | |
| (node is SimpleIdentifier && node.parent is ImportDirective) || | |
| node is ImportPrefixReference) { | |
| var range = _hoverRange(node, locationEntity); | |
| var hover = HoverInformation(range.offset, range.length); | |
| // element | |
| var element = ElementLocator.locate(node); | |
| if (element != null && element.isWildcardVariable) { | |
| return null; | |
| } | |
| if (element case var elementReference?) { | |
| // short code that illustrates the element meaning. | |
| hover.elementDescription = _elementDisplayString( | |
| node, | |
| elementReference, | |
| ); | |
| hover.elementKind = elementReference.kind.displayName; | |
| hover.isDeprecated = elementReference.isDeprecatedWithKind('use'); | |
| // not local element | |
| if (node.parent | |
| case GenericFunctionType( | |
| parent: Declaration( | |
| declaredFragment: Fragment(element: var declared), | |
| ) || | |
| FormalParameter( | |
| declaredFragment: Fragment(element: var declared), | |
| ) || | |
| GenericTypeAlias( | |
| declaredFragment: Fragment(element: var declared), | |
| ), | |
| ) | |
| when elementReference.enclosingElement | |
| is GenericFunctionTypeElement) { | |
| var list = hover.containingExecutableDescriptions ??= []; | |
| list.add(declared.displayName); | |
| elementReference = declared; | |
| } | |
| var enclosingElement = elementReference.enclosingElement; | |
| while (true) { | |
| if (enclosingElement case ExecutableElement( | |
| enclosingElement: var enclosing, | |
| :var displayName, | |
| )) { | |
| if (enclosingElement case ConstructorElement(name: 'new')) { | |
| displayName = 'new'; | |
| } else if (enclosingElement case LocalFunctionElement( | |
| firstFragment: Fragment(:var enclosingFragment?), | |
| )) { | |
| enclosing = enclosingFragment.element; | |
| } | |
| enclosingElement = enclosing; | |
| var list = hover.containingExecutableDescriptions ??= []; | |
| list.add(displayName); | |
| } else { | |
| break; | |
| } | |
| } | |
| // containing instance | |
| hover.containingClassDescription = _containingInstance( | |
| elementReference, | |
| ); | |
| if (elementReference.enclosingElement is! ExecutableElement || | |
| (hover.containingExecutableDescriptions?.isNotEmpty ?? false)) { | |
| // containing library | |
| var libraryInfo = _libraryInfo(elementReference); | |
| hover.containingLibraryName = libraryInfo?.libraryName; | |
| hover.containingLibraryPath = libraryInfo?.libraryPath; | |
| } | |
| // documentation | |
| hover.dartdoc = _documentationComputer.compute(elementReference)?.full; | |
| } | |
| // parameter | |
| hover.parameter = _parameterDisplayString(node); | |
| // types | |
| hover.staticType = _typeDisplayString(node, element); | |
| // done | |
| return hover; | |
| } | |
| // not an expression | |
| return null; | |
| } | |
| /// Gets the name of the containing class of [element]. | |
| String? _containingInstance(Element element) { | |
| var containingClass = element.thisOrAncestorOfType<InterfaceElement>(); | |
| return containingClass != null && containingClass != element | |
| ? containingClass.displayName | |
| : null; | |
| } | |
| /// Gets the display string for [element]. | |
| /// | |
| /// This is usually `element.getDisplayString()` but may contain additional | |
| /// information to disambiguate things like constructors from types (and | |
| /// whether they are const). | |
| String? _elementDisplayString(AstNode node, Element? element) { | |
| var displayString = element?.displayString( | |
| multiline: true, | |
| includePositionalParameterNames: true, | |
| ); | |
| if (displayString != null) { | |
| if (element is TypeParameterElement) { | |
| displayString = '<$displayString>'; | |
| } else if (node is InstanceCreationExpression && node.keyword == null) { | |
| var prefix = node.isConst ? '(const) ' : '(new) '; | |
| displayString = prefix + displayString; | |
| } else if (node is DotShorthandConstructorInvocation) { | |
| var prefix = node.isConst ? '(const) ' : '(new) '; | |
| displayString = prefix + displayString; | |
| } | |
| } | |
| return displayString; | |
| } | |
| /// Computes the range this hover applies to. | |
| /// | |
| /// This is usually the range of [entity] but may be adjusted for entities | |
| /// like constructor names. | |
| _OffsetLength _hoverRange(AstNode node, SyntacticEntity entity) { | |
| // For constructors, the location should cover the type name and | |
| // constructor name (for both calls and declarations). | |
| if (node is InstanceCreationExpression) { | |
| return ( | |
| offset: node.constructorName.offset, | |
| length: node.constructorName.length, | |
| ); | |
| } else if (node is DotShorthandConstructorInvocation) { | |
| return (offset: node.offset, length: node.length); | |
| } else if (node is ConstructorDeclaration) { | |
| var offset = | |
| node.typeName?.offset ?? | |
| node.newKeyword?.offset ?? | |
| node.factoryKeyword!.offset; | |
| var end = | |
| node.name?.end ?? | |
| node.typeName?.end ?? | |
| node.newKeyword?.end ?? | |
| node.factoryKeyword!.end; | |
| var length = end - offset; | |
| return (offset: offset, length: length); | |
| } else if (node is PrimaryConstructorDeclaration) { | |
| if (node.constructorName != null) { | |
| return (offset: entity.offset, length: entity.length); | |
| } | |
| var offset = node.typeName.offset; | |
| var end = node.typeName.end; | |
| var length = end - offset; | |
| return (offset: offset, length: length); | |
| } else { | |
| return (offset: entity.offset, length: entity.length); | |
| } | |
| } | |
| /// Returns information about the library that contains [element]. | |
| _LibraryInfo _libraryInfo(Element element) { | |
| var library = element.library; | |
| if (library == null) { | |
| return null; | |
| } | |
| var definingSource = library.firstFragment.source; | |
| var uri = definingSource.uri; | |
| var analysisSession = _unit.declaredFragment?.element.session; | |
| String? libraryName, libraryPath; | |
| // for 'file:' URIs, use the path after the package root | |
| if (uri.isScheme('file') && analysisSession != null) { | |
| var pathContext = analysisSession.resourceProvider.pathContext; | |
| var libraryFilePath = pathContext.fromUri(uri); | |
| var contextRoot = analysisSession.analysisContext.contextRoot; | |
| var workspace = contextRoot.workspace; | |
| // Prefer using the root of the package that contains this library, since | |
| // the context root could now be a Pub Workspace. If we don't find one, | |
| // fall back to the context root. | |
| var package = workspace.packages.packageForPath(libraryFilePath); | |
| var packageRootDir = package?.rootFolder.path ?? contextRoot.root.path; | |
| var relativePath = pathContext.relative( | |
| libraryFilePath, | |
| from: packageRootDir, | |
| ); | |
| if (pathContext.style == path.Style.windows) { | |
| var pathList = pathContext.split(relativePath); | |
| libraryName = pathList.join('/'); | |
| } else { | |
| libraryName = relativePath; | |
| } | |
| } else { | |
| libraryName = uri.toString(); | |
| } | |
| libraryPath = definingSource.fullName; | |
| return (libraryName: libraryName, libraryPath: libraryPath); | |
| } | |
| /// Returns the [SyntacticEntity] that should be used as the range for this | |
| /// hover. | |
| /// | |
| /// Returns `null` if there is no valid entity for this hover. | |
| SyntacticEntity? _locationEntity(AstNode node, int offset) { | |
| return switch (node) { | |
| BinaryExpression() => node.operator, | |
| ConditionalExpression() | |
| when offset >= node.question.offset && offset <= node.question.end => | |
| node.question, | |
| ConditionalExpression() | |
| when offset >= node.colon.offset && offset <= node.colon.end => | |
| node.colon, | |
| AssignmentExpression() => node.operator, | |
| PrefixExpression() => node.operator, | |
| PostfixExpression() => node.operator, | |
| CatchClauseParameter() => node.name, | |
| ClassDeclaration() => node.namePart.typeName, | |
| ConstructorDeclaration() => | |
| node.name ?? node.typeName ?? node.newKeyword ?? node.factoryKeyword, | |
| DeclaredIdentifier() => node.name, | |
| EnumConstantDeclaration() => node.name, | |
| EnumDeclaration() => node.namePart.typeName, | |
| Expression() => node, | |
| ExtensionDeclaration() => node.name, | |
| ExtensionTypeDeclaration() => node.namePart.typeName, | |
| FormalParameter() => node.name, | |
| FunctionDeclaration() => node.name, | |
| ImportPrefixReference() => node.name, | |
| LibraryDirective() => node.libraryKeyword, | |
| MethodDeclaration() => node.name, | |
| NamedArgument() => node.name, | |
| MixinDeclaration() => node.name, | |
| NameWithTypeParameters() => node.typeName, | |
| NamedType() => node.name, | |
| PatternFieldName() => node.name, | |
| PrimaryConstructorBody() => | |
| node.declaration?.constructorName ?? node.declaration?.typeName, | |
| PrimaryConstructorDeclaration() => node.constructorName ?? node.typeName, | |
| PrimaryConstructorName() => node.name, | |
| RecordLiteralNamedField() => node.name, | |
| TypeAlias() => node.name, | |
| VariableDeclaration() => node.name, | |
| VariablePattern() => node.name, | |
| WildcardPattern() => node.name, | |
| _ => null, | |
| }; | |
| } | |
| /// Gets the display string for the type of the parameter. | |
| /// | |
| /// Returns `null` if the node doesn't correspond to a parameter. | |
| String? _parameterDisplayString(AstNode node) { | |
| FormalParameterElement? parameter; | |
| if (node is Argument) { | |
| parameter = node.correspondingParameter; | |
| } | |
| return switch (parameter?.enclosingElement) { | |
| // Expressions passed as arguments to setters and binary expressions | |
| // will have parameters here but we don't want them to show as such in | |
| // hovers because information about those functions are already available | |
| // by hovering over the function name or the operator. | |
| SetterElement() => null, | |
| MethodElement method when method.isOperator => null, | |
| _ => _elementDisplayString(node, parameter), | |
| }; | |
| } | |
| /// Adjusts the target node for constructors. | |
| AstNode? _targetNode(AstNode node) { | |
| var parent = node.parent; | |
| var parent2 = parent?.parent; | |
| if (node is ClassNamePart) { | |
| return parent; | |
| } else if (parent is NamedType && | |
| parent2 is ConstructorName && | |
| parent2.parent is InstanceCreationExpression) { | |
| return parent2.parent; | |
| } else if (parent is ConstructorName && | |
| parent2 is InstanceCreationExpression) { | |
| return parent2; | |
| } else if (node is SimpleIdentifier && | |
| parent is ConstructorDeclaration && | |
| parent.name != null) { | |
| return parent; | |
| } else if (node is SimpleIdentifier && | |
| parent is DotShorthandConstructorInvocation) { | |
| return parent; | |
| } | |
| return node; | |
| } | |
| /// Returns information about the static type of [node]. | |
| String? _typeDisplayString(AstNode node, Element? element) { | |
| var parent = node.parent; | |
| DartType? staticType; | |
| if (node is NamedArgument) { | |
| staticType = node.correspondingParameter?.type; | |
| } else if (node is Expression && | |
| (element == null || | |
| element is VariableElement || | |
| element is GetterElement || | |
| element is SetterElement)) { | |
| staticType = _getTypeOfDeclarationOrReference(node); | |
| } else if (element is VariableElement) { | |
| staticType = element.type; | |
| } else if (parent is MethodInvocation && parent.methodName == node) { | |
| staticType = parent.staticInvokeType; | |
| if (staticType != null && staticType is DynamicType) { | |
| staticType = null; | |
| } | |
| } else if (parent is PrefixedIdentifier && parent.identifier == node) { | |
| staticType = parent.identifier.staticType; | |
| if (staticType != null && staticType is DynamicType) { | |
| staticType = null; | |
| } | |
| } else if (parent is DotShorthandInvocation && parent.memberName == node) { | |
| staticType = parent.staticInvokeType; | |
| if (staticType != null && staticType is DynamicType) { | |
| staticType = null; | |
| } | |
| } else if (node is PatternFieldName && parent is PatternField) { | |
| staticType = parent.pattern.matchedValueType; | |
| } else if (node is DartPattern) { | |
| staticType = node.matchedValueType; | |
| } | |
| return staticType?.getDisplayString(); | |
| } | |
| static DartType? _getTypeOfDeclarationOrReference(Expression node) { | |
| if (node is SimpleIdentifier) { | |
| var element = node.element; | |
| if (element is VariableElement) { | |
| if (node.inDeclarationContext()) { | |
| return element.type; | |
| } | |
| } | |
| var parent = node.parent; | |
| var parent2 = parent?.parent; | |
| if (parent is AssignmentExpression && parent.leftHandSide == node) { | |
| // Direct setter reference | |
| return parent.writeType; | |
| } else if (parent2 is AssignmentExpression && | |
| parent2.leftHandSide == parent) { | |
| if (parent is PrefixedIdentifier && parent.identifier == node) { | |
| // Prefixed setter (`myInstance.foo =`) | |
| return parent2.writeType; | |
| } else if (parent is PropertyAccess && parent.propertyName == node) { | |
| // Expression prefix (`A<int>().foo =`) | |
| return parent2.writeType; | |
| } | |
| } | |
| } | |
| return node.staticType; | |
| } | |
| } |