From d63bc8d4e1c8a06c0baf1a10bc12b826d67e0907 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Wed, 24 May 2023 14:36:24 -0700 Subject: [PATCH 1/3] Suppress error during getTypeOfExpression --- src/compiler/checker.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 597a506f264ae..fcf3f4a45e4fa 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1258,6 +1258,7 @@ export const enum CheckMode { RestBindingElement = 1 << 6, // 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 << 7, // Called from getTypeOfExpression, diagnostics may be omitted } /** @internal */ @@ -36753,7 +36754,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const rightType = getLastResult(state); Debug.assertIsDefined(rightType); - result = checkBinaryLikeExpressionWorker(node.left, node.operatorToken, node.right, leftType, rightType, node); + result = checkBinaryLikeExpressionWorker(node.left, node.operatorToken, node.right, leftType, rightType, state.checkMode, node); } state.skip = false; @@ -36824,7 +36825,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } const rightType = checkExpression(right, checkMode); - return checkBinaryLikeExpressionWorker(left, operatorToken, right, leftType, rightType, errorNode); + return checkBinaryLikeExpressionWorker(left, operatorToken, right, leftType, rightType, checkMode, errorNode); } function checkBinaryLikeExpressionWorker( @@ -36833,6 +36834,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { right: Expression, leftType: Type, rightType: Type, + checkMode?: CheckMode, errorNode?: Node ): Type { const operator = operatorToken.kind; @@ -36986,14 +36988,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { case SyntaxKind.ExclamationEqualsToken: case SyntaxKind.EqualsEqualsEqualsToken: case SyntaxKind.ExclamationEqualsEqualsToken: - if (isLiteralExpressionOfObject(left) || isLiteralExpressionOfObject(right)) { - 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"); + if (!(checkMode && checkMode & CheckMode.TypeOnly)) { + if (isLiteralExpressionOfObject(left) || isLiteralExpressionOfObject(right)) { + 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)); } - 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); case SyntaxKind.InKeyword: @@ -37348,7 +37351,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function checkConditionalExpression(node: ConditionalExpression, checkMode?: CheckMode): Type { - const type = checkTruthinessExpression(node.condition); + const type = checkTruthinessExpression(node.condition, checkMode); checkTestingKnownTruthyCallableOrAwaitableType(node.condition, type, node.whenTrue); const type1 = checkExpression(node.whenTrue, checkMode); const type2 = checkExpression(node.whenFalse, checkMode); @@ -37729,7 +37732,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } const startInvocationCount = flowInvocationCount; - const type = checkExpression(node); + const type = checkExpression(node, CheckMode.TypeOnly); // If control flow analysis was required to determine the type, it is worth caching. if (flowInvocationCount !== startInvocationCount) { const cache = flowTypeCache || (flowTypeCache = []); From 3343baec612f19b0d9bc76d33c97bb7822022911 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Wed, 24 May 2023 14:53:26 -0700 Subject: [PATCH 2/3] Add regression tests --- .../controlFlowNoIntermediateErrors.symbols | 43 ++++++++++ .../controlFlowNoIntermediateErrors.types | 80 +++++++++++++++++++ .../controlFlowNoIntermediateErrors.ts | 24 ++++++ 3 files changed, 147 insertions(+) create mode 100644 tests/baselines/reference/controlFlowNoIntermediateErrors.symbols create mode 100644 tests/baselines/reference/controlFlowNoIntermediateErrors.types create mode 100644 tests/cases/conformance/controlFlow/controlFlowNoIntermediateErrors.ts diff --git a/tests/baselines/reference/controlFlowNoIntermediateErrors.symbols b/tests/baselines/reference/controlFlowNoIntermediateErrors.symbols new file mode 100644 index 0000000000000..86d26f7884a4e --- /dev/null +++ b/tests/baselines/reference/controlFlowNoIntermediateErrors.symbols @@ -0,0 +1,43 @@ +=== tests/cases/conformance/controlFlow/controlFlowNoIntermediateErrors.ts === +// Repros from #46475 + +function f1() { +>f1 : Symbol(f1, Decl(controlFlowNoIntermediateErrors.ts, 0, 0)) + + let code: 0 | 1 | 2 = 0; +>code : Symbol(code, Decl(controlFlowNoIntermediateErrors.ts, 3, 7)) + + const otherCodes: (0 | 1 | 2)[] = [2, 0, 1, 0, 2, 2, 2, 0, 1, 0, 2, 1, 1, 0, 2, 1]; +>otherCodes : Symbol(otherCodes, Decl(controlFlowNoIntermediateErrors.ts, 4, 9)) + + for (const code2 of otherCodes) { +>code2 : Symbol(code2, Decl(controlFlowNoIntermediateErrors.ts, 5, 14)) +>otherCodes : Symbol(otherCodes, Decl(controlFlowNoIntermediateErrors.ts, 4, 9)) + + if (code2 === 0) { +>code2 : Symbol(code2, Decl(controlFlowNoIntermediateErrors.ts, 5, 14)) + + code = code === 2 ? 1 : 0; +>code : Symbol(code, Decl(controlFlowNoIntermediateErrors.ts, 3, 7)) +>code : Symbol(code, Decl(controlFlowNoIntermediateErrors.ts, 3, 7)) + } + else { + code = 2; +>code : Symbol(code, Decl(controlFlowNoIntermediateErrors.ts, 3, 7)) + } + } +} + +function f2() { +>f2 : Symbol(f2, Decl(controlFlowNoIntermediateErrors.ts, 13, 1)) + + let code: 0 | 1 = 0; +>code : Symbol(code, Decl(controlFlowNoIntermediateErrors.ts, 16, 7)) + + while (true) { + code = code === 1 ? 0 : 1; +>code : Symbol(code, Decl(controlFlowNoIntermediateErrors.ts, 16, 7)) +>code : Symbol(code, Decl(controlFlowNoIntermediateErrors.ts, 16, 7)) + } +} + diff --git a/tests/baselines/reference/controlFlowNoIntermediateErrors.types b/tests/baselines/reference/controlFlowNoIntermediateErrors.types new file mode 100644 index 0000000000000..2d6d9e0741861 --- /dev/null +++ b/tests/baselines/reference/controlFlowNoIntermediateErrors.types @@ -0,0 +1,80 @@ +=== tests/cases/conformance/controlFlow/controlFlowNoIntermediateErrors.ts === +// Repros from #46475 + +function f1() { +>f1 : () => void + + let code: 0 | 1 | 2 = 0; +>code : 0 | 1 | 2 +>0 : 0 + + const otherCodes: (0 | 1 | 2)[] = [2, 0, 1, 0, 2, 2, 2, 0, 1, 0, 2, 1, 1, 0, 2, 1]; +>otherCodes : (0 | 1 | 2)[] +>[2, 0, 1, 0, 2, 2, 2, 0, 1, 0, 2, 1, 1, 0, 2, 1] : (0 | 1 | 2)[] +>2 : 2 +>0 : 0 +>1 : 1 +>0 : 0 +>2 : 2 +>2 : 2 +>2 : 2 +>0 : 0 +>1 : 1 +>0 : 0 +>2 : 2 +>1 : 1 +>1 : 1 +>0 : 0 +>2 : 2 +>1 : 1 + + for (const code2 of otherCodes) { +>code2 : 0 | 1 | 2 +>otherCodes : (0 | 1 | 2)[] + + if (code2 === 0) { +>code2 === 0 : boolean +>code2 : 0 | 1 | 2 +>0 : 0 + + code = code === 2 ? 1 : 0; +>code = code === 2 ? 1 : 0 : 0 | 1 +>code : 0 | 1 | 2 +>code === 2 ? 1 : 0 : 0 | 1 +>code === 2 : boolean +>code : 0 | 1 | 2 +>2 : 2 +>1 : 1 +>0 : 0 + } + else { + code = 2; +>code = 2 : 2 +>code : 0 | 1 | 2 +>2 : 2 + } + } +} + +function f2() { +>f2 : () => void + + let code: 0 | 1 = 0; +>code : 0 | 1 +>0 : 0 + + while (true) { +>true : true + + code = code === 1 ? 0 : 1; +>code = code === 1 ? 0 : 1 : 0 | 1 +>code : 0 | 1 +>code === 1 ? 0 : 1 : 0 | 1 +>code === 1 : boolean +>code : 0 | 1 +>1 : 1 +>0 : 0 +>1 : 1 + } +} + diff --git a/tests/cases/conformance/controlFlow/controlFlowNoIntermediateErrors.ts b/tests/cases/conformance/controlFlow/controlFlowNoIntermediateErrors.ts new file mode 100644 index 0000000000000..670e2097090d4 --- /dev/null +++ b/tests/cases/conformance/controlFlow/controlFlowNoIntermediateErrors.ts @@ -0,0 +1,24 @@ +// @strict: true +// @noEmit: true + +// Repros from #46475 + +function f1() { + let code: 0 | 1 | 2 = 0; + const otherCodes: (0 | 1 | 2)[] = [2, 0, 1, 0, 2, 2, 2, 0, 1, 0, 2, 1, 1, 0, 2, 1]; + for (const code2 of otherCodes) { + if (code2 === 0) { + code = code === 2 ? 1 : 0; + } + else { + code = 2; + } + } +} + +function f2() { + let code: 0 | 1 = 0; + while (true) { + code = code === 1 ? 0 : 1; + } +} From d0b2c494df835cf8e7f8ab19140396b61697889d Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 26 May 2023 07:33:16 -0700 Subject: [PATCH 3/3] Add comment explaining why we suppress errors --- src/compiler/checker.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index fcf3f4a45e4fa..630c6f3accead 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -36988,6 +36988,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { 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)) { const eqType = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.EqualsEqualsEqualsToken;