// 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 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 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 { final List 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 = [], 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() != null) { return _doExpectCommentRefs; } // Ignore the SimpleIdentifiers that make up library directives. if (node.thisOrAncestorOfType() != 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() != 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; }