From 27ba8fce6c73f23e221d8437529c73716938ce94 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Tue, 25 Oct 2022 16:00:41 +0000 Subject: [PATCH] [analysis_server] Include type arguments in Type Hierarchy Fixes https://github.com/Dart-Code/Dart-Code/issues/4217. Change-Id: I8b8dec4ad25a9eb4a4f80dd036e8a9b61bb012d2 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/264981 Reviewed-by: Brian Wilkerson Commit-Queue: Brian Wilkerson --- .../protocol_custom_generated.dart | 193 ++++++++++++++++++ .../lib/lsp_protocol/protocol_generated.dart | 38 +++- .../computer_lazy_type_hierarchy.dart | 186 ++++++++++++----- .../lsp/handlers/handler_type_hierarchy.dart | 81 ++++---- .../test/lsp/type_hierarchy_test.dart | 33 +++ .../type_hierarchy_computer_test.dart | 79 +++++-- .../tool/lsp_spec/generate_all.dart | 37 ++++ .../tool/lsp_spec/meta_model_cleaner.dart | 5 +- 8 files changed, 545 insertions(+), 107 deletions(-) diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart index bdc2346da08..78f1364e3e1 100644 --- a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart +++ b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart @@ -1858,6 +1858,147 @@ class SnippetTextEdit implements TextEdit, ToJsonable { String toString() => jsonEncoder.convert(toJson()); } +class TypeHierarchyAnchor implements ToJsonable { + static const jsonHandler = LspJsonHandler( + TypeHierarchyAnchor.canParse, + TypeHierarchyAnchor.fromJson, + ); + + TypeHierarchyAnchor({ + required this.path, + required this.ref, + }); + static TypeHierarchyAnchor fromJson(Map json) { + final pathJson = json['path']; + final path = + (pathJson as List).map((item) => item as int).toList(); + final refJson = json['ref']; + final ref = refJson as String; + return TypeHierarchyAnchor( + path: path, + ref: ref, + ); + } + + /// Indices used to navigate from this anchor to the element. + final List path; + + /// The ElementLocation for this anchor element. + final String ref; + + @override + Map toJson() { + var result = {}; + result['path'] = path; + result['ref'] = ref; + return result; + } + + static bool canParse(Object? obj, LspJsonReporter reporter) { + if (obj is Map) { + if (!_canParseListInt(obj, reporter, 'path', + allowsUndefined: false, allowsNull: false)) { + return false; + } + return _canParseString(obj, reporter, 'ref', + allowsUndefined: false, allowsNull: false); + } else { + reporter.reportError('must be of type TypeHierarchyAnchor'); + return false; + } + } + + @override + bool operator ==(Object other) { + return other is TypeHierarchyAnchor && + other.runtimeType == TypeHierarchyAnchor && + listEqual(path, other.path, (int a, int b) => a == b) && + ref == other.ref; + } + + @override + int get hashCode => Object.hash( + lspHashCode(path), + ref, + ); + + @override + String toString() => jsonEncoder.convert(toJson()); +} + +class TypeHierarchyItemInfo implements ToJsonable { + static const jsonHandler = LspJsonHandler( + TypeHierarchyItemInfo.canParse, + TypeHierarchyItemInfo.fromJson, + ); + + TypeHierarchyItemInfo({ + this.anchor, + required this.ref, + }); + static TypeHierarchyItemInfo fromJson(Map json) { + final anchorJson = json['anchor']; + final anchor = anchorJson != null + ? TypeHierarchyAnchor.fromJson(anchorJson as Map) + : null; + final refJson = json['ref']; + final ref = refJson as String; + return TypeHierarchyItemInfo( + anchor: anchor, + ref: ref, + ); + } + + /// An anchor element that can be used to navigate to this element preserving + /// type arguments. + final TypeHierarchyAnchor? anchor; + + /// The ElementLocation for this element, used to re-locate the element when + /// subtypes/supertypes are fetched later. + final String ref; + + @override + Map toJson() { + var result = {}; + if (anchor != null) { + result['anchor'] = anchor?.toJson(); + } + result['ref'] = ref; + return result; + } + + static bool canParse(Object? obj, LspJsonReporter reporter) { + if (obj is Map) { + if (!_canParseTypeHierarchyAnchor(obj, reporter, 'anchor', + allowsUndefined: true, allowsNull: false)) { + return false; + } + return _canParseString(obj, reporter, 'ref', + allowsUndefined: false, allowsNull: false); + } else { + reporter.reportError('must be of type TypeHierarchyItemInfo'); + return false; + } + } + + @override + bool operator ==(Object other) { + return other is TypeHierarchyItemInfo && + other.runtimeType == TypeHierarchyItemInfo && + anchor == other.anchor && + ref == other.ref; + } + + @override + int get hashCode => Object.hash( + anchor, + ref, + ); + + @override + String toString() => jsonEncoder.convert(toJson()); +} + class ValidateRefactorResult implements ToJsonable { static const jsonHandler = LspJsonHandler( ValidateRefactorResult.canParse, @@ -2184,6 +2325,32 @@ bool _canParseListFlutterOutlineAttribute( return true; } +bool _canParseListInt( + Map map, LspJsonReporter reporter, String fieldName, + {required bool allowsUndefined, required bool allowsNull}) { + reporter.push(fieldName); + try { + if (!allowsUndefined && !map.containsKey(fieldName)) { + reporter.reportError('must not be undefined'); + return false; + } + final value = map[fieldName]; + final nullCheck = allowsNull || allowsUndefined; + if (!nullCheck && value == null) { + reporter.reportError('must not be null'); + return false; + } + if ((!nullCheck || value != null) && + (value is! List || value.any((item) => item is! int))) { + reporter.reportError('must be of type List'); + return false; + } + } finally { + reporter.pop(); + } + return true; +} + bool _canParseListOutline( Map map, LspJsonReporter reporter, String fieldName, {required bool allowsUndefined, required bool allowsNull}) { @@ -2395,6 +2562,32 @@ bool _canParseString( return true; } +bool _canParseTypeHierarchyAnchor( + Map map, LspJsonReporter reporter, String fieldName, + {required bool allowsUndefined, required bool allowsNull}) { + reporter.push(fieldName); + try { + if (!allowsUndefined && !map.containsKey(fieldName)) { + reporter.reportError('must not be undefined'); + return false; + } + final value = map[fieldName]; + final nullCheck = allowsNull || allowsUndefined; + if (!nullCheck && value == null) { + reporter.reportError('must not be null'); + return false; + } + if ((!nullCheck || value != null) && + !TypeHierarchyAnchor.canParse(value, reporter)) { + reporter.reportError('must be of type TypeHierarchyAnchor'); + return false; + } + } finally { + reporter.pop(); + } + return true; +} + bool _canParseUri( Map map, LspJsonReporter reporter, String fieldName, {required bool allowsUndefined, required bool allowsNull}) { diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart index 6879e1b0ae2..8951796bb1e 100644 --- a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart +++ b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart @@ -31448,7 +31448,9 @@ class TypeHierarchyItem implements ToJsonable { }); static TypeHierarchyItem fromJson(Map json) { final dataJson = json['data']; - final data = dataJson; + final data = dataJson != null + ? TypeHierarchyItemInfo.fromJson(dataJson as Map) + : null; final detailJson = json['detail']; final detail = detailJson as String?; final kindJson = json['kind']; @@ -31482,7 +31484,7 @@ class TypeHierarchyItem implements ToJsonable { /// supertypes or subtypes requests. It could also be used to identify the /// type hierarchy in the server, helping improve the performance on resolving /// supertypes and subtypes. - final LSPAny data; + final TypeHierarchyItemInfo? data; /// More detail for this item, e.g. the signature of a function. final String? detail; @@ -31511,7 +31513,7 @@ class TypeHierarchyItem implements ToJsonable { Map toJson() { var result = {}; if (data != null) { - result['data'] = data; + result['data'] = data?.toJson(); } if (detail != null) { result['detail'] = detail; @@ -31529,6 +31531,10 @@ class TypeHierarchyItem implements ToJsonable { static bool canParse(Object? obj, LspJsonReporter reporter) { if (obj is Map) { + if (!_canParseTypeHierarchyItemInfo(obj, reporter, 'data', + allowsUndefined: true, allowsNull: false)) { + return false; + } if (!_canParseString(obj, reporter, 'detail', allowsUndefined: true, allowsNull: false)) { return false; @@ -41981,6 +41987,32 @@ bool _canParseTypeHierarchyItem( return true; } +bool _canParseTypeHierarchyItemInfo( + Map map, LspJsonReporter reporter, String fieldName, + {required bool allowsUndefined, required bool allowsNull}) { + reporter.push(fieldName); + try { + if (!allowsUndefined && !map.containsKey(fieldName)) { + reporter.reportError('must not be undefined'); + return false; + } + final value = map[fieldName]; + final nullCheck = allowsNull || allowsUndefined; + if (!nullCheck && value == null) { + reporter.reportError('must not be null'); + return false; + } + if ((!nullCheck || value != null) && + !TypeHierarchyItemInfo.canParse(value, reporter)) { + reporter.reportError('must be of type TypeHierarchyItemInfo'); + return false; + } + } finally { + reporter.pop(); + } + return true; +} + bool _canParseUniquenessLevel( Map map, LspJsonReporter reporter, String fieldName, {required bool allowsUndefined, required bool allowsNull}) { diff --git a/pkg/analysis_server/lib/src/computer/computer_lazy_type_hierarchy.dart b/pkg/analysis_server/lib/src/computer/computer_lazy_type_hierarchy.dart index 155091a5387..2ad21309f38 100644 --- a/pkg/analysis_server/lib/src/computer/computer_lazy_type_hierarchy.dart +++ b/pkg/analysis_server/lib/src/computer/computer_lazy_type_hierarchy.dart @@ -10,6 +10,7 @@ import 'package:analyzer/dart/element/type.dart'; import 'package:analyzer/source/source_range.dart'; import 'package:analyzer/src/dart/ast/utilities.dart'; import 'package:analyzer/src/dart/element/element.dart'; +import 'package:analyzer/src/utilities/extensions/analysis_session.dart'; import 'package:collection/collection.dart'; /// A lazy computer for Type Hierarchies. @@ -32,12 +33,12 @@ class DartLazyTypeHierarchyComputer { DartLazyTypeHierarchyComputer(this._result); - /// Finds subtypes for [target]. + /// Finds subtypes for [Element] at [location]. Future?> findSubtypes( - TypeHierarchyItem target, + ElementLocation location, SearchEngine searchEngine, ) async { - final targetElement = _findTargetElement(target); + final targetElement = await _findTargetElement(location); if (targetElement is! InterfaceElement) { return null; } @@ -45,16 +46,37 @@ class DartLazyTypeHierarchyComputer { return _getSubtypes(targetElement, searchEngine); } - /// Finds supertypes for [target]. + /// Finds supertypes for the [Element] at [location]. + /// + /// If [anchor] is provided, it will be used to navigate to the element at + /// [location] to preserve type arguments that have been provided along the + /// way. + /// + /// Anchors are included in returned types (where necessary to preserve type + /// arguments) that can be used when calling for the next level of types. Future?> findSupertypes( - TypeHierarchyItem target, - ) async { - final targetElement = _findTargetElement(target); + ElementLocation location, { + TypeHierarchyAnchor? anchor, + }) async { + var targetElement = await _findTargetElement(location); if (targetElement == null) { return null; } + var targetType = targetElement.thisType; - return _getSupertypes(targetElement); + // If we were provided an anchor, use it to re-locate the target type so + // that any type arguments supplied along the way will be preserved in the + // new node. + if (anchor != null) { + targetType = + await _locateTargetFromAnchor(anchor, targetType) ?? targetType; + } + + // If we had no existing anchor, create one that starts from this target as + // the starting point for the new supertypes. + anchor ??= TypeHierarchyAnchor(location: location, path: []); + + return _getSupertypes(targetType, anchor: anchor); } /// Finds a target for starting type hierarchy navigation at [offset]. @@ -74,19 +96,15 @@ class DartLazyTypeHierarchyComputer { element = declaration?.declaredElement; } - return element != null ? TypeHierarchyItem.forElement(element) : null; + return element is InterfaceElement + ? TypeHierarchyItem.forType(element.thisType) + : null; } - Element? _findTargetElement(TypeHierarchyItem target) { - assert(target.file == _result.path); - // Locate the element by name instead of offset since this call my occur - // much later than the original find target request and it's possible the - // user has made changes since. - final targetElement = _result.unit.declarations - .where(_isValidTargetDeclaration) - .map((declaration) => declaration.declaredElement) - .firstWhereOrNull((element) => element?.name == target.displayName); - return targetElement; + /// Locate the [Element] referenced by [location]. + Future _findTargetElement(ElementLocation location) async { + final element = await _result.session.locateElement(location); + return element is InterfaceElement ? element : null; } /// Gets immediate sub types for the class/mixin [element]. @@ -129,20 +147,36 @@ class DartLazyTypeHierarchyComputer { /// Includes all elements that contribute implementation to the type /// such as supertypes and mixins, but not interfaces, constraints or /// extended types. - List _getSupertypes(Element element) { - final supertype = element is InterfaceElement ? element.supertype : null; - final interfaces = - element is InterfaceOrAugmentationElement ? element.interfaces : null; - final mixins = - element is InterfaceOrAugmentationElement ? element.mixins : null; - final superclassConstraints = - element is MixinElement ? element.superclassConstraints : null; - return [ + List _getSupertypes( + InterfaceType type, { + TypeHierarchyAnchor? anchor, + }) { + final supertype = type.superclass; + final interfaces = type.interfaces; + final mixins = type.mixins; + final superclassConstraints = type.superclassConstraints; + + final supertypes = [ if (supertype != null) TypeHierarchyRelatedItem.extends_(supertype), - ...?superclassConstraints?.map(TypeHierarchyRelatedItem.constrainedTo), - ...?interfaces?.map(TypeHierarchyRelatedItem.implements), - ...?mixins?.map(TypeHierarchyRelatedItem.mixesIn), + ...superclassConstraints.map(TypeHierarchyRelatedItem.constrainedTo), + ...interfaces.map(TypeHierarchyRelatedItem.implements), + ...mixins.map(TypeHierarchyRelatedItem.mixesIn), ]; + + if (anchor != null) { + supertypes.forEachIndexed((index, item) { + // We only need to carry the anchor along if the supertype has type + // arguments that we may be populating. + if (item._type.typeArguments.isNotEmpty) { + item._anchor = TypeHierarchyAnchor( + location: anchor.location, + path: [...anchor.path, index], + ); + } + }); + } + + return supertypes; } /// Returns whether [declaration] is a valid target for type hierarchy @@ -151,6 +185,37 @@ class DartLazyTypeHierarchyComputer { declaration is ClassDeclaration || declaration is MixinDeclaration || declaration is EnumDeclaration; + + /// Navigate to [target] from [anchor], preserving type arguments supplied + /// along the way. + Future _locateTargetFromAnchor( + TypeHierarchyAnchor anchor, InterfaceType target) async { + // Start from the anchor. + final anchorElement = await _findTargetElement(anchor.location); + final anchorPath = anchor.path; + + // Follow the provided path. + var type = anchorElement?.thisType; + for (int i = 0; i < anchorPath.length && type != null; i++) { + final index = anchorPath[i]; + final supertypes = _getSupertypes(type); + type = supertypes.length >= index + 1 ? supertypes[index]._type : null; + } + + // Verify the element we arrived at matches the targetElement to guard + // against code changes that made the path from the anchor invalid. + return type != null && type.element == target.element ? type : null; + } +} + +class TypeHierarchyAnchor { + /// The location of the anchor element. + final ElementLocation location; + + /// The supertype path from [location] to the target element. + final List path; + + TypeHierarchyAnchor({required this.location, required this.path}); } /// An item that can appear in a Type Hierarchy. @@ -158,6 +223,17 @@ class TypeHierarchyItem { /// The user-visible name for this item in the Type Hierarchy. final String displayName; + /// The location of the element being displayed. + /// + /// This is used to re-locate the element when calling + /// `findSubtypes`/`findSupertypes` so that if code has been modified since + /// the `findTarget` call the element can still be located (provided the + /// names/identifiers have not changed). + final ElementLocation location; + + /// The type being displayed. + final InterfaceType _type; + /// The file that contains the declaration of this item. final String file; @@ -168,17 +244,23 @@ class TypeHierarchyItem { final SourceRange codeRange; TypeHierarchyItem({ + required InterfaceType type, required this.displayName, + required this.location, required this.file, required this.nameRange, required this.codeRange, - }); + }) : _type = type; - TypeHierarchyItem.forElement(Element element) - : displayName = element.displayName, - nameRange = _nameRangeForElement(element), - codeRange = _codeRangeForElement(element), - file = element.source!.fullName; + TypeHierarchyItem.forType(InterfaceType type) + : this( + type: type, + displayName: _displayNameForType(type), + location: type.element.location!, + nameRange: _nameRangeForElement(type.element), + codeRange: _codeRangeForElement(type.element), + file: type.element.source.fullName, + ); /// Returns the [SourceRange] of the code for [element]. static SourceRange _codeRangeForElement(Element element) { @@ -187,6 +269,11 @@ class TypeHierarchyItem { return SourceRange(elementImpl.codeOffset!, elementImpl.codeLength!); } + /// Returns a name to display in the hierarchy for [type]. + static String _displayNameForType(InterfaceType type) { + return type.getDisplayString(withNullability: false); + } + /// Returns the [SourceRange] of the name for [element]. static SourceRange _nameRangeForElement(Element element) { element = element.nonSynthetic; @@ -208,32 +295,35 @@ enum TypeHierarchyItemRelationship { mixesIn, } -/// A supertype of subtype of a [TypeHierarchyItem]. +/// A supertype or subtype of a [TypeHierarchyItem]. class TypeHierarchyRelatedItem extends TypeHierarchyItem { /// The relationship this item has with the target item. final TypeHierarchyItemRelationship relationship; - TypeHierarchyRelatedItem.constrainedTo(InterfaceType type) - : this._forElement(type.element, - relationship: TypeHierarchyItemRelationship.constrainedTo); + TypeHierarchyAnchor? _anchor; + TypeHierarchyRelatedItem.constrainedTo(InterfaceType type) + : this._forType(type, + relationship: TypeHierarchyItemRelationship.constrainedTo); TypeHierarchyRelatedItem.extends_(InterfaceType type) - : this._forElement(type.element, + : this._forType(type, relationship: TypeHierarchyItemRelationship.extends_); TypeHierarchyRelatedItem.implements(InterfaceType type) - : this._forElement(type.element, + : this._forType(type, relationship: TypeHierarchyItemRelationship.implements); TypeHierarchyRelatedItem.mixesIn(InterfaceType type) - : this._forElement(type.element, + : this._forType(type, relationship: TypeHierarchyItemRelationship.mixesIn); TypeHierarchyRelatedItem.unknown(InterfaceType type) - : this._forElement(type.element, + : this._forType(type, relationship: TypeHierarchyItemRelationship.unknown); - TypeHierarchyRelatedItem._forElement(super.element, - {required this.relationship}) - : super.forElement(); + TypeHierarchyRelatedItem._forType(super.type, {required this.relationship}) + : super.forType(); + + /// An optional anchor element used to preserve type args. + TypeHierarchyAnchor? get anchor => _anchor; } diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_type_hierarchy.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_type_hierarchy.dart index b84fce3e4fc..e1519308b51 100644 --- a/pkg/analysis_server/lib/src/lsp/handlers/handler_type_hierarchy.dart +++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_type_hierarchy.dart @@ -4,8 +4,7 @@ import 'dart:async'; -import 'package:analysis_server/lsp_protocol/protocol_generated.dart'; -import 'package:analysis_server/lsp_protocol/protocol_special.dart'; +import 'package:analysis_server/lsp_protocol/protocol.dart'; import 'package:analysis_server/src/computer/computer_lazy_type_hierarchy.dart' as type_hierarchy; import 'package:analysis_server/src/lsp/handlers/handlers.dart'; @@ -14,6 +13,7 @@ import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/session.dart'; import 'package:analyzer/source/line_info.dart'; import 'package:analyzer/source/source_range.dart'; +import 'package:analyzer/src/dart/element/element.dart'; /// A handler for the initial "prepare" request for starting navigation with /// Type Hierarchy. @@ -86,22 +86,20 @@ class TypeHierarchySubtypesHandler extends MessageHandler< MessageInfo message, CancellationToken token) async { final item = params.item; + final data = item.data; final path = pathOfUri(item.uri); final unit = await path.mapResult(requireResolvedUnit); final computer = type_hierarchy.DartLazyTypeHierarchyComputer(unit.result); - // Convert the clients item back to one in the servers format so that we - // can use it to get sub/super types. - final target = toServerItem(item, unit.result.lineInfo); - - if (target == null) { + if (data == null) { return error( - ErrorCodes.ContentModified, - 'Content was modified since Type Hierarchy node was produced', + ErrorCodes.InvalidParams, + 'TypeHierarchyItem is missing the data field', ); } - final calls = await computer.findSubtypes(target, server.searchEngine); + final location = ElementLocationImpl.con2(data.ref); + final calls = await computer.findSubtypes(location, server.searchEngine); final results = calls != null ? _convertItems(unit.result, calls) : null; return success(results); } @@ -124,25 +122,38 @@ class TypeHierarchySupertypesHandler extends MessageHandler< MessageInfo message, CancellationToken token) async { final item = params.item; + final data = item.data; final path = pathOfUri(item.uri); final unit = await path.mapResult(requireResolvedUnit); final computer = type_hierarchy.DartLazyTypeHierarchyComputer(unit.result); - // Convert the clients item back to one in the servers format so that we - // can use it to get sub/super types. - final target = toServerItem(item, unit.result.lineInfo); - - if (target == null) { + if (data == null) { return error( - ErrorCodes.ContentModified, - 'Content was modified since Type Hierarchy node was produced', + ErrorCodes.InvalidParams, + 'TypeHierarchyItem is missing the data field', ); } - final calls = await computer.findSupertypes(target); + final location = ElementLocationImpl.con2(data.ref); + final anchor = _toServerAnchor(data); + final calls = await computer.findSupertypes(location, anchor: anchor); final results = calls != null ? _convertItems(unit.result, calls) : null; return success(results); } + + /// Reads the anchor from [data] (if available) and converts it to a server + /// [type_hierarchy.TypeHierarchyAnchor]. + type_hierarchy.TypeHierarchyAnchor? _toServerAnchor( + TypeHierarchyItemInfo data, + ) { + final anchor = data.anchor; + return anchor != null + ? type_hierarchy.TypeHierarchyAnchor( + location: ElementLocationImpl.con2(anchor.ref), + path: anchor.path, + ) + : null; + } } /// Utility methods used by all Type Hierarchy handlers. @@ -157,6 +168,8 @@ mixin _TypeHierarchyUtils { type_hierarchy.TypeHierarchyItem item, LineInfo lineInfo, ) { + final anchor = + item is type_hierarchy.TypeHierarchyRelatedItem ? item.anchor : null; return TypeHierarchyItem( name: item.displayName, detail: _detailFor(item), @@ -164,29 +177,15 @@ mixin _TypeHierarchyUtils { uri: Uri.file(item.file), range: sourceRangeToRange(lineInfo, item.codeRange), selectionRange: sourceRangeToRange(lineInfo, item.nameRange), - ); - } - - /// Converts an LSP [TypeHierarchyItem] supplied by the client back to a - /// server [type_hierarchy.TypeHierarchyItem] to use to look up items. - /// - /// Returns `null` if the supplied item is no longer valid (for example its - /// ranges are no longer valid in the current state of the document). - type_hierarchy.TypeHierarchyItem? toServerItem( - TypeHierarchyItem item, - LineInfo lineInfo, - ) { - final nameRange = toSourceRange(lineInfo, item.selectionRange); - final codeRange = toSourceRange(lineInfo, item.range); - if (nameRange.isError || codeRange.isError) { - return null; - } - - return type_hierarchy.TypeHierarchyItem( - displayName: item.name, - file: item.uri.toFilePath(), - nameRange: nameRange.result, - codeRange: codeRange.result, + data: TypeHierarchyItemInfo( + ref: item.location.encoding, + anchor: anchor != null + ? TypeHierarchyAnchor( + ref: anchor.location.encoding, + path: anchor.path, + ) + : null, + ), ); } diff --git a/pkg/analysis_server/test/lsp/type_hierarchy_test.dart b/pkg/analysis_server/test/lsp/type_hierarchy_test.dart index 3caceb41fd1..3930d9b361f 100644 --- a/pkg/analysis_server/test/lsp/type_hierarchy_test.dart +++ b/pkg/analysis_server/test/lsp/type_hierarchy_test.dart @@ -292,6 +292,39 @@ class MyCla^ss2 extends MyClass1 {} ])); } + /// Ensure that type arguments flow across multiple levels of the tree. + Future test_generics_typeArgsFlow() async { + final content = ''' +class A {} +class B extends A {} +class C extends B {} +class D extends C {} +class ^E extends D {} +'''; + await _prepareTypeHierarchy(content); + + // Walk the tree and collect names at each level. + var item = prepareResult; + var names = []; + while (item != null) { + names.add(item.name); + final supertypes = await typeHierarchySupertypes(item); + item = (supertypes != null && supertypes.isNotEmpty) + ? supertypes.single + : null; + } + + // Check for substituted type args. + expect(names, [ + 'E', + 'D', + 'C', + 'B', + 'A', + 'Object', + ]); + } + Future test_implements() async { final content = ''' /*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/ diff --git a/pkg/analysis_server/test/src/computer/type_hierarchy_computer_test.dart b/pkg/analysis_server/test/src/computer/type_hierarchy_computer_test.dart index f8003d0115c..5e9df760714 100644 --- a/pkg/analysis_server/test/src/computer/type_hierarchy_computer_test.dart +++ b/pkg/analysis_server/test/src/computer/type_hierarchy_computer_test.dart @@ -97,7 +97,7 @@ class TypeHierarchyComputerFindSubtypesTest extends AbstractTypeHierarchyTest { TypeHierarchyItem target) async { final result = await getResolvedUnit(target.file); return DartLazyTypeHierarchyComputer(result) - .findSubtypes(target, searchEngine); + .findSubtypes(target.location, searchEngine); } @override @@ -110,16 +110,16 @@ class TypeHierarchyComputerFindSubtypesTest extends AbstractTypeHierarchyTest { Future test_class_generic() async { final content = ''' -class My^Class1 {} -/*[0*/class /*[1*/MyClass2/*1]*/ implements MyClass1 {}/*0]*/ +class My^Class1 {} +/*[0*/class /*[1*/MyClass2/*1]*/ implements MyClass1 {}/*0]*/ '''; addTestSource(content); final target = await findTarget(); - final supertypes = await findSubtypes(target!); - expect(supertypes, [ + final subtypes = await findSubtypes(target!); + expect(subtypes, [ _isRelatedItem( - 'MyClass2', + 'MyClass2', testFile, relationship: TypeHierarchyItemRelationship.implements, codeRange: code.ranges[0].sourceRange, @@ -136,8 +136,8 @@ class ^MyClass1 {} addTestSource(content); final target = await findTarget(); - final supertypes = await findSubtypes(target!); - expect(supertypes, [ + final subtypes = await findSubtypes(target!); + expect(subtypes, [ _isRelatedItem( 'MyClass2', testFile, @@ -184,8 +184,8 @@ class ^MyClass1 {} addTestSource(content); final target = await findTarget(); - final supertypes = await findSubtypes(target!); - expect(supertypes, [ + final subtypes = await findSubtypes(target!); + expect(subtypes, [ _isRelatedItem( 'MyClass2', testFile, @@ -287,7 +287,9 @@ class TypeHierarchyComputerFindSupertypesTest Future?> findSupertypes( TypeHierarchyItem target) async { final result = await getResolvedUnit(target.file); - return DartLazyTypeHierarchyComputer(result).findSupertypes(target); + final anchor = target is TypeHierarchyRelatedItem ? target.anchor : null; + return DartLazyTypeHierarchyComputer(result) + .findSupertypes(target.location, anchor: anchor); } /// Test that if the file is modified between fetching a target and it's @@ -318,8 +320,8 @@ class ^MyClass2 extends MyClass1 {} Future test_class_generic() async { final content = ''' -/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/ -class ^MyClass2 implements MyClass1 {} +/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/ +class ^MyClass2 implements MyClass1 {} '''; addTestSource(content); @@ -328,7 +330,7 @@ class ^MyClass2 implements MyClass1 {} expect(supertypes, [ _isObject, _isRelatedItem( - 'MyClass1', + 'MyClass1', testFile, relationship: TypeHierarchyItemRelationship.implements, codeRange: code.ranges[0].sourceRange, @@ -337,6 +339,39 @@ class ^MyClass2 implements MyClass1 {} ]); } + /// Ensure that type arguments flow across multiple levels of the tree. + Future test_class_generic_typeArgsFlow() async { + final content = ''' +class A {} +class B extends A {} +class C extends B {} +class D extends C {} +class ^E extends D {} + '''; + addTestSource(content); + + // Walk the tree and collect names at each level. + var names = []; + var target = await findTarget(); + while (target != null) { + names.add(target.displayName); + final supertypes = await findSupertypes(target); + target = (supertypes != null && supertypes.isNotEmpty) + ? supertypes.single + : null; + } + + // Check for substituted type args. + expect(names, [ + 'E', + 'D', + 'C', + 'B', + 'A', + 'Object', + ]); + } + Future test_class_interfaces() async { final content = ''' /*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/ @@ -520,6 +555,22 @@ class TypeHierarchyComputerFindTargetTest extends AbstractTypeHierarchyTest { ); } + Future test_class_generic() async { + final content = ''' +/*[0*/class /*[1*/MyCl^ass1/*1]*/ {}/*0]*/ + '''; + + addTestSource(content); + await expectTarget( + _isItem( + 'MyClass1', + testFile, + codeRange: code.ranges[0].sourceRange, + nameRange: code.ranges[1].sourceRange, + ), + ); + } + Future test_class_keyword() async { final content = ''' /*[0*/cla^ss /*[1*/MyClass1/*1]*/ { diff --git a/pkg/analysis_server/tool/lsp_spec/generate_all.dart b/pkg/analysis_server/tool/lsp_spec/generate_all.dart index a58dda18966..1f4720c1fba 100644 --- a/pkg/analysis_server/tool/lsp_spec/generate_all.dart +++ b/pkg/analysis_server/tool/lsp_spec/generate_all.dart @@ -346,6 +346,43 @@ List getCustomClasses() { field('message', type: 'string', canBeUndefined: true), ], ), + interface( + 'TypeHierarchyAnchor', + [ + field( + 'ref', + type: 'string', + comment: 'The ElementLocation for this anchor element.', + ), + field( + 'path', + type: 'int', + array: true, + comment: 'Indices used to navigate from this anchor to the element.', + ), + ], + ), + interface( + 'TypeHierarchyItemInfo', + [ + field( + 'ref', + type: 'string', + comment: + 'The ElementLocation for this element, used to re-locate the ' + 'element when subtypes/supertypes are ' + 'fetched later.', + ), + field( + 'anchor', + type: 'TypeHierarchyAnchor', + comment: + 'An anchor element that can be used to navigate to this element ' + 'preserving type arguments.', + canBeUndefined: true, + ), + ], + ), TypeAlias( name: 'TextDocumentEditEdits', baseType: ArrayType( diff --git a/pkg/analysis_server/tool/lsp_spec/meta_model_cleaner.dart b/pkg/analysis_server/tool/lsp_spec/meta_model_cleaner.dart index 20abf899892..b9e5d54a82d 100644 --- a/pkg/analysis_server/tool/lsp_spec/meta_model_cleaner.dart +++ b/pkg/analysis_server/tool/lsp_spec/meta_model_cleaner.dart @@ -236,7 +236,10 @@ class LspMetaModelCleaner { }, 'TextDocumentEdit': { 'edits': 'TextDocumentEditEdits', - } + }, + 'TypeHierarchyItem': { + 'data': 'TypeHierarchyItemInfo', + }, }; final interface = improvedTypeMappings[interfaceName];