blob: 92a5c2cf3667f50403849602736c52608b659ad4 [file] [log] [blame]
// Copyright (c) 2023, 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/completion/dart/candidate_suggestion.dart';
import 'package:analysis_server/src/services/completion/dart/suggestion_collector.dart';
import 'package:analysis_server/src/utilities/extensions/ast.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
/// A helper class that produces candidate suggestions for the keywords that are
/// valid at the completion location.
class KeywordHelper {
/// The suggestion collector to which suggestions will be added.
final SuggestionCollector collector;
/// The feature set used to determine which keywords should be suggested.
final FeatureSet featureSet;
/// The offset of the completion location.
final int offset;
/// Initialize a newly created helper to add suggestions to the [collector].
KeywordHelper(
{required this.collector,
required this.featureSet,
required this.offset});
/// Add the keywords that are appropriate when the selection is in a class
/// declaration between the name of the class and the body. The [node] is the
/// class declaration containing the selection point.
void addClassDeclarationKeywords(ClassDeclaration node) {
// We intentionally add all keywords, even when they would be out of order,
// in order to help users discover what keywords are available. If the
// keywords are in the wrong order a diagnostic (and fix) will help them get
// the keywords in the correct location.
if (_isAbsentOrIn(node.extendsClause?.extendsKeyword)) {
addKeyword(Keyword.EXTENDS);
}
if (_isAbsentOrIn(node.withClause?.withKeyword)) {
addKeyword(Keyword.WITH);
}
if (_isAbsentOrIn(node.implementsClause?.implementsKeyword)) {
addKeyword(Keyword.IMPLEMENTS);
}
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of a member in a class.
void addClassMemberKeywords() {
addKeyword(Keyword.CONST);
addKeyword(Keyword.COVARIANT);
addKeyword(Keyword.DYNAMIC);
addKeyword(Keyword.FACTORY);
addKeyword(Keyword.FINAL);
addKeyword(Keyword.GET);
addKeyword(Keyword.OPERATOR);
addKeyword(Keyword.SET);
addKeyword(Keyword.STATIC);
addKeyword(Keyword.VAR);
addKeyword(Keyword.VOID);
addKeyword(Keyword.LATE);
}
/// Add the keywords that are appropriate when the selection is in a class
/// declaration before the `class` keyword. The [node] is the class
/// declaration containing the selection point.
void addClassModifiers(ClassDeclaration node) {
if (_isAbsentOrIn(node.abstractKeyword) && node.sealedKeyword == null) {
addKeyword(Keyword.ABSTRACT);
}
if (featureSet.isEnabled(Feature.class_modifiers) &&
featureSet.isEnabled(Feature.sealed_class)) {
if (node.baseKeyword == null &&
node.finalKeyword == null &&
node.interfaceKeyword == null &&
node.mixinKeyword == null &&
node.sealedKeyword == null) {
if (node.abstractKeyword == null) {
addKeyword(Keyword.SEALED);
} else {
// abstract ^ class A {}
addKeyword(Keyword.BASE);
addKeyword(Keyword.FINAL);
addKeyword(Keyword.INTERFACE);
addKeyword(Keyword.MIXIN);
}
}
if (node.baseKeyword != null && _isAbsentOrIn(node.mixinKeyword)) {
// base ^ class A {}
// abstract base ^ class A {}
addKeyword(Keyword.MIXIN);
}
if (node.mixinKeyword != null && _isAbsentOrIn(node.baseKeyword)) {
// abstract ^ mixin class A {}
addKeyword(Keyword.BASE);
}
}
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of an element in a collection [literal].
void addCollectionElementKeywords(
TypedLiteral literal, NodeList<CollectionElement> elements,
{bool mustBeStatic = false}) {
// TODO(brianwilkerson): Consider determining whether there is a comma before
// the selection and inserting the comma if there isn't one.
addKeyword(Keyword.FOR);
addKeyword(Keyword.IF);
// TODO(brianwilkerson): Consider replacing the lines above with the
// following lines:
// addKeywordFromText(Keyword.FOR, ' (^)');
// addKeywordFromText(Keyword.IF, ' (^)');
var preceedingElement = elements.elementBefore(offset);
if (preceedingElement != null) {
var nextToken = preceedingElement.endToken.next!;
if (nextToken.isSynthetic || offset <= nextToken.offset) {
if (preceedingElement.couldHaveTrailingElse) {
addKeyword(Keyword.ELSE);
} else {
var index = elements.indexOf(preceedingElement);
if (index > 0 && elements[index - 1].couldHaveTrailingElse) {
addKeyword(Keyword.ELSE);
}
}
}
}
addExpressionKeywords(literal, mustBeStatic: mustBeStatic);
}
/// Add the keywords that are appropriate when the selection is after the
/// directives.
void addCompilationUnitDeclarationKeywords() {
addKeyword(Keyword.ABSTRACT);
addKeyword(Keyword.CLASS);
addKeyword(Keyword.CONST);
addKeyword(Keyword.COVARIANT);
addKeyword(Keyword.DYNAMIC);
addKeyword(Keyword.ENUM);
addKeyword(Keyword.EXTERNAL);
addKeyword(Keyword.FINAL);
addKeyword(Keyword.MIXIN);
addKeyword(Keyword.TYPEDEF);
addKeyword(Keyword.VAR);
addKeyword(Keyword.VOID);
if (featureSet.isEnabled(Feature.extension_methods)) {
addKeyword(Keyword.EXTENSION);
}
addKeyword(Keyword.LATE);
if (featureSet.isEnabled(Feature.class_modifiers)) {
addKeyword(Keyword.BASE);
addKeyword(Keyword.INTERFACE);
}
if (featureSet.isEnabled(Feature.sealed_class)) {
addKeyword(Keyword.SEALED);
}
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of a constant expression. The flag [inConstantContext] should be
/// `true` if the expression is inside a constant context.
void addConstantExpressionKeywords({required bool inConstantContext}) {
// TODO(brianwilkerson): Use this method in place of `addExpressionKeywords`
// when in a constant context in order to not suggest invalid keywords.
addKeyword(Keyword.FALSE);
addKeyword(Keyword.NULL);
addKeyword(Keyword.TRUE);
if (!inConstantContext) {
addKeyword(Keyword.CONST);
}
}
/// Add the keywords that are appropriate when the selection is in the
/// [initializer] list of the given [constructor].
void addConstructorInitializerKeywords(
ConstructorDeclaration constructor, ConstructorInitializer? initializer) {
addKeyword(Keyword.ASSERT);
var initializers = constructor.initializers;
if (initializer == null || initializers.last == initializer) {
var last = initializers.lastNonSynthetic;
if (last == initializer ||
(last is! SuperConstructorInvocation &&
last is! RedirectingConstructorInvocation)) {
if (constructor.parent is! ExtensionTypeDeclaration) {
addKeyword(Keyword.SUPER);
}
addKeyword(Keyword.THIS);
}
} else if (initializer is ConstructorFieldInitializer) {
var equals = initializer.equals;
if (equals.end <= offset && offset <= equals.next!.offset) {
addKeyword(Keyword.THIS);
}
}
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of a directive in a compilation unit. The [before] directive is
/// the directive before the one being added.
void addDirectiveKeywords(CompilationUnit unit, Directive? before) {
// TODO(brianwilkerson): If we had both the members before and after the new
// directive, we could limit the keywords based on surrounding members.
if (before == null && !unit.directives.any((d) => d is LibraryDirective)) {
addKeyword(Keyword.LIBRARY);
}
addKeywordFromText(Keyword.IMPORT, " '^';");
addKeywordFromText(Keyword.EXPORT, " '^';");
addKeywordFromText(Keyword.PART, " '^';");
}
/// Add the keywords that are appropriate when the selection is in an enum
/// declaration between the name of the enum and the body. The [node] is the
/// enum declaration containing the selection point.
void addEnumDeclarationKeywords(EnumDeclaration node) {
// We intentionally add all keywords, even when they would be out of order,
// in order to help users discover what keywords are available. If the
// keywords are in the wrong order a diagnostic (and fix) will help them get
// the keywords in the correct location.
if (_isAbsentOrIn(node.withClause?.withKeyword)) {
addKeyword(Keyword.WITH);
}
if (_isAbsentOrIn(node.implementsClause?.implementsKeyword)) {
addKeyword(Keyword.IMPLEMENTS);
}
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of a member in an enum.
void addEnumMemberKeywords() {
addKeyword(Keyword.CONST);
addKeyword(Keyword.DYNAMIC);
addKeyword(Keyword.FINAL);
addKeyword(Keyword.GET);
addKeyword(Keyword.LATE);
addKeyword(Keyword.OPERATOR);
addKeyword(Keyword.SET);
addKeyword(Keyword.STATIC);
addKeyword(Keyword.VAR);
addKeyword(Keyword.VOID);
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of an expression. The [node] provides context to determine which
/// keywords to include.
void addExpressionKeywords(AstNode? node,
{bool mustBeConstant = false, bool mustBeStatic = false}) {
/// Return `true` if `const` should be suggested for the given [node].
bool constIsValid(AstNode? node) {
if (node is CollectionElement && node is! Expression) {
node = node.parent;
}
if (node is Expression) {
return !node.inConstantContext;
} else if (node is Block ||
node is EmptyStatement ||
node is ExpressionStatement ||
node is IfStatement) {
return true;
} else if (node is PatternVariableDeclaration) {
return true;
} else if (node is RecordPattern) {
// This might be a parenthesized pattern.
return node.fields.isEmpty;
} else if (node is SwitchPatternCase) {
return true;
} else if (node is SwitchStatement) {
return true;
} else if (node is VariableDeclaration) {
return !node.isConst;
} else if (node is VariableDeclarationStatement) {
return !node.variables.isConst;
} else if (node is WhenClause) {
return true;
}
return false;
}
/// Return `true` if `switch` should be suggested for the given [node].
bool switchIsValid(AstNode? node) {
if (node is CollectionElement && node is! Expression) {
node = node.parent;
}
if (node is SwitchPatternCase && offset <= node.colon.offset) {
return false;
}
return true;
}
addKeyword(Keyword.FALSE);
addKeyword(Keyword.NULL);
addKeyword(Keyword.TRUE);
if (node != null) {
if (constIsValid(node)) {
addKeyword(Keyword.CONST);
}
if (!mustBeConstant && !mustBeStatic) {
addKeyword(Keyword.SUPER);
addKeyword(Keyword.THIS);
}
if (node.inAsyncMethodOrFunction ||
node.inAsyncStarOrSyncStarMethodOrFunction) {
addKeyword(Keyword.AWAIT);
}
if (switchIsValid(node) && featureSet.isEnabled(Feature.patterns)) {
addKeyword(Keyword.SWITCH);
}
} else if (featureSet.isEnabled(Feature.patterns)) {
addKeyword(Keyword.SWITCH);
}
}
/// Add the keywords that are appropriate when the selection is in an
/// extension declaration between the name of the extension and the body. The
/// [node] is the extension declaration containing the selection point.
void addExtensionDeclarationKeywords(ExtensionDeclaration node) {
if (node.onKeyword.isSynthetic) {
addKeyword(Keyword.ON);
if (node.name == null && featureSet.isEnabled(Feature.inline_class)) {
addPseudoKeyword('type');
}
}
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of a member in an extension.
void addExtensionMemberKeywords({required bool isStatic}) {
addKeyword(Keyword.CONST);
addKeyword(Keyword.DYNAMIC);
addKeyword(Keyword.FINAL);
addKeyword(Keyword.GET);
if (!isStatic) addKeyword(Keyword.OPERATOR);
addKeyword(Keyword.SET);
if (!isStatic) addKeyword(Keyword.STATIC);
addKeyword(Keyword.VAR);
addKeyword(Keyword.VOID);
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of a member in an extension type.
void addExtensionTypeMemberKeywords({required bool isStatic}) {
addKeyword(Keyword.CONST);
addKeyword(Keyword.DYNAMIC);
addKeyword(Keyword.FINAL);
addKeyword(Keyword.GET);
if (!isStatic) addKeyword(Keyword.OPERATOR);
addKeyword(Keyword.SET);
if (!isStatic) addKeyword(Keyword.STATIC);
addKeyword(Keyword.VAR);
addKeyword(Keyword.VOID);
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of field declaration.
///
/// If the declaration consists of a single variable and the name of the
/// variable is a keyword, then the parser used a keyword as the name of the
/// variable as part of recovery and the [keyword] should be treated like the
/// keyword it really is.
void addFieldDeclarationKeywords(FieldDeclaration node, {Keyword? keyword}) {
if (_isAbsentOrIn(node.externalKeyword) && keyword != Keyword.EXTERNAL) {
addKeyword(Keyword.EXTERNAL);
}
var fields = node.fields;
if (fields.type == null) {
// TODO(brianwilkerson): We should probably not suggest types if `var` is
// being used.
addKeyword(Keyword.DYNAMIC);
addKeyword(Keyword.VOID);
}
if (!node.isStatic && keyword != Keyword.STATIC) {
if (_isAbsentOrIn(node.abstractKeyword) && keyword != Keyword.ABSTRACT) {
addKeyword(Keyword.ABSTRACT);
}
if (_isAbsentOrIn(node.covariantKeyword) &&
keyword != Keyword.COVARIANT) {
addKeyword(Keyword.COVARIANT);
}
if (_isAbsentOrIn(fields.lateKeyword) && keyword != Keyword.LATE) {
addKeyword(Keyword.LATE);
}
addKeyword(Keyword.STATIC);
}
var firstField = fields.variables.firstOrNull;
if (firstField != null) {
if (!firstField.isConst &&
!firstField.isFinal &&
keyword != Keyword.CONST &&
keyword != Keyword.FINAL &&
keyword != Keyword.VAR) {
addKeyword(Keyword.CONST);
addKeyword(Keyword.FINAL);
if (fields.type == null) {
addKeyword(Keyword.VAR);
}
}
}
}
/// Add the keywords that are appropriate when the selection is at the start
/// of a formal parameter in the given [parameterList].
void addFormalParameterKeywords(FormalParameterList parameterList) {
addKeyword(Keyword.COVARIANT);
addKeyword(Keyword.FINAL);
if (parameterList.inNamedGroup(offset)) {
addKeyword(Keyword.REQUIRED);
}
var parent = parameterList.parent;
if (parent is ConstructorDeclaration) {
if (featureSet.isEnabled(Feature.super_parameters)) {
addKeyword(Keyword.SUPER);
}
addKeyword(Keyword.THIS);
}
}
/// Add the keywords that are appropriate when the selection is before the `{`
/// or `=>` in a function body. The [body] is used to determine which keywords
/// are appropriate.
void addFunctionBodyModifiers(FunctionBody? body) {
if (_isAbsentOrIn(body?.keyword)) {
addKeyword(Keyword.ASYNC);
if (body is! ExpressionFunctionBody) {
addKeywordFromText(Keyword.ASYNC, '*');
addKeywordFromText(Keyword.SYNC, '*');
}
}
}
/// Add the keywords that are appropriate when the selection is in an import
/// directive between the URI and the semicolon. The [node] is the import
/// directive containing the selection point.
void addImportDirectiveKeywords(ImportDirective node) {
var deferredKeyword = node.deferredKeyword;
var asKeyword = node.asKeyword;
var firstCombinator = node.combinators.firstOrNull;
if (firstCombinator == null || offset < firstCombinator.offset) {
if (deferredKeyword == null) {
if (asKeyword == null) {
addKeywordFromText(Keyword.DEFERRED, ' as');
addKeyword(Keyword.AS);
addKeyword(Keyword.HIDE);
addKeyword(Keyword.SHOW);
} else if (offset < asKeyword.offset) {
addKeyword(Keyword.DEFERRED);
} else {
var prefix = node.prefix;
if (prefix != null && offset > prefix.end) {
addKeyword(Keyword.HIDE);
addKeyword(Keyword.SHOW);
}
}
} else if (offset > deferredKeyword.end && asKeyword == null) {
addKeyword(Keyword.AS);
} else {
addKeyword(Keyword.HIDE);
addKeyword(Keyword.SHOW);
}
} else {
addKeyword(Keyword.HIDE);
addKeyword(Keyword.SHOW);
}
}
/// Add a keyword suggestion to suggest the [keyword].
void addKeyword(Keyword keyword) {
collector.addSuggestion(KeywordSuggestion.fromKeyword(keyword: keyword));
}
/// Add a keyword suggestion to suggest the [keyword] followed by the
/// [annotatedText]. The annotated text is used in cases where there is
/// boilerplate that always follows the keyword that should also be suggested.
///
/// If the annotated text contains a caret (^), then the completion will use
/// the annotated text with the caret removed and the index of the caret will
/// be used as the selection offset. If the text doesn't contain a caret, then
/// the insert text will be the annotated text and the selection offset will
/// be at the end of the text.
void addKeywordFromText(Keyword keyword, String annotatedText) {
collector.addSuggestion(KeywordSuggestion.fromKeywordAndText(
keyword: keyword, annotatedText: annotatedText));
}
/// Add the keywords that are appropriate when the selection is in a mixin
/// declaration between the name of the mixin and the body. The [node] is the
/// mixin declaration containing the selection point.
void addMixinDeclarationKeywords(MixinDeclaration node) {
// We intentionally add all keywords, even when they would be out of order,
// in order to help users discover what keywords are available. If the
// keywords are in the wrong order a diagnostic (and fix) will help them get
// the keywords in the correct location.
if (_isAbsentOrIn(node.onClause?.onKeyword)) {
addKeyword(Keyword.ON);
}
if (_isAbsentOrIn(node.implementsClause?.implementsKeyword)) {
addKeyword(Keyword.IMPLEMENTS);
}
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of a member in a mixin.
void addMixinMemberKeywords() {
addKeyword(Keyword.CONST);
addKeyword(Keyword.COVARIANT);
addKeyword(Keyword.DYNAMIC);
addKeyword(Keyword.FINAL);
addKeyword(Keyword.GET);
addKeyword(Keyword.OPERATOR);
addKeyword(Keyword.SET);
addKeyword(Keyword.STATIC);
addKeyword(Keyword.VAR);
addKeyword(Keyword.VOID);
addKeyword(Keyword.LATE);
}
/// Add the keywords that are appropriate when the selection is in a mixin
/// declaration before the `mixin` keyword. The [node] is the mixin
/// declaration containing the selection point.
void addMixinModifiers(MixinDeclaration node) {
if (_isAbsentOrIn(node.baseKeyword)) {
addKeyword(Keyword.BASE);
}
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of a pattern.
void addPatternKeywords() {
addConstantExpressionKeywords(inConstantContext: false);
addVariablePatternKeywords();
}
/// Add a keyword suggestion to suggest the [keyword].
void addPseudoKeyword(String keyword) {
collector
.addSuggestion(KeywordSuggestion.fromPseudoKeyword(keyword: keyword));
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of a statement. The [node] provides context to determine which
/// keywords to include.
void addStatementKeywords(AstNode node) {
if (node.inAsyncMethodOrFunction) {
addKeyword(Keyword.AWAIT);
} else if (node.inAsyncStarOrSyncStarMethodOrFunction) {
addKeyword(Keyword.AWAIT);
addKeyword(Keyword.YIELD);
addKeywordFromText(Keyword.YIELD, '*');
}
if (node.inLoop) {
addKeyword(Keyword.BREAK);
addKeyword(Keyword.CONTINUE);
}
if (node.inSwitch) {
addKeyword(Keyword.BREAK);
}
addKeyword(Keyword.ASSERT);
addKeyword(Keyword.DO);
addKeyword(Keyword.DYNAMIC);
addKeyword(Keyword.FINAL);
addKeyword(Keyword.FOR);
addKeyword(Keyword.IF);
if (node.inCatchClause) {
addKeyword(Keyword.RETHROW);
}
addKeyword(Keyword.RETURN);
if (!featureSet.isEnabled(Feature.patterns)) {
// We don't suggest `switch` when patterns is enabled because `switch`
// will be suggested by `addExpressionKeywords`, which should always be
// called in conjunction with this method.
addKeyword(Keyword.SWITCH);
}
addKeyword(Keyword.THROW);
addKeyword(Keyword.TRY);
addKeyword(Keyword.VAR);
addKeyword(Keyword.VOID);
addKeyword(Keyword.WHILE);
if (node.inAsyncStarOrSyncStarMethodOrFunction) {
addKeyword(Keyword.YIELD);
addKeywordFromText(Keyword.YIELD, '*');
}
addKeyword(Keyword.LATE);
}
/// Add the keywords that are appropriate when the selection is after the
/// end of a `try` statement. [canHaveFinally] indicates whether it's valid to
/// suggest a `finally` clause.
void addTryClauseKeywords({required bool canHaveFinally}) {
addKeyword(Keyword.CATCH);
if (canHaveFinally) {
addKeyword(Keyword.FINALLY);
}
addKeyword(Keyword.ON);
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of a pattern.
void addVariablePatternKeywords() {
addKeyword(Keyword.FINAL);
addKeyword(Keyword.VAR);
}
/// Return `true` if the [token] is `null` or if the offset is toughing the
/// [token].
bool _isAbsentOrIn(Token? token) {
return token == null || (token.offset <= offset && offset <= token.end);
}
}
extension on CollectionElement? {
bool get couldHaveTrailingElse {
var finalElement = this;
while (finalElement is IfElement || finalElement is ForElement) {
if (finalElement is IfElement) {
var elseElement = finalElement.elseElement;
if (elseElement == null) {
break;
}
finalElement = elseElement;
} else if (finalElement is ForElement) {
finalElement = finalElement.body;
}
}
return finalElement is IfElement &&
finalElement.elseKeyword == null &&
!finalElement.thenElement.isSynthetic;
}
}
extension on FormalParameterList {
bool inNamedGroup(int offset) {
final leftDelimiter = this.leftDelimiter;
if (leftDelimiter == null ||
leftDelimiter.type != TokenType.OPEN_CURLY_BRACKET) {
return false;
}
var left = leftDelimiter.end;
var right = rightDelimiter?.offset ?? rightParenthesis.offset;
return left <= offset && offset <= right;
}
}
extension on NodeList<ConstructorInitializer> {
ConstructorInitializer get lastNonSynthetic {
final last = this.last;
if (last.beginToken.isSynthetic && length > 1) {
return this[length - 2];
}
return last;
}
}