diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 85b63ad0d7a..734cae63881 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -7324,21 +7324,18 @@ module ts { let isAsync = isAsyncFunctionLike(func); let type: Type; if (func.body.kind !== SyntaxKind.Block) { + type = checkExpressionCached(func.body, contextualMapper); if (isAsync) { // From within an async function you can return either a non-promise value or a promise. Any // Promise/A+ compatible implementation will always assimilate any foreign promise, so the - // return type of the body is not the return type of the consise body, rather it - // is the awaited type of the consise body, which we will wrap in the native Promise type - // later in this function. - type = checkAwaitedExpressionCached(func.body, contextualMapper); - } - else { - type = checkExpressionCached(func.body, contextualMapper); + // return type of the body should be unwrapped to its awaited type, which we will wrap in + // the native Promise type later in this function. + type = getAwaitedType(type, func, Diagnostics.Return_expression_in_async_function_does_not_have_a_valid_callable_then_member); } } else { // Aggregate the types of expressions within all the return statements. - let types = checkAndAggregateReturnExpressionTypes(func.body, contextualMapper); + let types = checkAndAggregateReturnExpressionTypes(func.body, contextualMapper, isAsync); if (types.length === 0) { if (isAsync) { // For an async function, the return type will not be void, but rather a Promise for void. @@ -7393,12 +7390,13 @@ module ts { forEachReturnStatement(body, returnStatement => { let expr = returnStatement.expression; if (expr) { - let type: Type; + let type = checkExpressionCached(expr, contextualMapper); if (isAsync) { - type = checkAwaitedExpressionCached(expr, contextualMapper); - } - else { - type = checkExpressionCached(expr, contextualMapper); + // From within an async function you can return either a non-promise value or a promise. Any + // Promise/A+ compatible implementation will always assimilate any foreign promise, so the + // return type of the body should be unwrapped to its awaited type, which should be wrapped in + // the native Promise type by the caller. + type = getAwaitedType(type, body.parent, Diagnostics.Return_expression_in_async_function_does_not_have_a_valid_callable_then_member); } if (!contains(aggregatedTypes, type)) { @@ -7515,6 +7513,7 @@ module ts { let isAsync = isAsyncFunctionLike(node); let returnType = node.type && getTypeFromTypeNode(node.type); + let promisedType: Type; if (returnType && isAsync) { promisedType = checkAsyncFunctionReturnType(node, returnType); @@ -7532,17 +7531,19 @@ module ts { // From within an async function you can return either a non-promise value or a promise. Any // Promise/A+ compatible implementation will always assimilate any foreign promise, so we // should not be checking assignability of a promise to the return type. Instead, we need to - // check assignability of the awaited type of the concise body against the promised type of + // check assignability of the awaited type of the expression body against the promised type of // its return type annotation. let exprType = checkExpression(node.body); if (returnType) { if (isAsync) { - checkTypeAssignableTo(getAwaitedType(exprType, node.body), promisedType, node.body); + let awaitedType = getAwaitedType(exprType, node.body, Diagnostics.Expression_body_for_async_arrow_function_does_not_have_a_valid_callable_then_member); + checkTypeAssignableTo(awaitedType, promisedType, node.body); } else { checkTypeAssignableTo(exprType, returnType, node.body); } } + checkFunctionExpressionBodies(node.body); } } @@ -8108,15 +8109,6 @@ module ts { return result; } - function checkAwaitedExpressionCached(node: Expression, contextualMapper?: TypeMapper): Type { - let links = getNodeLinks(node); - if (!links.resolvedAwaitedType) { - links.resolvedAwaitedType = getAwaitedType(checkExpressionCached(node, contextualMapper)); - } - - return links.resolvedAwaitedType; - } - function checkExpressionCached(node: Expression, contextualMapper?: TypeMapper): Type { let links = getNodeLinks(node); if (!links.resolvedType) { @@ -8960,7 +8952,7 @@ module ts { } function checkNonThenableType(type: Type, location?: Node, message?: DiagnosticMessage) { - if (!allConstituentTypesHaveKind(type, TypeFlags.Any) && isTypeAssignableTo(type, getGlobalThenableType())) { + if (!(type.flags & TypeFlags.Any) && isTypeAssignableTo(type, getGlobalThenableType())) { if (location) { if (!message) { message = Diagnostics.Operand_for_await_does_not_have_a_valid_callable_then_member; @@ -8991,7 +8983,7 @@ module ts { // } // - if (allConstituentTypesHaveKind(promise, TypeFlags.Any)) { + if (promise.flags & TypeFlags.Any) { return undefined; } @@ -9005,7 +8997,7 @@ module ts { } let thenFunction = getTypeOfPropertyOfType(promise, "then"); - if (thenFunction && allConstituentTypesHaveKind(thenFunction, TypeFlags.Any)) { + if (thenFunction && (thenFunction.flags & TypeFlags.Any)) { return undefined; } @@ -9015,7 +9007,7 @@ module ts { } let onfulfilledParameterType = getUnionType(map(thenSignatures, getTypeOfFirstParameterOfSignature)); - if (allConstituentTypesHaveKind(onfulfilledParameterType, TypeFlags.Any)) { + if (onfulfilledParameterType.flags & TypeFlags.Any) { return undefined; } @@ -9032,6 +9024,8 @@ module ts { return getTypeAtPosition(signature, 0); } + let alreadySeenTypesForAwait: boolean[] = []; + /** * Gets the "awaited type" of a type. * @param type The type to await. @@ -9039,58 +9033,81 @@ module ts { * Promise-like type; otherwise, it is the type of the expression. This is used to reflect * The runtime behavior of the `await` keyword. */ - function getAwaitedType(type: Type, location?: Node): Type { - // `seen` keeps track of types we've tried to await to avoid cycles. - // This is to protect against a bad actor with a mutually recursive promised type: - // - // declare class PromiseA { - // then(onfulfilled: (value: PromiseB) => any, onrejected?); - // } - // declare class PromiseB { - // then(onfulfilled: (value: PromiseA) => any, onrejected?); - // } - // - let seen: boolean[]; + function getAwaitedType(type: Type, location?: Node, message?: DiagnosticMessage): Type { + // reset the set of visited types + alreadySeenTypesForAwait.length = 0; while (true) { let promisedType = getPromisedType(type); - if (!promisedType) { - // this type could not be unwrapped further. We need to check to - // ensure it is not a thenable - if (checkNonThenableType(type, location)) { - return type; + if (promisedType === undefined) { + // The type was not a PromiseLike, so it could not be unwrapped any further. + // As long as the type does not have a known callable "then" property, then it is + // safe to return the type; otherwise, an error will have been reported in + // the call to checkNonThenableType and we will return unknownType. + // + // An example of a non-promise "thenable" might be: + // + // await { then(): void {} } + // + // The "thenable" does not match the minimal definition for a PromiseLike. When + // a Promise/A+-compatible or ES6 promise tries to adopt this value, the promise + // will never settle. We treat this as an error to help flag an early indicator + // of a runtime problem. If the user wants to return this value from an async + // function, they would need to wrap it in some other value. If they want it to + // be treated as a promise, they can cast to . + if (checkNonThenableType(type, location, message)) { + break; } - return unknownType; + type = unknownType; + break; } - if (!seen) { - if (promisedType === type) { - // if we have a bad actor in the form of a promise whose promised type is the same - // promise, return the unknown type as we cannot guess the shape. - // if this were the actual case in the JavaScript, this Promise would never resolve. - if (location) { - error(location, Diagnostics.Operand_for_await_does_not_have_a_valid_callable_then_member); + // Keep track of the type we're about to unwrap to avoid bad recursive promise types. + // See the comments below for more information. + alreadySeenTypesForAwait[type.id] = true; + + if (alreadySeenTypesForAwait[promisedType.id]) { + // We have a bad actor in the form of a promise whose promised type is the same + // promise type, or a mutually recursive promise. Return the unknown type as we cannot guess + // the shape. If this were the actual case in the JavaScript, this Promise would never resolve. + // + // An example of a bad actor with a singly-recursive promise type might be: + // + // interface BadPromise { + // then(onfulfilled: (value: BadPromise) => any, onrejected: (error: any) => any): BadPromise; + // } + // + // The above interface will pass the PromiseLike check, and return a promised type of `BadPromise`. + // Since this is a self reference, we don't want to keep recursing ad infinitum. + // + // An example of a bad actor in the form of a mutually-recursive promise type might be: + // + // interface BadPromiseA { + // then(onfulfilled: (value: BadPromiseB) => any, onrejected: (error: any) => any): BadPromiseB; + // } + // + // interface BadPromiseB { + // then(onfulfilled: (value: BadPromiseA) => any, onrejected: (error: any) => any): BadPromiseA; + // } + // + if (location) { + if (!message) { + message = Diagnostics.Operand_for_await_does_not_have_a_valid_callable_then_member; } - return unknownType; + error(location, message); } - seen = []; - seen[type.id] = true; - } - else if (seen[promisedType.id]) { - // if we've already seen this type, this is a promise that - // would never resolve. As above, we return the unknown type. - if (location) { - error(location, Diagnostics.Operand_for_await_does_not_have_a_valid_callable_then_member); - } - - return unknownType; + type = unknownType; + break; } - seen[promisedType.id] = true; type = promisedType; } + + // Cleanup, reset the set of visited types + alreadySeenTypesForAwait.length = 0; + return type; } /** @@ -9109,20 +9126,48 @@ module ts { function checkAsyncFunctionReturnType(node: SignatureDeclaration, returnType: Type): Type { let globalPromiseConstructorLikeType = getGlobalPromiseConstructorLikeType(); if (globalPromiseConstructorLikeType !== emptyObjectType) { - // get the constructor type of the return type + // The return type of an async function will be the type of the instance. For this + // to be a type compatible with our async function emit, we must also check that + // the type of the declaration (e.g. the static side or "constructor" type of the + // promise) is a compatible `PromiseConstructorLike`. + // + // An example might be (from lib.es6.d.ts): + // + // interface Promise { ... } + // interface PromiseConstructor { + // new (...): Promise; + // } + // declare var Promise: PromiseConstructor; + // + // When an async function declares a return type annotation of `Promise`, we + // need to get the type of the `Promise` variable declaration above, which would + // be `PromiseConstructor`. + // + // The same case applies to a class: + // + // declare class Promise { + // constructor(...); + // then(...): Promise; + // } + // + // When we get the type of the `Promise` symbol here, we get the type of the static + // side of the `Promise` class, which would be `{ new (...): Promise }`. let declaredType = returnType.symbol ? getTypeOfSymbol(returnType.symbol) : emptyObjectType; if (isTypeAssignableTo(declaredType, globalPromiseConstructorLikeType)) { - let promisedType = getPromisedType(returnType); - if (promisedType) { - // unwrap the promised type - let promiseConstructor = getPromiseConstructor(node); - if (promiseConstructor) { - checkExpressionOrQualifiedName(promiseConstructor); - } - - emitAwaiter = true; - return getAwaitedType(promisedType, node); + // Ensure we will emit the `__awaiter` helper. + emitAwaiter = true; + + // When we emit the async function, we need to ensure we emit any imports that might + // otherwise have been elided if the return type were only ever referenced in a type + // position. As such, we get the entity name of the type reference from the return + // type and check it as an expression. + let promiseConstructor = getPromiseConstructor(node); + if (promiseConstructor) { + checkExpressionOrQualifiedName(promiseConstructor); } + + // Get and return the awaited type of the return type. + return getAwaitedType(returnType, node, Diagnostics.An_async_function_or_method_must_have_a_valid_awaitable_return_type); } } @@ -10060,11 +10105,15 @@ module ts { error(node.expression, Diagnostics.Return_type_of_constructor_signature_must_be_assignable_to_the_instance_type_of_the_class); } } - else if (func.type && !isAccessor(func.kind) && isAsyncFunctionLike(func)) { - checkTypeAssignableTo(getAwaitedType(exprType), getAwaitedType(returnType), node.expression); - } else if (func.type || isGetAccessorWithAnnotatatedSetAccessor(func)) { - checkTypeAssignableTo(exprType, returnType, node.expression); + if (isAsyncFunctionLike(func)) { + let promisedType = getPromisedType(returnType); + let awaitedType = getAwaitedType(exprType, node.expression, Diagnostics.Return_expression_in_async_function_does_not_have_a_valid_callable_then_member); + checkTypeAssignableTo(awaitedType, promisedType, node.expression); + } + else { + checkTypeAssignableTo(exprType, returnType, node.expression); + } } } } diff --git a/src/compiler/diagnosticInformationMap.generated.ts b/src/compiler/diagnosticInformationMap.generated.ts index 3f781a26809..6469bf8b1bb 100644 --- a/src/compiler/diagnosticInformationMap.generated.ts +++ b/src/compiler/diagnosticInformationMap.generated.ts @@ -51,6 +51,8 @@ module ts { Accessors_are_only_available_when_targeting_ECMAScript_5_and_higher: { code: 1056, category: DiagnosticCategory.Error, key: "Accessors are only available when targeting ECMAScript 5 and higher." }, An_async_function_or_method_must_have_a_valid_awaitable_return_type: { code: 1057, category: DiagnosticCategory.Error, key: "An async function or method must have a valid awaitable return type." }, Operand_for_await_does_not_have_a_valid_callable_then_member: { code: 1058, category: DiagnosticCategory.Error, key: "Operand for 'await' does not have a valid callable 'then' member." }, + Return_expression_in_async_function_does_not_have_a_valid_callable_then_member: { code: 1059, category: DiagnosticCategory.Error, key: "Return expression in async function does not have a valid callable 'then' member." }, + Expression_body_for_async_arrow_function_does_not_have_a_valid_callable_then_member: { code: 1060, category: DiagnosticCategory.Error, key: "Expression body for async arrow function does not have a valid callable 'then' member." }, Enum_member_must_have_initializer: { code: 1061, category: DiagnosticCategory.Error, key: "Enum member must have initializer." }, An_export_assignment_cannot_be_used_in_a_namespace: { code: 1063, category: DiagnosticCategory.Error, key: "An export assignment cannot be used in a namespace." }, Ambient_enum_elements_can_only_have_integer_literal_initializers: { code: 1066, category: DiagnosticCategory.Error, key: "Ambient enum elements can only have integer literal initializers." }, @@ -376,7 +378,7 @@ module ts { Cannot_find_namespace_0: { code: 2503, category: DiagnosticCategory.Error, key: "Cannot find namespace '{0}'." }, Duplicate_identifier_0_Compiler_uses_declaration_1_to_support_async_functions: { code: 2520, category: DiagnosticCategory.Error, key: "Duplicate identifier '{0}'. Compiler uses declaration '{1}' to support async functions." }, Expression_resolves_to_variable_declaration_0_that_compiler_uses_to_support_async_functions: { code: 2521, category: DiagnosticCategory.Error, key: "Expression resolves to variable declaration '{0}' that compiler uses to support async functions." }, - The_arguments_object_cannot_be_referenced_in_an_async_arrow_function_Consider_using_a_standard_async_function_expression: { code: 2522, category: DiagnosticCategory.Error, key: "The 'arguments' object cannot be referenced in an async arrow function Consider using a standard async function expression." }, + The_arguments_object_cannot_be_referenced_in_an_async_arrow_function_Consider_using_a_standard_async_function_expression: { code: 2522, category: DiagnosticCategory.Error, key: "The 'arguments' object cannot be referenced in an async arrow function. Consider using a standard async function expression." }, Import_declaration_0_is_using_private_name_1: { code: 4000, category: DiagnosticCategory.Error, key: "Import declaration '{0}' is using private name '{1}'." }, Type_parameter_0_of_exported_class_has_or_is_using_private_name_1: { code: 4002, category: DiagnosticCategory.Error, key: "Type parameter '{0}' of exported class has or is using private name '{1}'." }, Type_parameter_0_of_exported_interface_has_or_is_using_private_name_1: { code: 4004, category: DiagnosticCategory.Error, key: "Type parameter '{0}' of exported interface has or is using private name '{1}'." }, diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index df419f7ec8c..b1e8f565790 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -191,6 +191,14 @@ "category": "Error", "code": 1058 }, + "Return expression in async function does not have a valid callable 'then' member.": { + "category": "Error", + "code": 1059 + }, + "Expression body for async arrow function does not have a valid callable 'then' member.": { + "category": "Error", + "code": 1060 + }, "Enum member must have initializer.": { "category": "Error", "code": 1061 @@ -1494,7 +1502,7 @@ "category": "Error", "code": 2521 }, - "The 'arguments' object cannot be referenced in an async arrow function Consider using a standard async function expression.": { + "The 'arguments' object cannot be referenced in an async arrow function. Consider using a standard async function expression.": { "category": "Error", "code": 2522 }, diff --git a/tests/baselines/reference/asyncArrowFunctionCapturesArguments_es6.errors.txt b/tests/baselines/reference/asyncArrowFunctionCapturesArguments_es6.errors.txt index 6b80a53f529..dcaba564e23 100644 --- a/tests/baselines/reference/asyncArrowFunctionCapturesArguments_es6.errors.txt +++ b/tests/baselines/reference/asyncArrowFunctionCapturesArguments_es6.errors.txt @@ -1,4 +1,4 @@ -tests/cases/conformance/async/es6/asyncArrowFunction/asyncArrowFunctionCapturesArguments_es6.ts(4,52): error TS2522: The 'arguments' object cannot be referenced in an async arrow function Consider using a standard async function expression. +tests/cases/conformance/async/es6/asyncArrowFunction/asyncArrowFunctionCapturesArguments_es6.ts(4,52): error TS2522: The 'arguments' object cannot be referenced in an async arrow function. Consider using a standard async function expression. ==== tests/cases/conformance/async/es6/asyncArrowFunction/asyncArrowFunctionCapturesArguments_es6.ts (1 errors) ==== @@ -7,7 +7,7 @@ tests/cases/conformance/async/es6/asyncArrowFunction/asyncArrowFunctionCapturesA function other() {} var fn = async () => await other.apply(this, arguments); ~~~~~~~~~ -!!! error TS2522: The 'arguments' object cannot be referenced in an async arrow function Consider using a standard async function expression. +!!! error TS2522: The 'arguments' object cannot be referenced in an async arrow function. Consider using a standard async function expression. } } \ No newline at end of file