From 0b6ad037b756ebffcac697c19f54ce5619da3c28 Mon Sep 17 00:00:00 2001
From: Ron Buckton <ron.buckton@microsoft.com>
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<number, string>' is not assignable to method's 'this' of type 'EPromise<never, string>'.
+  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<number, string>' is not assignable to method's 'this' of type 'EPromise<never, string>'.
+
+
+==== tests/cases/conformance/async/es2017/await_incorrectThisType.ts (2 errors) ====
+    // https://github.com/microsoft/TypeScript/issues/47711
+    type Either<E, A> = Left<E> | Right<A>;
+    type Left<E> = { tag: 'Left', e: E };
+    type Right<A> = { tag: 'Right', a: A };
+    
+    const mkLeft = <E>(e: E): Either<E, never> => ({ tag: 'Left', e });
+    const mkRight = <A>(a: A): Either<never, A> => ({ tag: 'Right', a });
+    
+    class EPromise<E, A> implements PromiseLike<A> {
+        static succeed<A>(a: A): EPromise<never, A> {
+            return new EPromise(Promise.resolve(mkRight(a)));
+        }
+    
+        static fail<E>(e: E): EPromise<E, never> {
+            return new EPromise(Promise.resolve(mkLeft(e)));
+        }
+    
+        constructor(readonly p: PromiseLike<Either<E, A>>) { }
+    
+        then<B = A, B1 = never>(
+            // EPromise can act as a Thenable only when `E` is `never`.
+            this: EPromise<never, A>,
+            onfulfilled?: ((value: A) => B | PromiseLike<B>) | null | undefined,
+            onrejected?: ((reason: any) => B1 | PromiseLike<B1>) | null | undefined
+        ): PromiseLike<B | B1> {
+            return this.p.then(
+                // Casting to `Right<A>` is safe here because we've eliminated the possibility of `Left<E>`.
+                either => onfulfilled?.((either as Right<A>).a) ?? (either as Right<A>).a as unknown as B,
+                onrejected
+            )
+        }
+    }
+    
+    const withTypedFailure: EPromise<number, string> = EPromise.fail(1);
+    
+    // Errors as expected:
+    //
+    // "The 'this' context of type 'EPromise<number, string>' is not assignable to method's
+    //     'this' of type 'EPromise<never, string>"
+    withTypedFailure.then(s => s.toUpperCase()).then(console.log);
+    ~~~~~~~~~~~~~~~~
+!!! error TS2684: The 'this' context of type 'EPromise<number, string>' is not assignable to method's 'this' of type 'EPromise<never, string>'.
+!!! 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<number, string>' is not assignable to method's 'this' of type 'EPromise<never, string>'.
+    }
\ 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<E, A> = Left<E> | Right<A>;
+type Left<E> = { tag: 'Left', e: E };
+type Right<A> = { tag: 'Right', a: A };
+
+const mkLeft = <E>(e: E): Either<E, never> => ({ tag: 'Left', e });
+const mkRight = <A>(a: A): Either<never, A> => ({ tag: 'Right', a });
+
+class EPromise<E, A> implements PromiseLike<A> {
+    static succeed<A>(a: A): EPromise<never, A> {
+        return new EPromise(Promise.resolve(mkRight(a)));
+    }
+
+    static fail<E>(e: E): EPromise<E, never> {
+        return new EPromise(Promise.resolve(mkLeft(e)));
+    }
+
+    constructor(readonly p: PromiseLike<Either<E, A>>) { }
+
+    then<B = A, B1 = never>(
+        // EPromise can act as a Thenable only when `E` is `never`.
+        this: EPromise<never, A>,
+        onfulfilled?: ((value: A) => B | PromiseLike<B>) | null | undefined,
+        onrejected?: ((reason: any) => B1 | PromiseLike<B1>) | null | undefined
+    ): PromiseLike<B | B1> {
+        return this.p.then(
+            // Casting to `Right<A>` is safe here because we've eliminated the possibility of `Left<E>`.
+            either => onfulfilled?.((either as Right<A>).a) ?? (either as Right<A>).a as unknown as B,
+            onrejected
+        )
+    }
+}
+
+const withTypedFailure: EPromise<number, string> = EPromise.fail(1);
+
+// Errors as expected:
+//
+// "The 'this' context of type 'EPromise<number, string>' is not assignable to method's
+//     'this' of type 'EPromise<never, string>"
+withTypedFailure.then(s => s.toUpperCase()).then(console.log);
+
+async function test() {
+    await withTypedFailure;
+}
\ No newline at end of file