blob: 068b30364fb382e1579744c0d1e821c7781a9071 [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/syntactic_entity.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/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, resourceProvider, 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.
var 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 = unit.nodeCovering(offset: offset, length: length);
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 ResourceProvider resourceProvider;
final int? requestedOffset;
final int? requestedLength;
_DartNavigationCollector(
this.collector,
this.resourceProvider,
this.requestedOffset,
this.requestedLength,
);
void _addRegion(
int offset,
int length,
protocol.ElementKind kind,
protocol.Location location, {
Fragment? targetFragment,
}) {
// Discard elements that don't span the offset/range given (if provided).
if (!_isWithinRequestedRange(offset, length)) {
return;
}
collector.addRegion(
offset,
length,
kind,
location,
targetFragment: targetFragment,
);
}
void _addRegionForElement(SyntacticEntity? nodeOrToken, Element? element) {
_addRegionForFragment(nodeOrToken, element?.nonSynthetic.firstFragment);
}
void _addRegionForFragment(SyntacticEntity? nodeOrToken, Fragment? fragment) {
if (nodeOrToken == null || fragment == null) return;
var offset = nodeOrToken.offset;
var length = nodeOrToken.length;
_addRegionForFragmentRange(offset, length, fragment);
}
void _addRegionForFragmentRange(
int? offset, int? length, Fragment? fragment) {
if (offset == null || length == null || fragment == null) return;
// If this fragment is for a synthetic element, use the first fragment for
// the non-synthetic element.
if (fragment.element.isSynthetic) {
fragment = fragment.element.nonSynthetic.firstFragment;
}
if (fragment.element == DynamicElementImpl.instance) {
return;
}
if (fragment.element is MultiplyDefinedElement) {
return;
}
// Discard elements that don't span the offset/range given (if provided).
if (!_isWithinRequestedRange(offset, length)) {
return;
}
var location = fragment.toLocation();
if (location == null) {
return;
}
_addRegion(
offset,
length,
fragment.toPluginElementKind,
location,
targetFragment: fragment,
);
}
void _addRegionForLibrary(int offset, int length, String fullPath) {
if (resourceProvider.getResource(fullPath).exists) {
if (_isWithinRequestedRange(offset, length)) {
collector.addRegion(
offset,
length,
protocol.ElementKind.LIBRARY,
protocol.Location(
fullPath,
0,
0,
0,
0,
endLine: 0,
endColumn: 0,
),
);
}
}
}
/// 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) {
var 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.element;
if (prefixElement is ClassElement) {
prefixElement = element;
}
computer._addRegionForElement(name.prefix, prefixElement);
// always constructor
computer._addRegionForElement(name.identifier, element);
} else {
computer._addRegionForElement(name, element);
}
computer._addRegionForElement(node.constructorName, element);
// type arguments
node.typeArguments?.accept(this);
// arguments
node.arguments?.accept(this);
}
@override
void visitAssignmentExpression(AssignmentExpression node) {
node.leftHandSide.accept(this);
computer._addRegionForElement(node.operator, node.element);
node.rightHandSide.accept(this);
}
@override
void visitBinaryExpression(BinaryExpression node) {
node.leftOperand.accept(this);
computer._addRegionForElement(node.operator, node.element);
node.rightOperand.accept(this);
}
@override
void visitClassDeclaration(ClassDeclaration node) {
computer._addRegionForFragment(node.name, node.declaredFragment);
super.visitClassDeclaration(node);
}
@override
void visitComment(Comment node) {
super.visitComment(node);
for (var link in _documentLinkVisitor
.findLinks(node)
.where((link) => link.targetUri.isScheme('file'))) {
computer._addRegionForLibrary(
link.offset, link.length, link.targetUri.toFilePath());
}
}
@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 resolvedUri = node.resolvedUri;
if (resolvedUri is DirectiveUriWithSource) {
var source = resolvedUri.source;
computer._addRegionForLibrary(
node.uri.offset, node.uri.length, source.fullName);
}
super.visitConfiguration(node);
}
@override
void visitConstructorDeclaration(ConstructorDeclaration node) {
node.metadata.accept(this);
// 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._addRegionForElement(
node.returnType, node.declaredFragment?.element);
} else {
node.returnType.accept(this);
computer._addRegionForFragment(nameToken, node.declaredFragment);
}
node.parameters.accept(this);
node.initializers.accept(this);
node.redirectedConstructor?.accept(this);
node.body.accept(this);
}
@override
void visitConstructorName(ConstructorName node) {
Element? element = node.element;
if (element == null) {
return;
}
// add regions
var namedType = node.type;
// [prefix].ClassName
{
var importPrefix = namedType.importPrefix;
if (importPrefix != null) {
computer._addRegionForElement(importPrefix.name, importPrefix.element);
}
// For a named constructor, the class name points at the class.
var classNameTargetElement =
node.name != null ? namedType.element : element;
computer._addRegionForElement(namedType.name, classNameTargetElement);
}
// <TypeA, TypeB>
namedType.typeArguments?.accept(this);
// optional "name"
if (node.name != null) {
computer._addRegionForElement(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.declaredFragment?.element.type;
if (inferredType is InterfaceType) {
computer._addRegionForElement(token, inferredType.element);
}
}
}
super.visitDeclaredIdentifier(node);
}
@override
void visitDeclaredVariablePattern(DeclaredVariablePattern node) {
if (node.declaredElement case BindPatternVariableElement(:var join?)) {
for (var variable in join.variables) {
computer._addRegionForElement(node.name, variable);
}
} else {
computer._addRegionForElement(node.name, node.declaredElement);
}
super.visitDeclaredVariablePattern(node);
}
@override
void visitEnumConstantDeclaration(EnumConstantDeclaration node) {
computer._addRegionForElement(node.name, node.constructorElement);
var arguments = node.arguments;
if (arguments != null) {
computer._addRegionForElement(
arguments.constructorSelector?.name,
node.constructorElement,
);
arguments.typeArguments?.accept(this);
arguments.argumentList.accept(this);
}
}
@override
void visitExportDirective(ExportDirective node) {
_addUriDirectiveRegion(node, node.libraryExport?.uri);
super.visitExportDirective(node);
}
@override
void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) {
computer._addRegionForFragment(node.name, node.declaredFragment);
super.visitExtensionTypeDeclaration(node);
}
@override
void visitFieldFormalParameter(FieldFormalParameter node) {
var element = node.declaredFragment?.element;
if (element != null) {
computer._addRegionForElement(node.thisKeyword, element.field);
computer._addRegionForElement(node.name, element.field);
}
node.type?.accept(this);
node.typeParameters?.accept(this);
node.parameters?.accept(this);
}
@override
void visitFunctionDeclaration(FunctionDeclaration node) {
computer._addRegionForFragment(node.name, node.declaredFragment);
super.visitFunctionDeclaration(node);
}
@override
void visitImportDirective(ImportDirective node) {
_addUriDirectiveRegion(node, node.libraryImport?.uri);
super.visitImportDirective(node);
}
@override
void visitImportPrefixReference(ImportPrefixReference node) {
var element = node.element;
if (element == null) return;
for (var fragment in element.fragments) {
computer._addRegionForFragment(node.name, fragment);
}
}
@override
void visitIndexExpression(IndexExpression node) {
super.visitIndexExpression(node);
var element = node.writeOrReadElement;
computer._addRegionForElement(node.leftBracket, element);
computer._addRegionForElement(node.rightBracket, element);
}
@override
void visitInstanceCreationExpression(InstanceCreationExpression node) {
if (node.constructorName.element == null) {
computer._addRegionForElement(
node.constructorName,
node.constructorName.type.element,
);
}
super.visitInstanceCreationExpression(node);
}
@override
void visitLibraryDirective(LibraryDirective node) {
computer._addRegionForElement(node.name, node.element);
super.visitLibraryDirective(node);
}
@override
void visitMethodDeclaration(MethodDeclaration node) {
computer._addRegionForFragment(node.name, node.declaredFragment);
super.visitMethodDeclaration(node);
}
@override
void visitNamedType(NamedType node) {
node.importPrefix?.accept(this);
computer._addRegionForElement(node.name, node.element);
node.typeArguments?.accept(this);
}
@override
void visitPartDirective(PartDirective node) {
var include = node.partInclude;
if (include != null) {
var uri = include.uri;
if (uri is DirectiveUriWithUnit) {
computer._addRegionForFragment(node.uri, uri.libraryFragment);
} else if (uri is DirectiveUriWithSource) {
var uriNode = node.uri;
var 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) {
var parentUnit = node.parent as CompilationUnit;
var parentFragment = parentUnit.declaredFragment;
computer._addRegionForFragment(
node.libraryName ?? node.uri,
parentFragment?.enclosingFragment,
);
super.visitPartOfDirective(node);
}
@override
void visitPatternField(covariant PatternFieldImpl node) {
var nameNode = node.name;
if (nameNode != null) {
var nameToken = nameNode.name ?? node.pattern.variablePattern?.name;
if (nameToken != null) {
computer._addRegionForElement(nameToken, node.element);
}
}
node.pattern.accept(this);
}
@override
void visitPostfixExpression(PostfixExpression node) {
super.visitPostfixExpression(node);
computer._addRegionForElement(node.operator, node.element);
}
@override
void visitPrefixExpression(PrefixExpression node) {
computer._addRegionForElement(node.operator, node.element);
super.visitPrefixExpression(node);
}
@override
void visitRedirectingConstructorInvocation(
RedirectingConstructorInvocation node,
) {
Element? element = node.element;
if (element != null && element.isSynthetic) {
element = element.enclosingElement;
}
// add region
computer._addRegionForElement(node.thisKeyword, element);
computer._addRegionForElement(node.constructorName, element);
// process arguments
node.argumentList.accept(this);
}
@override
void visitRepresentationDeclaration(RepresentationDeclaration node) {
if (node.constructorName?.name case var constructorName?) {
computer._addRegionForElement(
constructorName, node.constructorFragment?.element);
}
computer._addRegionForFragment(node.fieldName, node.fieldFragment);
super.visitRepresentationDeclaration(node);
}
@override
void visitSimpleFormalParameter(SimpleFormalParameter node) {
var nameToken = node.name;
if (nameToken != null) {
computer._addRegionForFragment(nameToken, node.declaredFragment);
}
super.visitSimpleFormalParameter(node);
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
var element = node.writeOrReadElement;
if (element case PrefixElement(:var fragments, :var name)) {
for (var fragment in fragments) {
computer._addRegionForFragmentRange(
node.offset,
name?.length,
fragment,
);
}
} else if (element case JoinPatternVariableElement(:var variables)) {
for (var variable in variables) {
computer._addRegionForElement(node, variable);
}
} else {
computer._addRegionForElement(node, element);
}
}
@override
void visitSuperConstructorInvocation(SuperConstructorInvocation node) {
Element? element = node.element;
if (element != null && element.isSynthetic) {
element = element.enclosingElement;
}
// add region
computer._addRegionForElement(node.superKeyword, element);
computer._addRegionForElement(node.constructorName, element);
// process arguments
node.argumentList.accept(this);
}
@override
void visitSuperFormalParameter(SuperFormalParameter node) {
var element = node.declaredFragment?.element;
if (element case SuperFormalParameterElementImpl element) {
var superParameter = element.superConstructorParameter;
computer._addRegionForElement(node.superKeyword, superParameter);
computer._addRegionForElement(node.name, superParameter);
}
node.type?.accept(this);
node.typeParameters?.accept(this);
node.parameters?.accept(this);
}
@override
void visitTypeParameter(TypeParameter node) {
computer._addRegionForFragment(node.name, node.declaredFragment);
super.visitTypeParameter(node);
}
@override
void visitVariableDeclaration(VariableDeclaration node) {
computer._addRegionForFragment(node.name, node.declaredFragment);
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) {
var firstType = variables[0].declaredFragment?.element.type;
if (firstType is! InterfaceType) {
return null;
}
var firstElement = firstType.element;
for (var i = 1; i < variables.length; i++) {
var type = variables[i].declaredFragment?.element.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._addRegionForElement(token!, element);
}
}
}
super.visitVariableDeclarationList(node);
}
/// If the [uri] references a value unit, then add the navigation region from
/// the [node] to the unit.
void _addUriDirectiveRegion(
UriBasedDirective node,
DirectiveUri? uri,
) {
if (uri is DirectiveUriWithUnit && uri.source.exists()) {
computer._addRegionForFragment(node.uri, uri.libraryFragment);
} else if (uri is DirectiveUriWithLibrary && uri.source.exists()) {
computer._addRegionForElement(node.uri, uri.library);
}
}
}
extension on Fragment {
protocol.ElementKind get toPluginElementKind {
// Preserve previous behaviour for compilation units, otherwise we will
// show them all as LIBRARY (which is what their elements are).
if (this is LibraryFragment && this != element.firstFragment) {
return protocol.ElementKind.COMPILATION_UNIT;
}
return element.kind.toPluginElementKind;
}
/// Create a location based on this element.
protocol.Location? toLocation({int? offset, int? length}) {
var libraryFragment = this.libraryFragment;
if (libraryFragment == null) {
return null;
}
var nameOffset = this.nameOffset;
var nameLength = name?.length;
if (nameOffset == null) {
// For unnamed constructors, use the type name as the target location.
if (this case ConstructorFragment self) {
nameOffset = self.typeNameOffset;
nameLength = self.typeName?.length;
}
}
if (nameLength != null && nameOffset != null) {
offset ??= nameOffset;
length ??= nameLength;
} else {
offset = 0;
length = 0;
}
var lineInfo = libraryFragment.lineInfo;
var offsetLocation = lineInfo.getLocation(offset);
var endLocation = lineInfo.getLocation(offset + length);
var startLine = offsetLocation.lineNumber;
var startColumn = offsetLocation.columnNumber;
var endLine = endLocation.lineNumber;
var endColumn = endLocation.columnNumber;
return protocol.Location(
libraryFragment.source.fullName, offset, length, startLine, startColumn,
endLine: endLine, endColumn: endColumn);
}
}