Skip to content

Commit 430d2d7

Browse files
authored
refactor: extract parser from utils.ts (#272)
1 parent 09e4ddf commit 430d2d7

File tree

5 files changed

+156
-148
lines changed

5 files changed

+156
-148
lines changed

src/index.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,7 @@ import { Context } from './context.js';
44
import { transform } from './transform.js';
55
import { transformTemplateBindings } from './transform-microsyntax.js';
66
import type { NGNode, RawNGComment, NGMicrosyntax } from './types.js';
7-
import {
8-
parseNgAction,
9-
parseNgBinding,
10-
parseNgInterpolationExpression,
11-
parseNgSimpleBinding,
12-
parseNgTemplateBindings,
13-
} from './utils.js';
7+
import * as angularParser from './parser.js';
148

159
function createParser(
1610
parse: (input: string) => { ast: ng.AST; comments: RawNGComment[] },
@@ -26,12 +20,17 @@ function createParser(
2620
};
2721
}
2822

29-
export const parseBinding = createParser(parseNgBinding);
30-
export const parseSimpleBinding = createParser(parseNgSimpleBinding);
23+
export const parseBinding = createParser(angularParser.parseBinding);
24+
export const parseSimpleBinding = createParser(
25+
angularParser.parseSimpleBinding,
26+
);
3127
export const parseInterpolationExpression = createParser(
32-
parseNgInterpolationExpression,
28+
angularParser.parseInterpolationExpression,
3329
);
34-
export const parseAction = createParser(parseNgAction);
30+
export const parseAction = createParser(angularParser.parseAction);
3531
export const parseTemplateBindings = (input: string): NGMicrosyntax =>
36-
transformTemplateBindings(parseNgTemplateBindings(input), new Context(input));
32+
transformTemplateBindings(
33+
angularParser.parseTemplateBindings(input),
34+
new Context(input),
35+
);
3736
export type { NGMicrosyntax, NGNode };

src/parser.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {
2+
Lexer,
3+
Parser,
4+
type ASTWithSource,
5+
type Interpolation,
6+
type ParserError,
7+
} from '@angular/compiler';
8+
import { type RawNGComment } from './types.js';
9+
10+
const NG_PARSE_FAKE_LOCATION = 'angular-estree-parser';
11+
const NG_PARSE_TEMPLATE_BINDINGS_FAKE_PREFIX = 'NgEstreeParser';
12+
const NG_PARSE_FAKE_ABSOLUTE_OFFSET = 0;
13+
const NG_PARSE_SHARED_PARAMS: readonly [string, number] = [
14+
NG_PARSE_FAKE_LOCATION,
15+
NG_PARSE_FAKE_ABSOLUTE_OFFSET,
16+
];
17+
18+
function createParser() {
19+
return new Parser(new Lexer());
20+
}
21+
22+
function parse(
23+
text: string,
24+
parse: (text: string, parser: Parser) => ASTWithSource,
25+
) {
26+
const parser = createParser();
27+
const { text: textToParse, comments } = extractComments(text, parser);
28+
const { ast, errors } = parse(textToParse, parser);
29+
assertAstErrors(errors);
30+
return { ast, comments };
31+
}
32+
33+
function parseBinding(text: string) {
34+
return parse(text, (text, parser) =>
35+
parser.parseBinding(text, ...NG_PARSE_SHARED_PARAMS),
36+
);
37+
}
38+
39+
function parseSimpleBinding(text: string) {
40+
return parse(text, (text, parser) =>
41+
parser.parseSimpleBinding(text, ...NG_PARSE_SHARED_PARAMS),
42+
);
43+
}
44+
45+
function parseAction(text: string) {
46+
return parse(text, (text, parser) =>
47+
parser.parseAction(text, false, ...NG_PARSE_SHARED_PARAMS),
48+
);
49+
}
50+
51+
function parseInterpolationExpression(text: string) {
52+
return parse(text, (text, parser) => {
53+
const result = parser.parseInterpolationExpression(
54+
text,
55+
...NG_PARSE_SHARED_PARAMS,
56+
);
57+
result.ast = (result.ast as Interpolation).expressions[0];
58+
return result;
59+
});
60+
}
61+
62+
function parseTemplateBindings(text: string) {
63+
const parser = createParser();
64+
const { templateBindings: ast, errors } = parser.parseTemplateBindings(
65+
NG_PARSE_TEMPLATE_BINDINGS_FAKE_PREFIX,
66+
text,
67+
NG_PARSE_FAKE_LOCATION,
68+
NG_PARSE_FAKE_ABSOLUTE_OFFSET,
69+
NG_PARSE_FAKE_ABSOLUTE_OFFSET,
70+
);
71+
assertAstErrors(errors);
72+
return ast;
73+
}
74+
75+
function assertAstErrors(errors: ParserError[]) {
76+
if (errors.length !== 0) {
77+
const [{ message }] = errors;
78+
throw new SyntaxError(
79+
message.replace(/^Parser Error: | at column \d+ in [^]*$/g, ''),
80+
);
81+
}
82+
}
83+
84+
function extractComments(
85+
text: string,
86+
parser: Parser,
87+
): { text: string; comments: RawNGComment[] } {
88+
// @ts-expect-error -- need to call private _commentStart
89+
const getCommentStart = parser._commentStart;
90+
const commentStart: number | null = getCommentStart(text);
91+
return commentStart === null
92+
? { text, comments: [] }
93+
: {
94+
text: text.slice(0, commentStart),
95+
comments: [
96+
{
97+
type: 'Comment',
98+
value: text.slice(commentStart + '//'.length),
99+
sourceSpan: { start: commentStart, end: text.length },
100+
},
101+
],
102+
};
103+
}
104+
105+
export {
106+
NG_PARSE_TEMPLATE_BINDINGS_FAKE_PREFIX,
107+
parseBinding,
108+
parseSimpleBinding,
109+
parseAction,
110+
parseInterpolationExpression,
111+
parseTemplateBindings,
112+
};

src/transform-microsyntax.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,8 @@ import type {
2121
NGNode,
2222
RawNGSpan,
2323
} from './types.js';
24-
import {
25-
NG_PARSE_TEMPLATE_BINDINGS_FAKE_PREFIX,
26-
toLowerCamelCase,
27-
} from './utils.js';
24+
import { NG_PARSE_TEMPLATE_BINDINGS_FAKE_PREFIX } from './parser.js';
25+
import { toLowerCamelCase } from './utils.js';
2826

2927
export function transformTemplateBindings(
3028
rawTemplateBindings: ng.TemplateBinding[],

src/utils.ts

Lines changed: 0 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,6 @@
11
import * as ng from '@angular/compiler';
2-
import { Lexer, Parser } from '@angular/compiler';
32
import type { RawNGComment, RawNGSpan } from './types.js';
43

5-
const NG_PARSE_FAKE_LOCATION = 'angular-estree-parser';
6-
export const NG_PARSE_TEMPLATE_BINDINGS_FAKE_PREFIX = 'NgEstreeParser';
7-
const NG_PARSE_FAKE_ABSOLUTE_OFFSET = 0;
8-
const NG_PARSE_SHARED_PARAMS: readonly [string, number] = [
9-
NG_PARSE_FAKE_LOCATION,
10-
NG_PARSE_FAKE_ABSOLUTE_OFFSET,
11-
];
12-
13-
function createNgParser() {
14-
return new Parser(new Lexer());
15-
}
16-
17-
function parseNg(
18-
input: string,
19-
parse: (astInput: string, ngParser: Parser) => ng.ASTWithSource,
20-
) {
21-
const ngParser = createNgParser();
22-
const { astInput, comments } = extractComments(input, ngParser);
23-
const { ast, errors } = parse(astInput, ngParser);
24-
assertAstErrors(errors);
25-
return { ast, comments };
26-
}
27-
28-
export function parseNgBinding(input: string) {
29-
return parseNg(input, (astInput, ngParser) =>
30-
ngParser.parseBinding(astInput, ...NG_PARSE_SHARED_PARAMS),
31-
);
32-
}
33-
34-
export function parseNgSimpleBinding(input: string) {
35-
return parseNg(input, (astInput, ngParser) =>
36-
ngParser.parseSimpleBinding(astInput, ...NG_PARSE_SHARED_PARAMS),
37-
);
38-
}
39-
40-
export function parseNgAction(input: string) {
41-
return parseNg(input, (astInput, ngParser) =>
42-
ngParser.parseAction(astInput, false, ...NG_PARSE_SHARED_PARAMS),
43-
);
44-
}
45-
46-
export function parseNgInterpolationExpression(input: string) {
47-
return parseNg(input, (astInput, ngParser) => {
48-
const result = ngParser.parseInterpolationExpression(
49-
astInput,
50-
...NG_PARSE_SHARED_PARAMS,
51-
);
52-
result.ast = (result.ast as ng.Interpolation).expressions[0];
53-
return result;
54-
});
55-
}
56-
57-
export function parseNgTemplateBindings(input: string) {
58-
const ngParser = createNgParser();
59-
const { templateBindings: ast, errors } = ngParser.parseTemplateBindings(
60-
NG_PARSE_TEMPLATE_BINDINGS_FAKE_PREFIX,
61-
input,
62-
NG_PARSE_FAKE_LOCATION,
63-
NG_PARSE_FAKE_ABSOLUTE_OFFSET,
64-
NG_PARSE_FAKE_ABSOLUTE_OFFSET,
65-
);
66-
assertAstErrors(errors);
67-
return ast;
68-
}
69-
70-
function assertAstErrors(errors: ng.ParserError[]) {
71-
if (errors.length !== 0) {
72-
const [{ message }] = errors;
73-
throw new SyntaxError(
74-
message.replace(/^Parser Error: | at column \d+ in [^]*$/g, ''),
75-
);
76-
}
77-
}
78-
79-
function extractComments(
80-
input: string,
81-
ngParser: Parser,
82-
): { astInput: string; comments: RawNGComment[] } {
83-
// @ts-expect-error need to call private _commentStart
84-
const commentStart: number | null = ngParser._commentStart(input);
85-
return commentStart === null
86-
? { astInput: input, comments: [] }
87-
: {
88-
astInput: input.slice(0, commentStart),
89-
comments: [
90-
{
91-
type: 'Comment',
92-
value: input.slice(commentStart + '//'.length),
93-
sourceSpan: { start: commentStart, end: input.length },
94-
},
95-
],
96-
};
97-
}
98-
994
// prettier-ignore
1005
export function getNgType(node: (ng.AST | RawNGComment) & { type?: string }) {
1016
if (node instanceof ng.Unary) { return 'Unary'; }

tests/transform.test.ts

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
11
import type * as ng from '@angular/compiler';
22
import type * as b from '@babel/types';
3-
import {
4-
parseAction,
5-
parseBinding,
6-
parseInterpolationExpression,
7-
parseSimpleBinding,
8-
} from '../src/index.js';
3+
import * as estreeParser from '../src/index.js';
94
import type { NGNode } from '../src/types.js';
10-
import {
11-
getNgType,
12-
parseNgAction,
13-
parseNgBinding,
14-
parseNgInterpolationExpression,
15-
parseNgSimpleBinding,
16-
} from '../src/utils.js';
5+
import { getNgType } from '../src/utils.js';
6+
import * as angularParser from '../src/parser.js';
177
import {
188
massageAst,
199
parseBabel,
@@ -22,7 +12,7 @@ import {
2212
} from './helpers.js';
2313

2414
describe.each`
25-
beforeType | afterType | input | action | binding | simple | interpolation
15+
angularType | estreeType | input | action | binding | simple | interpolation
2616
${'Binary'} | ${'BinaryExpression'} | ${' 0 - 1 '} | ${true} | ${true} | ${true} | ${true}
2717
${'Binary'} | ${'LogicalExpression'} | ${' a && b '} | ${true} | ${true} | ${true} | ${true}
2818
${'Binary'} | ${'LogicalExpression'} | ${' a ?? b '} | ${true} | ${true} | ${true} | ${true}
@@ -87,56 +77,60 @@ describe.each`
8777
${'Call'} | ${'OptionalCallExpression'} | ${' a ?. b ( ) '} | ${true} | ${true} | ${true} | ${true}
8878
${'SafeCall'} | ${'OptionalCallExpression'} | ${' a ?. b ?. ( ) '} | ${true} | ${true} | ${true} | ${true}
8979
${'SafePropertyRead'} | ${'OptionalMemberExpression'} | ${' a ?. b '} | ${true} | ${true} | ${true} | ${true}
90-
`('$input ($beforeType -> $afterType)', (fields) => {
91-
const { beforeType, afterType, input } = fields;
80+
`('$input ($angularType -> $estreeType)', (fields) => {
81+
const { angularType, estreeType, input } = fields;
9282

93-
let beforeNode: ng.AST | null = null;
94-
let afterNode: NGNode | null = null;
83+
let angularNode: ng.AST | null = null;
84+
let estreeNode: NGNode | null = null;
9585

9686
const testSection = (
9787
section: Extract<keyof typeof fields, string>,
98-
parseBefore: (input: string) => { ast: ng.AST },
99-
parseAfter: (input: string) => NGNode,
88+
parseAngular: (input: string) => { ast: ng.AST },
89+
parseEstree: (input: string) => NGNode,
10090
) => {
10191
if (fields[section]) {
10292
test(`allowed in ${section}`, () => {
103-
expect(() => (beforeNode = parseBefore(input).ast)).not.toThrow();
104-
expect(() => (afterNode = parseAfter(input))).not.toThrow();
93+
expect(() => (angularNode = parseAngular(input).ast)).not.toThrow();
94+
expect(() => (estreeNode = parseEstree(input))).not.toThrow();
10595
});
10696
} else {
10797
test(`disallowed in ${section}`, () => {
108-
expect(() => parseBefore(input)).toThrow();
109-
expect(() => parseAfter(input)).toThrow();
98+
expect(() => parseAngular(input)).toThrow();
99+
expect(() => parseEstree(input)).toThrow();
110100
});
111101
}
112102
};
113103

114-
testSection('action', parseNgAction, parseAction);
115-
testSection('binding', parseNgBinding, parseBinding);
116-
testSection('simple', parseNgSimpleBinding, parseSimpleBinding);
104+
testSection('action', angularParser.parseAction, estreeParser.parseAction);
105+
testSection('binding', angularParser.parseBinding, estreeParser.parseBinding);
106+
testSection(
107+
'simple',
108+
angularParser.parseSimpleBinding,
109+
estreeParser.parseSimpleBinding,
110+
);
117111
testSection(
118112
'interpolation',
119-
parseNgInterpolationExpression,
120-
parseInterpolationExpression,
113+
angularParser.parseInterpolationExpression,
114+
estreeParser.parseInterpolationExpression,
121115
);
122116

123117
test('ast', () => {
124-
expect(beforeNode).not.toEqual(null);
125-
expect(afterNode).not.toEqual(null);
118+
expect(angularNode).not.toEqual(null);
119+
expect(estreeNode).not.toEqual(null);
126120

127-
expect(getNgType(beforeNode!)).toEqual(beforeType);
128-
expect(afterNode!.type).toEqual(afterType);
121+
expect(getNgType(angularNode!)).toEqual(angularType);
122+
expect(estreeNode!.type).toEqual(estreeType);
129123

130-
if (afterNode!.type.startsWith('NG')) {
131-
expect(snapshotAst(afterNode, input)).toMatchSnapshot();
124+
if (estreeNode!.type.startsWith('NG')) {
125+
expect(snapshotAst(estreeNode, input)).toMatchSnapshot();
132126
} else {
133127
try {
134-
expect(afterNode).toEqual(massageAst(parseBabelExpression(input)));
128+
expect(estreeNode).toEqual(massageAst(parseBabelExpression(input)));
135129
} catch {
136130
const { comments, program } = parseBabel(input);
137131
const statement = program.body[0] as b.ExpressionStatement;
138132
expect(statement.type).toEqual('ExpressionStatement');
139-
expect(massageAst(afterNode)).toEqual(
133+
expect(massageAst(estreeNode)).toEqual(
140134
massageAst({ ...statement.expression, comments }),
141135
);
142136
}

0 commit comments

Comments
 (0)