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
105 changes: 55 additions & 50 deletions src/angular-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,32 @@ import {
import { type CommentLine } from './types.ts';
import { sourceSpanToLocationInformation } from './utils.ts';

let parseSourceSpan: ParseSourceSpan;
// https://github.com/angular/angular/blob/5e9707dc84e6590ec8c9d41e7d3be7deb2fa7c53/packages/compiler/test/expression_parser/utils/span.ts
function getFakeSpan(fileName = 'test.html') {
const file = new ParseSourceFile('', fileName);
const location = new ParseLocation(file, 0, 0, 0);
return new ParseSourceSpan(location, location);
function getParseSourceSpan() {
if (!parseSourceSpan) {
const file = new ParseSourceFile('', 'test.html');
const location = new ParseLocation(file, -1, -1, -1);
parseSourceSpan = new ParseSourceSpan(location, location);
}

return parseSourceSpan;
}

let parser: Parser;
function getParser() {
return (parser ??= new Parser(new Lexer()));
}

const getCommentStart = (text: string): number | null =>
// @ts-expect-error -- need to call private _commentStart
Parser.prototype._commentStart(text);

function extractComments(text: string, shouldExtractComment: boolean) {
const commentStart = shouldExtractComment ? getCommentStart(text) : null;
function extractComments(text: string) {
const commentStart = getCommentStart(text);

if (commentStart === null) {
return { text, comments: [] };
return [];
}

const comment: CommentLine = {
Expand All @@ -38,53 +48,48 @@ function extractComments(text: string, shouldExtractComment: boolean) {
}),
};

return { text, comments: [comment] };
return [comment];
}

function createAngularParseFunction<
T extends ASTWithSource | TemplateBindingParseResult,
>(parse: (text: string, parser: Parser) => T, shouldExtractComment = true) {
return (originalText: string) => {
const lexer = new Lexer();
const parser = new Parser(lexer);

const { text, comments } = extractComments(
originalText,
shouldExtractComment,
function throwErrors<
ResultType extends ASTWithSource | TemplateBindingParseResult,
>(result: ResultType) {
if (result.errors.length !== 0) {
const [{ message }] = result.errors;
throw new SyntaxError(
message.replace(/^Parser Error: | at column \d+ in [^]*$/g, ''),
);
const result = parse(text, parser);

if (result.errors.length !== 0) {
const [{ message }] = result.errors;
throw new SyntaxError(
message.replace(/^Parser Error: | at column \d+ in [^]*$/g, ''),
);
}
}

return { result, comments, text };
};
return result;
}

export const parseBinding = createAngularParseFunction((text, parser) =>
parser.parseBinding(text, getFakeSpan(), 0),
const createAstParser =
(
name:
| 'parseBinding'
| 'parseSimpleBinding'
| 'parseAction'
| 'parseInterpolationExpression',
) =>
(text: string) => ({
result: throwErrors<ASTWithSource>(
getParser()[name](text, getParseSourceSpan(), 0),
),
text,
comments: extractComments(text),
});

export const parseAction = createAstParser('parseAction');
export const parseBinding = createAstParser('parseBinding');
export const parseSimpleBinding = createAstParser('parseSimpleBinding');
export const parseInterpolationExpression = createAstParser(
'parseInterpolationExpression',
);

export const parseSimpleBinding = createAngularParseFunction((text, parser) =>
parser.parseSimpleBinding(text, getFakeSpan(), 0),
);

export const parseAction = createAngularParseFunction((text, parser) =>
parser.parseAction(text, getFakeSpan(), 0),
);

export const parseInterpolationExpression = createAngularParseFunction(
(text, parser) => parser.parseInterpolationExpression(text, getFakeSpan(), 0),
);

export const parseTemplateBindings = createAngularParseFunction(
(text, parser) => parser.parseTemplateBindings('', text, getFakeSpan(), 0, 0),
/* shouldExtractComment */ false,
);

export type AstParseResult = ReturnType<typeof parseBinding>;
export type MicroSyntaxParseResult = ReturnType<typeof parseTemplateBindings>;
export const parseTemplateBindings = (text: string) => ({
result: throwErrors<TemplateBindingParseResult>(
getParser().parseTemplateBindings('', text, getParseSourceSpan(), 0, 0),
),
text,
comments: [],
});
1 change: 1 addition & 0 deletions src/ast-transform/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { transformAst, transformAstNode } from './transform.ts';
12 changes: 12 additions & 0 deletions src/ast-transform/transform-array-expression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { type LiteralArray } from '@angular/compiler';
import type * as babel from '@babel/types';

import { type Transformer } from './transform.ts';

export const visitLiteralArray = (
node: LiteralArray,
transformer: Transformer,
): babel.ArrayExpression => ({
type: 'ArrayExpression',
elements: transformer.transformChildren<babel.Expression>(node.expressions),
});
8 changes: 8 additions & 0 deletions src/ast-transform/transform-ast-with-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { type ASTWithSource } from '@angular/compiler';

import { type Transformer } from './transform.ts';

export const visitASTWithSource = (
node: ASTWithSource,
transformer: Transformer,
) => transformer.transformChild(node.ast);
48 changes: 48 additions & 0 deletions src/ast-transform/transform-binary-expression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Binary } from '@angular/compiler';
import type * as babel from '@babel/types';

import { type Transformer } from './transform.ts';

const isAssignmentOperator = (
operator: Binary['operation'],
): operator is babel.AssignmentExpression['operator'] =>
Binary.isAssignmentOperation(operator);

const isLogicalOperator = (
operator: Binary['operation'],
): operator is babel.LogicalExpression['operator'] =>
operator === '&&' || operator === '||' || operator === '??';

export const visitBinary = (
node: Binary,
transformer: Transformer,
):
| babel.LogicalExpression
| babel.AssignmentExpression
| babel.BinaryExpression => {
const { operation: operator } = node;
const [left, right] = transformer.transformChildren<babel.Expression>([
node.left,
node.right,
]);

if (isLogicalOperator(operator)) {
return { type: 'LogicalExpression', operator, left, right };
}

if (isAssignmentOperator(operator)) {
return {
type: 'AssignmentExpression',
left: left as babel.MemberExpression,
right,
operator: operator,
};
}

return {
left,
right,
type: 'BinaryExpression',
operator: operator as babel.BinaryExpression['operator'],
};
};
45 changes: 45 additions & 0 deletions src/ast-transform/transform-call-expression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { type Call, type SafeCall } from '@angular/compiler';
import type * as babel from '@babel/types';

import { type Transformer } from './transform.ts';
import { isOptionalObjectOrCallee } from './utilities.ts';

const callOptions = { optional: false } as const;
const safeCallOptions = { optional: true } as const;

type VisitorCall = {
node: Call;
options: typeof callOptions;
};
type VisitorSafeCall = {
node: SafeCall;
options: typeof safeCallOptions;
};

const transformCall =
<Visitor extends VisitorCall | VisitorSafeCall>({
optional,
}: Visitor['options']) =>
(
node: Visitor['node'],
transformer: Transformer,
): babel.CallExpression | babel.OptionalCallExpression => {
const arguments_ = transformer.transformChildren<babel.Expression>(
node.args,
);
const callee = transformer.transformChild<babel.Expression>(node.receiver);

if (optional || isOptionalObjectOrCallee(callee)) {
return {
type: 'OptionalCallExpression',
callee,
arguments: arguments_,
optional,
};
}

return { type: 'CallExpression', callee, arguments: arguments_ };
};

export const visitCall = transformCall<VisitorCall>(callOptions);
export const visitSafeCall = transformCall<VisitorSafeCall>(safeCallOptions);
15 changes: 15 additions & 0 deletions src/ast-transform/transform-chained-expression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { type Chain } from '@angular/compiler';
import type * as babel from '@babel/types';

import type { NGChainedExpression } from '../types.ts';
import { type Transformer } from './transform.ts';

export const visitChain = (
node: Chain,
transformer: Transformer,
): Omit<NGChainedExpression, 'start' | 'end' | 'range'> => ({
type: 'NGChainedExpression',
expressions: transformer.transformChildren<babel.Expression>(
node.expressions,
),
});
23 changes: 23 additions & 0 deletions src/ast-transform/transform-conditional-expression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { type Conditional } from '@angular/compiler';
import type * as babel from '@babel/types';

import { type Transformer } from './transform.ts';

export const visitConditional = (
node: Conditional,
transformer: Transformer,
): babel.ConditionalExpression => {
const [test, consequent, alternate] =
transformer.transformChildren<babel.Expression>([
node.condition,
node.trueExp,
node.falseExp,
]);

return {
type: 'ConditionalExpression',
test,
consequent,
alternate,
};
};
17 changes: 17 additions & 0 deletions src/ast-transform/transform-interpolation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type Interpolation } from '@angular/compiler';

import { type Transformer } from './transform.ts';

export const visitInterpolation = (
node: Interpolation,
transformer: Transformer,
) => {
const { expressions } = node;

/* c8 ignore next 3 @preserve */
if (expressions.length !== 1) {
throw new Error("Unexpected 'Interpolation'");
}

return transformer.transformChild(expressions[0]);
};
41 changes: 41 additions & 0 deletions src/ast-transform/transform-literal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
type LiteralPrimitive,
type RegularExpressionLiteral,
} from '@angular/compiler';
import type * as babel from '@babel/types';

export const visitLiteralPrimitive = (
node: LiteralPrimitive,
):
| babel.BooleanLiteral
| babel.NumericLiteral
| babel.NullLiteral
| babel.StringLiteral
| babel.Identifier => {
const { value } = node;
switch (typeof value) {
case 'boolean':
return { type: 'BooleanLiteral', value };
case 'number':
return { type: 'NumericLiteral', value };
case 'object':
return { type: 'NullLiteral' };
case 'string':
return { type: 'StringLiteral', value };
case 'undefined':
return { type: 'Identifier', name: 'undefined' };
/* c8 ignore next 4 */
default:
throw new Error(
`Unexpected 'LiteralPrimitive' value type ${typeof value}`,
);
}
};

export const visitRegularExpressionLiteral = (
node: RegularExpressionLiteral,
): babel.RegExpLiteral => ({
type: 'RegExpLiteral',
pattern: node.body,
flags: node.flags ?? '',
});
Loading