From d19ac4d0d2a6f17fe3a94ed38361249c998c473e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 18 Oct 2024 09:55:14 +0200 Subject: [PATCH] Fixed member completions for `NoInfer`-wrapped unions --- src/compiler/checker.ts | 11 +++-- src/compiler/types.ts | 3 ++ src/services/completions.ts | 1 + tests/baselines/reference/api/typescript.d.ts | 2 + .../reference/narrowingNoInfer1.types | 12 +++--- tests/cases/fourslash/completionNoInfer1.ts | 41 +++++++++++++++++++ 6 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 tests/cases/fourslash/completionNoInfer1.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ce2f19bbca1fe..fc38a2df0ca78 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1927,6 +1927,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { isTypeParameterPossiblyReferenced, typeHasCallOrConstructSignatures, getSymbolFlags, + unwrapNoInferType, }; function getCandidateSignaturesForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node) { @@ -16513,6 +16514,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return !!(type.flags & TypeFlags.Substitution && (type as SubstitutionType).constraint.flags & TypeFlags.Unknown); } + function unwrapNoInferType(type: Type): Type; + function unwrapNoInferType(type: Type | undefined): Type | undefined; + function unwrapNoInferType(type: Type | undefined) { + return type && mapType(type, t => isNoInferType(t) ? (t as SubstitutionType).baseType : t); + } + function getSubstitutionType(baseType: Type, constraint: Type) { return constraint.flags & TypeFlags.AnyOrUnknown || constraint === baseType || baseType.flags & TypeFlags.Any ? baseType : @@ -29677,9 +29684,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function getNarrowableTypeForReference(type: Type, reference: Node, checkMode?: CheckMode) { - if (isNoInferType(type)) { - type = (type as SubstitutionType).baseType; - } + type = unwrapNoInferType(type); // When the type of a reference is or contains an instantiable type with a union type constraint, and // when the reference is in a constraint position (where it is known we'll obtain the apparent type) or // has a contextual type containing no top-level instantiables (meaning constraints will determine diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 374451a2aadb1..f072e803d82e5 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5426,6 +5426,9 @@ export interface TypeChecker { /** @internal */ typeHasCallOrConstructSignatures(type: Type): boolean; /** @internal */ getSymbolFlags(symbol: Symbol): SymbolFlags; /** @internal */ fillMissingTypeArguments(typeArguments: readonly Type[], typeParameters: readonly TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean): Type[]; + + unwrapNoInferType(type: Type): Type; + unwrapNoInferType(type: Type | undefined): Type | undefined; } /** @internal */ diff --git a/src/services/completions.ts b/src/services/completions.ts index 30e9ad40bf3f1..69dc50808094f 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -5637,6 +5637,7 @@ export function getPropertiesForObjectExpression(contextualType: Type, completio } function getApparentProperties(type: Type, node: ObjectLiteralExpression | JsxAttributes, checker: TypeChecker) { + type = checker.unwrapNoInferType(type); if (!type.isUnion()) return type.getApparentProperties(); return checker.getAllPossiblePropertiesOfTypes(filter(type.types, memberType => !(memberType.flags & TypeFlags.Primitive diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 0ed1d1e839fcd..7dd7d55caf7b2 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -6349,6 +6349,8 @@ declare namespace ts { * and the operation is cancelled, then it should be discarded, otherwise it is safe to keep. */ runWithCancellationToken(token: CancellationToken, cb: (checker: TypeChecker) => T): T; + unwrapNoInferType(type: Type): Type; + unwrapNoInferType(type: Type | undefined): Type | undefined; } enum NodeBuilderFlags { None = 0, diff --git a/tests/baselines/reference/narrowingNoInfer1.types b/tests/baselines/reference/narrowingNoInfer1.types index 690fcaa27be25..5c9f37eef3169 100644 --- a/tests/baselines/reference/narrowingNoInfer1.types +++ b/tests/baselines/reference/narrowingNoInfer1.types @@ -139,21 +139,21 @@ test2({ type: 'a' as const }, { type: 'b' as const }, (thing) => { > : ^^^^^^^ >thing.type : "a" | "b" > : ^^^^^^^^^ ->thing : NoInfer<{ type: "a"; }> | NoInfer<{ type: "b"; }> -> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>thing : { type: "a"; } | { type: "b"; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ >type : "a" | "b" > : ^^^^^^^^^ >"a" : "a" > : ^^^ thing; ->thing : NoInfer<{ type: "a"; }> -> : ^^^^^^^^^^^^^^^^^^^^^^^ +>thing : { type: "a"; } +> : ^^^^^^^^^^^^^^ } else { thing; ->thing : NoInfer<{ type: "b"; }> -> : ^^^^^^^^^^^^^^^^^^^^^^^ +>thing : { type: "b"; } +> : ^^^^^^^^^^^^^^ } }); diff --git a/tests/cases/fourslash/completionNoInfer1.ts b/tests/cases/fourslash/completionNoInfer1.ts new file mode 100644 index 0000000000000..c57645cf4db4d --- /dev/null +++ b/tests/cases/fourslash/completionNoInfer1.ts @@ -0,0 +1,41 @@ +/// + +// @strict: true +// @jsx: preserve + +// @Filename: /a.tsx +//// /// +//// /// +//// +//// interface A { +//// type: "a"; +//// value: string; +//// } +//// +//// interface B { +//// type: "b"; +//// size: number; +//// } +//// +//// type Union = A | B; +//// +//// function accept1(union: NoInfer) {} +//// accept1({ /*1*/ }); +//// accept1({ type: "a", /*2*/ }); +//// +//// function accept2(arg: { prop: NoInfer }) {} +//// accept2({ prop: { /*3*/ } }); +//// accept2({ prop: { type: "a", /*4*/ } }); +//// +//// function Accept3(props: NoInfer) {} +//// ; +//// ; + +verify.completions({ + marker: ["1", "3", "5"], + exact: ["size", "type", "value"], +}); +verify.completions({ + marker: ["2", "4", "6"], + exact: ["value"], +});