diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 80b61edd3657a..73b209dcbb171 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -913,6 +913,7 @@ import { NoSubstitutionTemplateLiteral, not, noTruncationMaximumTruncationLength, + NullishCoalesceExpression, NumberLiteralType, NumericLiteral, objectAllocator, @@ -1333,7 +1334,6 @@ export const enum CheckMode { RestBindingElement = 1 << 5, // Checking a type that is going to be used to determine the type of a rest binding element // e.g. in `const { a, ...rest } = foo`, when checking the type of `foo` to determine the type of `rest`, // we need to preserve generic types instead of substituting them for constraints - TypeOnly = 1 << 6, // Called from getTypeOfExpression, diagnostics may be omitted } /** @internal */ @@ -39809,15 +39809,39 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return state; } - checkNullishCoalesceOperands(node); - - const operator = node.operatorToken.kind; - if (operator === SyntaxKind.EqualsToken && (node.left.kind === SyntaxKind.ObjectLiteralExpression || node.left.kind === SyntaxKind.ArrayLiteralExpression)) { - state.skip = true; - setLastResult(state, checkDestructuringAssignment(node.left, checkExpression(node.right, checkMode), checkMode, node.right.kind === SyntaxKind.ThisKeyword)); - return state; + switch (node.operatorToken.kind) { + case SyntaxKind.EqualsToken: + if (node.left.kind !== SyntaxKind.ObjectLiteralExpression && node.left.kind !== SyntaxKind.ArrayLiteralExpression) { + break; + } + state.skip = true; + setLastResult(state, checkDestructuringAssignment(node.left, checkExpression(node.right, checkMode), checkMode, node.right.kind === SyntaxKind.ThisKeyword)); + break; + case SyntaxKind.LessThanToken: + case SyntaxKind.GreaterThanToken: + case SyntaxKind.LessThanEqualsToken: + case SyntaxKind.GreaterThanEqualsToken: + case SyntaxKind.EqualsEqualsToken: + case SyntaxKind.ExclamationEqualsToken: + case SyntaxKind.EqualsEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsEqualsToken: + // During control flow analysis: + // - it is possible for operands to temporarily have narrower types, and those narrower + // types may cause the operands to not be comparable. We don't want such errors reported (see #46475). + // - it's possible to run into circularities when resolving types of both type operands, + // we defer checking those subexpressions as the resulting type of their binary expression parent is known ahead of time (it's boolean). + // This can happen, for example, when inferring signatures with arguments depending (even indirectly) on such binary expressions. + // Those boolean arguments wouldn't even have to be viable sources for type arguments being inferred. + // + // For those reasons, we defer obtaining the operand types here and checking the related errors. + checkNodeDeferred(node); + state.skip = true; + setLastResult(state, booleanType); + break; + case SyntaxKind.QuestionQuestionToken: + checkNullishCoalesceOperands(node as NullishCoalesceExpression); + break; } - return state; } @@ -39910,29 +39934,27 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } - function checkNullishCoalesceOperands(node: BinaryExpression) { + function checkNullishCoalesceOperands(node: NullishCoalesceExpression) { const { left, operatorToken, right } = node; - if (operatorToken.kind === SyntaxKind.QuestionQuestionToken) { - if (isBinaryExpression(left) && (left.operatorToken.kind === SyntaxKind.BarBarToken || left.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) { - grammarErrorOnNode(left, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(left.operatorToken.kind), tokenToString(operatorToken.kind)); - } - if (isBinaryExpression(right) && (right.operatorToken.kind === SyntaxKind.BarBarToken || right.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) { - grammarErrorOnNode(right, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(right.operatorToken.kind), tokenToString(operatorToken.kind)); - } + if (isBinaryExpression(left) && (left.operatorToken.kind === SyntaxKind.BarBarToken || left.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) { + grammarErrorOnNode(left, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(left.operatorToken.kind), tokenToString(operatorToken.kind)); + } + if (isBinaryExpression(right) && (right.operatorToken.kind === SyntaxKind.BarBarToken || right.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) { + grammarErrorOnNode(right, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(right.operatorToken.kind), tokenToString(operatorToken.kind)); + } - const leftTarget = skipOuterExpressions(left, OuterExpressionKinds.All); - const nullishSemantics = getSyntacticNullishnessSemantics(leftTarget); - if (nullishSemantics !== PredicateSemantics.Sometimes) { - if (node.parent.kind === SyntaxKind.BinaryExpression) { - error(leftTarget, Diagnostics.This_binary_expression_is_never_nullish_Are_you_missing_parentheses); + const leftTarget = skipOuterExpressions(left, OuterExpressionKinds.All); + const nullishSemantics = getSyntacticNullishnessSemantics(leftTarget); + if (nullishSemantics !== PredicateSemantics.Sometimes) { + if (node.parent.kind === SyntaxKind.BinaryExpression) { + error(leftTarget, Diagnostics.This_binary_expression_is_never_nullish_Are_you_missing_parentheses); + } + else { + if (nullishSemantics === PredicateSemantics.Always) { + error(leftTarget, Diagnostics.This_expression_is_always_nullish); } else { - if (nullishSemantics === PredicateSemantics.Always) { - error(leftTarget, Diagnostics.This_expression_is_always_nullish); - } - else { - error(leftTarget, Diagnostics.Right_operand_of_is_unreachable_because_the_left_operand_is_never_nullish); - } + error(leftTarget, Diagnostics.Right_operand_of_is_unreachable_because_the_left_operand_is_never_nullish); } } } @@ -40068,7 +40090,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { switch (operator) { case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: - reportOperatorError(); + reportBinaryLikeExpressionOperatorError(leftType, operatorToken, rightType, errorNode); break; case SyntaxKind.AsteriskAsteriskToken: case SyntaxKind.AsteriskAsteriskEqualsToken: @@ -40080,7 +40102,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } // Exactly one of leftType/rightType is assignable to bigint else { - reportOperatorError(bothAreBigIntLike); + reportBinaryLikeExpressionOperatorError(leftType, operatorToken, rightType, errorNode, bothAreBigIntLike); resultType = errorType; } if (leftOk && rightOk) { @@ -40142,7 +40164,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } // Symbols are not allowed at all in arithmetic expressions - if (resultType && !checkForDisallowedESSymbolOperand(operator)) { + if (resultType && !checkForDisallowedESSymbolOperand(left, operator, right, leftType, rightType)) { return resultType; } @@ -40152,10 +40174,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // might be missing an await without doing an exhaustive check that inserting // await(s) will actually be a completely valid binary expression. const closeEnoughKind = TypeFlags.NumberLike | TypeFlags.BigIntLike | TypeFlags.StringLike | TypeFlags.AnyOrUnknown; - reportOperatorError((left, right) => + reportBinaryLikeExpressionOperatorError(leftType, operatorToken, rightType, errorNode, (left, right) => isTypeAssignableToKind(left, closeEnoughKind) && - isTypeAssignableToKind(right, closeEnoughKind) - ); + isTypeAssignableToKind(right, closeEnoughKind)); return anyType; } @@ -40163,44 +40184,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { checkAssignmentOperator(resultType); } return resultType; - case SyntaxKind.LessThanToken: - case SyntaxKind.GreaterThanToken: - case SyntaxKind.LessThanEqualsToken: - case SyntaxKind.GreaterThanEqualsToken: - if (checkForDisallowedESSymbolOperand(operator)) { - leftType = getBaseTypeOfLiteralTypeForComparison(checkNonNullType(leftType, left)); - rightType = getBaseTypeOfLiteralTypeForComparison(checkNonNullType(rightType, right)); - reportOperatorErrorUnless((left, right) => { - if (isTypeAny(left) || isTypeAny(right)) { - return true; - } - const leftAssignableToNumber = isTypeAssignableTo(left, numberOrBigIntType); - const rightAssignableToNumber = isTypeAssignableTo(right, numberOrBigIntType); - return leftAssignableToNumber && rightAssignableToNumber || - !leftAssignableToNumber && !rightAssignableToNumber && areTypesComparable(left, right); - }); - } - return booleanType; - case SyntaxKind.EqualsEqualsToken: - case SyntaxKind.ExclamationEqualsToken: - case SyntaxKind.EqualsEqualsEqualsToken: - case SyntaxKind.ExclamationEqualsEqualsToken: - // We suppress errors in CheckMode.TypeOnly (meaning the invocation came from getTypeOfExpression). During - // control flow analysis it is possible for operands to temporarily have narrower types, and those narrower - // types may cause the operands to not be comparable. We don't want such errors reported (see #46475). - if (!(checkMode && checkMode & CheckMode.TypeOnly)) { - if ( - (isLiteralExpressionOfObject(left) || isLiteralExpressionOfObject(right)) && - // only report for === and !== in JS, not == or != - (!isInJSFile(left) || (operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken)) - ) { - const eqType = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.EqualsEqualsEqualsToken; - error(errorNode, Diagnostics.This_condition_will_always_return_0_since_JavaScript_compares_objects_by_reference_not_value, eqType ? "false" : "true"); - } - checkNaNEquality(errorNode, operator, left, right); - reportOperatorErrorUnless((left, right) => isTypeEqualityComparableTo(left, right) || isTypeEqualityComparableTo(right, left)); - } - return booleanType; case SyntaxKind.InstanceOfKeyword: return checkInstanceOfExpression(left, right, leftType, rightType, checkMode); case SyntaxKind.InKeyword: @@ -40303,20 +40286,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { (isAccessExpression(node.right) || isIdentifier(node.right) && node.right.escapedText === "eval"); } - // Return true if there was no error, false if there was an error. - function checkForDisallowedESSymbolOperand(operator: PunctuationSyntaxKind): boolean { - const offendingSymbolOperand = maybeTypeOfKindConsideringBaseConstraint(leftType, TypeFlags.ESSymbolLike) ? left : - maybeTypeOfKindConsideringBaseConstraint(rightType, TypeFlags.ESSymbolLike) ? right : - undefined; - - if (offendingSymbolOperand) { - error(offendingSymbolOperand, Diagnostics.The_0_operator_cannot_be_applied_to_type_symbol, tokenToString(operator)); - return false; - } - - return true; - } - function getSuggestedBooleanOperator(operator: SyntaxKind): PunctuationSyntaxKind | undefined { switch (operator) { case SyntaxKind.BarToken: @@ -40384,45 +40353,115 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return false; } } + } + + function checkBinaryExpressionDeferred(node: BinaryExpression) { + const { left, operatorToken, right } = node; + const operator = operatorToken.kind; + let leftType = checkExpression(node.left); + let rightType = checkExpression(node.right); + switch (operator) { + case SyntaxKind.InstanceOfKeyword: + resolveUntypedCall(node as InstanceofExpression); + break; + case SyntaxKind.LessThanToken: + case SyntaxKind.GreaterThanToken: + case SyntaxKind.LessThanEqualsToken: + case SyntaxKind.GreaterThanEqualsToken: { + if (checkForDisallowedESSymbolOperand(left, operator, right, leftType, rightType)) { + leftType = getBaseTypeOfLiteralTypeForComparison(checkNonNullType(leftType, left)); + rightType = getBaseTypeOfLiteralTypeForComparison(checkNonNullType(rightType, right)); + reportOperatorErrorUnless((left, right) => { + if (isTypeAny(left) || isTypeAny(right)) { + return true; + } + const leftAssignableToNumber = isTypeAssignableTo(left, numberOrBigIntType); + const rightAssignableToNumber = isTypeAssignableTo(right, numberOrBigIntType); + return leftAssignableToNumber && rightAssignableToNumber || + !leftAssignableToNumber && !rightAssignableToNumber && areTypesComparable(left, right); + }); + } + break; + } + case SyntaxKind.EqualsEqualsToken: + case SyntaxKind.ExclamationEqualsToken: + case SyntaxKind.EqualsEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsEqualsToken: { + if ( + (isLiteralExpressionOfObject(left) || isLiteralExpressionOfObject(right)) && + // only report for === and !== in JS, not == or != + (!isInJSFile(left) || (operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken)) + ) { + const eqType = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.EqualsEqualsEqualsToken; + error(node, Diagnostics.This_condition_will_always_return_0_since_JavaScript_compares_objects_by_reference_not_value, eqType ? "false" : "true"); + } + checkNaNEquality(node, operator, left, right); + reportOperatorErrorUnless((left, right) => isTypeEqualityComparableTo(left, right) || isTypeEqualityComparableTo(right, left)); + break; + } + default: + return Debug.fail(); + } /** * Returns true if an error is reported */ function reportOperatorErrorUnless(typesAreCompatible: (left: Type, right: Type) => boolean): boolean { if (!typesAreCompatible(leftType, rightType)) { - reportOperatorError(typesAreCompatible); + reportBinaryLikeExpressionOperatorError(leftType, operatorToken, rightType, node, typesAreCompatible); return true; } return false; } - function reportOperatorError(isRelated?: (left: Type, right: Type) => boolean) { - let wouldWorkWithAwait = false; - const errNode = errorNode || operatorToken; - if (isRelated) { - const awaitedLeftType = getAwaitedTypeNoAlias(leftType); - const awaitedRightType = getAwaitedTypeNoAlias(rightType); - wouldWorkWithAwait = !(awaitedLeftType === leftType && awaitedRightType === rightType) - && !!(awaitedLeftType && awaitedRightType) - && isRelated(awaitedLeftType, awaitedRightType); + function checkNaNEquality(errorNode: Node | undefined, operator: SyntaxKind, left: Expression, right: Expression) { + const isLeftNaN = isGlobalNaN(skipParentheses(left)); + const isRightNaN = isGlobalNaN(skipParentheses(right)); + if (isLeftNaN || isRightNaN) { + const err = error(errorNode, Diagnostics.This_condition_will_always_return_0, tokenToString(operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.EqualsEqualsToken ? SyntaxKind.FalseKeyword : SyntaxKind.TrueKeyword)); + if (isLeftNaN && isRightNaN) return; + const operatorString = operator === SyntaxKind.ExclamationEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken ? tokenToString(SyntaxKind.ExclamationToken) : ""; + const location = isLeftNaN ? right : left; + const expression = skipParentheses(location); + addRelatedInfo(err, createDiagnosticForNode(location, Diagnostics.Did_you_mean_0, `${operatorString}Number.isNaN(${isEntityNameExpression(expression) ? entityNameToString(expression) : "..."})`)); } + } - let effectiveLeft = leftType; - let effectiveRight = rightType; - if (!wouldWorkWithAwait && isRelated) { - [effectiveLeft, effectiveRight] = getBaseTypesIfUnrelated(leftType, rightType, isRelated); - } - const [leftStr, rightStr] = getTypeNamesForErrorDisplay(effectiveLeft, effectiveRight); - if (!tryGiveBetterPrimaryError(errNode, wouldWorkWithAwait, leftStr, rightStr)) { - errorAndMaybeSuggestAwait( - errNode, - wouldWorkWithAwait, - Diagnostics.Operator_0_cannot_be_applied_to_types_1_and_2, - tokenToString(operatorToken.kind), - leftStr, - rightStr, - ); + function isGlobalNaN(expr: Expression): boolean { + if (isIdentifier(expr) && expr.escapedText === "NaN") { + const globalNaNSymbol = getGlobalNaNSymbol(); + return !!globalNaNSymbol && globalNaNSymbol === getResolvedSymbol(expr); } + return false; + } + } + + function reportBinaryLikeExpressionOperatorError(leftType: Type, operatorToken: BinaryOperatorToken, rightType: Type, errorNode?: Node, isRelated?: (left: Type, right: Type) => boolean) { + let wouldWorkWithAwait = false; + const errNode = errorNode || operatorToken; + if (isRelated) { + const awaitedLeftType = getAwaitedTypeNoAlias(leftType); + const awaitedRightType = getAwaitedTypeNoAlias(rightType); + wouldWorkWithAwait = !(awaitedLeftType === leftType && awaitedRightType === rightType) + && !!(awaitedLeftType && awaitedRightType) + && isRelated(awaitedLeftType, awaitedRightType); + } + + let effectiveLeft = leftType; + let effectiveRight = rightType; + if (!wouldWorkWithAwait && isRelated) { + [effectiveLeft, effectiveRight] = getBaseTypesIfUnrelated(leftType, rightType, isRelated); + } + const [leftStr, rightStr] = getTypeNamesForErrorDisplay(effectiveLeft, effectiveRight); + if (!tryGiveBetterPrimaryError(errNode, wouldWorkWithAwait, leftStr, rightStr)) { + errorAndMaybeSuggestAwait( + errNode, + wouldWorkWithAwait, + Diagnostics.Operator_0_cannot_be_applied_to_types_1_and_2, + tokenToString(operatorToken.kind), + leftStr, + rightStr, + ); } function tryGiveBetterPrimaryError(errNode: Node, maybeMissingAwait: boolean, leftStr: string, rightStr: string) { @@ -40442,27 +40481,20 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return undefined; } } + } - function checkNaNEquality(errorNode: Node | undefined, operator: SyntaxKind, left: Expression, right: Expression) { - const isLeftNaN = isGlobalNaN(skipParentheses(left)); - const isRightNaN = isGlobalNaN(skipParentheses(right)); - if (isLeftNaN || isRightNaN) { - const err = error(errorNode, Diagnostics.This_condition_will_always_return_0, tokenToString(operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.EqualsEqualsToken ? SyntaxKind.FalseKeyword : SyntaxKind.TrueKeyword)); - if (isLeftNaN && isRightNaN) return; - const operatorString = operator === SyntaxKind.ExclamationEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken ? tokenToString(SyntaxKind.ExclamationToken) : ""; - const location = isLeftNaN ? right : left; - const expression = skipParentheses(location); - addRelatedInfo(err, createDiagnosticForNode(location, Diagnostics.Did_you_mean_0, `${operatorString}Number.isNaN(${isEntityNameExpression(expression) ? entityNameToString(expression) : "..."})`)); - } - } + // Return true if there was no error, false if there was an error. + function checkForDisallowedESSymbolOperand(left: Expression, operator: PunctuationSyntaxKind, right: Expression, leftType: Type, rightType: Type): boolean { + const offendingSymbolOperand = maybeTypeOfKindConsideringBaseConstraint(leftType, TypeFlags.ESSymbolLike) ? left : + maybeTypeOfKindConsideringBaseConstraint(rightType, TypeFlags.ESSymbolLike) ? right : + undefined; - function isGlobalNaN(expr: Expression): boolean { - if (isIdentifier(expr) && expr.escapedText === "NaN") { - const globalNaNSymbol = getGlobalNaNSymbol(); - return !!globalNaNSymbol && globalNaNSymbol === getResolvedSymbol(expr); - } + if (offendingSymbolOperand) { + error(offendingSymbolOperand, Diagnostics.The_0_operator_cannot_be_applied_to_type_symbol, tokenToString(operator)); return false; } + + return true; } function getBaseTypesIfUnrelated(leftType: Type, rightType: Type, isRelated: (left: Type, right: Type) => boolean): [Type, Type] { @@ -40985,7 +41017,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } const startInvocationCount = flowInvocationCount; - const type = checkExpression(node, CheckMode.TypeOnly); + const type = checkExpression(node); // If control flow analysis was required to determine the type, it is worth caching. if (flowInvocationCount !== startInvocationCount) { const cache = flowTypeCache || (flowTypeCache = []); @@ -48883,9 +48915,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { checkExpression((node as VoidExpression).expression); break; case SyntaxKind.BinaryExpression: - if (isInstanceOfExpression(node)) { - resolveUntypedCall(node); - } + checkBinaryExpressionDeferred(node as BinaryExpression); break; } currentNode = saveCurrentNode; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 49938f0fc8249..1e82416a18057 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2655,6 +2655,11 @@ export interface AssignmentExpression readonly operatorToken: TOperator; } +/** @internal */ +export interface NullishCoalesceExpression extends BinaryExpression { + readonly operatorToken: Token; +} + export interface ObjectDestructuringAssignment extends AssignmentExpression { readonly left: ObjectLiteralExpression; } diff --git a/tests/baselines/reference/controlFlowInferFromExpressionsReturningBoolean1.symbols b/tests/baselines/reference/controlFlowInferFromExpressionsReturningBoolean1.symbols new file mode 100644 index 0000000000000..2a6d2c4de3a55 --- /dev/null +++ b/tests/baselines/reference/controlFlowInferFromExpressionsReturningBoolean1.symbols @@ -0,0 +1,149 @@ +//// [tests/cases/conformance/controlFlow/controlFlowInferFromExpressionsReturningBoolean1.ts] //// + +=== controlFlowInferFromExpressionsReturningBoolean1.ts === +// https://github.com/microsoft/TypeScript/issues/60130 + +function assert(c: C): asserts c { +>assert : Symbol(assert, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 0, 0)) +>C : Symbol(C, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 2, 16)) +>c : Symbol(c, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 2, 19)) +>C : Symbol(C, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 2, 16)) +>c : Symbol(c, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 2, 19)) + + if (!c) { +>c : Symbol(c, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 2, 19)) + + throw new TypeError("Assertion failed"); +>TypeError : Symbol(TypeError, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + } +} + +export function test1(lines: string[]): string[] { +>test1 : Symbol(test1, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 6, 1)) +>lines : Symbol(lines, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 8, 22)) + + let func: { +>func : Symbol(func, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 9, 5)) + + name: string; +>name : Symbol(name, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 9, 13)) + + lines: string[]; +>lines : Symbol(lines, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 10, 17)) + + } | null = null; + + for (const line of lines) { +>line : Symbol(line, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 14, 12)) +>lines : Symbol(lines, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 8, 22)) + + if (Math.random() < 0.5) { +>Math.random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --)) +>Math : Symbol(Math, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --)) + + func = { +>func : Symbol(func, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 9, 5)) + + name: "qwer", +>name : Symbol(name, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 16, 14)) + + lines: [line], +>lines : Symbol(lines, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 17, 21)) +>line : Symbol(line, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 14, 12)) + + }; + } else { + assert(func); +>assert : Symbol(assert, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 0, 0)) +>func : Symbol(func, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 9, 5)) + + const { name, lines } = func; +>name : Symbol(name, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 22, 13)) +>lines : Symbol(lines, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 22, 19)) +>func : Symbol(func, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 9, 5)) + + lines.push(line); +>lines.push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --)) +>lines : Symbol(lines, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 22, 19)) +>push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --)) +>line : Symbol(line, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 14, 12)) + + assert(name !== "abc"); +>assert : Symbol(assert, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 0, 0)) +>name : Symbol(name, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 22, 13)) + } + } + if (func) return func.lines; +>func : Symbol(func, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 9, 5)) +>func.lines : Symbol(lines, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 10, 17)) +>func : Symbol(func, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 9, 5)) +>lines : Symbol(lines, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 10, 17)) + + return lines; +>lines : Symbol(lines, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 8, 22)) +} + +declare function inferStuff(arg: T, checkStuff?: boolean): T; +>inferStuff : Symbol(inferStuff, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 29, 1)) +>T : Symbol(T, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 31, 28)) +>arg : Symbol(arg, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 31, 31)) +>T : Symbol(T, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 31, 28)) +>checkStuff : Symbol(checkStuff, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 31, 38)) +>T : Symbol(T, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 31, 28)) + +export function test2(lines: string[]): string[] { +>test2 : Symbol(test2, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 31, 64)) +>lines : Symbol(lines, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 33, 22)) + + let func: { +>func : Symbol(func, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 34, 5)) + + name: string; +>name : Symbol(name, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 34, 13)) + + } | null = null; + + for (const _ of lines) { +>_ : Symbol(_, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 38, 12)) +>lines : Symbol(lines, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 33, 22)) + + if (Math.random() < 0.5) { +>Math.random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --)) +>Math : Symbol(Math, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --)) + + func = { +>func : Symbol(func, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 34, 5)) + + name: "qwer", +>name : Symbol(name, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 40, 14)) + + }; + } else { + if (func) { +>func : Symbol(func, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 34, 5)) + + const { name } = func; +>name : Symbol(name, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 45, 15)) +>func : Symbol(func, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 34, 5)) + + func = inferStuff( +>func : Symbol(func, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 34, 5)) +>inferStuff : Symbol(inferStuff, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 29, 1)) + { + name: "other", +>name : Symbol(name, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 47, 11)) + + }, + name === "abc", +>name : Symbol(name, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 45, 15)) + + ); + } + } + } + + return lines; +>lines : Symbol(lines, Decl(controlFlowInferFromExpressionsReturningBoolean1.ts, 33, 22)) +} diff --git a/tests/baselines/reference/controlFlowInferFromExpressionsReturningBoolean1.types b/tests/baselines/reference/controlFlowInferFromExpressionsReturningBoolean1.types new file mode 100644 index 0000000000000..5ca9799fae6fb --- /dev/null +++ b/tests/baselines/reference/controlFlowInferFromExpressionsReturningBoolean1.types @@ -0,0 +1,254 @@ +//// [tests/cases/conformance/controlFlow/controlFlowInferFromExpressionsReturningBoolean1.ts] //// + +=== controlFlowInferFromExpressionsReturningBoolean1.ts === +// https://github.com/microsoft/TypeScript/issues/60130 + +function assert(c: C): asserts c { +>assert : (c: C) => asserts c +> : ^ ^^ ^^ ^^^^^ +>c : C +> : ^ + + if (!c) { +>!c : boolean +> : ^^^^^^^ +>c : C +> : ^ + + throw new TypeError("Assertion failed"); +>new TypeError("Assertion failed") : TypeError +> : ^^^^^^^^^ +>TypeError : TypeErrorConstructor +> : ^^^^^^^^^^^^^^^^^^^^ +>"Assertion failed" : "Assertion failed" +> : ^^^^^^^^^^^^^^^^^^ + } +} + +export function test1(lines: string[]): string[] { +>test1 : (lines: string[]) => string[] +> : ^ ^^ ^^^^^ +>lines : string[] +> : ^^^^^^^^ + + let func: { +>func : { name: string; lines: string[]; } | null +> : ^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^ + + name: string; +>name : string +> : ^^^^^^ + + lines: string[]; +>lines : string[] +> : ^^^^^^^^ + + } | null = null; + + for (const line of lines) { +>line : string +> : ^^^^^^ +>lines : string[] +> : ^^^^^^^^ + + if (Math.random() < 0.5) { +>Math.random() < 0.5 : boolean +> : ^^^^^^^ +>Math.random() : number +> : ^^^^^^ +>Math.random : () => number +> : ^^^^^^ +>Math : Math +> : ^^^^ +>random : () => number +> : ^^^^^^ +>0.5 : 0.5 +> : ^^^ + + func = { +>func = { name: "qwer", lines: [line], } : { name: string; lines: string[]; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>func : { name: string; lines: string[]; } | null +> : ^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^ +>{ name: "qwer", lines: [line], } : { name: string; lines: string[]; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + name: "qwer", +>name : string +> : ^^^^^^ +>"qwer" : "qwer" +> : ^^^^^^ + + lines: [line], +>lines : string[] +> : ^^^^^^^^ +>[line] : string[] +> : ^^^^^^^^ +>line : string +> : ^^^^^^ + + }; + } else { + assert(func); +>assert(func) : void +> : ^^^^ +>assert : (c: C) => asserts c +> : ^ ^^ ^^ ^^^^^ +>func : { name: string; lines: string[]; } | null +> : ^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^ + + const { name, lines } = func; +>name : string +> : ^^^^^^ +>lines : string[] +> : ^^^^^^^^ +>func : { name: string; lines: string[]; } +> : ^^^^^^^^ ^^^^^^^^^ ^^^ + + lines.push(line); +>lines.push(line) : number +> : ^^^^^^ +>lines.push : (...items: string[]) => number +> : ^^^^ ^^^^^^^^^^^^^^^ +>lines : string[] +> : ^^^^^^^^ +>push : (...items: string[]) => number +> : ^^^^ ^^^^^^^^^^^^^^^ +>line : string +> : ^^^^^^ + + assert(name !== "abc"); +>assert(name !== "abc") : void +> : ^^^^ +>assert : (c: C) => asserts c +> : ^ ^^ ^^ ^^^^^ +>name !== "abc" : boolean +> : ^^^^^^^ +>name : string +> : ^^^^^^ +>"abc" : "abc" +> : ^^^^^ + } + } + if (func) return func.lines; +>func : { name: string; lines: string[]; } | null +> : ^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^ +>func.lines : string[] +> : ^^^^^^^^ +>func : { name: string; lines: string[]; } +> : ^^^^^^^^ ^^^^^^^^^ ^^^ +>lines : string[] +> : ^^^^^^^^ + + return lines; +>lines : string[] +> : ^^^^^^^^ +} + +declare function inferStuff(arg: T, checkStuff?: boolean): T; +>inferStuff : (arg: T, checkStuff?: boolean) => T +> : ^ ^^ ^^ ^^ ^^^ ^^^^^ +>arg : T +> : ^ +>checkStuff : boolean | undefined +> : ^^^^^^^^^^^^^^^^^^^ + +export function test2(lines: string[]): string[] { +>test2 : (lines: string[]) => string[] +> : ^ ^^ ^^^^^ +>lines : string[] +> : ^^^^^^^^ + + let func: { +>func : { name: string; } | null +> : ^^^^^^^^ ^^^^^^^^^^ + + name: string; +>name : string +> : ^^^^^^ + + } | null = null; + + for (const _ of lines) { +>_ : string +> : ^^^^^^ +>lines : string[] +> : ^^^^^^^^ + + if (Math.random() < 0.5) { +>Math.random() < 0.5 : boolean +> : ^^^^^^^ +>Math.random() : number +> : ^^^^^^ +>Math.random : () => number +> : ^^^^^^ +>Math : Math +> : ^^^^ +>random : () => number +> : ^^^^^^ +>0.5 : 0.5 +> : ^^^ + + func = { +>func = { name: "qwer", } : { name: string; } +> : ^^^^^^^^^^^^^^^^^ +>func : { name: string; } | null +> : ^^^^^^^^ ^^^^^^^^^^ +>{ name: "qwer", } : { name: string; } +> : ^^^^^^^^^^^^^^^^^ + + name: "qwer", +>name : string +> : ^^^^^^ +>"qwer" : "qwer" +> : ^^^^^^ + + }; + } else { + if (func) { +>func : { name: string; } | null +> : ^^^^^^^^ ^^^^^^^^^^ + + const { name } = func; +>name : string +> : ^^^^^^ +>func : { name: string; } +> : ^^^^^^^^ ^^^ + + func = inferStuff( +>func = inferStuff( { name: "other", }, name === "abc", ) : { name: string; } +> : ^^^^^^^^^^^^^^^^^ +>func : { name: string; } | null +> : ^^^^^^^^ ^^^^^^^^^^ +>inferStuff( { name: "other", }, name === "abc", ) : { name: string; } +> : ^^^^^^^^^^^^^^^^^ +>inferStuff : (arg: T, checkStuff?: boolean) => T +> : ^ ^^ ^^ ^^ ^^^ ^^^^^ + { +>{ name: "other", } : { name: string; } +> : ^^^^^^^^^^^^^^^^^ + + name: "other", +>name : string +> : ^^^^^^ +>"other" : "other" +> : ^^^^^^^ + + }, + name === "abc", +>name === "abc" : boolean +> : ^^^^^^^ +>name : string +> : ^^^^^^ +>"abc" : "abc" +> : ^^^^^ + + ); + } + } + } + + return lines; +>lines : string[] +> : ^^^^^^^^ +} diff --git a/tests/baselines/reference/narrowingInCaseClauseAfterCaseClauseWithReturn.types b/tests/baselines/reference/narrowingInCaseClauseAfterCaseClauseWithReturn.types index bee05ff1bf3e1..a6c103e872383 100644 --- a/tests/baselines/reference/narrowingInCaseClauseAfterCaseClauseWithReturn.types +++ b/tests/baselines/reference/narrowingInCaseClauseAfterCaseClauseWithReturn.types @@ -94,7 +94,7 @@ function test1(arg: string | undefined) { } function test2(arg: string | undefined) { ->test2 : (arg: string | undefined) => "Not A, B, C or D" | "D" | "A, B or C" +>test2 : (arg: string | undefined) => "Not A, B, C or D" | "A, B or C" | "D" > : ^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ >arg : string | undefined > : ^^^^^^^^^^^^^^^^^^ diff --git a/tests/cases/conformance/controlFlow/controlFlowInferFromExpressionsReturningBoolean1.ts b/tests/cases/conformance/controlFlow/controlFlowInferFromExpressionsReturningBoolean1.ts new file mode 100644 index 0000000000000..0791fd02952ce --- /dev/null +++ b/tests/cases/conformance/controlFlow/controlFlowInferFromExpressionsReturningBoolean1.ts @@ -0,0 +1,61 @@ +// @strict: true +// @noEmit: true + +// https://github.com/microsoft/TypeScript/issues/60130 + +function assert(c: C): asserts c { + if (!c) { + throw new TypeError("Assertion failed"); + } +} + +export function test1(lines: string[]): string[] { + let func: { + name: string; + lines: string[]; + } | null = null; + + for (const line of lines) { + if (Math.random() < 0.5) { + func = { + name: "qwer", + lines: [line], + }; + } else { + assert(func); + const { name, lines } = func; + lines.push(line); + assert(name !== "abc"); + } + } + if (func) return func.lines; + return lines; +} + +declare function inferStuff(arg: T, checkStuff?: boolean): T; + +export function test2(lines: string[]): string[] { + let func: { + name: string; + } | null = null; + + for (const _ of lines) { + if (Math.random() < 0.5) { + func = { + name: "qwer", + }; + } else { + if (func) { + const { name } = func; + func = inferStuff( + { + name: "other", + }, + name === "abc", + ); + } + } + } + + return lines; +} \ No newline at end of file