Skip to content

Don't include already-covered cases in switch completions #51790

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/compiler/factory/nodeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
CallSignatureDeclaration,
CaseBlock,
CaseClause,
CaseKeyword,
CatchClause,
ClassDeclaration,
ClassExpression,
Expand Down Expand Up @@ -379,6 +380,11 @@ export function isImportKeyword(node: Node): node is ImportExpression {
return node.kind === SyntaxKind.ImportKeyword;
}

/** @internal */
export function isCaseKeyword(node: Node): node is CaseKeyword {
return node.kind === SyntaxKind.CaseKeyword;
}

// Names

export function isQualifiedName(node: Node): node is QualifiedName {
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,7 @@ export interface KeywordToken<TKind extends KeywordSyntaxKind> extends Token<TKi
export type AssertsKeyword = KeywordToken<SyntaxKind.AssertsKeyword>;
export type AssertKeyword = KeywordToken<SyntaxKind.AssertKeyword>;
export type AwaitKeyword = KeywordToken<SyntaxKind.AwaitKeyword>;
export type CaseKeyword = KeywordToken<SyntaxKind.CaseKeyword>;

/** @deprecated Use `AwaitKeyword` instead. */
export type AwaitKeywordToken = AwaitKeyword;
Expand Down
35 changes: 33 additions & 2 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ import {
isCallExpression,
isCaseBlock,
isCaseClause,
isCaseKeyword,
isCheckJsEnabledForFile,
isClassElement,
isClassLike,
Expand Down Expand Up @@ -191,6 +192,7 @@ import {
isNamedImports,
isNamedImportsOrExports,
isNamespaceImport,
isNodeDescendantOf,
isObjectBindingPattern,
isObjectLiteralExpression,
isObjectTypeDeclaration,
Expand Down Expand Up @@ -429,6 +431,7 @@ export const enum SymbolOriginInfoKind {
ResolvedExport = 1 << 5,
TypeOnlyAlias = 1 << 6,
ObjectLiteralMethod = 1 << 7,
Ignore = 1 << 8,

SymbolMemberNoExport = SymbolMember,
SymbolMemberExport = SymbolMember | Export,
Expand Down Expand Up @@ -507,6 +510,10 @@ function originIsObjectLiteralMethod(origin: SymbolOriginInfo | undefined): orig
return !!(origin && origin.kind & SymbolOriginInfoKind.ObjectLiteralMethod);
}

function originIsIgnore(origin: SymbolOriginInfo | undefined): boolean {
return !!(origin && origin.kind & SymbolOriginInfoKind.Ignore);
}

/** @internal */
export interface UniqueNameSet {
add(name: string): void;
Expand Down Expand Up @@ -859,7 +866,6 @@ function completionInfoFromData(
location,
propertyAccessToConvert,
keywordFilters,
literals,
symbolToOriginInfoMap,
recommendedCompletion,
isJsxInitializer,
Expand All @@ -868,9 +874,12 @@ function completionInfoFromData(
isRightOfOpenTag,
importStatementCompletion,
insideJsDocTagTypeExpression,
symbolToSortTextMap: symbolToSortTextMap,
symbolToSortTextMap,
hasUnresolvedAutoImports,
} = completionData;
let literals = completionData.literals;

const checker = program.getTypeChecker();

// Verify if the file is JSX language variant
if (getLanguageVariant(sourceFile.scriptKind) === LanguageVariant.JSX) {
Expand All @@ -880,6 +889,25 @@ function completionInfoFromData(
}
}

// When the completion is for the expression of a case clause (e.g. `case |`),
// filter literals & enum symbols whose values are already present in existing case clauses.
const caseClause = findAncestor(contextToken, isCaseClause);
if (caseClause && (isCaseKeyword(contextToken!) || isNodeDescendantOf(contextToken!, caseClause.expression))) {
const tracker = newCaseClauseTracker(checker, caseClause.parent.clauses);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool that this abstraction came in handy in an additional place 🎉

literals = literals.filter(literal => !tracker.hasValue(literal));
// The `symbols` array cannot be filtered directly, because to each symbol at position i in `symbols`,
// there might be a corresponding origin at position i in `symbolToOriginInfoMap`.
// So instead of filtering the `symbols` array, we mark symbols to be ignored.
symbols.forEach((symbol, i) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't filter the symbols array because the index of a symbol in this array is tied to the information in symbolToOriginInfoMap. I didn't want to create an abstraction on top of these two arrays and refactor everything, so I added an "ignore" flag to the origin info, but I'm open to suggestions on other ways to achieve this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are overdue to make a nice abstraction over a similar data structure to what we have so it’s easier to work with without sacrificing (and probably improving on) the reasonably efficient allocations we have going. This seems fine for now.

if (symbol.valueDeclaration && isEnumMember(symbol.valueDeclaration)) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the filtering won't work if someone is using e.g. an object literal as an enum, because checker.constantValue only works for enum members. But I'm hoping that filtering out literals and enum members already covers a lot of the cases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like something that can be expanded to readonly members with literal types if there’s demand for it 👍

const value = checker.getConstantValue(symbol.valueDeclaration);
if (value !== undefined && tracker.hasValue(value)) {
symbolToOriginInfoMap[i] = { kind: SymbolOriginInfoKind.Ignore };
}
}
});
}

const entries = createSortedArray<CompletionEntry>();
const isChecked = isCheckedFile(sourceFile, compilerOptions);
if (isChecked && !isNewIdentifierLocation && (!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) {
Expand Down Expand Up @@ -4509,6 +4537,9 @@ function getCompletionEntryDisplayNameForSymbol(
kind: CompletionKind,
jsxIdentifierExpected: boolean,
): CompletionEntryDisplayNameForSymbol | undefined {
if (originIsIgnore(origin)) {
return undefined;
}
const name = originIncludesSymbolName(origin) ? origin.symbolName : symbol.name;
if (name === undefined
// If the symbol is external module, don't show it in the completion list
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4547,6 +4547,7 @@ declare namespace ts {
type AssertsKeyword = KeywordToken<SyntaxKind.AssertsKeyword>;
type AssertKeyword = KeywordToken<SyntaxKind.AssertKeyword>;
type AwaitKeyword = KeywordToken<SyntaxKind.AwaitKeyword>;
type CaseKeyword = KeywordToken<SyntaxKind.CaseKeyword>;
/** @deprecated Use `AwaitKeyword` instead. */
type AwaitKeywordToken = AwaitKeyword;
/** @deprecated Use `AssertsKeyword` instead. */
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,7 @@ declare namespace ts {
type AssertsKeyword = KeywordToken<SyntaxKind.AssertsKeyword>;
type AssertKeyword = KeywordToken<SyntaxKind.AssertKeyword>;
type AwaitKeyword = KeywordToken<SyntaxKind.AwaitKeyword>;
type CaseKeyword = KeywordToken<SyntaxKind.CaseKeyword>;
/** @deprecated Use `AwaitKeyword` instead. */
type AwaitKeywordToken = AwaitKeyword;
/** @deprecated Use `AssertsKeyword` instead. */
Expand Down
30 changes: 30 additions & 0 deletions tests/cases/fourslash/switchCompletions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/// <reference path="fourslash.ts" />

//// enum E { A, B }
//// declare const e: E;
//// switch (e) {
//// case E.A:
//// return 0;
//// case E./*1*/
//// }
//// declare const f: 1 | 2 | 3;
//// switch (f) {
//// case 1:
//// return 1;
//// case /*2*/
//// }

verify.completions(
{
marker: "1",
isNewIdentifierLocation: false,
includes: ["B"],
excludes: "A",
},
{
marker: "2",
isNewIdentifierLocation: false,
excludes: "1",
includes: ["2", "3"],
}
);