Skip to content

Commit 2c87f21

Browse files
crisbetothePunderWoman
authored andcommitted
fix(compiler-cli): always parenthesize object literals in TCB
This is a follow-up to #67381 which introduced a subtle bug where depending on the type checking configuration, we may put an object literal directly in the TCB body which the TS compiler ends up interpreting as a block. These changes resolve the issue by always wrapping the literal in parentheses.
1 parent e8676b5 commit 2c87f21

File tree

3 files changed

+20
-9
lines changed

3 files changed

+20
-9
lines changed

packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,18 @@ class TcbExprTranslator implements AstVisitor {
178178

179179
let literal = `{ ${properties.join(', ')} }`;
180180

181-
// If strictLiteralTypes is disabled, array literals are cast to `any`.
182181
if (!this.config.strictLiteralTypes) {
183-
literal = `(${literal} as any)`;
182+
// If strictLiteralTypes is disabled, array literals are cast to `any`.
183+
literal = `${literal} as any`;
184184
}
185185

186-
return new TcbExpr(literal).addParseSpanInfo(ast.sourceSpan);
186+
const expression = new TcbExpr(literal).addParseSpanInfo(ast.sourceSpan);
187+
188+
// Always parenthesize the literal, because for DOM bindings we may put it
189+
// directly in the function body at which point TS might parse it as a block.
190+
expression.wrapForTypeChecker();
191+
192+
return expression;
187193
}
188194

189195
visitLiteralPrimitive(ast: LiteralPrimitive): TcbExpr {

packages/compiler-cli/src/ngtsc/typecheck/test/span_comments_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('type check blocks diagnostics', () => {
4040
// statement, which would wrap it into parenthesis that clutter the expected output.
4141
const TEMPLATE = '{{ m({foo: a, bar: b}) }}';
4242
expect(tcbWithSpans(TEMPLATE)).toContain(
43-
'(this).m /*3,4*/({ "foo" /*6,9*/: ((this).a /*11,12*/) /*11,12*/, "bar" /*14,17*/: ((this).b /*19,20*/) /*19,20*/ } /*5,21*/) /*3,22*/',
43+
'((this).m /*3,4*/(({ "foo" /*6,9*/: ((this).a /*11,12*/) /*11,12*/, "bar" /*14,17*/: ((this).b /*19,20*/) /*19,20*/ } /*5,21*/)) /*3,22*/)',
4444
);
4545
});
4646

@@ -49,7 +49,7 @@ describe('type check blocks diagnostics', () => {
4949
// statement, which would wrap it into parenthesis that clutter the expected output.
5050
const TEMPLATE = '{{ m({a, b}) }}';
5151
expect(tcbWithSpans(TEMPLATE)).toContain(
52-
'((this).m /*3,4*/({ "a" /*6,7*/: ((this).a /*6,7*/) /*6,7*/, "b" /*9,10*/: ((this).b /*9,10*/) /*9,10*/ } /*5,11*/) /*3,12*/)',
52+
'((this).m /*3,4*/(({ "a" /*6,7*/: ((this).a /*6,7*/) /*6,7*/, "b" /*9,10*/: ((this).b /*9,10*/) /*9,10*/ } /*5,11*/)) /*3,12*/)',
5353
);
5454
});
5555

packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,19 @@ describe('type check blocks', () => {
3131

3232
it('should generate literal map expressions', () => {
3333
const TEMPLATE = '{{ method({foo: a, bar: b}) }}';
34-
expect(tcb(TEMPLATE)).toContain('(this).method({ "foo": ((this).a), "bar": ((this).b) })');
34+
expect(tcb(TEMPLATE)).toContain('(this).method(({ "foo": ((this).a), "bar": ((this).b) }))');
3535
});
3636

3737
it('should generate literal array expressions', () => {
3838
const TEMPLATE = '{{ method([a, b]) }}';
3939
expect(tcb(TEMPLATE)).toContain('(this).method([((this).a), ((this).b)])');
4040
});
4141

42+
it('should parenthesize literal maps bound to DOM properties', () => {
43+
const TEMPLATE = '<div [class]="{foo: true, bar: false}"></div>';
44+
expect(tcb(TEMPLATE)).toContain('if (true) { ({ "foo": true, "bar": false }); }');
45+
});
46+
4247
it('should handle non-null assertions', () => {
4348
const TEMPLATE = `{{a!}}`;
4449
expect(tcb(TEMPLATE)).toContain('((((this).a))!)');
@@ -112,8 +117,8 @@ describe('type check blocks', () => {
112117
});
113118

114119
it('should handle "in" expressions', () => {
115-
expect(tcb(`{{'bar' in {bar: 'bar'} }}`)).toContain(`(("bar") in ({ "bar": "bar" }))`);
116-
expect(tcb(`{{!('bar' in {bar: 'bar'}) }}`)).toContain(`!((("bar") in ({ "bar": "bar" })))`);
120+
expect(tcb(`{{'bar' in {bar: 'bar'} }}`)).toContain(`(("bar") in (({ "bar": "bar" })))`);
121+
expect(tcb(`{{!('bar' in {bar: 'bar'}) }}`)).toContain(`!((("bar") in (({ "bar": "bar" }))))`);
117122
});
118123

119124
it('should handle "instanceof" expressions', () => {
@@ -1878,7 +1883,7 @@ describe('type check blocks', () => {
18781883
`;
18791884

18801885
expect(tcb(TEMPLATE)).toContain(
1881-
'new IntersectionObserver(null!, { "rootMargin": "123px" }); "" + ((this).main()); "" + ((this).placeholder());',
1886+
'new IntersectionObserver(null!, ({ "rootMargin": "123px" })); "" + ((this).main()); "" + ((this).placeholder());',
18821887
);
18831888
});
18841889
});

0 commit comments

Comments
 (0)