Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -306,15 +317,21 @@ 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),
ast.name,
);
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
Expand Down Expand Up @@ -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)[...]
Expand Down Expand Up @@ -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,
),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand Down Expand Up @@ -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);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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*/)',
);
});

Expand Down Expand Up @@ -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*/',
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });',
);
});

Expand Down Expand Up @@ -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 = {
Expand All @@ -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', () => {
Expand Down