Add support for finding the covering node

The intent is for this to replace NodeLocator and several other
mechanisms for computing a covering node so that we have a single
source of truth. It will also form the basis for some additional
utilities needed for code modifying features.

Change-Id: I33f48907145efbfe9dcba7b43cebb3284d061d1c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/263442
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Brian Wilkerson 2022-10-12 13:52:44 +00:00 committed by Commit Queue
parent be5fe7bfe8
commit aa7fb9e9e8
3 changed files with 379 additions and 0 deletions

View file

@ -0,0 +1,63 @@
// 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:analyzer/dart/ast/ast.dart';
extension AstNodeExtension on AstNode {
/// Return the minimal cover node for the range of characters beginning at the
/// [offset] with the given [length], or `null` if the range is outside the
/// range covered by the receiver.
///
/// The minimal covering node is the node, rooted at the receiver, with the
/// shortest length whose range completely includes the given range.
AstNode? nodeCovering({required int offset, int length = 0}) {
var end = offset + length;
/// Return `true` if the [node] contains the range.
bool containsOffset(AstNode node) {
if (length == 0) {
// The selection range is an insertion point. It is considered to be
// outside the node if it's
// - both immediately before the first token in the node and immediately
// after the previous token, or
// - both immediately after the last token in the node and immediately
// before the next token.
if (offset == node.offset && offset == node.beginToken.previous!.end) {
return false;
}
if (offset == node.end && offset == node.endToken.next!.offset) {
return false;
}
}
return node.offset <= offset && node.end >= end;
}
/// Return the child of the [node] that completely contains the [range], or
/// `null` if none of the children contain the range (which means that the
/// [node] is the covering node).
AstNode? childContainingRange(AstNode node) {
for (var entity in node.childEntities) {
if (entity is AstNode && containsOffset(entity)) {
return entity;
}
}
return null;
}
if (this is CompilationUnit) {
if (offset < 0 || end >= this.end) {
return null;
}
} else if (!containsOffset(this)) {
return null;
}
var previousNode = this;
var currentNode = childContainingRange(previousNode);
while (currentNode != null) {
previousNode = currentNode;
currentNode = childContainingRange(previousNode);
}
return previousNode;
}
}

View file

@ -0,0 +1,314 @@
// 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:analyzer/dart/ast/ast.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer/src/utilities/extensions/ast.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../../dart/resolution/context_collection_resolution.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(NodeCoveringTest);
});
}
@reflectiveTest
class NodeCoveringTest extends PubPackageResolutionTest {
Future<AstNode> coveringNode(String sourceCode) async {
var range = await _range(sourceCode);
var node =
result.unit.nodeCovering(offset: range.offset, length: range.length);
return node!;
}
void test_after_EOF() async {
await resolveTestCode('''
library myLib;
''');
var node = result.unit.nodeCovering(offset: 100, length: 20);
expect(node, null);
}
Future<void> test_after_lastNonEOF() async {
var node = await coveringNode('''
library myLib;
^
''');
node as CompilationUnit;
}
Future<void> test_before_firstNonEOF() async {
var node = await coveringNode('''
^
library myLib;
''');
node as CompilationUnit;
}
Future<void> test_between_classMembers() async {
var node = await coveringNode('''
class C {
void a() {}
^
void b() {}
}
''');
node as ClassDeclaration;
}
Future<void> test_between_commaAndIdentifier_arguments() async {
var node = await coveringNode('''
void f(int a, int b) {
f(a,^b);
}
''');
node as ArgumentList;
}
Future<void> test_between_commaAndIdentifier_parameters() async {
var node = await coveringNode('''
class C {
void m(int a,^int b) {}
}
''');
node as FormalParameterList;
}
Future<void> test_between_commaAndIdentifier_typeArguments() async {
var node = await coveringNode('''
var m = Map<int,^int>();
''');
node as TypeArgumentList;
}
Future<void> test_between_commaAndIdentifier_typeParameters() async {
var node = await coveringNode('''
class C<S,^T> {}
''');
node as TypeParameterList;
}
Future<void> test_between_declarations() async {
var node = await coveringNode('''
class A {}
^
class B {}
''');
node as CompilationUnit;
}
Future<void> test_between_directives() async {
var node = await coveringNode('''
library myLib;
^
import 'dart:core';
''');
node as CompilationUnit;
}
Future<void> test_between_identifierAndComma_arguments() async {
var node = await coveringNode('''
void f(int a, int b) {
f(a^, b);
}
''');
node as ArgumentList;
}
Future<void> test_between_identifierAndComma_parameters() async {
var node = await coveringNode('''
class C {
void m(int a^, int b) {}
}
''');
node as FormalParameterList;
}
Future<void> test_between_identifierAndComma_typeArguments() async {
var node = await coveringNode('''
var m = Map<int^, int>();
''');
node as TypeArgumentList;
}
Future<void> test_between_identifierAndComma_typeParameters() async {
var node = await coveringNode('''
class C<S^, T> {}
''');
node as TypeParameterList;
}
Future<void> test_between_identifierAndPeriod() async {
var node = await coveringNode('''
var x = o^.m();
''');
node as MethodInvocation;
}
Future<void> test_between_modifierAndFunctionBody() async {
var node = await coveringNode('''
void f() async^{}
''');
node as BlockFunctionBody;
}
Future<void> test_between_nameAndParameters_function() async {
var node = await coveringNode('''
void f^() {}
''');
node as FunctionDeclaration;
}
Future<void> test_between_nameAndParameters_method() async {
var node = await coveringNode('''
class C {
void m^() {}
}
''');
node as MethodDeclaration;
}
Future<void> test_between_periodAndIdentifier() async {
var node = await coveringNode('''
var x = o.^m();
''');
node as MethodInvocation;
}
Future<void> test_between_statements() async {
var node = await coveringNode('''
void f() {
var x = 0;
^
print(x);
}
''');
node as Block;
}
Future<void> test_inComment_beginning() async {
var node = await coveringNode('''
/// A [^B].
class C {}
''');
node as SimpleIdentifier;
}
Future<void> test_inComment_beginning_qualified() async {
var node = await coveringNode('''
/// A [B.^b].
class C {}
''');
node as PrefixedIdentifier;
}
Future<void> test_inComment_end() async {
var node = await coveringNode('''
/// A [B.b^].
class C {}
''');
node as SimpleIdentifier;
}
Future<void> test_inComment_middle() async {
var node = await coveringNode('''
/// A [B.b^b].
class C {}
''');
node as SimpleIdentifier;
}
Future<void> test_inName_class() async {
var node = await coveringNode('''
class A^B {}
''');
node as ClassDeclaration;
}
Future<void> test_inName_function() async {
var node = await coveringNode('''
void f^f() {}
''');
node as FunctionDeclaration;
}
Future<void> test_inName_method() async {
var node = await coveringNode('''
class C {
void m^m() {}
}
''');
node as MethodDeclaration;
}
Future<void> test_inOperator_assignment() async {
var node = await coveringNode('''
void f(int x) {
x +^= 3;
}
''');
node as AssignmentExpression;
}
Future<void> test_inOperator_nullAwareAccess() async {
var node = await coveringNode('''
var x = o?^.m();
''');
node as MethodInvocation;
}
Future<void> test_inOperator_postfix() async {
var node = await coveringNode('''
var x = y+^+;
''');
node as PostfixExpression;
}
Future<void> test_libraryKeyword() async {
var node = await coveringNode('''
libr^ary myLib;
''');
node as LibraryDirective;
}
Future<void> test_parentAndChildWithSameRange_blockFunctionBody() async {
var node = await coveringNode('''
void f() { ^ }
''');
node as Block;
var parent = node.parent;
parent as BlockFunctionBody;
expect(parent.offset, node.offset);
expect(parent.length, node.length);
}
Future<void> test_parentAndChildWithSameRange_implicitCall() async {
var node = await coveringNode('''
class C { void call() {} } Function f = C^();
''');
node as InstanceCreationExpression;
var parent = node.parent;
parent as ImplicitCallReference;
expect(parent.offset, node.offset);
expect(parent.length, node.length);
}
Future<SourceRange> _range(String sourceCode) async {
// TODO(brianwilkerson) Move TestCode to the analyzer package and make use
// of it here.
var offset = sourceCode.indexOf('^');
if (offset < 0 || sourceCode.indexOf('^', offset + 1) >= 0) {
fail('Tests must contain a single selection range');
}
var testCode =
sourceCode.substring(0, offset) + sourceCode.substring(offset + 1);
await resolveTestCode(testCode);
return SourceRange(offset, 0);
}
}

View file

@ -4,6 +4,7 @@
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'ast_test.dart' as ast;
import 'collection_test.dart' as collection;
import 'object_test.dart' as object;
import 'stream_test.dart' as stream;
@ -11,6 +12,7 @@ import 'string_test.dart' as string;
main() {
defineReflectiveSuite(() {
ast.main();
collection.main();
object.main();
stream.main();