mirror of
https://github.com/dart-lang/sdk
synced 2024-11-02 08:07:11 +00:00
[analysis_server] Add a lazy computer for type hierarchies
Change-Id: Ieb71cea26214e3b790b80e905b3d581f297512d4 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/263760 Commit-Queue: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
parent
17af3a4005
commit
eab05710b4
3 changed files with 558 additions and 0 deletions
|
@ -0,0 +1,173 @@
|
|||
// Copyright (c) 2022, 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:analysis_server/src/services/search/search_engine.dart';
|
||||
import 'package:analyzer/dart/analysis/results.dart';
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
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:collection/collection.dart';
|
||||
|
||||
/// A lazy computer for Type Hierarchies.
|
||||
///
|
||||
/// Unlike [TypeHierarchyComputer], this class computes hierarchies lazily and
|
||||
/// roughly follow the LSP model, which is:
|
||||
///
|
||||
/// 1. Client calls `findTarget(int offset)` to locate a starting point for
|
||||
/// navigation.
|
||||
/// 2. Client passes the returned target into `findSubtypes()` or
|
||||
/// `findSupertypes()` to find it's immediate sub/super types. This can be
|
||||
/// repeated recursively.
|
||||
///
|
||||
/// It is expected that clients will call the methods above at different times
|
||||
/// (as the user expands items in a tree). It is up to the caller to handle
|
||||
/// cases where a target may no longer be valid (due to file modifications) that
|
||||
/// may result in inconsistent results.
|
||||
class DartLazyTypeHierarchyComputer {
|
||||
final ResolvedUnitResult _result;
|
||||
|
||||
DartLazyTypeHierarchyComputer(this._result);
|
||||
|
||||
/// Finds subtypes for [target].
|
||||
Future<List<TypeHierarchyItem>?> findSubtypes(
|
||||
TypeHierarchyItem target,
|
||||
SearchEngine searchEngine,
|
||||
) async {
|
||||
final targetElement = _findTargetElement(target);
|
||||
if (targetElement is! InterfaceElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final subtypes = await _getSubtypes(targetElement, searchEngine);
|
||||
return _convert(subtypes);
|
||||
}
|
||||
|
||||
/// Finds supertypes for [target].
|
||||
Future<List<TypeHierarchyItem>?> findSupertypes(
|
||||
TypeHierarchyItem target,
|
||||
) async {
|
||||
final targetElement = _findTargetElement(target);
|
||||
if (targetElement == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final supertypes = _getSupertypes(targetElement);
|
||||
return _convert(supertypes);
|
||||
}
|
||||
|
||||
/// Finds a target for starting type hierarchy navigation at [offset].
|
||||
TypeHierarchyItem? findTarget(int offset) {
|
||||
final node = NodeLocator2(offset).searchWithin(_result.unit);
|
||||
|
||||
Element? element;
|
||||
|
||||
// Try named types.
|
||||
final type = node?.thisOrAncestorOfType<NamedType>();
|
||||
element = type?.name.staticElement;
|
||||
|
||||
if (element == null) {
|
||||
// Try enclosing class/mixins.
|
||||
final Declaration? declaration = node
|
||||
?.thisOrAncestorMatching((node) => _isValidTargetDeclaration(node));
|
||||
element = declaration?.declaredElement;
|
||||
}
|
||||
|
||||
return element != null ? TypeHierarchyItem.forElement(element) : null;
|
||||
}
|
||||
|
||||
List<TypeHierarchyItem> _convert(List<InterfaceType> types) =>
|
||||
types.map((type) => TypeHierarchyItem.forElement(type.element)).toList();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// Gets immediate sub types for the class/mixin [element].
|
||||
Future<List<InterfaceType>> _getSubtypes(
|
||||
InterfaceElement element, SearchEngine searchEngine) async {
|
||||
var matches = await searchEngine.searchSubtypes(element);
|
||||
return matches
|
||||
.map((match) => (match.element as InterfaceElement).thisType)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Gets immediate super types for the class/mixin [element].
|
||||
///
|
||||
/// Includes all elements that contribute implementation to the type
|
||||
/// such as supertypes and mixins, but not interfaces, constraints or
|
||||
/// extended types.
|
||||
List<InterfaceType> _getSupertypes(Element element) {
|
||||
final supertype = element is InterfaceElement ? element.supertype : null;
|
||||
final mixins =
|
||||
element is InterfaceOrAugmentationElement ? element.mixins : null;
|
||||
return [
|
||||
if (supertype != null) supertype,
|
||||
...?mixins,
|
||||
];
|
||||
}
|
||||
|
||||
/// Returns whether [declaration] is a valid target for type hierarchy
|
||||
/// navigation.
|
||||
bool _isValidTargetDeclaration(AstNode? declaration) =>
|
||||
// TODO(dantup): Should we handle `ClassAugmentationDeclaration`s?
|
||||
declaration is ClassDeclaration || declaration is MixinDeclaration;
|
||||
}
|
||||
|
||||
/// An item that can appear in a Type Hierarchy.
|
||||
class TypeHierarchyItem {
|
||||
/// The user-visible name for this item in the Type Hierarchy.
|
||||
final String displayName;
|
||||
|
||||
/// The file that contains the declaration of this item.
|
||||
final String file;
|
||||
|
||||
/// The range of the name at the declaration of this item.
|
||||
final SourceRange nameRange;
|
||||
|
||||
/// The range of the code for the declaration of this item.
|
||||
final SourceRange codeRange;
|
||||
|
||||
TypeHierarchyItem({
|
||||
required this.displayName,
|
||||
required this.file,
|
||||
required this.nameRange,
|
||||
required this.codeRange,
|
||||
});
|
||||
|
||||
TypeHierarchyItem.forElement(Element element)
|
||||
: displayName = element.displayName,
|
||||
nameRange = _nameRangeForElement(element),
|
||||
codeRange = _codeRangeForElement(element),
|
||||
file = element.source!.fullName;
|
||||
|
||||
/// Returns the [SourceRange] of the code for [element].
|
||||
static SourceRange _codeRangeForElement(Element element) {
|
||||
// Non-synthetic elements should always have code locations.
|
||||
final elementImpl = element.nonSynthetic as ElementImpl;
|
||||
return SourceRange(elementImpl.codeOffset!, elementImpl.codeLength!);
|
||||
}
|
||||
|
||||
/// Returns the [SourceRange] of the name for [element].
|
||||
static SourceRange _nameRangeForElement(Element element) {
|
||||
element = element.nonSynthetic;
|
||||
|
||||
// Some non-synthetic items can still have invalid nameOffsets (for example
|
||||
// a compilation unit). This should never happen here, but guard against it.
|
||||
assert(element.nameOffset != -1);
|
||||
return element.nameOffset == -1
|
||||
? SourceRange(0, 0)
|
||||
: SourceRange(element.nameOffset, element.nameLength);
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import 'import_elements_computer_test.dart' as import_elements_computer;
|
|||
import 'imported_elements_computer_test.dart' as imported_elements_computer;
|
||||
import 'outline_computer_test.dart' as outline_computer;
|
||||
import 'selection_range_computer_test.dart' as selection_range;
|
||||
import 'type_hierarchy_computer_test.dart' as type_hierarchy_computer_test;
|
||||
|
||||
void main() {
|
||||
defineReflectiveSuite(() {
|
||||
|
@ -25,5 +26,6 @@ void main() {
|
|||
imported_elements_computer.main();
|
||||
outline_computer.main();
|
||||
selection_range.main();
|
||||
type_hierarchy_computer_test.main();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,383 @@
|
|||
// Copyright (c) 2022, 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:analysis_server/src/computer/computer_lazy_type_hierarchy.dart';
|
||||
import 'package:analysis_server/src/services/search/search_engine.dart';
|
||||
import 'package:analysis_server/src/services/search/search_engine_internal.dart';
|
||||
import 'package:analyzer/dart/analysis/results.dart';
|
||||
import 'package:analyzer/source/source_range.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
||||
|
||||
import '../../abstract_single_unit.dart';
|
||||
import '../../utils/test_code_format.dart';
|
||||
|
||||
void main() {
|
||||
defineReflectiveSuite(() {
|
||||
defineReflectiveTests(TypeHierarchyComputerFindTargetTest);
|
||||
defineReflectiveTests(TypeHierarchyComputerFindSupertypesTest);
|
||||
defineReflectiveTests(TypeHierarchyComputerFindSubtypesTest);
|
||||
});
|
||||
}
|
||||
|
||||
abstract class AbstractTypeHierarchyTest extends AbstractSingleUnitTest {
|
||||
final startOfFile = SourceRange(0, 0);
|
||||
late TestCode code;
|
||||
|
||||
/// Matches a [TypeHierarchyItem] for [Object].
|
||||
Matcher get _isObject => TypeMatcher<TypeHierarchyItem>()
|
||||
.having((e) => e.displayName, 'displayName', 'Object')
|
||||
// Check some basic things without hard-coding values that will make
|
||||
// this test brittle.
|
||||
.having((e) => e.file, 'file', convertPath('/sdk/lib/core/core.dart'))
|
||||
.having((e) => e.nameRange.offset, 'nameRange.offset', isPositive)
|
||||
.having((e) => e.nameRange.length, 'nameRange.length', 'Object'.length)
|
||||
.having((e) => e.codeRange.offset, 'codeRange.offset', isPositive)
|
||||
.having((e) => e.codeRange.length, 'codeRange.length',
|
||||
greaterThan('class Object {}'.length));
|
||||
|
||||
@override
|
||||
void addTestSource(String content) {
|
||||
code = TestCode.parse(content);
|
||||
super.addTestSource(code.code);
|
||||
}
|
||||
|
||||
Future<TypeHierarchyItem?> findTarget() async {
|
||||
expect(code, isNotNull, reason: 'addTestSource should be called first');
|
||||
final result = await getResolvedUnit(testFile);
|
||||
return DartLazyTypeHierarchyComputer(result)
|
||||
.findTarget(code.position.offset);
|
||||
}
|
||||
|
||||
Future<ResolvedUnitResult> getResolvedUnit(String file) async =>
|
||||
await (await session).getResolvedUnit(file) as ResolvedUnitResult;
|
||||
|
||||
/// Matches a [TypeHierarchyItem] with the given name/kind/file.
|
||||
Matcher _isItem(
|
||||
String displayName,
|
||||
String file, {
|
||||
required SourceRange nameRange,
|
||||
required SourceRange codeRange,
|
||||
}) =>
|
||||
TypeMatcher<TypeHierarchyItem>()
|
||||
.having((e) => e.displayName, 'displayName', displayName)
|
||||
.having((e) => e.file, 'file', file)
|
||||
.having((e) => e.nameRange, 'nameRange', nameRange)
|
||||
.having((e) => e.codeRange, 'codeRange', codeRange);
|
||||
}
|
||||
|
||||
@reflectiveTest
|
||||
class TypeHierarchyComputerFindSubtypesTest extends AbstractTypeHierarchyTest {
|
||||
late SearchEngine searchEngine;
|
||||
|
||||
Future<List<TypeHierarchyItem>?> findSubtypes(
|
||||
TypeHierarchyItem target) async {
|
||||
final result = await getResolvedUnit(target.file);
|
||||
return DartLazyTypeHierarchyComputer(result)
|
||||
.findSubtypes(target, searchEngine);
|
||||
}
|
||||
|
||||
@override
|
||||
void setUp() {
|
||||
super.setUp();
|
||||
searchEngine = SearchEngineImpl([
|
||||
driverFor(testPackageRootPath),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> test_class_mixins() async {
|
||||
final content = '''
|
||||
/*[0*/class /*[1*/MyClass1/*1]*/ with MyMixin1 {}/*0]*/
|
||||
/*[2*/class /*[3*/MyClass2/*3]*/ with MyMixin1 {}/*2]*/
|
||||
mixin MyMi^xin1 {}
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
final target = await findTarget();
|
||||
final subtypes = await findSubtypes(target!);
|
||||
expect(subtypes, [
|
||||
_isItem(
|
||||
'MyClass1',
|
||||
testFile,
|
||||
codeRange: code.ranges[0].sourceRange,
|
||||
nameRange: code.ranges[1].sourceRange,
|
||||
),
|
||||
_isItem(
|
||||
'MyClass2',
|
||||
testFile,
|
||||
codeRange: code.ranges[2].sourceRange,
|
||||
nameRange: code.ranges[3].sourceRange,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> test_class_superclass() async {
|
||||
final content = '''
|
||||
class ^MyClass1 {}
|
||||
/*[0*/class /*[1*/MyClass2/*1]*/ extends MyClass1 {}/*0]*/
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
final target = await findTarget();
|
||||
final supertypes = await findSubtypes(target!);
|
||||
expect(supertypes, [
|
||||
_isItem(
|
||||
'MyClass2',
|
||||
testFile,
|
||||
codeRange: code.ranges[0].sourceRange,
|
||||
nameRange: code.ranges[1].sourceRange,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> test_mixin() async {
|
||||
final content = '''
|
||||
class MyCl^ass1 {}
|
||||
/*[0*/mixin /*[1*/MyMixin1/*1]*/ on MyClass1 {}/*0]*/
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
final target = await findTarget();
|
||||
final supertypes = await findSubtypes(target!);
|
||||
expect(supertypes, [
|
||||
_isItem(
|
||||
'MyMixin1',
|
||||
testFile,
|
||||
codeRange: code.ranges[0].sourceRange,
|
||||
nameRange: code.ranges[1].sourceRange,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@reflectiveTest
|
||||
class TypeHierarchyComputerFindSupertypesTest
|
||||
extends AbstractTypeHierarchyTest {
|
||||
Future<List<TypeHierarchyItem>?> findSupertypes(
|
||||
TypeHierarchyItem target) async {
|
||||
final result = await getResolvedUnit(target.file);
|
||||
return DartLazyTypeHierarchyComputer(result).findSupertypes(target);
|
||||
}
|
||||
|
||||
/// Test that if the file is modified between fetching a target and it's
|
||||
/// sub/supertypes it can still be located (by name).
|
||||
Future<void> test_class_afterModification() async {
|
||||
final content = '''
|
||||
/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/
|
||||
class ^MyClass2 extends MyClass1 {}
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
final target = await findTarget();
|
||||
|
||||
// Update the content so that offsets have changed since we got `target`.
|
||||
addTestSource('// extra\n$content');
|
||||
|
||||
final supertypes = await findSupertypes(target!);
|
||||
expect(supertypes, [
|
||||
_isItem(
|
||||
'MyClass1',
|
||||
testFile,
|
||||
codeRange: code.ranges[0].sourceRange,
|
||||
nameRange: code.ranges[1].sourceRange,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> test_class_mixins() async {
|
||||
final content = '''
|
||||
/*[0*/mixin /*[1*/MyMixin1/*1]*/ {}/*0]*/
|
||||
/*[2*/mixin /*[3*/MyMixin2/*3]*/ {}/*2]*/
|
||||
class ^MyClass1 with MyMixin1, MyMixin2 {}
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
final target = await findTarget();
|
||||
final supertypes = await findSupertypes(target!);
|
||||
expect(supertypes, [
|
||||
_isObject,
|
||||
_isItem(
|
||||
'MyMixin1',
|
||||
testFile,
|
||||
codeRange: code.ranges[0].sourceRange,
|
||||
nameRange: code.ranges[1].sourceRange,
|
||||
),
|
||||
_isItem(
|
||||
'MyMixin2',
|
||||
testFile,
|
||||
codeRange: code.ranges[2].sourceRange,
|
||||
nameRange: code.ranges[3].sourceRange,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> test_class_superclass() async {
|
||||
final content = '''
|
||||
/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/
|
||||
class ^MyClass2 extends MyClass1 {}
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
final target = await findTarget();
|
||||
final supertypes = await findSupertypes(target!);
|
||||
expect(supertypes, [
|
||||
_isItem(
|
||||
'MyClass1',
|
||||
testFile,
|
||||
codeRange: code.ranges[0].sourceRange,
|
||||
nameRange: code.ranges[1].sourceRange,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Mixins have no supertypes and are provided only for subtype search,
|
||||
/// mirroring that they appear in the supertypes list of classes.
|
||||
Future<void> test_mixin() async {
|
||||
final content = '''
|
||||
class MyClass1 {}
|
||||
mixin MyMix^in2 on MyClass1 {}
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
final target = await findTarget();
|
||||
final supertypes = await findSupertypes(target!);
|
||||
expect(supertypes, isEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
@reflectiveTest
|
||||
class TypeHierarchyComputerFindTargetTest extends AbstractTypeHierarchyTest {
|
||||
Future<void> expectNoTarget() async {
|
||||
await expectTarget(isNull);
|
||||
}
|
||||
|
||||
Future<void> expectTarget(Matcher matcher) async {
|
||||
final target = await findTarget();
|
||||
expect(target, matcher);
|
||||
}
|
||||
|
||||
Future<void> test_class_body() async {
|
||||
final content = '''
|
||||
/*[0*/class /*[1*/MyClass1/*1]*/ {
|
||||
int? a^;
|
||||
}/*0]*/
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
await expectTarget(
|
||||
_isItem(
|
||||
'MyClass1',
|
||||
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]*/ {
|
||||
}/*0]*/
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
await expectTarget(
|
||||
_isItem(
|
||||
'MyClass1',
|
||||
testFile,
|
||||
codeRange: code.ranges[0].sourceRange,
|
||||
nameRange: code.ranges[1].sourceRange,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> test_class_name() async {
|
||||
final content = '''
|
||||
/*[0*/class /*[1*/MyCla^ss1/*1]*/ {
|
||||
}/*0]*/
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
await expectTarget(
|
||||
_isItem(
|
||||
'MyClass1',
|
||||
testFile,
|
||||
codeRange: code.ranges[0].sourceRange,
|
||||
nameRange: code.ranges[1].sourceRange,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> test_invalid_topLevel_nonClass() async {
|
||||
final content = '''
|
||||
int? a^;
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
await expectNoTarget();
|
||||
}
|
||||
|
||||
Future<void> test_invalid_topLevel_whitespace() async {
|
||||
final content = '''
|
||||
int? a;
|
||||
^
|
||||
int? b;
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
await expectNoTarget();
|
||||
}
|
||||
|
||||
Future<void> test_mixin_body() async {
|
||||
final content = '''
|
||||
/*[0*/mixin /*[1*/MyMixin1/*1]*/ {
|
||||
^
|
||||
}/*0]*/
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
await expectTarget(
|
||||
_isItem(
|
||||
'MyMixin1',
|
||||
testFile,
|
||||
codeRange: code.ranges[0].sourceRange,
|
||||
nameRange: code.ranges[1].sourceRange,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> test_mixin_keyword() async {
|
||||
final content = '''
|
||||
/*[0*/mi^xin /*[1*/MyMixin1/*1]*/ {
|
||||
}/*0]*/
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
await expectTarget(
|
||||
_isItem(
|
||||
'MyMixin1',
|
||||
testFile,
|
||||
codeRange: code.ranges[0].sourceRange,
|
||||
nameRange: code.ranges[1].sourceRange,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> test_mixinName() async {
|
||||
final content = '''
|
||||
/*[0*/mixin /*[1*/MyMix^in1/*1]*/ {
|
||||
}/*0]*/
|
||||
''';
|
||||
|
||||
addTestSource(content);
|
||||
await expectTarget(
|
||||
_isItem(
|
||||
'MyMixin1',
|
||||
testFile,
|
||||
codeRange: code.ranges[0].sourceRange,
|
||||
nameRange: code.ranges[1].sourceRange,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue