blob: a4abe608864413703d04ce1439b72254a4f3839a [file] [log] [blame]
// Copyright (c) 2020, 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.
// @dart = 2.9
import 'package:analysis_server/src/protocol/protocol_internal.dart';
import 'package:analysis_server/src/protocol_server.dart' as protocol;
import 'package:analysis_server/src/services/completion/dart/keyword_contributor.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/syntactic_entity.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart' as element;
class ExpectedCompletion {
final String _filePath;
final SyntacticEntity _entity;
/// Some completions are special cased from the DAS "import" for instance is
/// suggested as a completion "import '';", the completion string here in this
/// instance would have the value "import '';".
final String _completionString;
final protocol.CompletionSuggestionKind _kind;
final int _lineNumber;
final int _columnNumber;
final protocol.ElementKind _elementKind;
ExpectedCompletion(this._filePath, this._entity, this._lineNumber,
this._columnNumber, this._kind, this._elementKind)
: _completionString = null;
/// Return an instance extracted from the decoded JSON [map].
factory ExpectedCompletion.fromJson(Map<String, dynamic> map) {
var jsonDecoder = ResponseDecoder(null);
var filePath = map['filePath'] as String;
var offset = map['offset'] as int;
var lineNumber = map['lineNumber'] as int;
var columnNumber = map['columnNumber'] as int;
var completionString = map['completionString'] as String;
var kind = map['kind'] != null
? protocol.CompletionSuggestionKind.fromJson(
jsonDecoder, '', map['kind'] as String)
: null;
var elementKind = map['elementKind'] != null
? protocol.ElementKind.fromJson(
jsonDecoder, '', map['elementKind'] as String)
: null;
return ExpectedCompletion.specialCompletionString(
filePath,
_SyntacticEntity(offset),
lineNumber,
columnNumber,
completionString,
kind,
elementKind);
}
ExpectedCompletion.specialCompletionString(
this._filePath,
this._entity,
this._lineNumber,
this._columnNumber,
this._completionString,
this._kind,
this._elementKind);
int get columnNumber => _columnNumber;
String get completion => _completionString ?? _entity.toString();
protocol.ElementKind get elementKind => _elementKind;
String get filePath => _filePath;
protocol.CompletionSuggestionKind get kind => _kind;
int get lineNumber => _lineNumber;
String get location => '$filePath:$lineNumber:$columnNumber';
int get offset => _entity.offset;
SyntacticEntity get syntacticEntity => _entity;
bool matches(protocol.CompletionSuggestion completionSuggestion) {
if (completionSuggestion.completion == completion) {
if (kind != null &&
completionSuggestion.kind != null &&
completionSuggestion.kind != kind) {
return false;
}
if (elementKind != null &&
completionSuggestion.element?.kind != null &&
completionSuggestion.element?.kind != elementKind) {
return false;
}
return true;
}
return false;
}
/// Return a map used to represent this expected completion in a JSON structure.
Map<String, dynamic> toJson() {
return {
'filePath': _filePath,
'offset': _entity.offset,
'lineNumber': _lineNumber,
'columnNumber': _columnNumber,
'completionString': _completionString,
if (_kind != null) 'kind': _kind.toJson(),
if (_elementKind != null) 'elementKind': _elementKind.toJson(),
};
}
@override
String toString() =>
"'$completion', kind = $kind, elementKind = $elementKind, $location";
}
class ExpectedCompletionsVisitor extends RecursiveAstVisitor<void> {
final List<ExpectedCompletion> expectedCompletions;
final String filePath;
CompilationUnit _enclosingCompilationUnit;
/// This boolean is set to enable whether or not we should assert that some
/// found keyword in Dart syntax should be in the completion set returned from
/// the analysis server. This is off by default because with syntax such as
/// "^get foo() => _foo;", "get" and "set" won't be suggested because this
/// syntax has specified the "get" modifier, i.e. it would be invalid to
/// include it again: "get get foo() => _foo;".
final bool _doExpectKeywordCompletions = false;
/// This boolean is set to enable if identifiers in comments should be
/// expected to be completed. The default is false as typos in a dartdoc
/// comment don't yield an error like Dart syntax mistakes would yield.
final bool _doExpectCommentRefs = false;
ExpectedCompletionsVisitor(this.filePath)
: expectedCompletions = <ExpectedCompletion>[],
assert(filePath != null);
void safelyRecordEntity(SyntacticEntity entity,
{protocol.CompletionSuggestionKind kind,
protocol.ElementKind elementKind}) {
// Only record if this entity is not null, has a length, etc.
if (entity != null && entity.offset > 0 && entity.length > 0) {
// Compute the line number at this offset
var lineNumber = _enclosingCompilationUnit.lineInfo
.getLocation(entity.offset)
.lineNumber;
var columnNumber = _enclosingCompilationUnit.lineInfo
.getLocation(entity.offset)
.columnNumber;
// Some special cases in the if and if-else blocks, 'import' from the
// DAS is "import '';" which we want to be sure to match.
if (entity.toString() == 'async') {
expectedCompletions.add(ExpectedCompletion.specialCompletionString(
filePath,
entity,
lineNumber,
columnNumber,
ASYNC_STAR,
kind,
elementKind));
} else if (entity.toString() == 'default') {
expectedCompletions.add(ExpectedCompletion.specialCompletionString(
filePath,
entity,
lineNumber,
columnNumber,
DEFAULT_COLON,
kind,
elementKind));
} else if (entity.toString() == 'deferred') {
expectedCompletions.add(ExpectedCompletion.specialCompletionString(
filePath,
entity,
lineNumber,
columnNumber,
DEFERRED_AS,
kind,
elementKind));
} else if (entity.toString() == 'export') {
expectedCompletions.add(ExpectedCompletion.specialCompletionString(
filePath,
entity,
lineNumber,
columnNumber,
EXPORT_STATEMENT,
kind,
elementKind));
} else if (entity.toString() == 'import') {
expectedCompletions.add(ExpectedCompletion.specialCompletionString(
filePath,
entity,
lineNumber,
columnNumber,
IMPORT_STATEMENT,
kind,
elementKind));
} else if (entity.toString() == 'part') {
expectedCompletions.add(ExpectedCompletion.specialCompletionString(
filePath,
entity,
lineNumber,
columnNumber,
PART_STATEMENT,
kind,
elementKind));
} else if (entity.toString() == 'sync') {
expectedCompletions.add(ExpectedCompletion.specialCompletionString(
filePath,
entity,
lineNumber,
columnNumber,
SYNC_STAR,
kind,
elementKind));
} else if (entity.toString() == 'yield') {
expectedCompletions.add(ExpectedCompletion.specialCompletionString(
filePath,
entity,
lineNumber,
columnNumber,
YIELD_STAR,
kind,
elementKind));
} else {
expectedCompletions.add(ExpectedCompletion(
filePath, entity, lineNumber, columnNumber, kind, elementKind));
}
}
}
void safelyRecordKeywordCompletion(SyntacticEntity entity) {
if (_doExpectKeywordCompletions) {
safelyRecordEntity(entity,
kind: protocol.CompletionSuggestionKind.KEYWORD);
}
}
@override
void visitAsExpression(AsExpression node) {
safelyRecordKeywordCompletion(node.asOperator);
super.visitAsExpression(node);
}
@override
void visitAssertInitializer(AssertInitializer node) {
safelyRecordKeywordCompletion(node.assertKeyword);
super.visitAssertInitializer(node);
}
@override
void visitAssertStatement(AssertStatement node) {
safelyRecordKeywordCompletion(node.assertKeyword);
super.visitAssertStatement(node);
}
@override
void visitAwaitExpression(AwaitExpression node) {
safelyRecordKeywordCompletion(node.awaitKeyword);
super.visitAwaitExpression(node);
}
@override
void visitBlockFunctionBody(BlockFunctionBody node) {
// 'async' | 'async' '*' | 'sync' '*':
safelyRecordKeywordCompletion(node.keyword);
super.visitBlockFunctionBody(node);
}
@override
void visitBooleanLiteral(BooleanLiteral node) {
// 'false' | 'true'
safelyRecordKeywordCompletion(node.literal);
super.visitBooleanLiteral(node);
}
@override
void visitBreakStatement(BreakStatement node) {
safelyRecordKeywordCompletion(node.breakKeyword);
super.visitBreakStatement(node);
}
@override
void visitCatchClause(CatchClause node) {
safelyRecordKeywordCompletion(node.catchKeyword);
safelyRecordKeywordCompletion(node.onKeyword);
super.visitCatchClause(node);
}
@override
void visitClassDeclaration(ClassDeclaration node) {
safelyRecordKeywordCompletion(node.abstractKeyword);
safelyRecordKeywordCompletion(node.classKeyword);
super.visitClassDeclaration(node);
}
@override
void visitClassTypeAlias(ClassTypeAlias node) {
safelyRecordKeywordCompletion(node.abstractKeyword);
safelyRecordKeywordCompletion(node.typedefKeyword);
super.visitClassTypeAlias(node);
}
@override
void visitCompilationUnit(CompilationUnit node) {
_enclosingCompilationUnit = node;
super.visitCompilationUnit(node);
}
@override
void visitConfiguration(Configuration node) {
safelyRecordKeywordCompletion(node.ifKeyword);
super.visitConfiguration(node);
}
@override
void visitConstructorDeclaration(ConstructorDeclaration node) {
safelyRecordKeywordCompletion(node.externalKeyword);
safelyRecordKeywordCompletion(node.constKeyword);
safelyRecordKeywordCompletion(node.factoryKeyword);
super.visitConstructorDeclaration(node);
}
@override
void visitConstructorFieldInitializer(ConstructorFieldInitializer node) {
safelyRecordKeywordCompletion(node.thisKeyword);
super.visitConstructorFieldInitializer(node);
}
@override
void visitContinueStatement(ContinueStatement node) {
safelyRecordKeywordCompletion(node.continueKeyword);
super.visitContinueStatement(node);
}
@override
void visitDeclaredIdentifier(DeclaredIdentifier node) {
// 'final', 'const' or 'var'
safelyRecordKeywordCompletion(node.keyword);
super.visitDeclaredIdentifier(node);
}
@override
void visitDoStatement(DoStatement node) {
safelyRecordKeywordCompletion(node.doKeyword);
safelyRecordKeywordCompletion(node.whileKeyword);
super.visitDoStatement(node);
}
@override
void visitEnumDeclaration(EnumDeclaration node) {
safelyRecordKeywordCompletion(node.enumKeyword);
super.visitEnumDeclaration(node);
}
@override
void visitExportDirective(ExportDirective node) {
safelyRecordKeywordCompletion(node.keyword);
super.visitExportDirective(node);
}
@override
void visitExpressionFunctionBody(ExpressionFunctionBody node) {
safelyRecordKeywordCompletion(node.keyword);
super.visitExpressionFunctionBody(node);
}
@override
void visitExtendsClause(ExtendsClause node) {
safelyRecordKeywordCompletion(node.extendsKeyword);
super.visitExtendsClause(node);
}
@override
void visitExtensionDeclaration(ExtensionDeclaration node) {
safelyRecordKeywordCompletion(node.extensionKeyword);
safelyRecordKeywordCompletion(node.onKeyword);
super.visitExtensionDeclaration(node);
}
@override
void visitFieldDeclaration(FieldDeclaration node) {
safelyRecordKeywordCompletion(node.abstractKeyword);
safelyRecordKeywordCompletion(node.covariantKeyword);
safelyRecordKeywordCompletion(node.externalKeyword);
safelyRecordKeywordCompletion(node.staticKeyword);
super.visitFieldDeclaration(node);
}
@override
void visitFieldFormalParameter(FieldFormalParameter node) {
// 'final', 'const' or 'var'
safelyRecordKeywordCompletion(node.keyword);
safelyRecordKeywordCompletion(node.thisKeyword);
super.visitFieldFormalParameter(node);
}
@override
void visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) {
safelyRecordKeywordCompletion(node.inKeyword);
super.visitForEachPartsWithDeclaration(node);
}
@override
void visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) {
safelyRecordKeywordCompletion(node.inKeyword);
super.visitForEachPartsWithIdentifier(node);
}
@override
void visitForElement(ForElement node) {
safelyRecordKeywordCompletion(node.awaitKeyword);
safelyRecordKeywordCompletion(node.forKeyword);
super.visitForElement(node);
}
@override
void visitForStatement(ForStatement node) {
safelyRecordKeywordCompletion(node.awaitKeyword);
safelyRecordKeywordCompletion(node.forKeyword);
super.visitForStatement(node);
}
@override
void visitFunctionDeclaration(FunctionDeclaration node) {
safelyRecordKeywordCompletion(node.externalKeyword);
// 'get' or 'set':
safelyRecordKeywordCompletion(node.propertyKeyword);
super.visitFunctionDeclaration(node);
}
@override
void visitFunctionTypeAlias(FunctionTypeAlias node) {
safelyRecordKeywordCompletion(node.typedefKeyword);
super.visitFunctionTypeAlias(node);
}
@override
void visitGenericFunctionType(GenericFunctionType node) {
safelyRecordKeywordCompletion(node.functionKeyword);
super.visitGenericFunctionType(node);
}
@override
void visitGenericTypeAlias(GenericTypeAlias node) {
safelyRecordKeywordCompletion(node.typedefKeyword);
super.visitGenericTypeAlias(node);
}
@override
void visitHideCombinator(HideCombinator node) {
safelyRecordKeywordCompletion(node.keyword);
super.visitHideCombinator(node);
}
@override
void visitIfElement(IfElement node) {
safelyRecordKeywordCompletion(node.ifKeyword);
safelyRecordKeywordCompletion(node.elseKeyword);
super.visitIfElement(node);
}
@override
void visitIfStatement(IfStatement node) {
safelyRecordKeywordCompletion(node.ifKeyword);
safelyRecordKeywordCompletion(node.elseKeyword);
super.visitIfStatement(node);
}
@override
void visitImplementsClause(ImplementsClause node) {
safelyRecordKeywordCompletion(node.implementsKeyword);
super.visitImplementsClause(node);
}
@override
void visitImportDirective(ImportDirective node) {
safelyRecordKeywordCompletion(node.keyword);
safelyRecordKeywordCompletion(node.asKeyword);
safelyRecordKeywordCompletion(node.deferredKeyword);
super.visitImportDirective(node);
}
@override
void visitInstanceCreationExpression(InstanceCreationExpression node) {
// Here we explicitly do not record 'new' as we don't suggest it in the
// completion service.
// https://dart-review.googlesource.com/c/sdk/+/131020
if (node.keyword?.type == Keyword.CONST) {
safelyRecordKeywordCompletion(node.keyword);
}
super.visitInstanceCreationExpression(node);
}
@override
void visitIsExpression(IsExpression node) {
safelyRecordKeywordCompletion(node.isOperator);
super.visitIsExpression(node);
}
@override
void visitLibraryDirective(LibraryDirective node) {
safelyRecordKeywordCompletion(node.libraryKeyword);
super.visitLibraryDirective(node);
}
@override
void visitListLiteral(ListLiteral node) {
safelyRecordKeywordCompletion(node.constKeyword);
super.visitListLiteral(node);
}
@override
void visitMethodDeclaration(MethodDeclaration node) {
safelyRecordKeywordCompletion(node.externalKeyword);
safelyRecordKeywordCompletion(node.modifierKeyword);
safelyRecordKeywordCompletion(node.operatorKeyword);
safelyRecordKeywordCompletion(node.propertyKeyword);
super.visitMethodDeclaration(node);
}
@override
void visitMixinDeclaration(MixinDeclaration node) {
safelyRecordKeywordCompletion(node.mixinKeyword);
super.visitMixinDeclaration(node);
}
@override
void visitNativeClause(NativeClause node) {
safelyRecordKeywordCompletion(node.nativeKeyword);
super.visitNativeClause(node);
}
@override
void visitNativeFunctionBody(NativeFunctionBody node) {
safelyRecordKeywordCompletion(node.nativeKeyword);
super.visitNativeFunctionBody(node);
}
@override
void visitNullLiteral(NullLiteral node) {
safelyRecordKeywordCompletion(node.literal);
super.visitNullLiteral(node);
}
@override
void visitOnClause(OnClause node) {
safelyRecordKeywordCompletion(node.onKeyword);
super.visitOnClause(node);
}
@override
void visitPartDirective(PartDirective node) {
safelyRecordKeywordCompletion(node.partKeyword);
super.visitPartDirective(node);
}
@override
void visitPartOfDirective(PartOfDirective node) {
safelyRecordKeywordCompletion(node.partKeyword);
safelyRecordKeywordCompletion(node.ofKeyword);
super.visitPartOfDirective(node);
}
@override
void visitRedirectingConstructorInvocation(
RedirectingConstructorInvocation node) {
safelyRecordKeywordCompletion(node.thisKeyword);
super.visitRedirectingConstructorInvocation(node);
}
@override
void visitRethrowExpression(RethrowExpression node) {
safelyRecordKeywordCompletion(node.rethrowKeyword);
return super.visitRethrowExpression(node);
}
@override
void visitReturnStatement(ReturnStatement node) {
safelyRecordKeywordCompletion(node.returnKeyword);
super.visitReturnStatement(node);
}
@override
void visitSetOrMapLiteral(SetOrMapLiteral node) {
safelyRecordKeywordCompletion(node.constKeyword);
super.visitSetOrMapLiteral(node);
}
@override
void visitShowCombinator(ShowCombinator node) {
safelyRecordKeywordCompletion(node.keyword);
super.visitShowCombinator(node);
}
@override
void visitSimpleFormalParameter(SimpleFormalParameter node) {
// 'final', 'const' or 'var'
safelyRecordKeywordCompletion(node.keyword);
safelyRecordKeywordCompletion(node.covariantKeyword);
super.visitSimpleFormalParameter(node);
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
if (_doIncludeSimpleIdentifier(node)) {
protocol.ElementKind elementKind;
if (node.staticElement?.kind != null) {
elementKind = protocol.convertElementKind(node.staticElement.kind);
// If the completed element kind is a getter or setter set the element
// kind to null as the exact kind from the DAS is unknown at this
// point.
if (elementKind == protocol.ElementKind.GETTER ||
elementKind == protocol.ElementKind.SETTER) {
elementKind = null;
}
// Map PREFIX element kinds to LIBRARY kinds as this is the ElementKind
// used for prefixes reported from the completion engine.
if (elementKind == protocol.ElementKind.PREFIX) {
elementKind = protocol.ElementKind.LIBRARY;
}
// For identifiers in a FieldFormalParameter, i.e. the 'foo' in some
// ClassName(this.foo), set the elementKind to FIELD which is what the
// completion engine does (elementKind before this if statement is a
// PARAMETER).
if (node.parent is FieldFormalParameter) {
elementKind = protocol.ElementKind.FIELD;
}
// Class references that are constructor calls are constructor kinds,
// unless either:
// 1) the constructor is a named constructor, i.e. some "Foo.bar()",
// the "Foo" in this case is a class,
// 2) or, there is an explicit const or new keyword before the
// constructor invocation in which case the "Foo" above is
// considered a constructor still
if (elementKind == protocol.ElementKind.CLASS) {
if (node.parent?.parent is ConstructorName) {
var constructorName = node.parent.parent as ConstructorName;
var instanceCreationExpression = constructorName.parent;
if (instanceCreationExpression is InstanceCreationExpression &&
constructorName.type.name == node) {
if (instanceCreationExpression.keyword != null ||
constructorName.name == null) {
elementKind = protocol.ElementKind.CONSTRUCTOR;
}
}
}
}
}
safelyRecordEntity(node, elementKind: elementKind);
}
super.visitSimpleIdentifier(node);
}
@override
void visitSuperConstructorInvocation(SuperConstructorInvocation node) {
safelyRecordKeywordCompletion(node.superKeyword);
super.visitSuperConstructorInvocation(node);
}
@override
void visitSuperExpression(SuperExpression node) {
safelyRecordKeywordCompletion(node.superKeyword);
super.visitSuperExpression(node);
}
@override
void visitSwitchCase(SwitchCase node) {
safelyRecordKeywordCompletion(node.keyword);
super.visitSwitchCase(node);
}
@override
void visitSwitchDefault(SwitchDefault node) {
safelyRecordKeywordCompletion(node.keyword);
super.visitSwitchDefault(node);
}
@override
void visitSwitchStatement(SwitchStatement node) {
safelyRecordKeywordCompletion(node.switchKeyword);
super.visitSwitchStatement(node);
}
@override
void visitThisExpression(ThisExpression node) {
safelyRecordKeywordCompletion(node.thisKeyword);
super.visitThisExpression(node);
}
@override
void visitThrowExpression(ThrowExpression node) {
safelyRecordKeywordCompletion(node.throwKeyword);
super.visitThrowExpression(node);
}
@override
void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
safelyRecordKeywordCompletion(node.externalKeyword);
super.visitTopLevelVariableDeclaration(node);
}
@override
void visitTryStatement(TryStatement node) {
safelyRecordKeywordCompletion(node.tryKeyword);
safelyRecordKeywordCompletion(node.finallyKeyword);
super.visitTryStatement(node);
}
@override
void visitTypeParameter(TypeParameter node) {
safelyRecordKeywordCompletion(node.extendsKeyword);
super.visitTypeParameter(node);
}
@override
void visitVariableDeclarationList(VariableDeclarationList node) {
// 'final', 'const' or 'var'
safelyRecordKeywordCompletion(node.keyword);
safelyRecordKeywordCompletion(node.lateKeyword);
super.visitVariableDeclarationList(node);
}
@override
void visitWhileStatement(WhileStatement node) {
safelyRecordKeywordCompletion(node.whileKeyword);
super.visitWhileStatement(node);
}
@override
void visitWithClause(WithClause node) {
safelyRecordKeywordCompletion(node.withKeyword);
super.visitWithClause(node);
}
@override
void visitYieldStatement(YieldStatement node) {
safelyRecordKeywordCompletion(node.yieldKeyword);
super.visitYieldStatement(node);
}
bool _doIncludeSimpleIdentifier(SimpleIdentifier node) {
// Do not continue if this node is synthetic, or if the node is in a
// declaration context
if (node == null || node.isSynthetic || node.inDeclarationContext()) {
return false;
}
// If the type of the SimpleIdentifier is dynamic, don't include.
if (node.staticType != null && node.staticType.isDynamic) {
return false;
}
// If we are in a comment reference context, return if we should include
// such identifiers.
if (node.thisOrAncestorOfType<CommentReference>() != null) {
return _doExpectCommentRefs;
}
// Ignore the SimpleIdentifiers that make up library directives.
if (node.thisOrAncestorOfType<LibraryDirective>() != null) {
return false;
}
// TODO (jwren) If there is a mode of completing at a token location where
// the token is removed before the completion query happens, then this
// should be disabled in such a case:
// Named arguments, i.e. the 'foo' in 'method_call(foo: 1)' should not be
// included, by design, the completion engine won't suggest named arguments
// already it the source.
//
// The null check, node.staticElement == null, handles the cases where the
// invocation is unknown, i.e. the 'arg' in 'foo.bar(arg: 1)', where foo is
// dynamic.
if ((node.staticElement == null ||
node.staticElement?.kind == element.ElementKind.PARAMETER) &&
node.parent is Label &&
node.thisOrAncestorOfType<ArgumentList>() != null) {
return false;
}
return true;
}
}
class _SyntacticEntity implements SyntacticEntity {
@override
final int offset;
_SyntacticEntity(this.offset);
@override
int get end => offset + length;
@override
int get length => 0;
}