[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:
Danny Tuppeny 2022-10-12 19:22:17 +00:00 committed by Commit Queue
parent 17af3a4005
commit eab05710b4
3 changed files with 558 additions and 0 deletions

View file

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

View file

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

View file

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