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.
This commit is contained in:
Kristiyan Kostadinov
2026-03-04 10:32:43 +01:00
committed by Jessica Janiuk
parent e8676b5412
commit 2c87f21abe
3 changed files with 20 additions and 9 deletions
@@ -178,12 +178,18 @@ class TcbExprTranslator implements AstVisitor {
let literal = `{ ${properties.join(', ')} }`;
// If strictLiteralTypes is disabled, array literals are cast to `any`.
if (!this.config.strictLiteralTypes) {
literal = `(${literal} as any)`;
// If strictLiteralTypes is disabled, array literals are cast to `any`.
literal = `${literal} as any`;
}
return new TcbExpr(literal).addParseSpanInfo(ast.sourceSpan);
const expression = new TcbExpr(literal).addParseSpanInfo(ast.sourceSpan);
// Always parenthesize the literal, because for DOM bindings we may put it
// directly in the function body at which point TS might parse it as a block.
expression.wrapForTypeChecker();
return expression;
}
visitLiteralPrimitive(ast: LiteralPrimitive): TcbExpr {
@@ -40,7 +40,7 @@ describe('type check blocks diagnostics', () => {
// statement, which would wrap it into parenthesis that clutter the expected output.
const TEMPLATE = '{{ m({foo: a, bar: b}) }}';
expect(tcbWithSpans(TEMPLATE)).toContain(
'(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*/',
'((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*/)',
);
});
@@ -49,7 +49,7 @@ describe('type check blocks diagnostics', () => {
// statement, which would wrap it into parenthesis that clutter the expected output.
const TEMPLATE = '{{ m({a, b}) }}';
expect(tcbWithSpans(TEMPLATE)).toContain(
'((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*/)',
'((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*/)',
);
});
@@ -31,7 +31,7 @@ describe('type check blocks', () => {
it('should generate literal map expressions', () => {
const TEMPLATE = '{{ method({foo: a, bar: b}) }}';
expect(tcb(TEMPLATE)).toContain('(this).method({ "foo": ((this).a), "bar": ((this).b) })');
expect(tcb(TEMPLATE)).toContain('(this).method(({ "foo": ((this).a), "bar": ((this).b) }))');
});
it('should generate literal array expressions', () => {
@@ -39,6 +39,11 @@ describe('type check blocks', () => {
expect(tcb(TEMPLATE)).toContain('(this).method([((this).a), ((this).b)])');
});
it('should parenthesize literal maps bound to DOM properties', () => {
const TEMPLATE = '<div [class]="{foo: true, bar: false}"></div>';
expect(tcb(TEMPLATE)).toContain('if (true) { ({ "foo": true, "bar": false }); }');
});
it('should handle non-null assertions', () => {
const TEMPLATE = `{{a!}}`;
expect(tcb(TEMPLATE)).toContain('((((this).a))!)');
@@ -112,8 +117,8 @@ describe('type check blocks', () => {
});
it('should handle "in" expressions', () => {
expect(tcb(`{{'bar' in {bar: 'bar'} }}`)).toContain(`(("bar") in ({ "bar": "bar" }))`);
expect(tcb(`{{!('bar' in {bar: 'bar'}) }}`)).toContain(`!((("bar") in ({ "bar": "bar" })))`);
expect(tcb(`{{'bar' in {bar: 'bar'} }}`)).toContain(`(("bar") in (({ "bar": "bar" })))`);
expect(tcb(`{{!('bar' in {bar: 'bar'}) }}`)).toContain(`!((("bar") in (({ "bar": "bar" }))))`);
});
it('should handle "instanceof" expressions', () => {
@@ -1878,7 +1883,7 @@ describe('type check blocks', () => {
`;
expect(tcb(TEMPLATE)).toContain(
'new IntersectionObserver(null!, { "rootMargin": "123px" }); "" + ((this).main()); "" + ((this).placeholder());',
'new IntersectionObserver(null!, ({ "rootMargin": "123px" })); "" + ((this).main()); "" + ((this).placeholder());',
);
});
});