fix(compiler-cli): generate valid TS 5.6 type checking code (#57303)

Currently in some scenarios the compiler generates code like `null as any ? foo : bar` which will be invalid with [an upcoming TypeScript change](https://devblogs.microsoft.com/typescript/announcing-typescript-5-6-beta/#disallowed-nullish-and-truthy-checks). These changes switch to generating `0 as any` which is exempt from the change.

**Note:** I'm not starting the work to fully get us on TS 5.6 until the 18.2 release comes out, but this change is necessary to unblock an internal team.

PR Close #57303
This commit is contained in:
Kristiyan Kostadinov
2024-08-08 13:11:37 +02:00
committed by Andrew Kushnir
parent 0761e9aeb5
commit ca55b3d454
4 changed files with 55 additions and 26 deletions
@@ -40,8 +40,19 @@ import {TypeCheckingConfig} from '../api';
import {addParseSpanInfo, wrapForDiagnostics, wrapForTypeChecker} from './diagnostics';
import {tsCastToAny, tsNumericExpression} from './ts_util';
export const NULL_AS_ANY = ts.factory.createAsExpression(
ts.factory.createNull(),
/**
* Expression that is cast to any. Currently represented as `0 as any`.
*
* Historically this expression was using `null as any`, but a newly-added check in TypeScript 5.6
* (https://devblogs.microsoft.com/typescript/announcing-typescript-5-6-beta/#disallowed-nullish-and-truthy-checks)
* started flagging it as always being nullish. Other options that were considered:
* - `NaN as any` or `Infinity as any` - not used, because they don't work if the `noLib` compiler
* option is enabled. Also they require more characters.
* - Some flavor of function call, like `isNan(0) as any` - requires even more characters than the
* NaN option and has the same issue with `noLib`.
*/
export const ANY_EXPRESSION = ts.factory.createAsExpression(
ts.factory.createNumericLiteral('0'),
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
const UNDEFINED = ts.factory.createIdentifier('undefined');
@@ -306,7 +317,7 @@ class AstTranslator implements AstVisitor {
if (this.config.strictSafeNavigationTypes) {
// Basically, the return here is either the type of the complete expression with a null-safe
// property read, or `undefined`. So a ternary is used to create an "or" type:
// "a?.b" becomes (null as any ? a!.b : undefined)
// "a?.b" becomes (0 as any ? a!.b : undefined)
// The type of this expression is (typeof a!.b) | undefined, which is exactly as desired.
const expr = ts.factory.createPropertyAccessExpression(
ts.factory.createNonNullExpression(receiver),
@@ -314,7 +325,13 @@ class AstTranslator implements AstVisitor {
);
addParseSpanInfo(expr, ast.nameSpan);
node = ts.factory.createParenthesizedExpression(
ts.factory.createConditionalExpression(NULL_AS_ANY, undefined, expr, undefined, UNDEFINED),
ts.factory.createConditionalExpression(
ANY_EXPRESSION,
undefined,
expr,
undefined,
UNDEFINED,
),
);
} else if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) {
// Emulate a View Engine bug where 'any' is inferred for the left-hand side of the safe
@@ -345,14 +362,20 @@ class AstTranslator implements AstVisitor {
// The form of safe property reads depends on whether strictness is in use.
if (this.config.strictSafeNavigationTypes) {
// "a?.[...]" becomes (null as any ? a![...] : undefined)
// "a?.[...]" becomes (0 as any ? a![...] : undefined)
const expr = ts.factory.createElementAccessExpression(
ts.factory.createNonNullExpression(receiver),
key,
);
addParseSpanInfo(expr, ast.sourceSpan);
node = ts.factory.createParenthesizedExpression(
ts.factory.createConditionalExpression(NULL_AS_ANY, undefined, expr, undefined, UNDEFINED),
ts.factory.createConditionalExpression(
ANY_EXPRESSION,
undefined,
expr,
undefined,
UNDEFINED,
),
);
} else if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) {
// "a?.[...]" becomes (a as any)[...]
@@ -420,14 +443,20 @@ class AstTranslator implements AstVisitor {
args: ts.Expression[],
): ts.Expression {
if (this.config.strictSafeNavigationTypes) {
// "a?.method(...)" becomes (null as any ? a!.method(...) : undefined)
// "a?.method(...)" becomes (0 as any ? a!.method(...) : undefined)
const call = ts.factory.createCallExpression(
ts.factory.createNonNullExpression(expr),
undefined,
args,
);
return ts.factory.createParenthesizedExpression(
ts.factory.createConditionalExpression(NULL_AS_ANY, undefined, call, undefined, UNDEFINED),
ts.factory.createConditionalExpression(
ANY_EXPRESSION,
undefined,
call,
undefined,
UNDEFINED,
),
);
}
@@ -69,7 +69,7 @@ import {
} from './diagnostics';
import {DomSchemaChecker} from './dom';
import {Environment} from './environment';
import {astToTypescript, NULL_AS_ANY} from './expression';
import {astToTypescript, ANY_EXPRESSION} from './expression';
import {OutOfBandDiagnosticRecorder} from './oob';
import {
tsCallMethod,
@@ -763,7 +763,7 @@ class TcbInvalidReferenceOp extends TcbOp {
override execute(): ts.Identifier {
const id = this.tcb.allocateId();
this.scope.addStatement(tsCreateVariable(id, NULL_AS_ANY));
this.scope.addStatement(tsCreateVariable(id, ANY_EXPRESSION));
return id;
}
}
@@ -2785,7 +2785,7 @@ class TcbExpressionTranslator {
this.tcb.oobRecorder.missingPipe(this.tcb.id, ast);
// Use an 'any' value to at least allow the rest of the expression to be checked.
pipe = NULL_AS_ANY;
pipe = ANY_EXPRESSION;
} else if (
pipeMeta.isExplicitlyDeferred &&
this.tcb.boundTarget.getEagerlyUsedPipes().includes(ast.name)
@@ -2795,7 +2795,7 @@ class TcbExpressionTranslator {
this.tcb.oobRecorder.deferredPipeUsedEagerly(this.tcb.id, ast);
// Use an 'any' value to at least allow the rest of the expression to be checked.
pipe = NULL_AS_ANY;
pipe = ANY_EXPRESSION;
} else {
// Use a variable declared as the pipe's type.
pipe = this.tcb.env.pipeInst(
@@ -2916,7 +2916,7 @@ function tcbCallTypeCtor(
} else {
// A type constructor is required to be called with all input properties, so any unset
// inputs are simply assigned a value of type `any` to ignore them.
return ts.factory.createPropertyAssignment(propertyName, NULL_AS_ANY);
return ts.factory.createPropertyAssignment(propertyName, ANY_EXPRESSION);
}
});
@@ -85,7 +85,7 @@ describe('type check blocks diagnostics', () => {
it('should annotate safe calls', () => {
const TEMPLATE = `{{ method?.(a, b) }}`;
expect(tcbWithSpans(TEMPLATE)).toContain(
'((null as any ? (((this).method /*3,9*/) /*3,9*/)!(((this).a /*12,13*/) /*12,13*/, ((this).b /*15,16*/) /*15,16*/) : undefined) /*3,17*/)',
'((0 as any ? (((this).method /*3,9*/) /*3,9*/)!(((this).a /*12,13*/) /*12,13*/, ((this).b /*15,16*/) /*15,16*/) : undefined) /*3,17*/)',
);
});
@@ -141,21 +141,21 @@ describe('type check blocks diagnostics', () => {
it('should annotate safe property access', () => {
const TEMPLATE = `{{ a?.b }}`;
expect(tcbWithSpans(TEMPLATE)).toContain(
'(null as any ? (((this).a /*3,4*/) /*3,4*/)!.b /*6,7*/ : undefined) /*3,7*/',
'(0 as any ? (((this).a /*3,4*/) /*3,4*/)!.b /*6,7*/ : undefined) /*3,7*/',
);
});
it('should annotate safe method calls', () => {
const TEMPLATE = `{{ a?.method(b) }}`;
expect(tcbWithSpans(TEMPLATE)).toContain(
'((null as any ? (null as any ? (((this).a /*3,4*/) /*3,4*/)!.method /*6,12*/ : undefined) /*3,12*/!(((this).b /*13,14*/) /*13,14*/) : undefined) /*3,15*/)',
'((0 as any ? (0 as any ? (((this).a /*3,4*/) /*3,4*/)!.method /*6,12*/ : undefined) /*3,12*/!(((this).b /*13,14*/) /*13,14*/) : undefined) /*3,15*/)',
);
});
it('should annotate safe keyed reads', () => {
const TEMPLATE = `{{ a?.[0] }}`;
expect(tcbWithSpans(TEMPLATE)).toContain(
'(null as any ? (((this).a /*3,4*/) /*3,4*/)![0 /*7,8*/] /*3,9*/ : undefined) /*3,9*/',
'(0 as any ? (((this).a /*3,4*/) /*3,4*/)![0 /*7,8*/] /*3,9*/ : undefined) /*3,9*/',
);
});
@@ -157,7 +157,7 @@ describe('type check blocks', () => {
'const _ctor1: <T extends string = any>(init: Pick<i0.Dir<T>, "fieldA" | "fieldB">) => i0.Dir<T> = null!;',
);
expect(actual).toContain(
'var _t1 = _ctor1({ "fieldA": (((this).foo)), "fieldB": null as any });',
'var _t1 = _ctor1({ "fieldA": (((this).foo)), "fieldB": 0 as any });',
);
});
@@ -1235,11 +1235,11 @@ describe('type check blocks', () => {
it('should use undefined for safe navigation operations when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain(
'(null as any ? (null as any ? (((this).a))!.method : undefined)!() : undefined)',
'(0 as any ? (0 as any ? (((this).a))!.method : undefined)!() : undefined)',
);
expect(block).toContain('(null as any ? (((this).a))!.b : undefined)');
expect(block).toContain('(null as any ? (((this).a))![0] : undefined)');
expect(block).toContain('(null as any ? (((((this).a)).optionalMethod))!() : undefined)');
expect(block).toContain('(0 as any ? (((this).a))!.b : undefined)');
expect(block).toContain('(0 as any ? (((this).a))![0] : undefined)');
expect(block).toContain('(0 as any ? (((((this).a)).optionalMethod))!() : undefined)');
});
it("should use an 'any' type for safe navigation operations when disabled", () => {
const DISABLED_CONFIG: TypeCheckingConfig = {
@@ -1258,13 +1258,13 @@ describe('type check blocks', () => {
const TEMPLATE = `{{a.method()?.b}} {{a()?.method()}} {{a.method()?.[0]}} {{a.method()?.otherMethod?.()}}`;
it('should check the presence of a property/method on the receiver when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('(null as any ? ((((this).a)).method())!.b : undefined)');
expect(block).toContain('(0 as any ? ((((this).a)).method())!.b : undefined)');
expect(block).toContain(
'(null as any ? (null as any ? ((this).a())!.method : undefined)!() : undefined)',
'(0 as any ? (0 as any ? ((this).a())!.method : undefined)!() : undefined)',
);
expect(block).toContain('(null as any ? ((((this).a)).method())![0] : undefined)');
expect(block).toContain('(0 as any ? ((((this).a)).method())![0] : undefined)');
expect(block).toContain(
'(null as any ? ((null as any ? ((((this).a)).method())!.otherMethod : undefined))!() : undefined)',
'(0 as any ? ((0 as any ? ((((this).a)).method())!.otherMethod : undefined))!() : undefined)',
);
});
it('should not check the presence of a property/method on the receiver when disabled', () => {