Skip to content

Commit 43aab6a

Browse files
committed
fix(@angular/build): support parenthesized expressions in oxc linker
Unwrap parenthesized expressions in `OxcAstHost` to ensure parity with Babel and prevent fatal linker errors when parsing functions returning parenthesized expressions (such as `resolveMetadata: () => ({ ... })` in deferred component metadata) and other parenthesized declaration properties. Fixes #34129
1 parent 42eaa97 commit 43aab6a

3 files changed

Lines changed: 149 additions & 11 deletions

File tree

packages/angular/build/src/tools/angular/linker/oxc-ast-host.ts

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,20 @@ function isNode(node: unknown): node is Node {
2626
return typeof node === 'object' && node !== null && 'type' in node;
2727
}
2828

29+
function unwrapParentheses(node: unknown): unknown {
30+
while (isNode(node) && node.type === 'ParenthesizedExpression') {
31+
node = node.expression;
32+
}
33+
34+
return node;
35+
}
36+
2937
/**
3038
* An implementation of `AstHost` that queries information from `oxc-parser` AST nodes.
3139
*/
3240
export class OxcAstHost implements AstHost<unknown> {
3341
getSymbolName(node: unknown): string | null {
42+
node = unwrapParentheses(node);
3443
if (!isNode(node)) {
3544
return null;
3645
}
@@ -47,10 +56,13 @@ export class OxcAstHost implements AstHost<unknown> {
4756
}
4857

4958
isStringLiteral(node: unknown): node is StringLiteral {
59+
node = unwrapParentheses(node);
60+
5061
return isNode(node) && node.type === 'Literal' && typeof node.value === 'string';
5162
}
5263

5364
parseStringLiteral(str: unknown): string {
65+
str = unwrapParentheses(str);
5466
if (!this.isStringLiteral(str)) {
5567
throw new FatalLinkerError(str as object, 'Unsupported syntax, expected a string literal.');
5668
}
@@ -59,10 +71,13 @@ export class OxcAstHost implements AstHost<unknown> {
5971
}
6072

6173
isNumericLiteral(node: unknown): node is NumericLiteral {
74+
node = unwrapParentheses(node);
75+
6276
return isNode(node) && node.type === 'Literal' && typeof node.value === 'number';
6377
}
6478

6579
parseNumericLiteral(num: unknown): number {
80+
num = unwrapParentheses(num);
6681
if (!this.isNumericLiteral(num)) {
6782
throw new FatalLinkerError(num as object, 'Unsupported syntax, expected a numeric literal.');
6883
}
@@ -71,6 +86,7 @@ export class OxcAstHost implements AstHost<unknown> {
7186
}
7287

7388
isBooleanLiteral(node: unknown): node is BooleanLiteral | UnaryExpression {
89+
node = unwrapParentheses(node);
7490
if (!isNode(node)) {
7591
return false;
7692
}
@@ -81,6 +97,7 @@ export class OxcAstHost implements AstHost<unknown> {
8197
}
8298

8399
parseBooleanLiteral(bool: unknown): boolean {
100+
bool = unwrapParentheses(bool);
84101
if (isNode(bool)) {
85102
if (bool.type === 'Literal' && typeof bool.value === 'boolean') {
86103
return bool.value;
@@ -94,14 +111,19 @@ export class OxcAstHost implements AstHost<unknown> {
94111
}
95112

96113
isNull(node: unknown): node is NullLiteral {
114+
node = unwrapParentheses(node);
115+
97116
return isNode(node) && node.type === 'Literal' && node.value === null;
98117
}
99118

100119
isArrayLiteral(node: unknown): node is ArrayExpression {
120+
node = unwrapParentheses(node);
121+
101122
return isNode(node) && node.type === 'ArrayExpression';
102123
}
103124

104125
parseArrayLiteral(array: unknown): unknown[] {
126+
array = unwrapParentheses(array);
105127
if (!this.isArrayLiteral(array)) {
106128
throw new FatalLinkerError(array as object, 'Unsupported syntax, expected an array literal.');
107129
}
@@ -115,23 +137,27 @@ export class OxcAstHost implements AstHost<unknown> {
115137
'Unsupported syntax, element in array not to be empty.',
116138
);
117139
}
118-
if (element.type === 'SpreadElement') {
140+
const unwrappedElement = unwrapParentheses(element);
141+
if (isNode(unwrappedElement) && unwrappedElement.type === 'SpreadElement') {
119142
throw new FatalLinkerError(
120-
element as object,
143+
unwrappedElement as object,
121144
'Unsupported syntax, element in array not to use spread syntax.',
122145
);
123146
}
124-
result.push(element);
147+
result.push(unwrappedElement);
125148
}
126149

127150
return result;
128151
}
129152

130153
isObjectLiteral(node: unknown): node is ObjectExpression {
154+
node = unwrapParentheses(node);
155+
131156
return isNode(node) && node.type === 'ObjectExpression';
132157
}
133158

134159
parseObjectLiteral(obj: unknown): Map<string, unknown> {
160+
obj = unwrapParentheses(obj);
135161
if (!this.isObjectLiteral(obj)) {
136162
throw new FatalLinkerError(obj as object, 'Unsupported syntax, expected an object literal.');
137163
}
@@ -146,7 +172,13 @@ export class OxcAstHost implements AstHost<unknown> {
146172
);
147173
}
148174

149-
const keyNode = property.key;
175+
const keyNode = unwrapParentheses(property.key);
176+
if (!isNode(keyNode)) {
177+
throw new FatalLinkerError(
178+
property.key as object,
179+
'Unsupported syntax, expected a property name.',
180+
);
181+
}
150182

151183
let key: string;
152184
if (keyNode.type === 'Identifier') {
@@ -162,13 +194,14 @@ export class OxcAstHost implements AstHost<unknown> {
162194
);
163195
}
164196

165-
result.set(key, property.value);
197+
result.set(key, unwrapParentheses(property.value));
166198
}
167199

168200
return result;
169201
}
170202

171203
isFunctionExpression(node: unknown): node is FunctionNode | ArrowFunctionExpression {
204+
node = unwrapParentheses(node);
172205
if (!isNode(node)) {
173206
return false;
174207
}
@@ -181,6 +214,7 @@ export class OxcAstHost implements AstHost<unknown> {
181214
}
182215

183216
parseReturnValue(fn: unknown): unknown {
217+
fn = unwrapParentheses(fn);
184218
if (!this.isFunctionExpression(fn)) {
185219
throw new FatalLinkerError(fn as object, 'Unsupported syntax, expected a function.');
186220
}
@@ -191,7 +225,7 @@ export class OxcAstHost implements AstHost<unknown> {
191225
}
192226

193227
if (body.type !== 'BlockStatement') {
194-
return body;
228+
return unwrapParentheses(body);
195229
}
196230

197231
const statements = body.body;
@@ -217,10 +251,11 @@ export class OxcAstHost implements AstHost<unknown> {
217251
);
218252
}
219253

220-
return stmt.argument;
254+
return unwrapParentheses(stmt.argument);
221255
}
222256

223257
parseParameters(fn: unknown): unknown[] {
258+
fn = unwrapParentheses(fn);
224259
if (!this.isFunctionExpression(fn)) {
225260
throw new FatalLinkerError(fn as object, 'Unsupported syntax, expected a function.');
226261
}
@@ -229,38 +264,44 @@ export class OxcAstHost implements AstHost<unknown> {
229264
}
230265

231266
isCallExpression(node: unknown): node is CallExpression {
267+
node = unwrapParentheses(node);
268+
232269
return isNode(node) && node.type === 'CallExpression';
233270
}
234271

235272
parseCallee(call: unknown): unknown {
273+
call = unwrapParentheses(call);
236274
if (!this.isCallExpression(call)) {
237275
throw new FatalLinkerError(call as object, 'Unsupported syntax, expected a call expression.');
238276
}
239277

240-
return call.callee;
278+
return unwrapParentheses(call.callee);
241279
}
242280

243281
parseArguments(call: unknown): unknown[] {
282+
call = unwrapParentheses(call);
244283
if (!this.isCallExpression(call)) {
245284
throw new FatalLinkerError(call as object, 'Unsupported syntax, expected a call expression.');
246285
}
247286

248287
const result: unknown[] = [];
249288

250289
for (const arg of call.arguments) {
251-
if (arg.type === 'SpreadElement') {
290+
const unwrappedArg = unwrapParentheses(arg);
291+
if (isNode(unwrappedArg) && unwrappedArg.type === 'SpreadElement') {
252292
throw new FatalLinkerError(
253-
arg as object,
293+
unwrappedArg as object,
254294
'Unsupported syntax, argument not to use spread syntax.',
255295
);
256296
}
257-
result.push(arg);
297+
result.push(unwrappedArg);
258298
}
259299

260300
return result;
261301
}
262302

263303
getRange(node: unknown): Range {
304+
node = unwrapParentheses(node);
264305
if (!isNode(node) || typeof node.start !== 'number' || typeof node.end !== 'number') {
265306
throw new FatalLinkerError(
266307
node as object,

packages/angular/build/src/tools/angular/linker/oxc-ast-host_spec.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ describe('OxcAstHost', () => {
3434
it('should return the name of an identifier', () => {
3535
const expr = parseExpression('foo');
3636
expect(host.getSymbolName(expr)).toBe('foo');
37+
expect(host.getSymbolName(parseExpression('(foo)'))).toBe('foo');
3738
});
3839

3940
it('should return the property name of a member expression', () => {
4041
const expr = parseExpression('foo.bar');
4142
expect(host.getSymbolName(expr)).toBe('bar');
43+
expect(host.getSymbolName(parseExpression('(foo.bar)'))).toBe('bar');
4244
});
4345

4446
it('should return null for non-identifier or computed member expressions', () => {
@@ -53,6 +55,10 @@ describe('OxcAstHost', () => {
5355
const expr = parseExpression('"hello"');
5456
expect(host.isStringLiteral(expr)).toBe(true);
5557
expect(host.parseStringLiteral(expr)).toBe('hello');
58+
59+
const parenthesized = parseExpression('("hello")');
60+
expect(host.isStringLiteral(parenthesized)).toBe(true);
61+
expect(host.parseStringLiteral(parenthesized)).toBe('hello');
5662
});
5763

5864
it('should throw when parsing non-string literals', () => {
@@ -67,6 +73,10 @@ describe('OxcAstHost', () => {
6773
const expr = parseExpression('123');
6874
expect(host.isNumericLiteral(expr)).toBe(true);
6975
expect(host.parseNumericLiteral(expr)).toBe(123);
76+
77+
const parenthesized = parseExpression('(123)');
78+
expect(host.isNumericLiteral(parenthesized)).toBe(true);
79+
expect(host.parseNumericLiteral(parenthesized)).toBe(123);
7080
});
7181

7282
it('should throw when parsing non-numeric literals', () => {
@@ -84,6 +94,10 @@ describe('OxcAstHost', () => {
8494
expect(host.parseBooleanLiteral(trueExpr)).toBe(true);
8595
expect(host.isBooleanLiteral(falseExpr)).toBe(true);
8696
expect(host.parseBooleanLiteral(falseExpr)).toBe(false);
97+
98+
const parenthesizedTrue = parseExpression('(true)');
99+
expect(host.isBooleanLiteral(parenthesizedTrue)).toBe(true);
100+
expect(host.parseBooleanLiteral(parenthesizedTrue)).toBe(true);
87101
});
88102

89103
it('should recognize and parse minified boolean literals (!0 and !1)', () => {
@@ -93,6 +107,10 @@ describe('OxcAstHost', () => {
93107
expect(host.parseBooleanLiteral(trueExpr)).toBe(true);
94108
expect(host.isBooleanLiteral(falseExpr)).toBe(true);
95109
expect(host.parseBooleanLiteral(falseExpr)).toBe(false);
110+
111+
const parenthesizedMinified = parseExpression('(!0)');
112+
expect(host.isBooleanLiteral(parenthesizedMinified)).toBe(true);
113+
expect(host.parseBooleanLiteral(parenthesizedMinified)).toBe(true);
96114
});
97115

98116
it('should return false for invalid boolean expressions', () => {
@@ -106,6 +124,18 @@ describe('OxcAstHost', () => {
106124
const expr = parseExpression('[1, "a", true]');
107125
expect(host.isArrayLiteral(expr)).toBe(true);
108126
expect(host.parseArrayLiteral(expr).length).toBe(3);
127+
128+
const parenthesized = parseExpression('([1, "a", true])');
129+
expect(host.isArrayLiteral(parenthesized)).toBe(true);
130+
expect(host.parseArrayLiteral(parenthesized).length).toBe(3);
131+
});
132+
133+
it('should unwrap parenthesized elements in array literals', () => {
134+
const expr = parseExpression('[(1), ("a")]');
135+
const elements = host.parseArrayLiteral(expr);
136+
expect(elements.length).toBe(2);
137+
expect(host.isNumericLiteral(elements[0])).toBe(true);
138+
expect(host.isStringLiteral(elements[1])).toBe(true);
109139
});
110140

111141
it('should throw when array contains empty elements or spread syntax', () => {
@@ -115,6 +145,9 @@ describe('OxcAstHost', () => {
115145
expect(() => host.parseArrayLiteral(parseExpression('[1, ...a]'))).toThrowError(
116146
FatalLinkerError,
117147
);
148+
expect(() => host.parseArrayLiteral(parseExpression('[1, ...(a)]'))).toThrowError(
149+
FatalLinkerError,
150+
);
118151
});
119152
});
120153

@@ -130,6 +163,16 @@ describe('OxcAstHost', () => {
130163
expect(map.has('3')).toBe(true);
131164
});
132165

166+
it('should recognize and parse parenthesized object literals', () => {
167+
const expr = parseExpression('({ a: (1), b: ("c") })');
168+
expect(host.isObjectLiteral(expr)).toBe(true);
169+
170+
const map = host.parseObjectLiteral(expr);
171+
expect(map.size).toBe(2);
172+
expect(host.isNumericLiteral(map.get('a'))).toBe(true);
173+
expect(host.isStringLiteral(map.get('b'))).toBe(true);
174+
});
175+
133176
it('should throw when object literal contains spread or non-property assignments', () => {
134177
expect(() => host.parseObjectLiteral(parseExpression('{ ...a }'))).toThrowError(
135178
FatalLinkerError,
@@ -147,6 +190,21 @@ describe('OxcAstHost', () => {
147190
expect(host.isNumericLiteral(returnValue)).toBe(true);
148191
});
149192

193+
it('should parse parenthesized arrow functions and concise bodies returning parenthesized object literals', () => {
194+
const expr = parseExpression('() => ({ a: 1 })');
195+
expect(host.isFunctionExpression(expr)).toBe(true);
196+
197+
const returnValue = host.parseReturnValue(expr);
198+
expect(host.isObjectLiteral(returnValue)).toBe(true);
199+
const map = host.parseObjectLiteral(returnValue);
200+
expect(map.has('a')).toBe(true);
201+
202+
const wrappedArrow = parseExpression('((a) => (42))');
203+
expect(host.isFunctionExpression(wrappedArrow)).toBe(true);
204+
expect(host.parseParameters(wrappedArrow).length).toBe(1);
205+
expect(host.isNumericLiteral(host.parseReturnValue(wrappedArrow))).toBe(true);
206+
});
207+
150208
it('should parse return value from function with block statement containing single return', () => {
151209
const stmt = parseStatement('function foo(a) { return "hello"; }');
152210
expect(host.isFunctionExpression(stmt)).toBe(true);
@@ -155,6 +213,14 @@ describe('OxcAstHost', () => {
155213
expect(host.isStringLiteral(returnValue)).toBe(true);
156214
});
157215

216+
it('should parse return value from function returning parenthesized expression', () => {
217+
const stmt = parseStatement('function foo() { return ({ a: 1 }); }');
218+
expect(host.isFunctionExpression(stmt)).toBe(true);
219+
220+
const returnValue = host.parseReturnValue(stmt);
221+
expect(host.isObjectLiteral(returnValue)).toBe(true);
222+
});
223+
158224
it('should throw when function body has multiple statements or no return', () => {
159225
expect(() =>
160226
host.parseReturnValue(parseStatement('function foo() { const x = 1; return x; }')),
@@ -171,6 +237,14 @@ describe('OxcAstHost', () => {
171237
expect(host.isCallExpression(expr)).toBe(true);
172238
expect(host.getSymbolName(host.parseCallee(expr))).toBe('foo');
173239
expect(host.parseArguments(expr).length).toBe(2);
240+
241+
const parenthesized = parseExpression('((foo)((1), ("a")))');
242+
expect(host.isCallExpression(parenthesized)).toBe(true);
243+
expect(host.getSymbolName(host.parseCallee(parenthesized))).toBe('foo');
244+
const args = host.parseArguments(parenthesized);
245+
expect(args.length).toBe(2);
246+
expect(host.isNumericLiteral(args[0])).toBe(true);
247+
expect(host.isStringLiteral(args[1])).toBe(true);
174248
});
175249

176250
it('should throw when call expression arguments contain spread syntax', () => {

0 commit comments

Comments
 (0)