[analysis_server] Support occurrences (Document Highlights) with different lengths
+ split unnamed constructors from classes
Fixes https://github.com/Dart-Code/Dart-Code/issues/4454
Change-Id: I42e7346b7c22348cf6424d15c5c35a24e31c85b3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/406822
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analysis_server/lib/src/domains/analysis/occurrences_dart.dart b/pkg/analysis_server/lib/src/domains/analysis/occurrences_dart.dart
index f9eb1a3..1c269e0 100644
--- a/pkg/analysis_server/lib/src/domains/analysis/occurrences_dart.dart
+++ b/pkg/analysis_server/lib/src/domains/analysis/occurrences_dart.dart
@@ -5,30 +5,44 @@
import 'package:analysis_server/plugin/analysis/occurrences/occurrences_core.dart';
import 'package:analysis_server/src/protocol_server.dart' as protocol;
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/element2.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/element/element.dart';
void addDartOccurrences(OccurrencesCollector collector, CompilationUnit unit) {
- var visitor = _DartUnitOccurrencesComputerVisitor();
+ var visitor = DartUnitOccurrencesComputerVisitor();
unit.accept(visitor);
- visitor.elementsOffsets.forEach((engineElement, offsets) {
- var length = engineElement.name3?.length ?? 0;
+ visitor.elementsOffsetLengths.forEach((engineElement, offsetLengths) {
+ // For legacy protocol, we only support occurrences with the same
+ // length, so we must filter the offset to only those that match the length
+ // from the element.
var serverElement = protocol.convertElement2(engineElement);
+ // Prefer the length from the mapped element over the element directly,
+ // because 'name3' may contain 'new' for constructors which doesn't match
+ // what is in the source.
+ var length =
+ serverElement.location?.length ?? engineElement.name3?.length ?? 0;
+ var offsets =
+ offsetLengths
+ .where((offsetLength) => offsetLength.$2 == length)
+ .map((offsetLength) => offsetLength.$1)
+ .toList();
+
var occurrences = protocol.Occurrences(serverElement, offsets, length);
collector.addOccurrences(occurrences);
});
}
-class _DartUnitOccurrencesComputerVisitor extends RecursiveAstVisitor<void> {
- final Map<Element2, List<int>> elementsOffsets = {};
+class DartUnitOccurrencesComputerVisitor extends RecursiveAstVisitor<void> {
+ final Map<Element2, List<(int, int)>> elementsOffsetLengths = {};
@override
void visitAssignedVariablePattern(AssignedVariablePattern node) {
var element = node.element2;
if (element != null) {
- _addOccurrence(element, node.name.offset);
+ _addOccurrence(element, node.name);
}
super.visitAssignedVariablePattern(node);
@@ -36,14 +50,14 @@
@override
void visitClassDeclaration(ClassDeclaration node) {
- _addOccurrence(node.declaredFragment!.element, node.name.offset);
+ _addOccurrence(node.declaredFragment!.element, node.name);
super.visitClassDeclaration(node);
}
@override
void visitClassTypeAlias(ClassTypeAlias node) {
- _addOccurrence(node.declaredFragment!.element, node.name.offset);
+ _addOccurrence(node.declaredFragment!.element, node.name);
super.visitClassTypeAlias(node);
}
@@ -51,43 +65,64 @@
@override
void visitConstructorDeclaration(ConstructorDeclaration node) {
if (node.name case var name?) {
- _addOccurrence(node.declaredFragment!.element, name.offset);
+ _addOccurrence(node.declaredFragment!.element, name);
+ } else {
+ _addOccurrenceAt(
+ node.declaredFragment!.element,
+ node.returnType.offset,
+ node.returnType.length,
+ );
}
super.visitConstructorDeclaration(node);
}
@override
+ void visitConstructorName(ConstructorName node) {
+ // For unnamed constructors, we add an occurence for the constructor at
+ // the location of the returnType.
+ if (node.name == null) {
+ var element = node.element;
+ if (element != null) {
+ _addOccurrence(element, node.type.name2);
+ }
+ return; // skip visitNamedType.
+ }
+
+ super.visitConstructorName(node);
+ }
+
+ @override
void visitDeclaredIdentifier(DeclaredIdentifier node) {
- _addOccurrence(node.declaredFragment!.element, node.name.offset);
+ _addOccurrence(node.declaredFragment!.element, node.name);
super.visitDeclaredIdentifier(node);
}
@override
void visitDeclaredVariablePattern(DeclaredVariablePattern node) {
- _addOccurrence(node.declaredElement2!, node.name.offset);
+ _addOccurrence(node.declaredElement2!, node.name);
super.visitDeclaredVariablePattern(node);
}
@override
void visitEnumConstantDeclaration(EnumConstantDeclaration node) {
- _addOccurrence(node.declaredFragment!.element, node.name.offset);
+ _addOccurrence(node.declaredFragment!.element, node.name);
super.visitEnumConstantDeclaration(node);
}
@override
void visitEnumDeclaration(EnumDeclaration node) {
- _addOccurrence(node.declaredFragment!.element, node.name.offset);
+ _addOccurrence(node.declaredFragment!.element, node.name);
super.visitEnumDeclaration(node);
}
@override
void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) {
- _addOccurrence(node.declaredFragment!.element, node.name.offset);
+ _addOccurrence(node.declaredFragment!.element, node.name);
super.visitExtensionTypeDeclaration(node);
}
@@ -98,7 +133,7 @@
if (declaredElement is FieldFormalParameterElement2) {
var field = declaredElement.field2;
if (field != null) {
- _addOccurrence(field, node.name.offset);
+ _addOccurrence(field, node.name);
}
}
@@ -107,40 +142,40 @@
@override
void visitFunctionDeclaration(FunctionDeclaration node) {
- _addOccurrence(node.declaredFragment!.element, node.name.offset);
+ _addOccurrence(node.declaredFragment!.element, node.name);
super.visitFunctionDeclaration(node);
}
@override
void visitFunctionTypeAlias(FunctionTypeAlias node) {
- _addOccurrence(node.declaredFragment!.element, node.name.offset);
+ _addOccurrence(node.declaredFragment!.element, node.name);
super.visitFunctionTypeAlias(node);
}
@override
void visitGenericTypeAlias(GenericTypeAlias node) {
- _addOccurrence(node.declaredFragment!.element, node.name.offset);
+ _addOccurrence(node.declaredFragment!.element, node.name);
super.visitGenericTypeAlias(node);
}
@override
void visitImportPrefixReference(ImportPrefixReference node) {
- _addOccurrence(node.element2!, node.offset);
+ _addOccurrence(node.element2!, node.name);
super.visitImportPrefixReference(node);
}
@override
void visitMethodDeclaration(MethodDeclaration node) {
- _addOccurrence(node.declaredFragment!.element, node.name.offset);
+ _addOccurrence(node.declaredFragment!.element, node.name);
super.visitMethodDeclaration(node);
}
@override
void visitMixinDeclaration(MixinDeclaration node) {
- _addOccurrence(node.declaredFragment!.element, node.name.offset);
+ _addOccurrence(node.declaredFragment!.element, node.name);
super.visitMixinDeclaration(node);
}
@@ -149,7 +184,7 @@
void visitNamedType(NamedType node) {
var element = node.element2;
if (element != null) {
- _addOccurrence(element, node.name2.offset);
+ _addOccurrence(element, node.name2);
}
super.visitNamedType(node);
@@ -165,7 +200,7 @@
? pattern.name
: node.name?.name;
if (element != null && name != null) {
- _addOccurrence(element, name.offset);
+ _addOccurrence(element, name);
}
super.visitPatternField(node);
}
@@ -173,10 +208,7 @@
@override
void visitRepresentationDeclaration(RepresentationDeclaration node) {
if (node.constructorName case var constructorName?) {
- _addOccurrence(
- node.constructorFragment!.element,
- constructorName.name.offset,
- );
+ _addOccurrence(node.constructorFragment!.element, constructorName.name);
}
super.visitRepresentationDeclaration(node);
@@ -186,7 +218,7 @@
void visitSimpleFormalParameter(SimpleFormalParameter node) {
var nameToken = node.name;
if (nameToken != null) {
- _addOccurrence(node.declaredFragment!.element, nameToken.offset);
+ _addOccurrence(node.declaredFragment!.element, nameToken);
}
super.visitSimpleFormalParameter(node);
@@ -194,36 +226,50 @@
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
+ // For unnamed constructors, we don't want to add an occurrence for the
+ // class name here because visitConstructorDeclaration will have added one
+ // for the constructor (not the type).
+ if (node.parent case ConstructorDeclaration(
+ :var name,
+ :var returnType,
+ ) when name == null && node == returnType) {
+ return;
+ }
+
var element = node.writeOrReadElement2;
if (element != null) {
- _addOccurrence(element, node.offset);
+ _addOccurrence(element, node.token);
}
return super.visitSimpleIdentifier(node);
}
@override
void visitSuperFormalParameter(SuperFormalParameter node) {
- _addOccurrence(node.declaredFragment!.element, node.name.offset);
+ _addOccurrence(node.declaredFragment!.element, node.name);
super.visitSuperFormalParameter(node);
}
@override
void visitVariableDeclaration(VariableDeclaration node) {
- _addOccurrence(node.declaredFragment!.element, node.name.offset);
+ _addOccurrence(node.declaredFragment!.element, node.name);
super.visitVariableDeclaration(node);
}
- void _addOccurrence(Element2 element, int offset) {
+ void _addOccurrence(Element2 element, Token token) {
+ _addOccurrenceAt(element, token.offset, token.length);
+ }
+
+ void _addOccurrenceAt(Element2 element, int offset, int length) {
var canonicalElement = _canonicalizeElement(element);
if (canonicalElement == null || element == DynamicElementImpl.instance) {
return;
}
- var offsets = elementsOffsets[canonicalElement];
- if (offsets == null) {
- offsets = <int>[];
- elementsOffsets[canonicalElement] = offsets;
+ var offsetLengths = elementsOffsetLengths[canonicalElement];
+ if (offsetLengths == null) {
+ offsetLengths = <(int, int)>[];
+ elementsOffsetLengths[canonicalElement] = offsetLengths;
}
- offsets.add(offset);
+ offsetLengths.add((offset, length));
}
Element2? _canonicalizeElement(Element2 element) {
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_document_highlights.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_document_highlights.dart
index 8026a69..9627da6 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_document_highlights.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_document_highlights.dart
@@ -3,12 +3,12 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/lsp_protocol/protocol.dart';
-import 'package:analysis_server/src/domains/analysis/occurrences.dart';
import 'package:analysis_server/src/domains/analysis/occurrences_dart.dart';
import 'package:analysis_server/src/lsp/error_or.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analysis_server/src/lsp/registration/feature_registration.dart';
+import 'package:collection/collection.dart';
typedef StaticOptions = Either2<bool, DocumentHighlightOptions>;
@@ -45,8 +45,8 @@
var offset = unit.mapResultSync((unit) => toOffset(unit.lineInfo, pos));
return (unit, offset).mapResults((unit, requestedOffset) async {
- var collector = OccurrencesCollectorImpl();
- addDartOccurrences(collector, unit.unit);
+ var visitor = DartUnitOccurrencesComputerVisitor();
+ unit.unit.accept(visitor);
/// Checks whether an Occurrence offset/length spans the requested
/// offset.
@@ -54,23 +54,38 @@
/// It's possible multiple occurrences might match because some nodes
/// such as object destructuring might match multiple elements (for
/// example the object getter and a declared variable).
- bool spansRequestedPosition(int offset, int length) {
+ bool spansRequestedPosition((int, int) offsetLength) {
+ var (offset, length) = offsetLength;
return offset <= requestedOffset && offset + length >= requestedOffset;
}
- // Find an occurrence that has an instance that spans the position.
- var occurrences =
- collector.allOccurrences
+ // Find the groups (elements) of offset/lengths that contains an
+ // offset/length that spans the requested range. There may be multiple
+ // matches here if the source element is in multiple groups.
+ var matchingSet =
+ visitor.elementsOffsetLengths.values
.where(
- (occurrence) => occurrence.offsets.any(
- (offset) => spansRequestedPosition(offset, occurrence.length),
+ (offsetLengths) => offsetLengths.any(spansRequestedPosition),
+ )
+ .flattened
+ .toSet();
+
+ // No matches will return an empty list (not null) because that prevents
+ // the editor falling back to a text search.
+ var highlights =
+ matchingSet
+ .map(
+ (offsetLength) => DocumentHighlight(
+ range: toRange(
+ unit.lineInfo,
+ offsetLength.$1,
+ offsetLength.$2,
+ ),
),
)
.toList();
- // No matches will return an empty list (not null) because that prevents
- // the editor falling back to a text search.
- return success(toHighlights(unit.lineInfo, occurrences));
+ return success(highlights);
});
}
}
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index 2790d9a..2ba55fa 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -1438,22 +1438,6 @@
}
}
-List<lsp.DocumentHighlight> toHighlights(
- server.LineInfo lineInfo,
- List<server.Occurrences> occurrences,
-) {
- return occurrences
- .map(
- (occurrence) => occurrence.offsets.map(
- (offset) => lsp.DocumentHighlight(
- range: toRange(lineInfo, offset, occurrence.length),
- ),
- ),
- )
- .flattenedToSet
- .toList();
-}
-
lsp.Location toLocation(
ClientUriConverter uriConverter,
server.Location location,
diff --git a/pkg/analysis_server/lib/src/protocol_server.dart b/pkg/analysis_server/lib/src/protocol_server.dart
index cd220a1..2cf4331 100644
--- a/pkg/analysis_server/lib/src/protocol_server.dart
+++ b/pkg/analysis_server/lib/src/protocol_server.dart
@@ -320,8 +320,15 @@
return null;
}
var fragment = element.firstFragment;
- var offset = fragment.nameOffset2 ?? 0;
- var length = fragment.name2?.length ?? 0;
+ var (offset, length) = switch (fragment) {
+ // For unnamed constructors, treat the type name as the element location
+ // instead of using 0,0.
+ engine.ConstructorFragment(:var typeNameOffset, :var typeName) =>
+ fragment.nameOffset2 != null
+ ? (fragment.nameOffset2 ?? 0, fragment.name2.length)
+ : (typeNameOffset ?? 0, typeName?.length ?? 0),
+ _ => (fragment.nameOffset2 ?? 0, fragment.name2?.length ?? 0),
+ };
var range = engine.SourceRange(offset, length);
return _locationForArgs2(fragment, range);
}
diff --git a/pkg/analysis_server/test/analysis/notification_occurrences_test.dart b/pkg/analysis_server/test/analysis/notification_occurrences_test.dart
index 354cf53..0fa59d6 100644
--- a/pkg/analysis_server/test/analysis/notification_occurrences_test.dart
+++ b/pkg/analysis_server/test/analysis/notification_occurrences_test.dart
@@ -50,7 +50,7 @@
expect(testOccurrences.element.name, elementName ?? range.text);
expect(
testOccurrences.offsets,
- containsAll(code.ranges.map((r) => r.sourceRange.offset)),
+ unorderedEquals(code.ranges.map((r) => r.sourceRange.offset)),
);
}
@@ -421,8 +421,8 @@
Future<void> test_pattern_cast_typeName() async {
await assertOccurrences(kind: ElementKind.CLASS, '''
-String f((num, /*[0*/String/*0]*/) record) {
- var (i as int, s as /*[1*/String/*1]*/) = record;
+/*[0*/String/*0]*/ f((num, /*[1*/String/*1]*/) record) {
+ var (i as int, s as /*[2*/String/*2]*/) = record;
}
);
''');
@@ -664,6 +664,38 @@
''');
}
+ Future<void> test_type_class_constructors() async {
+ await assertOccurrences(kind: ElementKind.CLASS, '''
+class /*[0*/A/*0]*/ {
+ A(); // Unnamed constructor is own entity
+ /*[1*/A/*1]*/.named();
+}
+
+/*[2*/A/*2]*/ a = A(); // Unnamed constructor is own entity
+var b = /*[3*/A/*3]*/.new();
+var c = /*[4*/A/*4]*/.new;
+ ''');
+ }
+
+ /// The type name in unnamed constructors are their own entity and not
+ /// part of the type name.
+ ///
+ /// For the legacy protocol, "new" is not treated as a reference to the
+ /// constructor because the protocol currently only supports same-length
+ /// occurrences.
+ Future<void> test_type_class_constructors_unnamed() async {
+ await assertOccurrences(kind: ElementKind.CONSTRUCTOR, '''
+class A {
+ /*[0*/A/*0]*/();
+ A.named();
+}
+
+A a = /*[1*/A/*1]*/();
+var b = A.new();
+var c = A.new;
+ ''');
+ }
+
Future<void> test_type_class_definition() async {
await assertOccurrences(kind: ElementKind.CLASS, '''
class /*[0*/A/*0]*/ {}
diff --git a/pkg/analysis_server/test/lsp/document_highlights_test.dart b/pkg/analysis_server/test/lsp/document_highlights_test.dart
index d875fb0..568a5f9 100644
--- a/pkg/analysis_server/test/lsp/document_highlights_test.dart
+++ b/pkg/analysis_server/test/lsp/document_highlights_test.dart
@@ -212,6 +212,34 @@
}
''');
+ Future<void> test_type_class_constructors() async {
+ await _testMarkedContent('''
+class /*[0*/A^/*0]*/ {
+ A(); // Unnamed constructor is own entity
+ /*[1*/A/*1]*/.named();
+}
+
+/*[2*/A/*2]*/ a = A(); // Unnamed constructor is own entity
+var b = /*[3*/A/*3]*/.new();
+var c = /*[4*/A/*4]*/.new;
+''');
+ }
+
+ /// The type name in unnamed constructors are their own entity and not
+ /// part of the type name.
+ Future<void> test_type_class_constructors_unnamed() async {
+ await _testMarkedContent('''
+class A {
+ /*[0*/A^/*0]*/();
+ A.named();
+}
+
+A a = /*[1*/A/*1]*/();
+var b = A./*[2*/new/*2]*/();
+var c = A./*[3*/new/*3]*/;
+''');
+ }
+
Future<void> test_typeAlias_class_declaration() => _testMarkedContent('''
class MyClass {}
mixin MyMixin {}