From 56a360c7ff1f5ff2ed7108559fd56287e422459c Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Thu, 2 Feb 2017 21:36:09 -0800 Subject: [PATCH] Unify getIteratedTypeOf* functions --- src/compiler/checker.ts | 368 ++++++++++++++++------------------------ 1 file changed, 146 insertions(+), 222 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ba5b46d2f03..c756b081a88 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11232,9 +11232,7 @@ namespace ts { if (contextualReturnType) { return node.asteriskToken ? contextualReturnType - : functionFlags & FunctionFlags.Async - ? getIteratedTypeOfAsyncIterableIterator(contextualReturnType) // AsyncGenerator function - : getIteratedTypeOfIterableIterator(contextualReturnType); // Generator function + : getIteratedTypeOfGenerator(contextualReturnType, (functionFlags & FunctionFlags.Async) !== 0); } } @@ -14362,7 +14360,7 @@ namespace ts { if (yieldExpression.asteriskToken) { // A yield* expression effectively yields everything that its operand yields type = functionFlags & FunctionFlags.Async - ? checkIteratedTypeOfIterableOrAsyncIterable(type, yieldExpression.expression) // AsyncGenerator function + ? checkIteratedTypeOfAsyncIterable(type, yieldExpression.expression) // AsyncGenerator function : checkIteratedTypeOrElementType(type, yieldExpression.expression, /*allowStringInput*/ false); // Generator function } if (!contains(aggregatedTypes, type)) { @@ -15342,7 +15340,7 @@ namespace ts { const nodeIsYieldStar = !!node.asteriskToken; if (nodeIsYieldStar) { expressionElementType = functionFlags & FunctionFlags.Async - ? checkIteratedTypeOfIterableOrAsyncIterable(expressionType, node.expression) // AsyncGenerator function + ? checkIteratedTypeOfAsyncIterable(expressionType, node.expression) // AsyncGenerator function : checkIteratedTypeOrElementType(expressionType, node.expression, /*allowStringInput*/ false); // Generator function } @@ -15350,9 +15348,7 @@ namespace ts { // has no explicit return type because the return type is directly computed // from the yield expressions. if (func.type) { - const signatureElementType = functionFlags & FunctionFlags.Async - ? getIteratedTypeOfAsyncIterableIterator(getTypeFromTypeNode(func.type)) || anyType // AsyncGenerator function - : getIteratedTypeOfIterableIterator(getTypeFromTypeNode(func.type)) || anyType; // Generator function + const signatureElementType = getIteratedTypeOfGenerator(getTypeFromTypeNode(func.type), (functionFlags & FunctionFlags.Async) !== 0) || anyType; if (nodeIsYieldStar) { checkTypeAssignableTo(expressionElementType, signatureElementType, node.expression, /*headMessage*/ undefined); } @@ -15856,9 +15852,7 @@ namespace ts { error(node.type, Diagnostics.A_generator_cannot_have_a_void_type_annotation); } else { - const generatorElementType = functionFlags & FunctionFlags.Async - ? getIteratedTypeOfAsyncIterableIterator(returnType) || anyType // AsyncGenerator function - : getIteratedTypeOfIterableIterator(returnType) || anyType; // Generator function + const generatorElementType = getIteratedTypeOfGenerator(returnType, (functionFlags & FunctionFlags.Async) !== 0) || anyType; const iterableIteratorInstantiation = functionFlags & FunctionFlags.Async ? createAsyncIterableIteratorType(generatorElementType) // AsyncGenerator function : createIterableIteratorType(generatorElementType); // Generator function @@ -17962,7 +17956,7 @@ namespace ts { function checkRightHandSideOfForOf(rhsExpression: Expression, awaitModifier: AwaitKeywordToken | undefined): Type { const expressionType = checkNonNullExpression(rhsExpression); return awaitModifier - ? checkIteratedTypeOfIterableOrAsyncIterable(expressionType, rhsExpression) + ? checkIteratedTypeOfAsyncIterable(expressionType, rhsExpression) : checkIteratedTypeOrElementType(expressionType, rhsExpression, /*allowStringInput*/ true); } @@ -17974,6 +17968,14 @@ namespace ts { return getIteratedTypeOrElementType(inputType, errorNode, allowStringInput, /*checkAssignability*/ true) || anyType; } + function checkIteratedTypeOfAsyncIterable(inputType: Type, errorNode: Node): Type { + if (isTypeAny(inputType)) { + return inputType; + } + + return getIteratedTypeOfIterable(inputType, errorNode, /*isAsyncIterable*/ true, /*allowNonAsyncIterables*/ true, /*checkAssignability*/ true) || anyType; + } + /** * When consuming an iterable type in a for..of, spread, or iterator destructuring assignment * we want to get the iterated type of an iterable for ES2015 or later, or the iterated type @@ -17987,15 +17989,8 @@ namespace ts { // or higher, or when downlevelIteration is supplied. if (uplevelIteration || downlevelIteration) { // We only report errors for an invalid iterable type in ES2015 or higher. - const iteratedType = getIteratedTypeOfIterable(inputType, uplevelIteration ? errorNode : undefined); + const iteratedType = getIteratedTypeOfIterable(inputType, uplevelIteration ? errorNode : undefined, /*isAsyncIterable*/ false, /*allowNonAsyncIterables*/ false, checkAssignability); if (iteratedType || uplevelIteration) { - // Normally we would perform this check in `checkIteratedTypeOrElementType`, - // but we need to perform it here as `checkIteratedTypeOrElementType` won't - // have enough context to know whether the input type actually conforms - // to `Iterable`. - if (checkAssignability && errorNode && iteratedType) { - checkTypeAssignableTo(inputType, createIterableType(iteratedType), errorNode); - } return iteratedType; } } @@ -18020,6 +18015,7 @@ namespace ts { else if (arrayType.flags & TypeFlags.StringLike) { arrayType = neverType; } + hasStringConstituent = arrayType !== inputType; if (hasStringConstituent) { if (languageVersion < ScriptTarget.ES5) { @@ -18065,37 +18061,27 @@ namespace ts { return getUnionType([arrayElementType, stringType], /*subtypeReduction*/ true); } + return arrayElementType; } - function checkIteratedTypeOfIterableOrAsyncIterable(asyncIterable: Type, errorNode: Node): Type { - // A yield* in an async generator function will first attempt to create an async - // iterator by calling `[Symbol.asyncIterator]()`. If the operand does not have a callable - // `Symbol.asyncIterator` method, it then tries to call `[Symbol.iterator]()` and wrap the - // result in an async iterator. - const elementType = getIteratedTypeOfAsyncIterable(asyncIterable, errorNode, /*allowIterables*/ true); - // Now even though we have extracted the iteratedType, we will have to validate that the type - // passed in is actually an Iterable. - if (errorNode && elementType) { - const asyncIterableType = createAsyncIterableType(elementType); - const iterableType = createIterableType(elementType); - checkTypeAssignableTo(asyncIterable, getUnionType([asyncIterableType, iterableType], /*subtypeReduction*/ true), errorNode); - } - - return elementType || anyType; - } - - /** - * We want to treat type as an iterable, and get the type it is an async iterable of. The - * async iterable must have the following structure (annotated with the names of the - * variables below): + * We want to treat type as an iterable, and get the type it is an iterable of. The iterable + * must have the following structure (annotated with the names of the variables below): * - * { // asyncIterable - * [Symbol.asyncIterator]: { // asyncIteratorMethod - * (): AsyncIterator + * { // iterable + * [Symbol.iterator]: { // iteratorMethod + * (): Iterator + * } + * } + * + * For an async iterable, we expect the following structure: + * + * { // iterable + * [Symbol.asyncIterator]: { // iteratorMethod + * (): AsyncIterator + * } * } - * } * * T is the type we are after. At every level that involves analyzing return types * of signatures, we union the return types of all the signatures. @@ -18107,84 +18093,134 @@ namespace ts { * caller requested it. Then the caller can decide what to do in the case where there is no iterated * type. This is different from returning anyType, because that would signify that we have matched the * whole pattern and that T (above) is 'any'. + * + * For a **for-of** statement, `yield*` (in a normal generator), spread, array + * destructuring, or normal generator we will only ever look for a `[Symbol.iterator]()` + * method. + * + * For an async generator we will only ever look at the `[Symbol.asyncIterator]()` method. + * + * For a **for-await-of** statement or a `yield*` in an async generator we will look for + * the `[Symbol.asyncIterator]()` method first, and then the `[Symbol.iterator]()` method. */ - function getIteratedTypeOfAsyncIterable(type: Type, errorNode: Node, allowIterables: boolean): Type { + function getIteratedTypeOfIterable(type: Type, errorNode: Node | undefined, isAsyncIterable: boolean, allowNonAsyncIterables: boolean, checkAssignability: boolean): Type | undefined { if (isTypeAny(type)) { return undefined; } - const typeAsAsyncIterable = type; - if (typeAsAsyncIterable.iteratedTypeOfAsyncIterable) { - return typeAsAsyncIterable.iteratedTypeOfAsyncIterable; + const typeAsIterable = type; + if (isAsyncIterable ? typeAsIterable.iteratedTypeOfAsyncIterable : typeAsIterable.iteratedTypeOfIterable) { + return isAsyncIterable ? typeAsIterable.iteratedTypeOfAsyncIterable : typeAsIterable.iteratedTypeOfIterable; } - // As an optimization, if the type is instantiated directly using the - // globalAsyncIterableType (AsyncIterable) or globalAsyncIterableIteratorType - // (AsyncIterableIterator), then just grab its type argument. - if (isReferenceToType(type, getGlobalAsyncIterableType(/*reportErrors*/ false)) || - isReferenceToType(type, getGlobalAsyncIterableIteratorType(/*reportErrors*/ false))) { - return typeAsAsyncIterable.iteratedTypeOfAsyncIterable = (type).typeArguments[0]; + if (isAsyncIterable) { + // As an optimization, if the type is an instantiation of the global `AsyncIterable` + // or the global `AsyncIterableIterator` then just grab its type argument. + if (isReferenceToType(type, getGlobalAsyncIterableType(/*reportErrors*/ false)) || + isReferenceToType(type, getGlobalAsyncIterableIteratorType(/*reportErrors*/ false))) { + return typeAsIterable.iteratedTypeOfAsyncIterable = (type).typeArguments[0]; + } } - if (allowIterables) { + if (!isAsyncIterable || allowNonAsyncIterables) { + // As an optimization, if the type is an instantiation of the global `Iterable` or + // `IterableIterator` then just grab its type argument. if (isReferenceToType(type, getGlobalIterableType(/*reportErrors*/ false)) || isReferenceToType(type, getGlobalIterableIteratorType(/*reportErrors*/ false))) { - return typeAsAsyncIterable.iteratedTypeOfAsyncIterable = (type).typeArguments[0]; + return isAsyncIterable + ? typeAsIterable.iteratedTypeOfAsyncIterable = (type).typeArguments[0] + : typeAsIterable.iteratedTypeOfIterable = (type).typeArguments[0]; } } - const asyncIteratorMethod = getTypeOfPropertyOfType(type, getPropertyNameForKnownSymbolName("asyncIterator")); - if (isTypeAny(asyncIteratorMethod)) { - return undefined; + let iteratorMethodSignatures: Signature[]; + let mayBeIterable = false; + if (isAsyncIterable) { + const iteratorMethod = getTypeOfPropertyOfType(type, getPropertyNameForKnownSymbolName("asyncIterator")); + if (isTypeAny(iteratorMethod)) { + return undefined; + } + iteratorMethodSignatures = iteratorMethod && getSignaturesOfType(iteratorMethod, SignatureKind.Call); } - const asyncIteratorMethodSignatures = asyncIteratorMethod ? getSignaturesOfType(asyncIteratorMethod, SignatureKind.Call) : emptyArray; - if (asyncIteratorMethodSignatures.length === 0) { - if (allowIterables) { - const iteratedType = getIteratedTypeOfIterable(type, /*errorNode*/ undefined); - if (iteratedType) { - return typeAsAsyncIterable.iteratedTypeOfAsyncIterable = iteratedType; - } + if (!isAsyncIterable || (allowNonAsyncIterables && !some(iteratorMethodSignatures))) { + const iteratorMethod = getTypeOfPropertyOfType(type, getPropertyNameForKnownSymbolName("iterator")); + if (isTypeAny(iteratorMethod)) { + return undefined; } - - if (errorNode) { - error(errorNode, Diagnostics.Type_must_have_a_Symbol_asyncIterator_method_that_returns_an_async_iterator); - } - return undefined; + iteratorMethodSignatures = iteratorMethod && getSignaturesOfType(iteratorMethod, SignatureKind.Call); + mayBeIterable = true; } - return typeAsAsyncIterable.iteratedTypeOfAsyncIterable = getIteratedTypeOfAsyncIterator(getUnionType(map(asyncIteratorMethodSignatures, getReturnTypeOfSignature), /*subtypeReduction*/ true), errorNode); + if (some(iteratorMethodSignatures)) { + const iteratorMethodReturnType = getUnionType(map(iteratorMethodSignatures, getReturnTypeOfSignature), /*subtypeReduction*/ true); + const iteratedType = getIteratedTypeOfIterator(iteratorMethodReturnType, errorNode, /*isAsyncIterator*/ false); + if (checkAssignability && errorNode && iteratedType) { + // If `checkAssignability` was specified, we were called from + // `checkIteratedTypeOrElementType`. As such, we need to validate that + // the type passed in is actually an Iterable. + checkTypeAssignableTo(type, mayBeIterable + ? createIterableType(iteratedType) + : createAsyncIterableType(iteratedType), errorNode); + } + return isAsyncIterable + ? typeAsIterable.iteratedTypeOfAsyncIterable = iteratedType + : typeAsIterable.iteratedTypeOfIterable = iteratedType; + } + + if (errorNode) { + error(errorNode, + isAsyncIterable + ? Diagnostics.Type_must_have_a_Symbol_asyncIterator_method_that_returns_an_async_iterator + : Diagnostics.Type_must_have_a_Symbol_iterator_method_that_returns_an_iterator); + } + + return undefined; } /** - * This function has very similar logic as getElementTypeOfAsyncIterable, except that it operates on - * AsyncIterators instead of AsyncIterables. Here is the structure: + * This function has very similar logic as getIteratedTypeOfIterable, except that it operates on + * Iterators instead of Iterables. Here is the structure: * - * { // asyncIterator + * { // iterator + * next: { // nextMethod + * (): { // nextResult + * value: T // nextValue + * } + * } + * } + * + * For an async iterator, we expect the following structure: + * + * { // iterator * next: { // nextMethod * (): PromiseLike<{ // nextResult * value: T // nextValue * }> * } * } - * */ - function getIteratedTypeOfAsyncIterator(type: Type, errorNode: Node): Type { + function getIteratedTypeOfIterator(type: Type, errorNode: Node | undefined, isAsyncIterator: boolean): Type | undefined { if (isTypeAny(type)) { return undefined; } - const typeAsAsyncIterator = type; - if (typeAsAsyncIterator.iteratedTypeOfAsyncIterator) { - return typeAsAsyncIterator.iteratedTypeOfAsyncIterator; + const typeAsIterator = type; + if (isAsyncIterator ? typeAsIterator.iteratedTypeOfAsyncIterator : typeAsIterator.iteratedTypeOfIterator) { + return isAsyncIterator ? typeAsIterator.iteratedTypeOfAsyncIterator : typeAsIterator.iteratedTypeOfIterator; } - // As an optimization, if the type is instantiated directly using the - // globalAsyncIteratorType (AsyncIterator), then just grab its type argument. - if (isReferenceToType(type, getGlobalAsyncIteratorType(/*reportErrors*/ false))) { - return typeAsAsyncIterator.iteratedTypeOfAsyncIterator = (type).typeArguments[0]; + // As an optimization, if the type is an instantiation of the global `Iterator` (for + // a non-async iterator) or the global `AsyncIterator` (for an async-iterator) then + // just grab its type argument. + const getIteratorType = isAsyncIterator ? getGlobalAsyncIteratorType : getGlobalIteratorType; + if (isReferenceToType(type, getIteratorType(/*reportErrors*/ false))) { + return isAsyncIterator + ? typeAsIterator.iteratedTypeOfAsyncIterator = (type).typeArguments[0] + : typeAsIterator.iteratedTypeOfIterator = (type).typeArguments[0]; } + // Both async and non-async iterators must have a `next` method. const nextMethod = getTypeOfPropertyOfType(type, "next"); if (isTypeAny(nextMethod)) { return undefined; @@ -18193,166 +18229,54 @@ namespace ts { const nextMethodSignatures = nextMethod ? getSignaturesOfType(nextMethod, SignatureKind.Call) : emptyArray; if (nextMethodSignatures.length === 0) { if (errorNode) { - error(errorNode, Diagnostics.An_async_iterator_must_have_a_next_method); + error(errorNode, isAsyncIterator + ? Diagnostics.An_async_iterator_must_have_a_next_method + : Diagnostics.An_iterator_must_have_a_next_method); } return undefined; } - const nextResult = getUnionType(map(nextMethodSignatures, getReturnTypeOfSignature), /*subtypeReduction*/ true); + let nextResult = getUnionType(map(nextMethodSignatures, getReturnTypeOfSignature), /*subtypeReduction*/ true); if (isTypeAny(nextResult)) { return undefined; } - const awaitedResult = getAwaitedTypeOfPromise(nextResult, errorNode); - if (isTypeAny(awaitedResult)) { - return undefined; + // For an async iterator, we must get the awaited type of the return type. + if (isAsyncIterator) { + nextResult = getAwaitedTypeOfPromise(nextResult, errorNode); + if (isTypeAny(nextResult)) { + return undefined; + } } - const nextValue = awaitedResult && getTypeOfPropertyOfType(awaitedResult, "value"); + const nextValue = nextResult && getTypeOfPropertyOfType(nextResult, "value"); if (!nextValue) { if (errorNode) { - error(errorNode, Diagnostics.The_type_returned_by_the_next_method_of_an_async_iterator_must_be_a_promise_for_a_type_with_a_value_property); + error(errorNode, isAsyncIterator + ? Diagnostics.The_type_returned_by_the_next_method_of_an_async_iterator_must_be_a_promise_for_a_type_with_a_value_property + : Diagnostics.The_type_returned_by_the_next_method_of_an_iterator_must_have_a_value_property); } return undefined; } - return typeAsAsyncIterator.iteratedTypeOfAsyncIterator = nextValue; - } - - function getIteratedTypeOfAsyncIterableIterator(type: Type): Type { - if (isTypeAny(type)) { - return undefined; - } - return getIteratedTypeOfAsyncIterable(type, /*errorNode*/ undefined, /*allowIterables*/ false) - || getIteratedTypeOfAsyncIterator(type, /*errorNode*/ undefined); + return isAsyncIterator + ? typeAsIterator.iteratedTypeOfAsyncIterator = nextValue + : typeAsIterator.iteratedTypeOfIterator = nextValue; } /** - * We want to treat type as an iterable, and get the type it is an iterable of. The iterable - * must have the following structure (annotated with the names of the variables below): - * - * { // iterable - * [Symbol.iterator]: { // iteratorMethod - * (): Iterator - * } - * } - * - * T is the type we are after. At every level that involves analyzing return types - * of signatures, we union the return types of all the signatures. - * - * Another thing to note is that at any step of this process, we could run into a dead end, - * meaning either the property is missing, or we run into the anyType. If either of these things - * happens, we return undefined to signal that we could not find the iterated type. If a property - * is missing, and the previous step did not result in 'any', then we also give an error if the - * caller requested it. Then the caller can decide what to do in the case where there is no iterated - * type. This is different from returning anyType, because that would signify that we have matched the - * whole pattern and that T (above) is 'any'. + * A generator may have a return type of `Iterator`, `Iterable`, or + * `IterableIterator`. An async generator may have a return type of `AsyncIterator`, + * `AsyncIterable`, or `AsyncIterableIterator`. This function can be used to extract + * the iterated type from this return type for contextual typing and verifying signatures. */ - function getIteratedTypeOfIterable(type: Type, errorNode: Node): Type { - if (isTypeAny(type)) { + function getIteratedTypeOfGenerator(returnType: Type, isAsyncGenerator: boolean): Type { + if (isTypeAny(returnType)) { return undefined; } - const typeAsIterable = type; - if (typeAsIterable.iteratedTypeOfIterable) { - return typeAsIterable.iteratedTypeOfIterable; - } - - // As an optimization, if the type is instantiated directly using the - // globalIterableType (Iterable) or globalIterableIteratorType (IterableIterator), - // then just grab its type argument. - if (isReferenceToType(type, getGlobalIterableType(/*reportErrors*/ false)) || - isReferenceToType(type, getGlobalIterableIteratorType(/*reportErrors*/ false))) { - return typeAsIterable.iteratedTypeOfIterable = (type).typeArguments[0]; - } - - const propertyName = getPropertyNameForKnownSymbolName("iterator"); - const iteratorMethod = getTypeOfPropertyOfType(type, propertyName); - if (isTypeAny(iteratorMethod)) { - return undefined; - } - - const iteratorMethodSignatures = iteratorMethod ? getSignaturesOfType(iteratorMethod, SignatureKind.Call) : emptyArray; - if (iteratorMethodSignatures.length === 0) { - if (errorNode) { - error(errorNode, Diagnostics.Type_must_have_a_Symbol_iterator_method_that_returns_an_iterator); - } - return undefined; - } - - return typeAsIterable.iteratedTypeOfIterable = getIteratedTypeOfIterator(getUnionType(map(iteratorMethodSignatures, getReturnTypeOfSignature), /*subtypeReduction*/ true), errorNode); - } - - /** - * This function has very similar logic as getIteratedTypeOfIterable, except that it operates on - * Iterators instead of Iterables. Here is the structure: - * - * { // iterator - * next: { // iteratorNextFunction - * (): { // iteratorNextResult - * value: T // iteratorNextValue - * } - * } - * } - * - */ - function getIteratedTypeOfIterator(type: Type, errorNode: Node): Type { - if (isTypeAny(type)) { - return undefined; - } - - const typeAsIterator = type; - if (typeAsIterator.iteratedTypeOfIterator) { - return typeAsIterator.iteratedTypeOfIterator; - } - - // As an optimization, if the type is instantiated directly using the globalIteratorType (Iterator), - // then just grab its type argument. - if (isReferenceToType(type, getGlobalIteratorType(/*reportErrors*/ false))) { - return typeAsIterator.iteratedTypeOfIterator = (type).typeArguments[0]; - } - - const iteratorNextFunction = getTypeOfPropertyOfType(type, "next"); - if (isTypeAny(iteratorNextFunction)) { - return undefined; - } - - const iteratorNextFunctionSignatures = iteratorNextFunction ? getSignaturesOfType(iteratorNextFunction, SignatureKind.Call) : emptyArray; - if (iteratorNextFunctionSignatures.length === 0) { - if (errorNode) { - error(errorNode, Diagnostics.An_iterator_must_have_a_next_method); - } - return undefined; - } - - const iteratorNextResult = getUnionType(map(iteratorNextFunctionSignatures, getReturnTypeOfSignature), /*subtypeReduction*/ true); - if (isTypeAny(iteratorNextResult)) { - return undefined; - } - - const iteratorNextValue = getTypeOfPropertyOfType(iteratorNextResult, "value"); - if (!iteratorNextValue) { - if (errorNode) { - error(errorNode, Diagnostics.The_type_returned_by_the_next_method_of_an_iterator_must_have_a_value_property); - } - return undefined; - } - - return typeAsIterator.iteratedTypeOfIterator = iteratorNextValue; - } - - /** - * A generator may have a return type of Iterator, Iterable, or IterableIterator. - * This function can be used to extract the iterated type from this return type for - * contextual typing and verifying signatures. - */ - function getIteratedTypeOfIterableIterator(type: Type): Type { - if (isTypeAny(type)) { - return undefined; - } - - return getIteratedTypeOfIterable(type, /*errorNode*/ undefined) || - getIteratedTypeOfIterator(type, /*errorNode*/ undefined); + return getIteratedTypeOfIterable(returnType, /*errorNode*/ undefined, isAsyncGenerator, /*allowNonAsyncIterables*/ false, /*checkAssignability*/ false) + || getIteratedTypeOfIterator(returnType, /*errorNode*/ undefined, isAsyncGenerator); } function checkBreakOrContinueStatement(node: BreakOrContinueStatement) {