From b71e7b93c2510420809117d2ca3e6d9d4b7bf2a3 Mon Sep 17 00:00:00 2001 From: tsushanth <78000697+tsushanth@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:18:57 -0700 Subject: [PATCH] fix(completions): exclude `default` keyword from expression-body arrow function completions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `default` is not a valid expression so it should not appear in keyword completions when completing inside an expression-bodied arrow function (`(x) => `). It was being included because: 1. `tryGetFunctionLikeBodyCompletionContainer` missed the expression-body case: `getRelevantTokens` sets `contextToken` to the `=>` token (the preceding token before the identifier/keyword being typed), so `prev` is `undefined` on the first `findAncestor` iteration and the check `prev === node.body` never matches. 2. Without a detected container, `getGlobalCompletions` fell back to `KeywordCompletionFilters.All`, which includes every `isFunctionLikeBodyKeyword` — and `default` passed that test because it is neither a contextual keyword nor a class-member keyword. Fix: - Detect the expression-body `=>` context directly in `tryGetFunctionLikeBodyCompletionContainer`: when `contextToken` is `EqualsGreaterThanToken` whose parent is an `ArrowFunction` with a non-block body, return that arrow function as the container. - Add `FunctionLikeExpressionBodyKeywords` filter that delegates to `isFunctionLikeBodyKeyword` but excludes `DefaultKeyword`. - In `getGlobalCompletions`, use the new filter when the detected container has a non-block body, so `default` is omitted only in expression contexts (block bodies inside switch statements are unaffected). - Update the two fourslash tests that had TODOs acknowledging this bug. Fixes #63463 --- src/services/completions.ts | 41 +++++++++++++++---- ...pletionListAtEndOfWordInArrowFunction02.ts | 7 ++-- ...pletionListAtEndOfWordInArrowFunction03.ts | 5 ++- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 28d29136dab89..76a146b4dad98 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -136,6 +136,7 @@ import { isAwaitExpression, isBigIntLiteral, isBinaryExpression, + isBlock, isBindingElement, isBindingPattern, isBreakOrContinueStatement, @@ -592,15 +593,16 @@ export type SymbolSortTextMap = (SortText | undefined)[]; // dprint-ignore const enum KeywordCompletionFilters { - None, // No keywords - All, // Every possible keyword (TODO: This is never appropriate) - ClassElementKeywords, // Keywords inside class body - InterfaceElementKeywords, // Keywords inside interface body - ConstructorParameterKeywords, // Keywords at constructor parameter - FunctionLikeBodyKeywords, // Keywords at function like body + None, // No keywords + All, // Every possible keyword (TODO: This is never appropriate) + ClassElementKeywords, // Keywords inside class body + InterfaceElementKeywords, // Keywords inside interface body + ConstructorParameterKeywords, // Keywords at constructor parameter + FunctionLikeBodyKeywords, // Keywords at function like body (block body) + FunctionLikeExpressionBodyKeywords, // Keywords at expression-bodied function (no `default`) TypeAssertionKeywords, TypeKeywords, - TypeKeyword, // Literally just `type` + TypeKeyword, // Literally just `type` Last = TypeKeyword, } @@ -3982,7 +3984,15 @@ function getCompletionData( } function getGlobalCompletions(): void { - keywordFilters = tryGetFunctionLikeBodyCompletionContainer(contextToken) ? KeywordCompletionFilters.FunctionLikeBodyKeywords : KeywordCompletionFilters.All; + const functionLikeContainer = tryGetFunctionLikeBodyCompletionContainer(contextToken); + if (functionLikeContainer) { + keywordFilters = functionLikeContainer.body && !isBlock(functionLikeContainer.body) + ? KeywordCompletionFilters.FunctionLikeExpressionBodyKeywords + : KeywordCompletionFilters.FunctionLikeBodyKeywords; + } + else { + keywordFilters = KeywordCompletionFilters.All; + } // Get all entities in the current scope. completionKind = CompletionKind.Global; @@ -4816,6 +4826,19 @@ function getCompletionData( function tryGetFunctionLikeBodyCompletionContainer(contextToken: Node): FunctionLikeDeclaration | undefined { if (contextToken) { + // When completing the expression body of an arrow function (e.g. `(x) => x|` + // or `(x) => |`), `getRelevantTokens` sets contextToken to the `=>` token + // (the preceding token before the identifier or keyword being typed). The + // findAncestor loop below starts at contextToken.parent (the ArrowFunction) + // and never matches because `prev` is undefined on the first iteration. + // Detect this case directly so callers can use the right keyword filter. + if ( + contextToken.kind === SyntaxKind.EqualsGreaterThanToken + && isArrowFunction(contextToken.parent) + && !isBlock(contextToken.parent.body) + ) { + return contextToken.parent; + } let prev: Node; const container = findAncestor(contextToken.parent, (node: Node) => { if (isClassLike(node)) { @@ -5488,6 +5511,8 @@ function getTypescriptKeywordCompletions(keywordFilter: KeywordCompletionFilters || isTypeKeyword(kind) && kind !== SyntaxKind.UndefinedKeyword; case KeywordCompletionFilters.FunctionLikeBodyKeywords: return isFunctionLikeBodyKeyword(kind); + case KeywordCompletionFilters.FunctionLikeExpressionBodyKeywords: + return isFunctionLikeBodyKeyword(kind) && kind !== SyntaxKind.DefaultKeyword; case KeywordCompletionFilters.ClassElementKeywords: return isClassMemberCompletionKeyword(kind); case KeywordCompletionFilters.InterfaceElementKeywords: diff --git a/tests/cases/fourslash/completionListAtEndOfWordInArrowFunction02.ts b/tests/cases/fourslash/completionListAtEndOfWordInArrowFunction02.ts index 690f01ab79375..7067ade19820e 100644 --- a/tests/cases/fourslash/completionListAtEndOfWordInArrowFunction02.ts +++ b/tests/cases/fourslash/completionListAtEndOfWordInArrowFunction02.ts @@ -4,10 +4,11 @@ verify.completions({ marker: "1", - // TODO: should not include 'default' keyword at an expression location includes: [ "d", "defaultIsAnInvalidParameterName", - { name: "default", sortText: completion.SortText.GlobalsOrKeywords } - ] + ], + excludes: [ + "default", + ], }); diff --git a/tests/cases/fourslash/completionListAtEndOfWordInArrowFunction03.ts b/tests/cases/fourslash/completionListAtEndOfWordInArrowFunction03.ts index 7aea1a726e28b..b90b5acaec139 100644 --- a/tests/cases/fourslash/completionListAtEndOfWordInArrowFunction03.ts +++ b/tests/cases/fourslash/completionListAtEndOfWordInArrowFunction03.ts @@ -6,7 +6,8 @@ verify.completions({ marker: "1", includes: [ "defaultIsAnInvalidParameterName", - // This should probably stop working in the future. - { name: "default", text: "default", kind: "keyword", sortText: completion.SortText.GlobalsOrKeywords }, + ], + excludes: [ + "default", ], });