mirror of
https://github.com/dart-lang/sdk
synced 2024-09-05 00:13:50 +00:00
Code complete identifiers not directly referenced by the analyzer AST
The fasta parser sometimes "drops" unexpected tokens during recovery. When this happens, the dropped or skipped token is not reported to the listeners, and thus does not become part of the analyzer's AST. These tokens are still in the token stream, but will not be found by the existing CompletionTarget mechanism. For example, the fasta parser parses 'if (v i) {}' as 'if (v) {}' and drops the 'i' token. This CL introduces a new CompletionTarget.droppedToken field. If a keyword or identifier has been dropped by the parser, but overlaps the code completion offset, then that token is placed into this new field. In the example above, when code completing the 'i', the CompletionTarget will now be containingNode = '(v)' entity = ')' droppedToken = 'i' Change-Id: I15e43529ab4a72de9500c521be278fa13ae68d99 Reviewed-on: https://dart-review.googlesource.com/31285 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Dan Rubel <danrubel@google.com>
This commit is contained in:
parent
914720524a
commit
1231e90fee
|
@ -48,6 +48,8 @@ class _KeywordVisitor extends GeneralizingAstVisitor {
|
||||||
: this.request = request,
|
: this.request = request,
|
||||||
this.entity = request.target.entity;
|
this.entity = request.target.entity;
|
||||||
|
|
||||||
|
Token get droppedToken => request.target.droppedToken;
|
||||||
|
|
||||||
bool isEmptyBody(FunctionBody body) =>
|
bool isEmptyBody(FunctionBody body) =>
|
||||||
body is EmptyFunctionBody ||
|
body is EmptyFunctionBody ||
|
||||||
(body is BlockFunctionBody && body.beginToken.isSynthetic);
|
(body is BlockFunctionBody && body.beginToken.isSynthetic);
|
||||||
|
@ -338,6 +340,23 @@ class _KeywordVisitor extends GeneralizingAstVisitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
visitParenthesizedExpression(ParenthesizedExpression node) {
|
||||||
|
Expression expression = node.expression;
|
||||||
|
if (expression is Identifier || expression is PropertyAccess) {
|
||||||
|
if (entity == node.rightParenthesis) {
|
||||||
|
var next = expression.endToken.next;
|
||||||
|
if (next == entity || next == droppedToken) {
|
||||||
|
// Fasta parses `if (x i^)` as `if (x ^) where the `i` is in the token
|
||||||
|
// stream but not part of the ParenthesizedExpression.
|
||||||
|
_addSuggestion(Keyword.IS, DART_RELEVANCE_HIGH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_addExpressionKeywords(node);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
visitIfStatement(IfStatement node) {
|
visitIfStatement(IfStatement node) {
|
||||||
if (_isPreviousTokenSynthetic(entity, TokenType.CLOSE_PAREN)) {
|
if (_isPreviousTokenSynthetic(entity, TokenType.CLOSE_PAREN)) {
|
||||||
|
|
|
@ -1452,14 +1452,8 @@ class A {
|
||||||
test_method_async() async {
|
test_method_async() async {
|
||||||
addTestSource('class A { foo() ^}');
|
addTestSource('class A { foo() ^}');
|
||||||
await computeSuggestions();
|
await computeSuggestions();
|
||||||
if (usingFastaParser) {
|
assertSuggestKeywords(CLASS_BODY_KEYWORDS,
|
||||||
assertSuggestKeywords([],
|
pseudoKeywords: ['async', 'async*', 'sync*']);
|
||||||
pseudoKeywords: ['async', 'async*', 'sync*'],
|
|
||||||
relevance: DART_RELEVANCE_HIGH);
|
|
||||||
} else {
|
|
||||||
assertSuggestKeywords(CLASS_BODY_KEYWORDS,
|
|
||||||
pseudoKeywords: ['async', 'async*', 'sync*']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test_method_async2() async {
|
test_method_async2() async {
|
||||||
|
|
|
@ -87,6 +87,17 @@ class CompletionTarget {
|
||||||
*/
|
*/
|
||||||
final AstNode containingNode;
|
final AstNode containingNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "dropped" identifier or keyword which the completed text will replace,
|
||||||
|
* or `null` if none.
|
||||||
|
*
|
||||||
|
* For the purposes of code completion, a "dropped" token is an identifier
|
||||||
|
* or keyword that is part of the token stream, but that the parser has
|
||||||
|
* skipped and not reported in to the parser listeners, meaning that it is
|
||||||
|
* not part of the AST.
|
||||||
|
*/
|
||||||
|
Token droppedToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The entity which the completed text will replace (or which will be
|
* The entity which the completed text will replace (or which will be
|
||||||
* displaced once the completed text is inserted). This may be an AstNode or
|
* displaced once the completed text is inserted). This may be an AstNode or
|
||||||
|
@ -235,7 +246,9 @@ class CompletionTarget {
|
||||||
Object entity, this.isCommentText)
|
Object entity, this.isCommentText)
|
||||||
: this.containingNode = containingNode,
|
: this.containingNode = containingNode,
|
||||||
this.entity = entity,
|
this.entity = entity,
|
||||||
this.argIndex = _computeArgIndex(containingNode, entity);
|
this.argIndex = _computeArgIndex(containingNode, entity),
|
||||||
|
this.droppedToken =
|
||||||
|
_computeDroppedToken(containingNode, entity, offset);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return `true` if the [containingNode] is a cascade
|
* Return `true` if the [containingNode] is a cascade
|
||||||
|
@ -263,7 +276,8 @@ class CompletionTarget {
|
||||||
bool isKeywordOrIdentifier(Token token) =>
|
bool isKeywordOrIdentifier(Token token) =>
|
||||||
token.type.isKeyword || token.type == TokenType.IDENTIFIER;
|
token.type.isKeyword || token.type == TokenType.IDENTIFIER;
|
||||||
|
|
||||||
Token token = entity is AstNode ? (entity as AstNode).beginToken : entity;
|
Token token = droppedToken ??
|
||||||
|
(entity is AstNode ? (entity as AstNode).beginToken : entity);
|
||||||
if (token != null && requestOffset < token.offset) {
|
if (token != null && requestOffset < token.offset) {
|
||||||
token = token.previous;
|
token = token.previous;
|
||||||
}
|
}
|
||||||
|
@ -388,6 +402,52 @@ class CompletionTarget {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Token _computeDroppedToken(
|
||||||
|
AstNode containingNode, Object entity, int offset) {
|
||||||
|
// Find the last token of the member before the entity.
|
||||||
|
var previousMember;
|
||||||
|
for (var member in containingNode.childEntities) {
|
||||||
|
if (entity == member) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (member is! Comment && member is! CommentToken) {
|
||||||
|
previousMember = member;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Token token;
|
||||||
|
if (previousMember is AstNode) {
|
||||||
|
token = previousMember.endToken;
|
||||||
|
} else if (previousMember is Token) {
|
||||||
|
token = previousMember;
|
||||||
|
}
|
||||||
|
if (token == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the first token of the entity (which may be the entity itself).
|
||||||
|
Token endSearch;
|
||||||
|
if (entity is AstNode) {
|
||||||
|
endSearch = entity.beginToken;
|
||||||
|
} else if (entity is Token) {
|
||||||
|
endSearch = entity;
|
||||||
|
}
|
||||||
|
if (endSearch == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a dropped token that overlaps the offset.
|
||||||
|
token = token.next;
|
||||||
|
while (token != endSearch && !token.isEof) {
|
||||||
|
if (token.isKeywordOrIdentifier &&
|
||||||
|
token.offset <= offset &&
|
||||||
|
offset <= token.end) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
token = token.next;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the offset is contained in a preceding comment token
|
* Determine if the offset is contained in a preceding comment token
|
||||||
* and return that token, otherwise return `null`.
|
* and return that token, otherwise return `null`.
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:analyzer/dart/ast/ast.dart';
|
import 'package:analyzer/dart/ast/ast.dart';
|
||||||
import 'package:analyzer/src/dart/analysis/driver.dart';
|
import 'package:analyzer/src/dart/analysis/driver.dart';
|
||||||
|
import 'package:analyzer/src/generated/parser.dart' as analyzer;
|
||||||
import 'package:analyzer/src/generated/source.dart';
|
import 'package:analyzer/src/generated/source.dart';
|
||||||
import 'package:analyzer_plugin/src/utilities/completion/completion_target.dart';
|
import 'package:analyzer_plugin/src/utilities/completion/completion_target.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
@ -25,6 +26,8 @@ class CompletionTargetTest extends AbstractContextTest {
|
||||||
int completionOffset;
|
int completionOffset;
|
||||||
CompletionTarget target;
|
CompletionTarget target;
|
||||||
|
|
||||||
|
bool get usingFastaParser => analyzer.Parser.useFasta;
|
||||||
|
|
||||||
Future<Null> addTestSource(String content) async {
|
Future<Null> addTestSource(String content) async {
|
||||||
expect(completionOffset, isNull, reason: 'Call addTestSource exactly once');
|
expect(completionOffset, isNull, reason: 'Call addTestSource exactly once');
|
||||||
completionOffset = content.indexOf('^');
|
completionOffset = content.indexOf('^');
|
||||||
|
@ -39,12 +42,16 @@ class CompletionTargetTest extends AbstractContextTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Null> assertTarget(entityText, nodeText,
|
Future<Null> assertTarget(entityText, nodeText,
|
||||||
{int argIndex: null, bool isFunctionalArgument: false}) async {
|
{int argIndex: null,
|
||||||
|
bool isFunctionalArgument: false,
|
||||||
|
String droppedToken}) async {
|
||||||
void assertCommon() {
|
void assertCommon() {
|
||||||
expect(target.entity.toString(), entityText, reason: 'entity');
|
expect(target.entity.toString(), entityText, reason: 'entity');
|
||||||
expect(target.containingNode.toString(), nodeText,
|
expect(target.containingNode.toString(), nodeText,
|
||||||
reason: 'containingNode');
|
reason: 'containingNode');
|
||||||
expect(target.argIndex, argIndex, reason: 'argIndex');
|
expect(target.argIndex, argIndex, reason: 'argIndex');
|
||||||
|
expect(target.droppedToken?.toString(), droppedToken ?? isNull,
|
||||||
|
reason: 'droppedToken');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assert with parsed unit
|
// Assert with parsed unit
|
||||||
|
@ -403,6 +410,16 @@ class CompletionTargetTest extends AbstractContextTest {
|
||||||
'// normal comment ', 'class C2 {zoo(z) {} String name;}');
|
'// normal comment ', 'class C2 {zoo(z) {} String name;}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test_IfStatement_droppedToken() async {
|
||||||
|
// Comment ClassDeclaration CompilationUnit
|
||||||
|
await addTestSource('main() { if (v i^) }');
|
||||||
|
if (usingFastaParser) {
|
||||||
|
await assertTarget(')', '(v)', droppedToken: 'i');
|
||||||
|
} else {
|
||||||
|
await assertTarget('i;', 'if (v) i;');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test_MethodDeclaration_inLineComment2() async {
|
test_MethodDeclaration_inLineComment2() async {
|
||||||
// Comment ClassDeclaration CompilationUnit
|
// Comment ClassDeclaration CompilationUnit
|
||||||
await addTestSource('''
|
await addTestSource('''
|
||||||
|
@ -555,13 +572,13 @@ class C2 {
|
||||||
test_SwitchStatement_c() async {
|
test_SwitchStatement_c() async {
|
||||||
// Token('c') SwitchStatement
|
// Token('c') SwitchStatement
|
||||||
await addTestSource('main() { switch(x) {c^} }');
|
await addTestSource('main() { switch(x) {c^} }');
|
||||||
await assertTarget('}', 'switch (x) {}');
|
await assertTarget('}', 'switch (x) {}', droppedToken: 'c');
|
||||||
}
|
}
|
||||||
|
|
||||||
test_SwitchStatement_c2() async {
|
test_SwitchStatement_c2() async {
|
||||||
// Token('c') SwitchStatement
|
// Token('c') SwitchStatement
|
||||||
await addTestSource('main() { switch(x) { c^ } }');
|
await addTestSource('main() { switch(x) { c^ } }');
|
||||||
await assertTarget('}', 'switch (x) {}');
|
await assertTarget('}', 'switch (x) {}', droppedToken: 'c');
|
||||||
}
|
}
|
||||||
|
|
||||||
test_SwitchStatement_empty() async {
|
test_SwitchStatement_empty() async {
|
||||||
|
|
Loading…
Reference in a new issue