[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 <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Danny Tuppeny 2022-10-25 16:00:41 +00:00 committed by Commit Queue
parent 96294014e3
commit 27ba8fce6c
8 changed files with 545 additions and 107 deletions

View file

@ -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<String, Object?> json) {
final pathJson = json['path'];
final path =
(pathJson as List<Object?>).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<int> path;
/// The ElementLocation for this anchor element.
final String ref;
@override
Map<String, Object?> toJson() {
var result = <String, Object?>{};
result['path'] = path;
result['ref'] = ref;
return result;
}
static bool canParse(Object? obj, LspJsonReporter reporter) {
if (obj is Map<String, Object?>) {
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<String, Object?> json) {
final anchorJson = json['anchor'];
final anchor = anchorJson != null
? TypeHierarchyAnchor.fromJson(anchorJson as Map<String, Object?>)
: 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<String, Object?> toJson() {
var result = <String, Object?>{};
if (anchor != null) {
result['anchor'] = anchor?.toJson();
}
result['ref'] = ref;
return result;
}
static bool canParse(Object? obj, LspJsonReporter reporter) {
if (obj is Map<String, Object?>) {
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<String, Object?> 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<Object?> || value.any((item) => item is! int))) {
reporter.reportError('must be of type List<int>');
return false;
}
} finally {
reporter.pop();
}
return true;
}
bool _canParseListOutline(
Map<String, Object?> map, LspJsonReporter reporter, String fieldName,
{required bool allowsUndefined, required bool allowsNull}) {
@ -2395,6 +2562,32 @@ bool _canParseString(
return true;
}
bool _canParseTypeHierarchyAnchor(
Map<String, Object?> 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<String, Object?> map, LspJsonReporter reporter, String fieldName,
{required bool allowsUndefined, required bool allowsNull}) {

View file

@ -31448,7 +31448,9 @@ class TypeHierarchyItem implements ToJsonable {
});
static TypeHierarchyItem fromJson(Map<String, Object?> json) {
final dataJson = json['data'];
final data = dataJson;
final data = dataJson != null
? TypeHierarchyItemInfo.fromJson(dataJson as Map<String, Object?>)
: 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<String, Object?> toJson() {
var result = <String, Object?>{};
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<String, Object?>) {
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<String, Object?> 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<String, Object?> map, LspJsonReporter reporter, String fieldName,
{required bool allowsUndefined, required bool allowsNull}) {

View file

@ -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<List<TypeHierarchyRelatedItem>?> 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<List<TypeHierarchyRelatedItem>?> 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<InterfaceElement?> _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<TypeHierarchyRelatedItem> _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<TypeHierarchyRelatedItem> _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<InterfaceType?> _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<int> 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;
}

View file

@ -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,
),
);
}

View file

@ -292,6 +292,39 @@ class MyCla^ss2 extends MyClass1 {}
]));
}
/// Ensure that type arguments flow across multiple levels of the tree.
Future<void> test_generics_typeArgsFlow() async {
final content = '''
class A<T1, T2> {}
class B<T1, T2> extends A<T1, T2> {}
class C<T1> extends B<T1, String> {}
class D extends C<int> {}
class ^E extends D {}
''';
await _prepareTypeHierarchy(content);
// Walk the tree and collect names at each level.
var item = prepareResult;
var names = <String>[];
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<int>',
'B<int, String>',
'A<int, String>',
'Object',
]);
}
Future<void> test_implements() async {
final content = '''
/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/

View file

@ -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<void> test_class_generic() async {
final content = '''
class My^Class1<T> {}
/*[0*/class /*[1*/MyClass2/*1]*/ implements MyClass1<String> {}/*0]*/
class My^Class1<T1, T2> {}
/*[0*/class /*[1*/MyClass2/*1]*/<T1> implements MyClass1<T1, String> {}/*0]*/
''';
addTestSource(content);
final target = await findTarget();
final supertypes = await findSubtypes(target!);
expect(supertypes, [
final subtypes = await findSubtypes(target!);
expect(subtypes, [
_isRelatedItem(
'MyClass2',
'MyClass2<T1>',
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<List<TypeHierarchyItem>?> 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<void> test_class_generic() async {
final content = '''
/*[0*/class /*[1*/MyClass1/*1]*/<T> {}/*0]*/
class ^MyClass2 implements MyClass1<String> {}
/*[0*/class /*[1*/MyClass1/*1]*/<T1, T2> {}/*0]*/
class ^MyClass2<T1> implements MyClass1<T1, String> {}
''';
addTestSource(content);
@ -328,7 +330,7 @@ class ^MyClass2 implements MyClass1<String> {}
expect(supertypes, [
_isObject,
_isRelatedItem(
'MyClass1',
'MyClass1<T1, String>',
testFile,
relationship: TypeHierarchyItemRelationship.implements,
codeRange: code.ranges[0].sourceRange,
@ -337,6 +339,39 @@ class ^MyClass2 implements MyClass1<String> {}
]);
}
/// Ensure that type arguments flow across multiple levels of the tree.
Future<void> test_class_generic_typeArgsFlow() async {
final content = '''
class A<T1, T2> {}
class B<T1, T2> extends A<T1, T2> {}
class C<T1> extends B<T1, String> {}
class D extends C<int> {}
class ^E extends D {}
''';
addTestSource(content);
// Walk the tree and collect names at each level.
var names = <String>[];
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<int>',
'B<int, String>',
'A<int, String>',
'Object',
]);
}
Future<void> test_class_interfaces() async {
final content = '''
/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/
@ -520,6 +555,22 @@ class TypeHierarchyComputerFindTargetTest extends AbstractTypeHierarchyTest {
);
}
Future<void> test_class_generic() async {
final content = '''
/*[0*/class /*[1*/MyCl^ass1/*1]*/<T1, T2> {}/*0]*/
''';
addTestSource(content);
await expectTarget(
_isItem(
'MyClass1<T1, T2>',
testFile,
codeRange: code.ranges[0].sourceRange,
nameRange: code.ranges[1].sourceRange,
),
);
}
Future<void> test_class_keyword() async {
final content = '''
/*[0*/cla^ss /*[1*/MyClass1/*1]*/ {

View file

@ -346,6 +346,43 @@ List<LspEntity> 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(

View file

@ -236,7 +236,10 @@ class LspMetaModelCleaner {
},
'TextDocumentEdit': {
'edits': 'TextDocumentEditEdits',
}
},
'TypeHierarchyItem': {
'data': 'TypeHierarchyItemInfo',
},
};
final interface = improvedTypeMappings[interfaceName];