Skip to content
Merged
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
63 changes: 52 additions & 11 deletions packages/angular/build/src/tools/angular/linker/oxc-ast-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,20 @@ function isNode(node: unknown): node is Node {
return typeof node === 'object' && node !== null && 'type' in node;
}

function unwrapParentheses(node: unknown): unknown {
while (isNode(node) && node.type === 'ParenthesizedExpression') {
node = node.expression;
}

return node;
}

/**
* An implementation of `AstHost` that queries information from `oxc-parser` AST nodes.
*/
export class OxcAstHost implements AstHost<unknown> {
getSymbolName(node: unknown): string | null {
node = unwrapParentheses(node);
if (!isNode(node)) {
return null;
}
Expand All @@ -47,10 +56,13 @@ export class OxcAstHost implements AstHost<unknown> {
}

isStringLiteral(node: unknown): node is StringLiteral {
node = unwrapParentheses(node);

return isNode(node) && node.type === 'Literal' && typeof node.value === 'string';
}

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

isNumericLiteral(node: unknown): node is NumericLiteral {
node = unwrapParentheses(node);

return isNode(node) && node.type === 'Literal' && typeof node.value === 'number';
}

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

isBooleanLiteral(node: unknown): node is BooleanLiteral | UnaryExpression {
node = unwrapParentheses(node);
if (!isNode(node)) {
return false;
}
Expand All @@ -81,6 +97,7 @@ export class OxcAstHost implements AstHost<unknown> {
}

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

isNull(node: unknown): node is NullLiteral {
node = unwrapParentheses(node);

return isNode(node) && node.type === 'Literal' && node.value === null;
}

isArrayLiteral(node: unknown): node is ArrayExpression {
node = unwrapParentheses(node);

return isNode(node) && node.type === 'ArrayExpression';
}

parseArrayLiteral(array: unknown): unknown[] {
array = unwrapParentheses(array);
if (!this.isArrayLiteral(array)) {
throw new FatalLinkerError(array as object, 'Unsupported syntax, expected an array literal.');
}
Expand All @@ -115,23 +137,27 @@ export class OxcAstHost implements AstHost<unknown> {
'Unsupported syntax, element in array not to be empty.',
);
}
if (element.type === 'SpreadElement') {
const unwrappedElement = unwrapParentheses(element);
if (isNode(unwrappedElement) && unwrappedElement.type === 'SpreadElement') {
throw new FatalLinkerError(
element as object,
unwrappedElement as object,
'Unsupported syntax, element in array not to use spread syntax.',
);
}
result.push(element);
result.push(unwrappedElement);
}

return result;
}

isObjectLiteral(node: unknown): node is ObjectExpression {
node = unwrapParentheses(node);

return isNode(node) && node.type === 'ObjectExpression';
}

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

const keyNode = property.key;
const keyNode = unwrapParentheses(property.key);
if (!isNode(keyNode)) {
throw new FatalLinkerError(
property.key as object,
'Unsupported syntax, expected a property name.',
);
}

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

result.set(key, property.value);
result.set(key, unwrapParentheses(property.value));
}

return result;
}

isFunctionExpression(node: unknown): node is FunctionNode | ArrowFunctionExpression {
node = unwrapParentheses(node);
if (!isNode(node)) {
return false;
}
Expand All @@ -181,6 +214,7 @@ export class OxcAstHost implements AstHost<unknown> {
}

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

if (body.type !== 'BlockStatement') {
return body;
return unwrapParentheses(body);
}

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

return stmt.argument;
return unwrapParentheses(stmt.argument);
}

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

isCallExpression(node: unknown): node is CallExpression {
node = unwrapParentheses(node);

return isNode(node) && node.type === 'CallExpression';
}

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

return call.callee;
return unwrapParentheses(call.callee);
}

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

const result: unknown[] = [];

for (const arg of call.arguments) {
if (arg.type === 'SpreadElement') {
const unwrappedArg = unwrapParentheses(arg);
if (isNode(unwrappedArg) && unwrappedArg.type === 'SpreadElement') {
throw new FatalLinkerError(
arg as object,
unwrappedArg as object,
'Unsupported syntax, argument not to use spread syntax.',
);
}
result.push(arg);
result.push(unwrappedArg);
}

return result;
}

getRange(node: unknown): Range {
node = unwrapParentheses(node);
if (!isNode(node) || typeof node.start !== 'number' || typeof node.end !== 'number') {
throw new FatalLinkerError(
node as object,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ describe('OxcAstHost', () => {
it('should return the name of an identifier', () => {
const expr = parseExpression('foo');
expect(host.getSymbolName(expr)).toBe('foo');
expect(host.getSymbolName(parseExpression('(foo)'))).toBe('foo');
});

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

it('should return null for non-identifier or computed member expressions', () => {
Expand All @@ -53,6 +55,10 @@ describe('OxcAstHost', () => {
const expr = parseExpression('"hello"');
expect(host.isStringLiteral(expr)).toBe(true);
expect(host.parseStringLiteral(expr)).toBe('hello');

const parenthesized = parseExpression('("hello")');
expect(host.isStringLiteral(parenthesized)).toBe(true);
expect(host.parseStringLiteral(parenthesized)).toBe('hello');
});

it('should throw when parsing non-string literals', () => {
Expand All @@ -67,6 +73,10 @@ describe('OxcAstHost', () => {
const expr = parseExpression('123');
expect(host.isNumericLiteral(expr)).toBe(true);
expect(host.parseNumericLiteral(expr)).toBe(123);

const parenthesized = parseExpression('(123)');
expect(host.isNumericLiteral(parenthesized)).toBe(true);
expect(host.parseNumericLiteral(parenthesized)).toBe(123);
});

it('should throw when parsing non-numeric literals', () => {
Expand All @@ -84,6 +94,10 @@ describe('OxcAstHost', () => {
expect(host.parseBooleanLiteral(trueExpr)).toBe(true);
expect(host.isBooleanLiteral(falseExpr)).toBe(true);
expect(host.parseBooleanLiteral(falseExpr)).toBe(false);

const parenthesizedTrue = parseExpression('(true)');
expect(host.isBooleanLiteral(parenthesizedTrue)).toBe(true);
expect(host.parseBooleanLiteral(parenthesizedTrue)).toBe(true);
});

it('should recognize and parse minified boolean literals (!0 and !1)', () => {
Expand All @@ -93,6 +107,10 @@ describe('OxcAstHost', () => {
expect(host.parseBooleanLiteral(trueExpr)).toBe(true);
expect(host.isBooleanLiteral(falseExpr)).toBe(true);
expect(host.parseBooleanLiteral(falseExpr)).toBe(false);

const parenthesizedMinified = parseExpression('(!0)');
expect(host.isBooleanLiteral(parenthesizedMinified)).toBe(true);
expect(host.parseBooleanLiteral(parenthesizedMinified)).toBe(true);
});

it('should return false for invalid boolean expressions', () => {
Expand All @@ -106,6 +124,18 @@ describe('OxcAstHost', () => {
const expr = parseExpression('[1, "a", true]');
expect(host.isArrayLiteral(expr)).toBe(true);
expect(host.parseArrayLiteral(expr).length).toBe(3);

const parenthesized = parseExpression('([1, "a", true])');
expect(host.isArrayLiteral(parenthesized)).toBe(true);
expect(host.parseArrayLiteral(parenthesized).length).toBe(3);
});

it('should unwrap parenthesized elements in array literals', () => {
const expr = parseExpression('[(1), ("a")]');
const elements = host.parseArrayLiteral(expr);
expect(elements.length).toBe(2);
expect(host.isNumericLiteral(elements[0])).toBe(true);
expect(host.isStringLiteral(elements[1])).toBe(true);
});

it('should throw when array contains empty elements or spread syntax', () => {
Expand All @@ -115,6 +145,9 @@ describe('OxcAstHost', () => {
expect(() => host.parseArrayLiteral(parseExpression('[1, ...a]'))).toThrowError(
FatalLinkerError,
);
expect(() => host.parseArrayLiteral(parseExpression('[1, ...(a)]'))).toThrowError(
FatalLinkerError,
);
});
});

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

it('should recognize and parse parenthesized object literals', () => {
const expr = parseExpression('({ a: (1), b: ("c") })');
expect(host.isObjectLiteral(expr)).toBe(true);

const map = host.parseObjectLiteral(expr);
expect(map.size).toBe(2);
expect(host.isNumericLiteral(map.get('a'))).toBe(true);
expect(host.isStringLiteral(map.get('b'))).toBe(true);
});

it('should throw when object literal contains spread or non-property assignments', () => {
expect(() => host.parseObjectLiteral(parseExpression('{ ...a }'))).toThrowError(
FatalLinkerError,
Expand All @@ -147,6 +190,21 @@ describe('OxcAstHost', () => {
expect(host.isNumericLiteral(returnValue)).toBe(true);
});

it('should parse parenthesized arrow functions and concise bodies returning parenthesized object literals', () => {
const expr = parseExpression('() => ({ a: 1 })');
expect(host.isFunctionExpression(expr)).toBe(true);

const returnValue = host.parseReturnValue(expr);
expect(host.isObjectLiteral(returnValue)).toBe(true);
const map = host.parseObjectLiteral(returnValue);
expect(map.has('a')).toBe(true);

const wrappedArrow = parseExpression('((a) => (42))');
expect(host.isFunctionExpression(wrappedArrow)).toBe(true);
expect(host.parseParameters(wrappedArrow).length).toBe(1);
expect(host.isNumericLiteral(host.parseReturnValue(wrappedArrow))).toBe(true);
});

it('should parse return value from function with block statement containing single return', () => {
const stmt = parseStatement('function foo(a) { return "hello"; }');
expect(host.isFunctionExpression(stmt)).toBe(true);
Expand All @@ -155,6 +213,14 @@ describe('OxcAstHost', () => {
expect(host.isStringLiteral(returnValue)).toBe(true);
});

it('should parse return value from function returning parenthesized expression', () => {
const stmt = parseStatement('function foo() { return ({ a: 1 }); }');
expect(host.isFunctionExpression(stmt)).toBe(true);

const returnValue = host.parseReturnValue(stmt);
expect(host.isObjectLiteral(returnValue)).toBe(true);
});

it('should throw when function body has multiple statements or no return', () => {
expect(() =>
host.parseReturnValue(parseStatement('function foo() { const x = 1; return x; }')),
Expand All @@ -171,6 +237,14 @@ describe('OxcAstHost', () => {
expect(host.isCallExpression(expr)).toBe(true);
expect(host.getSymbolName(host.parseCallee(expr))).toBe('foo');
expect(host.parseArguments(expr).length).toBe(2);

const parenthesized = parseExpression('((foo)((1), ("a")))');
expect(host.isCallExpression(parenthesized)).toBe(true);
expect(host.getSymbolName(host.parseCallee(parenthesized))).toBe('foo');
const args = host.parseArguments(parenthesized);
expect(args.length).toBe(2);
expect(host.isNumericLiteral(args[0])).toBe(true);
expect(host.isStringLiteral(args[1])).toBe(true);
});

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