mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Andrew Kushnir
parent
0761e9aeb5
commit
ca55b3d454
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user