Skip to content

Commit ad354c2

Browse files
authored
Don't include already-covered cases in switch completions (#51790)
* WIP: filter existing values in case completions * filter existing enum symbols * add comment * fix lint errors * update baselines * add comment
1 parent 0c23344 commit ad354c2

File tree

6 files changed

+72
-2
lines changed

6 files changed

+72
-2
lines changed

src/compiler/factory/nodeTests.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
CallSignatureDeclaration,
2424
CaseBlock,
2525
CaseClause,
26+
CaseKeyword,
2627
CatchClause,
2728
ClassDeclaration,
2829
ClassExpression,
@@ -380,6 +381,11 @@ export function isImportKeyword(node: Node): node is ImportExpression {
380381
return node.kind === SyntaxKind.ImportKeyword;
381382
}
382383

384+
/** @internal */
385+
export function isCaseKeyword(node: Node): node is CaseKeyword {
386+
return node.kind === SyntaxKind.CaseKeyword;
387+
}
388+
383389
// Names
384390

385391
export function isQualifiedName(node: Node): node is QualifiedName {

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,7 @@ export interface KeywordToken<TKind extends KeywordSyntaxKind> extends Token<TKi
15831583
export type AssertsKeyword = KeywordToken<SyntaxKind.AssertsKeyword>;
15841584
export type AssertKeyword = KeywordToken<SyntaxKind.AssertKeyword>;
15851585
export type AwaitKeyword = KeywordToken<SyntaxKind.AwaitKeyword>;
1586+
export type CaseKeyword = KeywordToken<SyntaxKind.CaseKeyword>;
15861587

15871588
/** @deprecated Use `AwaitKeyword` instead. */
15881589
export type AwaitKeywordToken = AwaitKeyword;

src/services/completions.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ import {
128128
isCallExpression,
129129
isCaseBlock,
130130
isCaseClause,
131+
isCaseKeyword,
131132
isCheckJsEnabledForFile,
132133
isClassElement,
133134
isClassLike,
@@ -194,6 +195,7 @@ import {
194195
isNamedImports,
195196
isNamedImportsOrExports,
196197
isNamespaceImport,
198+
isNodeDescendantOf,
197199
isObjectBindingPattern,
198200
isObjectLiteralExpression,
199201
isObjectTypeDeclaration,
@@ -435,6 +437,7 @@ export const enum SymbolOriginInfoKind {
435437
ResolvedExport = 1 << 5,
436438
TypeOnlyAlias = 1 << 6,
437439
ObjectLiteralMethod = 1 << 7,
440+
Ignore = 1 << 8,
438441

439442
SymbolMemberNoExport = SymbolMember,
440443
SymbolMemberExport = SymbolMember | Export,
@@ -513,6 +516,10 @@ function originIsObjectLiteralMethod(origin: SymbolOriginInfo | undefined): orig
513516
return !!(origin && origin.kind & SymbolOriginInfoKind.ObjectLiteralMethod);
514517
}
515518

519+
function originIsIgnore(origin: SymbolOriginInfo | undefined): boolean {
520+
return !!(origin && origin.kind & SymbolOriginInfoKind.Ignore);
521+
}
522+
516523
/** @internal */
517524
export interface UniqueNameSet {
518525
add(name: string): void;
@@ -865,7 +872,6 @@ function completionInfoFromData(
865872
location,
866873
propertyAccessToConvert,
867874
keywordFilters,
868-
literals,
869875
symbolToOriginInfoMap,
870876
recommendedCompletion,
871877
isJsxInitializer,
@@ -875,9 +881,12 @@ function completionInfoFromData(
875881
isRightOfDotOrQuestionDot,
876882
importStatementCompletion,
877883
insideJsDocTagTypeExpression,
878-
symbolToSortTextMap: symbolToSortTextMap,
884+
symbolToSortTextMap,
879885
hasUnresolvedAutoImports,
880886
} = completionData;
887+
let literals = completionData.literals;
888+
889+
const checker = program.getTypeChecker();
881890

882891
// Verify if the file is JSX language variant
883892
if (getLanguageVariant(sourceFile.scriptKind) === LanguageVariant.JSX) {
@@ -887,6 +896,25 @@ function completionInfoFromData(
887896
}
888897
}
889898

899+
// When the completion is for the expression of a case clause (e.g. `case |`),
900+
// filter literals & enum symbols whose values are already present in existing case clauses.
901+
const caseClause = findAncestor(contextToken, isCaseClause);
902+
if (caseClause && (isCaseKeyword(contextToken!) || isNodeDescendantOf(contextToken!, caseClause.expression))) {
903+
const tracker = newCaseClauseTracker(checker, caseClause.parent.clauses);
904+
literals = literals.filter(literal => !tracker.hasValue(literal));
905+
// The `symbols` array cannot be filtered directly, because to each symbol at position i in `symbols`,
906+
// there might be a corresponding origin at position i in `symbolToOriginInfoMap`.
907+
// So instead of filtering the `symbols` array, we mark symbols to be ignored.
908+
symbols.forEach((symbol, i) => {
909+
if (symbol.valueDeclaration && isEnumMember(symbol.valueDeclaration)) {
910+
const value = checker.getConstantValue(symbol.valueDeclaration);
911+
if (value !== undefined && tracker.hasValue(value)) {
912+
symbolToOriginInfoMap[i] = { kind: SymbolOriginInfoKind.Ignore };
913+
}
914+
}
915+
});
916+
}
917+
890918
const entries = createSortedArray<CompletionEntry>();
891919
const isChecked = isCheckedFile(sourceFile, compilerOptions);
892920
if (isChecked && !isNewIdentifierLocation && (!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) {
@@ -4533,6 +4561,9 @@ function getCompletionEntryDisplayNameForSymbol(
45334561
kind: CompletionKind,
45344562
jsxIdentifierExpected: boolean,
45354563
): CompletionEntryDisplayNameForSymbol | undefined {
4564+
if (originIsIgnore(origin)) {
4565+
return undefined;
4566+
}
45364567
const name = originIncludesSymbolName(origin) ? origin.symbolName : symbol.name;
45374568
if (name === undefined
45384569
// If the symbol is external module, don't show it in the completion list

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4553,6 +4553,7 @@ declare namespace ts {
45534553
type AssertsKeyword = KeywordToken<SyntaxKind.AssertsKeyword>;
45544554
type AssertKeyword = KeywordToken<SyntaxKind.AssertKeyword>;
45554555
type AwaitKeyword = KeywordToken<SyntaxKind.AwaitKeyword>;
4556+
type CaseKeyword = KeywordToken<SyntaxKind.CaseKeyword>;
45564557
/** @deprecated Use `AwaitKeyword` instead. */
45574558
type AwaitKeywordToken = AwaitKeyword;
45584559
/** @deprecated Use `AssertsKeyword` instead. */

tests/baselines/reference/api/typescript.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,7 @@ declare namespace ts {
619619
type AssertsKeyword = KeywordToken<SyntaxKind.AssertsKeyword>;
620620
type AssertKeyword = KeywordToken<SyntaxKind.AssertKeyword>;
621621
type AwaitKeyword = KeywordToken<SyntaxKind.AwaitKeyword>;
622+
type CaseKeyword = KeywordToken<SyntaxKind.CaseKeyword>;
622623
/** @deprecated Use `AwaitKeyword` instead. */
623624
type AwaitKeywordToken = AwaitKeyword;
624625
/** @deprecated Use `AssertsKeyword` instead. */
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
//// enum E { A, B }
4+
//// declare const e: E;
5+
//// switch (e) {
6+
//// case E.A:
7+
//// return 0;
8+
//// case E./*1*/
9+
//// }
10+
//// declare const f: 1 | 2 | 3;
11+
//// switch (f) {
12+
//// case 1:
13+
//// return 1;
14+
//// case /*2*/
15+
//// }
16+
17+
verify.completions(
18+
{
19+
marker: "1",
20+
isNewIdentifierLocation: false,
21+
includes: ["B"],
22+
excludes: "A",
23+
},
24+
{
25+
marker: "2",
26+
isNewIdentifierLocation: false,
27+
excludes: "1",
28+
includes: ["2", "3"],
29+
}
30+
);

0 commit comments

Comments
 (0)