blob: 149ebff2ee9bd8c5c7d5007ebdd733e7336ff6ac [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 'package:analyzer/dart/analysis/results.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/file_system/file_system.dart';
import 'package:analyzer/src/dart/ast/ast.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/document_links.dart';
import 'package:analyzer_plugin/utilities/navigation/navigation.dart';
NavigationCollector computeDartNavigation(
ResourceProvider resourceProvider,
NavigationCollector collector,
ParsedUnitResult result,
int? offset,
int? length) {
var dartCollector = _DartNavigationCollector(collector, offset, length);
var unit = result.unit;
var visitor = _DartNavigationComputerVisitor(
resourceProvider: resourceProvider,
computer: dartCollector,
unit: result,
);
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;
}
// Consider the angle brackets for type arguments part of the leading type,
// otherwise we don't navigate in the common situation of having the type name
// selected, where VS Code provides the end of the selection as the position
// to search.
//
// In `A^<String>` node will be TypeArgumentList and we will never find A if
// we start visiting from there.
if (current is TypeArgumentList && parent != null) {
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,
protocol.ElementKind kind,
protocol.Location location, {
Element? targetElement,
}) {
// Discard elements that don't span the offset/range given (if provided).
if (!_isWithinRequestedRange(offset, length)) {
return;
}
collector.addRegion(offset, length, kind, location,
targetElement: targetElement);
}
void _addRegionForElement(int offset, int length, Element element) {
element = element.nonSynthetic;
if (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 location = element.toLocation();
if (location == null) {
return;
}
_addRegion(offset, length, element.kind.toPluginElementKind, location,
targetElement: element);
}
void _addRegionForNode(AstNode? node, Element? element) {
if (node == null || element == null) {
return;
}
var offset = node.offset;
var length = node.length;
_addRegionForElement(offset, length, element);
}
void _addRegionForToken(Token token, Element? element) {
if (element == null) {
return;
}
var offset = token.offset;
var length = token.length;
_addRegionForElement(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 ParsedUnitResult unit;
final DartDocumentLinkVisitor _documentLinkVisitor;
final _DartNavigationCollector computer;
_DartNavigationComputerVisitor({
required this.resourceProvider,
required this.unit,
required this.computer,
}) : _documentLinkVisitor = DartDocumentLinkVisitor(resourceProvider, unit);
@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 visitClassDeclaration(ClassDeclaration node) {
computer._addRegionForToken(node.name, node.declaredElement);
super.visitClassDeclaration(node);
}
@override
void visitComment(Comment node) {
super.visitComment(node);
for (final link in _documentLinkVisitor.findLinks(node)) {
computer._addRegion(
link.offset,
link.length,
protocol.ElementKind.LIBRARY,
protocol.Location(
link.targetPath,
0,
0,
0,
0,
endLine: 0,
endColumn: 0,
),
);
}
}
@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) {
final resolvedUri = node.resolvedUri;
if (resolvedUri is DirectiveUriWithSource) {
final source = resolvedUri.source;
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 nameToken = node.name;
if (nameToken == null) {
computer._addRegionForNode(node.returnType, node.declaredElement);
} else {
node.returnType.accept(this);
computer._addRegionForToken(nameToken, node.declaredElement);
}
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 namedType = node.type;
// [prefix].ClassName
{
final importPrefix = namedType.importPrefix;
if (importPrefix != null) {
computer._addRegionForToken(importPrefix.name, importPrefix.element);
}
// For a named constructor, the class name points at the class.
var classNameTargetElement =
node.name != null ? namedType.element : element;
computer._addRegionForToken(namedType.name2, classNameTargetElement);
}
// <TypeA, TypeB>
namedType.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;
if (inferredType is InterfaceType) {
computer._addRegionForToken(token, inferredType.element);
}
}
}
super.visitDeclaredIdentifier(node);
}
@override
void visitDeclaredVariablePattern(DeclaredVariablePattern node) {
computer._addRegionForToken(node.name, node.declaredElement);
super.visitDeclaredVariablePattern(node);
}
@override
void visitEnumConstantDeclaration(EnumConstantDeclaration node) {
computer._addRegionForToken(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) {
var libraryElement = exportElement.exportedLibrary;
_addUriDirectiveRegion(node, libraryElement);
}
super.visitExportDirective(node);
}
@override
void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) {
computer._addRegionForToken(node.name, node.declaredElement);
super.visitExtensionTypeDeclaration(node);
}
@override
void visitFieldFormalParameter(FieldFormalParameter node) {
final element = node.declaredElement;
if (element is FieldFormalParameterElementImpl) {
computer._addRegionForToken(node.thisKeyword, element.field);
computer._addRegionForToken(node.name, element.field);
}
node.type?.accept(this);
node.typeParameters?.accept(this);
node.parameters?.accept(this);
}
@override
void visitFunctionDeclaration(FunctionDeclaration node) {
computer._addRegionForToken(node.name, node.declaredElement);
super.visitFunctionDeclaration(node);
}
@override
void visitImportDirective(ImportDirective node) {
var importElement = node.element;
if (importElement != null) {
var libraryElement = importElement.importedLibrary;
_addUriDirectiveRegion(node, libraryElement);
}
super.visitImportDirective(node);
}
@override
void visitImportPrefixReference(ImportPrefixReference node) {
computer._addRegionForToken(node.name, node.element);
}
@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.name2, node.element);
}
@override
void visitMethodDeclaration(MethodDeclaration node) {
computer._addRegionForToken(node.name, node.declaredElement);
super.visitMethodDeclaration(node);
}
@override
void visitNamedType(NamedType node) {
node.importPrefix?.accept(this);
computer._addRegionForToken(node.name2, node.element);
node.typeArguments?.accept(this);
}
@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 visitPatternField(covariant PatternFieldImpl node) {
final nameNode = node.name;
if (nameNode != null) {
final nameToken = nameNode.name ?? node.pattern.variablePattern?.name;
if (nameToken != null) {
computer._addRegionForToken(nameToken, node.element);
}
}
node.pattern.accept(this);
}
@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 visitRepresentationDeclaration(RepresentationDeclaration node) {
if (node.constructorName?.name case final constructorName?) {
computer._addRegionForToken(constructorName, node.constructorElement);
}
computer._addRegionForToken(node.fieldName, node.fieldElement);
super.visitRepresentationDeclaration(node);
}
@override
void visitSimpleFormalParameter(SimpleFormalParameter node) {
final nameToken = node.name;
if (nameToken != null) {
computer._addRegionForToken(nameToken, node.declaredElement);
}
super.visitSimpleFormalParameter(node);
}
@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._addRegionForToken(node.name, superParameter);
}
node.type?.accept(this);
node.typeParameters?.accept(this);
node.parameters?.accept(this);
}
@override
void visitTypeParameter(TypeParameter node) {
computer._addRegionForToken(node.name, node.declaredElement);
super.visitTypeParameter(node);
}
@override
void visitVariableDeclaration(VariableDeclaration node) {
computer._addRegionForToken(node.name, node.declaredElement);
super.visitVariableDeclaration(node);
}
@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) {
final firstType = variables[0].declaredElement?.type;
if (firstType is! InterfaceType) {
return null;
}
var firstElement = firstType.element;
for (var i = 1; i < variables.length; i++) {
final type = variables[i].declaredElement?.type;
if (type is! InterfaceType) {
return null;
}
if (type.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, LibraryElement? element) {
var source = element?.source;
if (source != null) {
if (resourceProvider.getResource(source.fullName).exists) {
computer._addRegionForNode(node.uri, element);
}
}
}
}