| // 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 'package:analyzer/dart/ast/ast.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/file_system/file_system.dart'; |
| import 'package:analyzer/src/dart/ast/extensions.dart'; |
| import 'package:analyzer/src/dart/ast/utilities.dart'; |
| import 'package:analyzer/src/dart/element/element.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol; |
| import 'package:analyzer_plugin/utilities/analyzer_converter.dart'; |
| import 'package:analyzer_plugin/utilities/navigation/navigation.dart'; |
| |
| NavigationCollector computeDartNavigation( |
| ResourceProvider resourceProvider, |
| NavigationCollector collector, |
| CompilationUnit unit, |
| int? offset, |
| int? length) { |
| var dartCollector = _DartNavigationCollector(collector, offset, length); |
| var visitor = _DartNavigationComputerVisitor(resourceProvider, dartCollector); |
| if (offset == null || length == null) { |
| unit.accept(visitor); |
| } else { |
| var node = _getNodeForRange(unit, offset, length); |
| if (node != null) { |
| node = _getNavigationTargetNode(node); |
| } |
| node?.accept(visitor); |
| } |
| return collector; |
| } |
| |
| /// Gets the nearest node that should be used for navigation. |
| /// |
| /// This is usually the outermost node with the same offset as node but in some |
| /// cases may be a different ancestor where required to produce the correct |
| /// result. |
| AstNode _getNavigationTargetNode(AstNode node) { |
| AstNode? current = node; |
| while (current != null && |
| current.parent != null && |
| current.offset == current.parent!.offset) { |
| current = current.parent; |
| } |
| current ??= node; |
| |
| // To navigate to formal params, we need to visit the parameter and not just |
| // the identifier but they don't start at the same offset as they have a |
| // prefix. |
| final parent = current.parent; |
| if (parent is FormalParameter) { |
| current = parent; |
| } |
| |
| return current; |
| } |
| |
| AstNode? _getNodeForRange(CompilationUnit unit, int offset, int length) { |
| var node = NodeLocator(offset, offset + length).searchWithin(unit); |
| for (var n = node; n != null; n = n.parent) { |
| if (n is Directive) { |
| return n; |
| } |
| } |
| return node; |
| } |
| |
| /// A Dart specific wrapper around [NavigationCollector]. |
| class _DartNavigationCollector { |
| final NavigationCollector collector; |
| final int? requestedOffset; |
| final int? requestedLength; |
| |
| _DartNavigationCollector( |
| this.collector, this.requestedOffset, this.requestedLength); |
| |
| void _addRegion(int offset, int length, Element? element) { |
| element = element?.nonSynthetic; |
| if (element == null || element == DynamicElementImpl.instance) { |
| return; |
| } |
| if (element.location == null) { |
| return; |
| } |
| // Discard elements that don't span the offset/range given (if provided). |
| if (!_isWithinRequestedRange(offset, length)) { |
| return; |
| } |
| var converter = AnalyzerConverter(); |
| var kind = converter.convertElementKind(element.kind); |
| var location = converter.locationFromElement(element); |
| if (location == null) { |
| return; |
| } |
| |
| collector.addRegion(offset, length, kind, location, targetElement: element); |
| } |
| |
| void _addRegionForNode(AstNode? node, Element? element) { |
| if (node == null) { |
| return; |
| } |
| var offset = node.offset; |
| var length = node.length; |
| _addRegion(offset, length, element); |
| } |
| |
| void _addRegionForToken(Token token, Element? element) { |
| var offset = token.offset; |
| var length = token.length; |
| _addRegion(offset, length, element); |
| } |
| |
| /// Checks if offset/length intersect with the range the user requested |
| /// navigation regions for. |
| /// |
| /// If the request did not specify a range, always returns true. |
| bool _isWithinRequestedRange(int offset, int length) { |
| final requestedOffset = this.requestedOffset; |
| if (requestedOffset == null) { |
| return true; |
| } |
| if (offset > requestedOffset + (requestedLength ?? 0)) { |
| // Starts after the requested range. |
| return false; |
| } |
| if (offset + length < requestedOffset) { |
| // Ends before the requested range. |
| return false; |
| } |
| return true; |
| } |
| } |
| |
| class _DartNavigationComputerVisitor extends RecursiveAstVisitor<void> { |
| final ResourceProvider resourceProvider; |
| final _DartNavigationCollector computer; |
| |
| _DartNavigationComputerVisitor(this.resourceProvider, this.computer); |
| |
| @override |
| void visitAnnotation(Annotation node) { |
| var element = node.element; |
| if (element is ConstructorElement && element.isSynthetic) { |
| element = element.enclosingElement; |
| } |
| var name = node.name; |
| if (name is PrefixedIdentifier) { |
| // use constructor in: @PrefixClass.constructorName |
| var prefixElement = name.prefix.staticElement; |
| if (prefixElement is ClassElement) { |
| prefixElement = element; |
| } |
| computer._addRegionForNode(name.prefix, prefixElement); |
| // always constructor |
| computer._addRegionForNode(name.identifier, element); |
| } else { |
| computer._addRegionForNode(name, element); |
| } |
| computer._addRegionForNode(node.constructorName, element); |
| // type arguments |
| node.typeArguments?.accept(this); |
| // arguments |
| node.arguments?.accept(this); |
| } |
| |
| @override |
| void visitAssignmentExpression(AssignmentExpression node) { |
| node.leftHandSide.accept(this); |
| computer._addRegionForToken(node.operator, node.staticElement); |
| node.rightHandSide.accept(this); |
| } |
| |
| @override |
| void visitBinaryExpression(BinaryExpression node) { |
| node.leftOperand.accept(this); |
| computer._addRegionForToken(node.operator, node.staticElement); |
| node.rightOperand.accept(this); |
| } |
| |
| @override |
| void visitComment(Comment node) { |
| for (var commentReference in node.references) { |
| commentReference.accept(this); |
| } |
| |
| var inToolAnnotation = false; |
| for (var token in node.tokens) { |
| if (token.isEof) { |
| break; |
| } |
| var strValue = token.toString(); |
| if (strValue.isEmpty) { |
| continue; |
| } |
| |
| if (inToolAnnotation) { |
| if (strValue.contains('{@end-tool}')) { |
| inToolAnnotation = false; |
| } else if (strValue.contains('** See code in ')) { |
| var startIndex = strValue.indexOf('** See code in ') + 15; |
| var endIndex = strValue.indexOf('.dart') + 5; |
| var pathSnippet = strValue.substring(startIndex, endIndex); |
| var parentPath = |
| _computeParentWithExamplesAPI(node, resourceProvider); |
| if (parentPath != null) { |
| var start = token.offset + startIndex; |
| var end = token.offset + endIndex; |
| computer.collector.addRegion( |
| start, |
| end - start, |
| protocol.ElementKind.LIBRARY, |
| protocol.Location( |
| resourceProvider.pathContext.join(parentPath, pathSnippet), |
| 0, |
| 0, |
| 0, |
| 0, |
| endLine: 0, |
| endColumn: 0)); |
| } |
| } |
| } else if (strValue.contains('{@tool ')) { |
| inToolAnnotation = true; |
| } |
| } |
| } |
| |
| @override |
| void visitCompilationUnit(CompilationUnit unit) { |
| // prepare top-level nodes sorted by their offsets |
| var nodes = <AstNode>[]; |
| nodes.addAll(unit.directives); |
| nodes.addAll(unit.declarations); |
| nodes.sort((a, b) { |
| return a.offset - b.offset; |
| }); |
| // visit sorted nodes |
| for (var node in nodes) { |
| node.accept(this); |
| } |
| } |
| |
| @override |
| void visitConfiguration(Configuration node) { |
| var source = node.uriSource; |
| if (source != null) { |
| if (resourceProvider.getResource(source.fullName).exists) { |
| // TODO(brianwilkerson) If the analyzer ever resolves the URI to a |
| // library, use that library element to create the region. |
| var uriNode = node.uri; |
| if (computer._isWithinRequestedRange(uriNode.offset, uriNode.length)) { |
| computer.collector.addRegion( |
| uriNode.offset, |
| uriNode.length, |
| protocol.ElementKind.LIBRARY, |
| protocol.Location(source.fullName, 0, 0, 0, 0, |
| endLine: 0, endColumn: 0)); |
| } |
| } |
| } |
| super.visitConfiguration(node); |
| } |
| |
| @override |
| void visitConstructorDeclaration(ConstructorDeclaration node) { |
| // For a default constructor, override the class name to be the declaration |
| // itself rather than linking to the class. |
| var name = node.name; |
| if (name == null) { |
| computer._addRegionForNode(node.returnType, node.declaredElement); |
| } else { |
| node.returnType.accept(this); |
| name.accept(this); |
| } |
| node.parameters.accept(this); |
| node.initializers.accept(this); |
| node.redirectedConstructor?.accept(this); |
| node.body.accept(this); |
| } |
| |
| @override |
| void visitConstructorName(ConstructorName node) { |
| Element? element = node.staticElement; |
| if (element == null) { |
| return; |
| } |
| // add regions |
| var typeName = node.type; |
| // [prefix].ClassName |
| { |
| var name = typeName.name; |
| var className = name; |
| if (name is PrefixedIdentifier) { |
| name.prefix.accept(this); |
| className = name.identifier; |
| } |
| // For a named constructor, the class name points at the class. |
| var classNameTargetElement = |
| node.name != null ? className.staticElement : element; |
| computer._addRegionForNode(className, classNameTargetElement); |
| } |
| // <TypeA, TypeB> |
| typeName.typeArguments?.accept(this); |
| // optional "name" |
| if (node.name != null) { |
| computer._addRegionForNode(node.name, element); |
| } |
| } |
| |
| @override |
| void visitDeclaredIdentifier(DeclaredIdentifier node) { |
| if (node.type == null) { |
| var token = node.keyword; |
| if (token != null && token.keyword == Keyword.VAR) { |
| var inferredType = node.declaredElement?.type; |
| var element = inferredType?.element; |
| if (element != null) { |
| computer._addRegionForToken(token, element); |
| } |
| } |
| } |
| super.visitDeclaredIdentifier(node); |
| } |
| |
| @override |
| void visitEnumConstantDeclaration(EnumConstantDeclaration node) { |
| computer._addRegionForNode(node.name, node.constructorElement); |
| |
| var arguments = node.arguments; |
| if (arguments != null) { |
| computer._addRegionForNode( |
| arguments.constructorSelector?.name, |
| node.constructorElement, |
| ); |
| arguments.typeArguments?.accept(this); |
| arguments.argumentList.accept(this); |
| } |
| } |
| |
| @override |
| void visitExportDirective(ExportDirective node) { |
| var exportElement = node.element; |
| if (exportElement != null) { |
| Element? libraryElement = exportElement.exportedLibrary; |
| _addUriDirectiveRegion(node, libraryElement); |
| } |
| super.visitExportDirective(node); |
| } |
| |
| @override |
| void visitFieldFormalParameter(FieldFormalParameter node) { |
| final element = node.declaredElement; |
| if (element is FieldFormalParameterElementImpl) { |
| computer._addRegionForToken(node.thisKeyword, element.field); |
| computer._addRegionForNode(node.identifier, element.field); |
| } |
| |
| node.type?.accept(this); |
| node.typeParameters?.accept(this); |
| node.parameters?.accept(this); |
| } |
| |
| @override |
| void visitImportDirective(ImportDirective node) { |
| var importElement = node.element2; |
| if (importElement != null) { |
| Element? libraryElement = importElement.importedLibrary; |
| _addUriDirectiveRegion(node, libraryElement); |
| } |
| super.visitImportDirective(node); |
| } |
| |
| @override |
| void visitIndexExpression(IndexExpression node) { |
| super.visitIndexExpression(node); |
| var element = node.writeOrReadElement; |
| computer._addRegionForToken(node.leftBracket, element); |
| computer._addRegionForToken(node.rightBracket, element); |
| } |
| |
| @override |
| void visitLibraryDirective(LibraryDirective node) { |
| computer._addRegionForNode(node.name, node.element); |
| } |
| |
| @override |
| void visitPartDirective(PartDirective node) { |
| final element = node.element; |
| if (element is PartElement) { |
| final uri = element.uri; |
| if (uri is DirectiveUriWithUnit) { |
| computer._addRegionForNode(node.uri, uri.unit); |
| } else if (uri is DirectiveUriWithSource) { |
| final uriNode = node.uri; |
| final source = uri.source; |
| computer.collector.addRegion( |
| uriNode.offset, |
| uriNode.length, |
| protocol.ElementKind.FILE, |
| protocol.Location(source.fullName, 0, 0, 0, 0, |
| endLine: 0, endColumn: 0), |
| ); |
| } |
| } |
| |
| super.visitPartDirective(node); |
| } |
| |
| @override |
| void visitPartOfDirective(PartOfDirective node) { |
| computer._addRegionForNode(node.libraryName ?? node.uri, node.element); |
| super.visitPartOfDirective(node); |
| } |
| |
| @override |
| void visitPostfixExpression(PostfixExpression node) { |
| super.visitPostfixExpression(node); |
| computer._addRegionForToken(node.operator, node.staticElement); |
| } |
| |
| @override |
| void visitPrefixExpression(PrefixExpression node) { |
| computer._addRegionForToken(node.operator, node.staticElement); |
| super.visitPrefixExpression(node); |
| } |
| |
| @override |
| void visitRedirectingConstructorInvocation( |
| RedirectingConstructorInvocation node) { |
| Element? element = node.staticElement; |
| if (element != null && element.isSynthetic) { |
| element = element.enclosingElement; |
| } |
| // add region |
| computer._addRegionForToken(node.thisKeyword, element); |
| computer._addRegionForNode(node.constructorName, element); |
| // process arguments |
| node.argumentList.accept(this); |
| } |
| |
| @override |
| void visitSimpleIdentifier(SimpleIdentifier node) { |
| var element = node.writeOrReadElement; |
| computer._addRegionForNode(node, element); |
| } |
| |
| @override |
| void visitSuperConstructorInvocation(SuperConstructorInvocation node) { |
| Element? element = node.staticElement; |
| if (element != null && element.isSynthetic) { |
| element = element.enclosingElement; |
| } |
| // add region |
| computer._addRegionForToken(node.superKeyword, element); |
| computer._addRegionForNode(node.constructorName, element); |
| // process arguments |
| node.argumentList.accept(this); |
| } |
| |
| @override |
| void visitSuperFormalParameter(SuperFormalParameter node) { |
| var element = node.declaredElement; |
| if (element is SuperFormalParameterElementImpl) { |
| var superParameter = element.superConstructorParameter; |
| computer._addRegionForToken(node.superKeyword, superParameter); |
| computer._addRegionForNode(node.identifier, superParameter); |
| } |
| |
| node.type?.accept(this); |
| node.typeParameters?.accept(this); |
| node.parameters?.accept(this); |
| } |
| |
| @override |
| void visitVariableDeclarationList(VariableDeclarationList node) { |
| /// Return the element for the type inferred for each of the variables in |
| /// the given list of [variables], or `null` if not all variable have the |
| /// same inferred type. |
| Element? getCommonElement(List<VariableDeclaration> variables) { |
| var firstElement = variables[0].declaredElement?.type.element; |
| if (firstElement == null) { |
| return null; |
| } |
| for (var i = 1; i < variables.length; i++) { |
| var element = variables[1].declaredElement?.type.element; |
| if (element != firstElement) { |
| return null; |
| } |
| } |
| return firstElement; |
| } |
| |
| if (node.type == null) { |
| var token = node.keyword; |
| if (token?.keyword == Keyword.VAR) { |
| var element = getCommonElement(node.variables); |
| if (element != null) { |
| computer._addRegionForToken(token!, element); |
| } |
| } |
| } |
| super.visitVariableDeclarationList(node); |
| } |
| |
| /// If the source of the given [element] (referenced by the [node]) exists, |
| /// then add the navigation region from the [node] to the [element]. |
| void _addUriDirectiveRegion(UriBasedDirective node, Element? element) { |
| var source = element?.source; |
| if (source != null) { |
| if (resourceProvider.getResource(source.fullName).exists) { |
| computer._addRegionForNode(node.uri, element); |
| } |
| } |
| } |
| |
| /// Given some [Comment], compute and return the parent directory absolute |
| /// path which contains the directories 'examples/api/'. Null is returned if |
| /// such directories are not found. |
| String? _computeParentWithExamplesAPI( |
| Comment node, ResourceProvider resourceProvider) { |
| var source = |
| node.thisOrAncestorOfType<CompilationUnit>()?.declaredElement?.source; |
| if (source == null) { |
| return null; |
| } |
| |
| var file = resourceProvider.getFile(source.fullName); |
| if (!file.exists) { |
| return null; |
| } |
| var parent = file.parent; |
| while (parent != parent.parent) { |
| var examplesFolder = parent.getChildAssumingFolder('examples'); |
| if (examplesFolder.exists && |
| examplesFolder.getChildAssumingFolder('api').exists) { |
| return parent.path; |
| } |
| parent = parent.parent; |
| } |
| return null; |
| } |
| } |