Skip to content

Support completions for enums in switch cases #16864

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

Closed
wants to merge 1 commit into from
Closed
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
10 changes: 10 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ namespace ts {
location = getParseTreeNode(location);
return resolveName(location, name, meaning, /*nameNotFoundMessage*/ undefined, name);
},
getRemainingSwitchCaseTypes,
};

const tupleTypes: GenericType[] = [];
Expand Down Expand Up @@ -11077,6 +11078,15 @@ namespace ts {
return links.switchTypes;
}

function getRemainingSwitchCaseTypes(node: SwitchStatement): Type[] | undefined {
Copy link
Contributor

Choose a reason for hiding this comment

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

i would move this to services, and use getTypeAtLocation for switch labels.

const type = getTypeOfExpression(node.expression);
if (!isLiteralType(type)) return undefined;

const types = type.flags & TypeFlags.Union ? (type as ts.UnionType).types : [type];
const switchTypes = mapDefined(node.caseBlock.clauses, getTypeOfSwitchClause);
return types.filter(t => !contains(switchTypes, t));
}

function eachTypeContainedIn(source: Type, types: Type[]) {
return source.flags & TypeFlags.Union ? !forEach((<UnionType>source).types, t => !contains(types, t)) : contains(types, source);
}
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2648,6 +2648,9 @@ namespace ts {

/* @internal */ getJsxNamespace(): string;
/* @internal */ resolveNameAtLocation(location: Node, name: string, meaning: SymbolFlags): Symbol | undefined;

/** Types that are not already covered by some switch case. */
/* @internal */ getRemainingSwitchCaseTypes(node: SwitchStatement): Type[] | undefined;
}

export enum NodeBuilderFlags {
Expand Down
54 changes: 40 additions & 14 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ namespace ts.Completions {
return undefined;
}

const { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, request, keywordFilters } = completionData;
const { symbols, useFullSymbolNames, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, request, keywordFilters } = completionData;

if (sourceFile.languageVariant === LanguageVariant.JSX &&
location && location.parent && location.parent.kind === SyntaxKind.JsxClosingElement) {
Expand Down Expand Up @@ -56,15 +56,15 @@ namespace ts.Completions {
const entries: CompletionEntry[] = [];

if (isSourceFileJavaScript(sourceFile)) {
const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log);
const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, useFullSymbolNames);
addRange(entries, getJavaScriptCompletionEntries(sourceFile, location.pos, uniqueNames, compilerOptions.target));
}
else {
if ((!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) {
return undefined;
}

getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log);
getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, useFullSymbolNames);
}

// TODO add filter for keyword based on type/value/namespace and also location
Expand Down Expand Up @@ -107,11 +107,11 @@ namespace ts.Completions {
return entries;
}

function createCompletionEntry(symbol: Symbol, location: Node, performCharacterChecks: boolean, typeChecker: TypeChecker, target: ScriptTarget): CompletionEntry {
function createCompletionEntry(symbol: Symbol, location: Node, performCharacterChecks: boolean, typeChecker: TypeChecker, target: ScriptTarget, useFullSymbolNames: boolean): CompletionEntry {
// Try to get a valid display name for this symbol, if we could not find one, then ignore it.
// We would like to only show things that can be added after a dot, so for instance numeric properties can
// not be accessed with a dot (a.1 <- invalid)
const displayName = getCompletionEntryDisplayNameForSymbol(typeChecker, symbol, target, performCharacterChecks, location);
const displayName = getCompletionEntryDisplayNameForSymbol(typeChecker, symbol, target, performCharacterChecks, location, useFullSymbolNames);
if (!displayName) {
return undefined;
}
Expand All @@ -132,12 +132,12 @@ namespace ts.Completions {
};
}

function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: Push<CompletionEntry>, location: Node, performCharacterChecks: boolean, typeChecker: TypeChecker, target: ScriptTarget, log: Log): Map<string> {
function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: Push<CompletionEntry>, location: Node, performCharacterChecks: boolean, typeChecker: TypeChecker, target: ScriptTarget, log: Log, useFullSymbolNames?: boolean): Map<string> {
const start = timestamp();
const uniqueNames = createMap<string>();
if (symbols) {
for (const symbol of symbols) {
const entry = createCompletionEntry(symbol, location, performCharacterChecks, typeChecker, target);
const entry = createCompletionEntry(symbol, location, performCharacterChecks, typeChecker, target, useFullSymbolNames);
if (entry) {
const id = escapeIdentifier(entry.name);
if (!uniqueNames.get(id)) {
Expand Down Expand Up @@ -300,13 +300,13 @@ namespace ts.Completions {
// Compute all the completion symbols again.
const completionData = getCompletionData(typeChecker, log, sourceFile, position);
if (completionData) {
const { symbols, location } = completionData;
const { symbols, useFullSymbolNames, location } = completionData;

// Find the symbol with the matching entry name.
// We don't need to perform character checks here because we're only comparing the
// name against 'entryName' (which is known to be good), not building a new
// completion entry.
const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(typeChecker, s, compilerOptions.target, /*performCharacterChecks*/ false, location) === entryName ? s : undefined);
const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(typeChecker, s, compilerOptions.target, /*performCharacterChecks*/ false, location, useFullSymbolNames) === entryName ? s : undefined);

if (symbol) {
const { displayParts, documentation, symbolKind, tags } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All);
Expand Down Expand Up @@ -344,20 +344,21 @@ namespace ts.Completions {
// Compute all the completion symbols again.
const completionData = getCompletionData(typeChecker, log, sourceFile, position);
if (completionData) {
const { symbols, location } = completionData;
const { symbols, useFullSymbolNames, location } = completionData;

// Find the symbol with the matching entry name.
// We don't need to perform character checks here because we're only comparing the
// name against 'entryName' (which is known to be good), not building a new
// completion entry.
return forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(typeChecker, s, compilerOptions.target, /*performCharacterChecks*/ false, location) === entryName ? s : undefined);
return forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(typeChecker, s, compilerOptions.target, /*performCharacterChecks*/ false, location, useFullSymbolNames) === entryName ? s : undefined);
}

return undefined;
}

interface CompletionData {
symbols: Symbol[];
useFullSymbolNames: boolean;
isGlobalCompletion: boolean;
isMemberCompletion: boolean;
isNewIdentifierLocation: boolean;
Expand Down Expand Up @@ -440,7 +441,7 @@ namespace ts.Completions {
}

if (request) {
return { symbols: undefined, isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, request, keywordFilters: KeywordCompletionFilters.None };
return { symbols: undefined, useFullSymbolNames: false, isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, request, keywordFilters: KeywordCompletionFilters.None };
}

if (!insideJsDocTagTypeExpression) {
Expand Down Expand Up @@ -474,6 +475,7 @@ namespace ts.Completions {
let isRightOfDot = false;
let isRightOfOpenTag = false;
let isStartingCloseTag = false;
let isRightOfCase = false;

let location = getTouchingPropertyName(sourceFile, position, insideJsDocTagTypeExpression); // TODO: GH#15853
if (contextToken) {
Expand All @@ -499,6 +501,10 @@ namespace ts.Completions {
return undefined;
}
}
else if (contextToken.kind === SyntaxKind.CaseKeyword) {
node = contextToken.parent as CaseClause;
isRightOfCase = true;
}
else if (sourceFile.languageVariant === LanguageVariant.JSX) {
// <UI.Test /* completion position */ />
// If the tagname is a property access expression, we will then walk up to the top most of property access expression.
Expand Down Expand Up @@ -540,11 +546,15 @@ namespace ts.Completions {
let isMemberCompletion: boolean;
let isNewIdentifierLocation: boolean;
let keywordFilters = KeywordCompletionFilters.None;
const useFullSymbolNames = isRightOfCase;
let symbols: Symbol[] = [];

if (isRightOfDot) {
getTypeScriptMemberSymbols();
}
else if (isRightOfCase) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't this restrict completions to only enum completions and prevent completions for locals? That is too restrictive.

getSwitchCaseSymbols(node as CaseClause);
}
else if (isRightOfOpenTag) {
const tagSymbols = typeChecker.getJsxIntrinsicTagNames();
if (tryGetGlobalSymbols()) {
Expand Down Expand Up @@ -577,7 +587,7 @@ namespace ts.Completions {

log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));

return { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters };
return { symbols, useFullSymbolNames, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters };

type JSDocTagWithTypeExpression = JSDocAugmentsTag | JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;

Expand Down Expand Up @@ -633,6 +643,20 @@ namespace ts.Completions {
}
}

function getSwitchCaseSymbols(clause: CaseClause): void {
isGlobalCompletion = false;
isMemberCompletion = true; // Set to true to avoid getting keyword completions. TODO: This really needs a refactor.
isNewIdentifierLocation = false;

const switchStatement = clause.parent.parent;
forEach(typeChecker.getRemainingSwitchCaseTypes(switchStatement), type => {
// TODO: GH#16863: Support regular number and string types, although those have no symbols.
if (type.flags & TypeFlags.EnumLiteral) {
symbols.push(type.symbol);
}
});
}

function addTypeProperties(type: Type) {
if (type) {
// Filter private properties
Expand Down Expand Up @@ -1598,7 +1622,9 @@ namespace ts.Completions {
*
* @return undefined if the name is of external module otherwise a name with striped of any quote
*/
function getCompletionEntryDisplayNameForSymbol(typeChecker: TypeChecker, symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean, location: Node): string {
function getCompletionEntryDisplayNameForSymbol(typeChecker: TypeChecker, symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean, location: Node, useFullSymbolNames: boolean): string {
if (useFullSymbolNames) return typeChecker.symbolToString(symbol, location);

const displayName: string = getDeclaredName(typeChecker, symbol, location);

if (displayName) {
Expand Down
16 changes: 16 additions & 0 deletions tests/cases/fourslash/completionsSwitchCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/// <reference path='fourslash.ts'/>

////enum E { A, B }
////declare const e: E;
////switch (e) {
//// case /*1*/
////}
////switch (e) {
//// case E.B:
//// case /*2*/:
//// default:
//// case nonsense:
////}

verify.completionsAt("1", ["E.A", "E.B"]);
verify.completionsAt("2", ["E.A"]);