Skip to content

Commit 73ea7be

Browse files
Refactor with AstVisitor (#361)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 39c6403 commit 73ea7be

27 files changed

Lines changed: 764 additions & 543 deletions

src/angular-parser.ts

Lines changed: 55 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,32 @@ import {
1111
import { type CommentLine } from './types.ts';
1212
import { sourceSpanToLocationInformation } from './utils.ts';
1313

14+
let parseSourceSpan: ParseSourceSpan;
1415
// https://github.com/angular/angular/blob/5e9707dc84e6590ec8c9d41e7d3be7deb2fa7c53/packages/compiler/test/expression_parser/utils/span.ts
15-
function getFakeSpan(fileName = 'test.html') {
16-
const file = new ParseSourceFile('', fileName);
17-
const location = new ParseLocation(file, 0, 0, 0);
18-
return new ParseSourceSpan(location, location);
16+
function getParseSourceSpan() {
17+
if (!parseSourceSpan) {
18+
const file = new ParseSourceFile('', 'test.html');
19+
const location = new ParseLocation(file, -1, -1, -1);
20+
parseSourceSpan = new ParseSourceSpan(location, location);
21+
}
22+
23+
return parseSourceSpan;
24+
}
25+
26+
let parser: Parser;
27+
function getParser() {
28+
return (parser ??= new Parser(new Lexer()));
1929
}
2030

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

25-
function extractComments(text: string, shouldExtractComment: boolean) {
26-
const commentStart = shouldExtractComment ? getCommentStart(text) : null;
35+
function extractComments(text: string) {
36+
const commentStart = getCommentStart(text);
2737

2838
if (commentStart === null) {
29-
return { text, comments: [] };
39+
return [];
3040
}
3141

3242
const comment: CommentLine = {
@@ -38,53 +48,48 @@ function extractComments(text: string, shouldExtractComment: boolean) {
3848
}),
3949
};
4050

41-
return { text, comments: [comment] };
51+
return [comment];
4252
}
4353

44-
function createAngularParseFunction<
45-
T extends ASTWithSource | TemplateBindingParseResult,
46-
>(parse: (text: string, parser: Parser) => T, shouldExtractComment = true) {
47-
return (originalText: string) => {
48-
const lexer = new Lexer();
49-
const parser = new Parser(lexer);
50-
51-
const { text, comments } = extractComments(
52-
originalText,
53-
shouldExtractComment,
54+
function throwErrors<
55+
ResultType extends ASTWithSource | TemplateBindingParseResult,
56+
>(result: ResultType) {
57+
if (result.errors.length !== 0) {
58+
const [{ message }] = result.errors;
59+
throw new SyntaxError(
60+
message.replace(/^Parser Error: | at column \d+ in [^]*$/g, ''),
5461
);
55-
const result = parse(text, parser);
56-
57-
if (result.errors.length !== 0) {
58-
const [{ message }] = result.errors;
59-
throw new SyntaxError(
60-
message.replace(/^Parser Error: | at column \d+ in [^]*$/g, ''),
61-
);
62-
}
62+
}
6363

64-
return { result, comments, text };
65-
};
64+
return result;
6665
}
6766

68-
export const parseBinding = createAngularParseFunction((text, parser) =>
69-
parser.parseBinding(text, getFakeSpan(), 0),
67+
const createAstParser =
68+
(
69+
name:
70+
| 'parseBinding'
71+
| 'parseSimpleBinding'
72+
| 'parseAction'
73+
| 'parseInterpolationExpression',
74+
) =>
75+
(text: string) => ({
76+
result: throwErrors<ASTWithSource>(
77+
getParser()[name](text, getParseSourceSpan(), 0),
78+
),
79+
text,
80+
comments: extractComments(text),
81+
});
82+
83+
export const parseAction = createAstParser('parseAction');
84+
export const parseBinding = createAstParser('parseBinding');
85+
export const parseSimpleBinding = createAstParser('parseSimpleBinding');
86+
export const parseInterpolationExpression = createAstParser(
87+
'parseInterpolationExpression',
7088
);
71-
72-
export const parseSimpleBinding = createAngularParseFunction((text, parser) =>
73-
parser.parseSimpleBinding(text, getFakeSpan(), 0),
74-
);
75-
76-
export const parseAction = createAngularParseFunction((text, parser) =>
77-
parser.parseAction(text, getFakeSpan(), 0),
78-
);
79-
80-
export const parseInterpolationExpression = createAngularParseFunction(
81-
(text, parser) => parser.parseInterpolationExpression(text, getFakeSpan(), 0),
82-
);
83-
84-
export const parseTemplateBindings = createAngularParseFunction(
85-
(text, parser) => parser.parseTemplateBindings('', text, getFakeSpan(), 0, 0),
86-
/* shouldExtractComment */ false,
87-
);
88-
89-
export type AstParseResult = ReturnType<typeof parseBinding>;
90-
export type MicroSyntaxParseResult = ReturnType<typeof parseTemplateBindings>;
89+
export const parseTemplateBindings = (text: string) => ({
90+
result: throwErrors<TemplateBindingParseResult>(
91+
getParser().parseTemplateBindings('', text, getParseSourceSpan(), 0, 0),
92+
),
93+
text,
94+
comments: [],
95+
});

src/ast-transform/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { transformAst, transformAstNode } from './transform.ts';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { type LiteralArray } from '@angular/compiler';
2+
import type * as babel from '@babel/types';
3+
4+
import { type Transformer } from './transform.ts';
5+
6+
export const visitLiteralArray = (
7+
node: LiteralArray,
8+
transformer: Transformer,
9+
): babel.ArrayExpression => ({
10+
type: 'ArrayExpression',
11+
elements: transformer.transformChildren<babel.Expression>(node.expressions),
12+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { type ASTWithSource } from '@angular/compiler';
2+
3+
import { type Transformer } from './transform.ts';
4+
5+
export const visitASTWithSource = (
6+
node: ASTWithSource,
7+
transformer: Transformer,
8+
) => transformer.transformChild(node.ast);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Binary } from '@angular/compiler';
2+
import type * as babel from '@babel/types';
3+
4+
import { type Transformer } from './transform.ts';
5+
6+
const isAssignmentOperator = (
7+
operator: Binary['operation'],
8+
): operator is babel.AssignmentExpression['operator'] =>
9+
Binary.isAssignmentOperation(operator);
10+
11+
const isLogicalOperator = (
12+
operator: Binary['operation'],
13+
): operator is babel.LogicalExpression['operator'] =>
14+
operator === '&&' || operator === '||' || operator === '??';
15+
16+
export const visitBinary = (
17+
node: Binary,
18+
transformer: Transformer,
19+
):
20+
| babel.LogicalExpression
21+
| babel.AssignmentExpression
22+
| babel.BinaryExpression => {
23+
const { operation: operator } = node;
24+
const [left, right] = transformer.transformChildren<babel.Expression>([
25+
node.left,
26+
node.right,
27+
]);
28+
29+
if (isLogicalOperator(operator)) {
30+
return { type: 'LogicalExpression', operator, left, right };
31+
}
32+
33+
if (isAssignmentOperator(operator)) {
34+
return {
35+
type: 'AssignmentExpression',
36+
left: left as babel.MemberExpression,
37+
right,
38+
operator: operator,
39+
};
40+
}
41+
42+
return {
43+
left,
44+
right,
45+
type: 'BinaryExpression',
46+
operator: operator as babel.BinaryExpression['operator'],
47+
};
48+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { type Call, type SafeCall } from '@angular/compiler';
2+
import type * as babel from '@babel/types';
3+
4+
import { type Transformer } from './transform.ts';
5+
import { isOptionalObjectOrCallee } from './utilities.ts';
6+
7+
const callOptions = { optional: false } as const;
8+
const safeCallOptions = { optional: true } as const;
9+
10+
type VisitorCall = {
11+
node: Call;
12+
options: typeof callOptions;
13+
};
14+
type VisitorSafeCall = {
15+
node: SafeCall;
16+
options: typeof safeCallOptions;
17+
};
18+
19+
const transformCall =
20+
<Visitor extends VisitorCall | VisitorSafeCall>({
21+
optional,
22+
}: Visitor['options']) =>
23+
(
24+
node: Visitor['node'],
25+
transformer: Transformer,
26+
): babel.CallExpression | babel.OptionalCallExpression => {
27+
const arguments_ = transformer.transformChildren<babel.Expression>(
28+
node.args,
29+
);
30+
const callee = transformer.transformChild<babel.Expression>(node.receiver);
31+
32+
if (optional || isOptionalObjectOrCallee(callee)) {
33+
return {
34+
type: 'OptionalCallExpression',
35+
callee,
36+
arguments: arguments_,
37+
optional,
38+
};
39+
}
40+
41+
return { type: 'CallExpression', callee, arguments: arguments_ };
42+
};
43+
44+
export const visitCall = transformCall<VisitorCall>(callOptions);
45+
export const visitSafeCall = transformCall<VisitorSafeCall>(safeCallOptions);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { type Chain } from '@angular/compiler';
2+
import type * as babel from '@babel/types';
3+
4+
import type { NGChainedExpression } from '../types.ts';
5+
import { type Transformer } from './transform.ts';
6+
7+
export const visitChain = (
8+
node: Chain,
9+
transformer: Transformer,
10+
): Omit<NGChainedExpression, 'start' | 'end' | 'range'> => ({
11+
type: 'NGChainedExpression',
12+
expressions: transformer.transformChildren<babel.Expression>(
13+
node.expressions,
14+
),
15+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { type Conditional } from '@angular/compiler';
2+
import type * as babel from '@babel/types';
3+
4+
import { type Transformer } from './transform.ts';
5+
6+
export const visitConditional = (
7+
node: Conditional,
8+
transformer: Transformer,
9+
): babel.ConditionalExpression => {
10+
const [test, consequent, alternate] =
11+
transformer.transformChildren<babel.Expression>([
12+
node.condition,
13+
node.trueExp,
14+
node.falseExp,
15+
]);
16+
17+
return {
18+
type: 'ConditionalExpression',
19+
test,
20+
consequent,
21+
alternate,
22+
};
23+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { type Interpolation } from '@angular/compiler';
2+
3+
import { type Transformer } from './transform.ts';
4+
5+
export const visitInterpolation = (
6+
node: Interpolation,
7+
transformer: Transformer,
8+
) => {
9+
const { expressions } = node;
10+
11+
/* c8 ignore next 3 @preserve */
12+
if (expressions.length !== 1) {
13+
throw new Error("Unexpected 'Interpolation'");
14+
}
15+
16+
return transformer.transformChild(expressions[0]);
17+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {
2+
type LiteralPrimitive,
3+
type RegularExpressionLiteral,
4+
} from '@angular/compiler';
5+
import type * as babel from '@babel/types';
6+
7+
export const visitLiteralPrimitive = (
8+
node: LiteralPrimitive,
9+
):
10+
| babel.BooleanLiteral
11+
| babel.NumericLiteral
12+
| babel.NullLiteral
13+
| babel.StringLiteral
14+
| babel.Identifier => {
15+
const { value } = node;
16+
switch (typeof value) {
17+
case 'boolean':
18+
return { type: 'BooleanLiteral', value };
19+
case 'number':
20+
return { type: 'NumericLiteral', value };
21+
case 'object':
22+
return { type: 'NullLiteral' };
23+
case 'string':
24+
return { type: 'StringLiteral', value };
25+
case 'undefined':
26+
return { type: 'Identifier', name: 'undefined' };
27+
/* c8 ignore next 4 */
28+
default:
29+
throw new Error(
30+
`Unexpected 'LiteralPrimitive' value type ${typeof value}`,
31+
);
32+
}
33+
};
34+
35+
export const visitRegularExpressionLiteral = (
36+
node: RegularExpressionLiteral,
37+
): babel.RegExpLiteral => ({
38+
type: 'RegExpLiteral',
39+
pattern: node.body,
40+
flags: node.flags ?? '',
41+
});

0 commit comments

Comments
 (0)