From 0b6ad037b756ebffcac697c19f54ce5619da3c28 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 3 May 2022 19:52:46 -0700 Subject: [PATCH] Report error for invalid 'this' type during 'await' --- src/compiler/checker.ts | 37 ++++++++++-- .../await_incorrectThisType.errors.txt | 57 +++++++++++++++++++ .../async/es2017/await_incorrectThisType.ts | 48 ++++++++++++++++ 3 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 tests/baselines/reference/await_incorrectThisType.errors.txt create mode 100644 tests/cases/conformance/async/es2017/await_incorrectThisType.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 8dc1e1df76fd7..bf44fae48db20 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -36256,7 +36256,7 @@ namespace ts { * @param type The type of the promise. * @remarks The "promised type" of a type is the type of the "value" parameter of the "onfulfilled" callback. */ - function getPromisedTypeOfPromise(type: Type, errorNode?: Node): Type | undefined { + function getPromisedTypeOfPromise(type: Type, errorNode?: Node, thisTypeForErrorOut?: { value?: Type }): Type | undefined { // // { // type // then( // thenFunction @@ -36298,7 +36298,30 @@ namespace ts { return undefined; } - const onfulfilledParameterType = getTypeWithFacts(getUnionType(map(thenSignatures, getTypeOfFirstParameterOfSignature)), TypeFacts.NEUndefinedOrNull); + let thisTypeForError: Type | undefined; + let candidates: Signature[] | undefined; + for (const thenSignature of thenSignatures) { + const thisType = getThisTypeOfSignature(thenSignature); + if (thisType && thisType !== voidType && !isTypeRelatedTo(type, thisType, subtypeRelation)) { + thisTypeForError = thisType; + } + else { + candidates = append(candidates, thenSignature); + } + } + + if (!candidates) { + Debug.assertIsDefined(thisTypeForError); + if (thisTypeForErrorOut) { + thisTypeForErrorOut.value = thisTypeForError; + } + if (errorNode) { + error(errorNode, Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1, typeToString(type), typeToString(thisTypeForError)); + } + return undefined; + } + + const onfulfilledParameterType = getTypeWithFacts(getUnionType(map(candidates, getTypeOfFirstParameterOfSignature)), TypeFacts.NEUndefinedOrNull); if (isTypeAny(onfulfilledParameterType)) { return undefined; } @@ -36445,7 +36468,8 @@ namespace ts { return typeAsAwaitable.awaitedTypeOfType = mapType(type, mapper); } - const promisedType = getPromisedTypeOfPromise(type); + const thisTypeForErrorOut: { value: Type | undefined } = { value: undefined }; + const promisedType = getPromisedTypeOfPromise(type, /*errorNode*/ undefined, thisTypeForErrorOut); if (promisedType) { if (type.id === promisedType.id || awaitedTypeStack.lastIndexOf(promisedType.id) >= 0) { // Verify that we don't have a bad actor in the form of a promise whose @@ -36518,7 +36542,12 @@ namespace ts { if (isThenableType(type)) { if (errorNode) { Debug.assertIsDefined(diagnosticMessage); - error(errorNode, diagnosticMessage, arg0); + let chain: DiagnosticMessageChain | undefined; + if (thisTypeForErrorOut.value) { + chain = chainDiagnosticMessages(chain, Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1, typeToString(type), typeToString(thisTypeForErrorOut.value)); + } + chain = chainDiagnosticMessages(chain, diagnosticMessage, arg0); + diagnostics.add(createDiagnosticForNodeFromMessageChain(errorNode, chain)); } return undefined; } diff --git a/tests/baselines/reference/await_incorrectThisType.errors.txt b/tests/baselines/reference/await_incorrectThisType.errors.txt new file mode 100644 index 0000000000000..eea59d55e6a23 --- /dev/null +++ b/tests/baselines/reference/await_incorrectThisType.errors.txt @@ -0,0 +1,57 @@ +tests/cases/conformance/async/es2017/await_incorrectThisType.ts(40,1): error TS2684: The 'this' context of type 'EPromise' is not assignable to method's 'this' of type 'EPromise'. + Type 'number' is not assignable to type 'never'. +tests/cases/conformance/async/es2017/await_incorrectThisType.ts(43,5): error TS1320: Type of 'await' operand must either be a valid promise or must not contain a callable 'then' member. + The 'this' context of type 'EPromise' is not assignable to method's 'this' of type 'EPromise'. + + +==== tests/cases/conformance/async/es2017/await_incorrectThisType.ts (2 errors) ==== + // https://github.com/microsoft/TypeScript/issues/47711 + type Either = Left | Right; + type Left = { tag: 'Left', e: E }; + type Right = { tag: 'Right', a: A }; + + const mkLeft = (e: E): Either => ({ tag: 'Left', e }); + const mkRight = (a: A): Either => ({ tag: 'Right', a }); + + class EPromise implements PromiseLike { + static succeed(a: A): EPromise { + return new EPromise(Promise.resolve(mkRight(a))); + } + + static fail(e: E): EPromise { + return new EPromise(Promise.resolve(mkLeft(e))); + } + + constructor(readonly p: PromiseLike>) { } + + then( + // EPromise can act as a Thenable only when `E` is `never`. + this: EPromise, + onfulfilled?: ((value: A) => B | PromiseLike) | null | undefined, + onrejected?: ((reason: any) => B1 | PromiseLike) | null | undefined + ): PromiseLike { + return this.p.then( + // Casting to `Right` is safe here because we've eliminated the possibility of `Left`. + either => onfulfilled?.((either as Right).a) ?? (either as Right).a as unknown as B, + onrejected + ) + } + } + + const withTypedFailure: EPromise = EPromise.fail(1); + + // Errors as expected: + // + // "The 'this' context of type 'EPromise' is not assignable to method's + // 'this' of type 'EPromise" + withTypedFailure.then(s => s.toUpperCase()).then(console.log); + ~~~~~~~~~~~~~~~~ +!!! error TS2684: The 'this' context of type 'EPromise' is not assignable to method's 'this' of type 'EPromise'. +!!! error TS2684: Type 'number' is not assignable to type 'never'. + + async function test() { + await withTypedFailure; + ~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS1320: Type of 'await' operand must either be a valid promise or must not contain a callable 'then' member. +!!! error TS1320: The 'this' context of type 'EPromise' is not assignable to method's 'this' of type 'EPromise'. + } \ No newline at end of file diff --git a/tests/cases/conformance/async/es2017/await_incorrectThisType.ts b/tests/cases/conformance/async/es2017/await_incorrectThisType.ts new file mode 100644 index 0000000000000..7672591dd924d --- /dev/null +++ b/tests/cases/conformance/async/es2017/await_incorrectThisType.ts @@ -0,0 +1,48 @@ +// @target: esnext +// @noEmit: true +// @noTypesAndSymbols: true + +// https://github.com/microsoft/TypeScript/issues/47711 +type Either = Left | Right; +type Left = { tag: 'Left', e: E }; +type Right = { tag: 'Right', a: A }; + +const mkLeft = (e: E): Either => ({ tag: 'Left', e }); +const mkRight = (a: A): Either => ({ tag: 'Right', a }); + +class EPromise implements PromiseLike { + static succeed(a: A): EPromise { + return new EPromise(Promise.resolve(mkRight(a))); + } + + static fail(e: E): EPromise { + return new EPromise(Promise.resolve(mkLeft(e))); + } + + constructor(readonly p: PromiseLike>) { } + + then( + // EPromise can act as a Thenable only when `E` is `never`. + this: EPromise, + onfulfilled?: ((value: A) => B | PromiseLike) | null | undefined, + onrejected?: ((reason: any) => B1 | PromiseLike) | null | undefined + ): PromiseLike { + return this.p.then( + // Casting to `Right` is safe here because we've eliminated the possibility of `Left`. + either => onfulfilled?.((either as Right).a) ?? (either as Right).a as unknown as B, + onrejected + ) + } +} + +const withTypedFailure: EPromise = EPromise.fail(1); + +// Errors as expected: +// +// "The 'this' context of type 'EPromise' is not assignable to method's +// 'this' of type 'EPromise" +withTypedFailure.then(s => s.toUpperCase()).then(console.log); + +async function test() { + await withTypedFailure; +} \ No newline at end of file