Skip to content

Commit bd9e711

Browse files
committed
Include source node inferences in string literal completions deeper in arguments
1 parent 8af8f3c commit bd9e711

9 files changed

+99
-51
lines changed

src/compiler/checker.ts

+20
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
addRange,
77
addRelatedInfo,
88
addSyntheticLeadingComment,
9+
addToSeen,
910
AliasDeclarationNode,
1011
AllAccessorDeclarations,
1112
AmbientModuleDeclaration,
@@ -254,6 +255,7 @@ import {
254255
getContainingClassStaticBlock,
255256
getContainingFunction,
256257
getContainingFunctionOrClassStaticBlock,
258+
getContextualTypeFromParent,
257259
getDeclarationModifierFlagsFromSymbol,
258260
getDeclarationOfKind,
259261
getDeclarationsOfKind,
@@ -1655,6 +1657,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
16551657
getTypeOfPropertyOfContextualType,
16561658
getFullyQualifiedName,
16571659
getResolvedSignature: (node, candidatesOutArray, argumentCount) => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.Normal),
1660+
getContextualStringLiteralCompletionTypes,
16581661
getCandidateSignaturesForStringLiteralCompletions,
16591662
getResolvedSignatureForSignatureHelp: (node, candidatesOutArray, argumentCount) => runWithoutResolvedSignatureCaching(node, () => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.IsForSignatureHelp)),
16601663
getExpandedParameters,
@@ -1835,6 +1838,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
18351838
typeHasCallOrConstructSignatures,
18361839
};
18371840

1841+
function getContextualStringLiteralCompletionTypes(expression: Expression) {
1842+
const seen = new Map<string, true>();
1843+
1844+
return [
1845+
...getStringLiteralTypes(getContextualTypeFromParent(expression, checker, ContextFlags.None), seen),
1846+
...getStringLiteralTypes(getContextualTypeFromParent(expression, checker, ContextFlags.Completions), seen)
1847+
]
1848+
}
1849+
1850+
function getStringLiteralTypes(type: Type | undefined, uniques = new Map<string, true>()): readonly StringLiteralType[] {
1851+
if (!type) return emptyArray;
1852+
// skip constraint
1853+
type = type.flags & TypeFlags.TypeParameter ? getBaseConstraintOfType(type) || type : type;
1854+
return type.flags & TypeFlags.Union ? flatMap((type as UnionType).types, t => getStringLiteralTypes(t, uniques)) :
1855+
type.flags & TypeFlags.StringLiteral && !(type.flags & TypeFlags.EnumLiteral) && addToSeen(uniques, (type as StringLiteralType).value) ? [type as StringLiteralType] : emptyArray;
1856+
}
1857+
18381858
function getCandidateSignaturesForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node) {
18391859
const candidatesSet = new Set<Signature>();
18401860
const candidates: Signature[] = [];

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5033,6 +5033,7 @@ export interface TypeChecker {
50335033
*/
50345034
getResolvedSignature(node: CallLikeExpression, candidatesOutArray?: Signature[], argumentCount?: number): Signature | undefined;
50355035
/** @internal */ getResolvedSignatureForSignatureHelp(node: CallLikeExpression, candidatesOutArray?: Signature[], argumentCount?: number): Signature | undefined;
5036+
/** @internal */ getContextualStringLiteralCompletionTypes(expression: Expression): StringLiteralType[];
50365037
/** @internal */ getCandidateSignaturesForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node): Signature[];
50375038
/** @internal */ getExpandedParameters(sig: Signature): readonly (readonly Symbol[])[];
50385039
/** @internal */ hasEffectiveRestParameter(sig: Signature): boolean;

src/compiler/utilities.ts

+34
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
ContainerFlags,
7979
contains,
8080
containsPath,
81+
ContextFlags,
8182
createGetCanonicalFileName,
8283
createMultiMap,
8384
createScanner,
@@ -118,6 +119,7 @@ import {
118119
EntityNameOrEntityNameExpression,
119120
EnumDeclaration,
120121
EqualityComparer,
122+
EqualityOperator,
121123
equalOwnProperties,
122124
EqualsToken,
123125
equateValues,
@@ -10464,3 +10466,35 @@ export function hasResolutionModeOverride(node: ImportTypeNode | ImportDeclarati
1046410466
}
1046510467
return !!getResolutionModeOverride(node.attributes);
1046610468
}
10469+
10470+
/** @internal */
10471+
export function isEqualityOperatorKind(kind: SyntaxKind): kind is EqualityOperator {
10472+
switch (kind) {
10473+
case SyntaxKind.EqualsEqualsEqualsToken:
10474+
case SyntaxKind.EqualsEqualsToken:
10475+
case SyntaxKind.ExclamationEqualsEqualsToken:
10476+
case SyntaxKind.ExclamationEqualsToken:
10477+
return true;
10478+
default:
10479+
return false;
10480+
}
10481+
}
10482+
10483+
/** @internal */
10484+
export function getContextualTypeFromParent(node: Expression, checker: TypeChecker, contextFlags?: ContextFlags): Type | undefined {
10485+
const parent = walkUpParenthesizedExpressions(node.parent);
10486+
switch (parent.kind) {
10487+
case SyntaxKind.NewExpression:
10488+
return checker.getContextualType(parent as NewExpression, contextFlags);
10489+
case SyntaxKind.BinaryExpression: {
10490+
const { left, operatorToken, right } = parent as BinaryExpression;
10491+
return isEqualityOperatorKind(operatorToken.kind)
10492+
? checker.getTypeAtLocation(node === right ? left : right)
10493+
: checker.getContextualType(node, contextFlags);
10494+
}
10495+
case SyntaxKind.CaseClause:
10496+
return checker.getTypeAtLocation((parent as CaseClause).parent.parent.expression);
10497+
default:
10498+
return checker.getContextualType(node, contextFlags);
10499+
}
10500+
}

src/services/completions.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ import {
105105
getReplacementSpanForContextToken,
106106
getRootDeclaration,
107107
getSourceFileOfModule,
108-
getSwitchedType,
109108
getSymbolId,
110109
getSynthesizedDeepClone,
111110
getTokenAtPosition,
@@ -3109,7 +3108,7 @@ function getContextualType(previousToken: Node, position: number, sourceFile: So
31093108
return checker.getContextualType(parent as Expression);
31103109
case SyntaxKind.CaseKeyword:
31113110
const caseClause = tryCast(parent, isCaseClause);
3112-
return caseClause ? getSwitchedType(caseClause, checker) : undefined;
3111+
return caseClause ? checker.getTypeAtLocation(caseClause.parent.parent.expression) : undefined;
31133112
case SyntaxKind.OpenBraceToken:
31143113
return isJsxExpression(parent) && !isJsxElement(parent.parent) && !isJsxFragment(parent.parent) ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined;
31153114
default:

src/services/stringCompletions.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
388388
// });
389389
return stringLiteralCompletionsForObjectLiteral(typeChecker, parent.parent);
390390
}
391-
return fromContextualType() || fromContextualType(ContextFlags.None);
391+
return toStringLiteralCompletionsFromTypes(typeChecker.getContextualStringLiteralCompletionTypes(node));
392392

393393
case SyntaxKind.ElementAccessExpression: {
394394
const { expression, argumentExpression } = parent as ElementAccessExpression;
@@ -435,7 +435,7 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
435435
const literals = contextualTypes.types.filter(literal => !tracker.hasValue(literal.value));
436436
return { kind: StringLiteralCompletionKind.Types, types: literals, isNewIdentifier: false };
437437
default:
438-
return fromContextualType() || fromContextualType(ContextFlags.None);
438+
return toStringLiteralCompletionsFromTypes(typeChecker.getContextualStringLiteralCompletionTypes(node));
439439
}
440440

441441
function fromUnionableLiteralType(grandParent: Node): StringLiteralCompletionsFromTypes | StringLiteralCompletionsFromProperties | undefined {
@@ -479,14 +479,14 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
479479
function fromContextualType(contextFlags: ContextFlags = ContextFlags.Completions): StringLiteralCompletionsFromTypes | undefined {
480480
// Get completion for string literal from string literal type
481481
// i.e. var x: "hi" | "hello" = "/*completion position*/"
482-
const types = getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker, contextFlags));
483-
if (!types.length) {
484-
return;
485-
}
486-
return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier: false };
482+
return toStringLiteralCompletionsFromTypes(getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker, contextFlags)));
487483
}
488484
}
489485

486+
function toStringLiteralCompletionsFromTypes(types: readonly StringLiteralType[]): StringLiteralCompletionsFromTypes | undefined {
487+
return types.length ? { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier: false } : undefined;
488+
}
489+
490490
function walkUpParentheses(node: Node) {
491491
switch (node.kind) {
492492
case SyntaxKind.ParenthesizedType:

src/services/utilities.ts

+1-40
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import {
2929
CompilerOptions,
3030
ConditionalExpression,
3131
contains,
32-
ContextFlags,
3332
createPrinterWithRemoveCommentsOmitTrailingSemicolon,
3433
createRange,
3534
createScanner,
@@ -59,7 +58,6 @@ import {
5958
EndOfFileToken,
6059
endsWith,
6160
ensureScriptKind,
62-
EqualityOperator,
6361
escapeString,
6462
ExportAssignment,
6563
ExportDeclaration,
@@ -88,6 +86,7 @@ import {
8886
FunctionLikeDeclaration,
8987
getAssignmentDeclarationKind,
9088
getCombinedNodeFlagsAlwaysIncludeJSDoc,
89+
getContextualTypeFromParent,
9190
getDirectoryPath,
9291
getEmitModuleKind,
9392
getEmitScriptTarget,
@@ -371,7 +370,6 @@ import {
371370
VariableDeclaration,
372371
visitEachChild,
373372
VoidExpression,
374-
walkUpParenthesizedExpressions,
375373
YieldExpression,
376374
} from "./_namespaces/ts";
377375

@@ -3359,25 +3357,6 @@ export function needsParentheses(expression: Expression): boolean {
33593357
|| (isAsExpression(expression) || isSatisfiesExpression(expression)) && isObjectLiteralExpression(expression.expression);
33603358
}
33613359

3362-
/** @internal */
3363-
export function getContextualTypeFromParent(node: Expression, checker: TypeChecker, contextFlags?: ContextFlags): Type | undefined {
3364-
const parent = walkUpParenthesizedExpressions(node.parent);
3365-
switch (parent.kind) {
3366-
case SyntaxKind.NewExpression:
3367-
return checker.getContextualType(parent as NewExpression, contextFlags);
3368-
case SyntaxKind.BinaryExpression: {
3369-
const { left, operatorToken, right } = parent as BinaryExpression;
3370-
return isEqualityOperatorKind(operatorToken.kind)
3371-
? checker.getTypeAtLocation(node === right ? left : right)
3372-
: checker.getContextualType(node, contextFlags);
3373-
}
3374-
case SyntaxKind.CaseClause:
3375-
return getSwitchedType(parent as CaseClause, checker);
3376-
default:
3377-
return checker.getContextualType(node, contextFlags);
3378-
}
3379-
}
3380-
33813360
/** @internal */
33823361
export function quote(sourceFile: SourceFile, preferences: UserPreferences, text: string): string {
33833362
// Editors can pass in undefined or empty string - we want to infer the preference in those cases.
@@ -3386,19 +3365,6 @@ export function quote(sourceFile: SourceFile, preferences: UserPreferences, text
33863365
return quotePreference === QuotePreference.Single ? `'${stripQuotes(quoted).replace(/'/g, "\\'").replace(/\\"/g, '"')}'` : quoted;
33873366
}
33883367

3389-
/** @internal */
3390-
export function isEqualityOperatorKind(kind: SyntaxKind): kind is EqualityOperator {
3391-
switch (kind) {
3392-
case SyntaxKind.EqualsEqualsEqualsToken:
3393-
case SyntaxKind.EqualsEqualsToken:
3394-
case SyntaxKind.ExclamationEqualsEqualsToken:
3395-
case SyntaxKind.ExclamationEqualsToken:
3396-
return true;
3397-
default:
3398-
return false;
3399-
}
3400-
}
3401-
34023368
/** @internal */
34033369
export function isStringLiteralOrTemplate(node: Node): node is StringLiteralLike | TemplateExpression | TaggedTemplateExpression {
34043370
switch (node.kind) {
@@ -3417,11 +3383,6 @@ export function hasIndexSignature(type: Type): boolean {
34173383
return !!type.getStringIndexType() || !!type.getNumberIndexType();
34183384
}
34193385

3420-
/** @internal */
3421-
export function getSwitchedType(caseClause: CaseClause, checker: TypeChecker): Type | undefined {
3422-
return checker.getTypeAtLocation(caseClause.parent.parent.expression);
3423-
}
3424-
34253386
/** @internal */
34263387
export const ANONYMOUS = "anonymous function";
34273388

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @strict: true
4+
5+
//// declare function createMachine<T>(config: {
6+
//// initial: keyof T;
7+
//// states: {
8+
//// [K in keyof T]: {
9+
//// on?: Record<string, keyof T>;
10+
//// };
11+
//// };
12+
//// }): void;
13+
////
14+
//// createMachine({
15+
//// initial: "a",
16+
//// states: {
17+
//// a: {
18+
//// on: {
19+
//// NEXT: "/*1*/",
20+
//// },
21+
//// },
22+
//// b: {
23+
//// on: {
24+
//// NEXT: "/*2*/",
25+
//// },
26+
//// },
27+
//// },
28+
//// });
29+
30+
verify.completions({
31+
marker: ["1", "2"],
32+
exact: ["a", "b"]
33+
})

tests/cases/fourslash/typeErrorAfterStringCompletionsInNestedCall.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
goTo.marker("1");
2727
edit.insert(`x`)
28-
verify.completions({ exact: ["MORNING", "LUNCH_TIME", "ALOHA"] });
28+
verify.completions({ exact: ["ALOHAx", "MORNING", "LUNCH_TIME", "ALOHA"] });
2929
verify.getSemanticDiagnostics([{
3030
code: 2322,
3131
message: `Type 'RaiseActionObject<{ type: "ALOHAx"; }>' is not assignable to type 'RaiseActionObject<GreetingEvent>'.\n Type '{ type: "ALOHAx"; }' is not assignable to type 'GreetingEvent'.\n Type '{ type: "ALOHAx"; }' is not assignable to type '{ type: "ALOHA"; }'.\n Types of property 'type' are incompatible.\n Type '"ALOHAx"' is not assignable to type '"ALOHA"'.`,

tests/cases/fourslash/typeErrorAfterStringCompletionsInNestedCall2.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,5 @@
5050

5151
goTo.marker("1");
5252
edit.insert(`x`)
53-
verify.completions({ exact: ["FOO", "BAR"] });
53+
verify.completions({ exact: ["BARx", "FOO", "BAR"] });
5454
verify.baselineSyntacticAndSemanticDiagnostics()

0 commit comments

Comments
 (0)