Skip to content

Commit de6dadf

Browse files
committed
Improve error handling
1 parent d214ebd commit de6dadf

2 files changed

Lines changed: 101 additions & 32 deletions

File tree

src/angular-parser.ts

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
type ASTWithSource,
33
Lexer,
4+
ParseError,
45
ParseLocation,
56
Parser,
67
ParseSourceFile,
@@ -11,16 +12,29 @@ import {
1112
import { type CommentLine } from './types.ts';
1213
import { sourceSpanToLocationInformation } from './utils.ts';
1314

14-
let parseSourceSpan: ParseSourceSpan;
15-
// https://github.com/angular/angular/blob/5e9707dc84e6590ec8c9d41e7d3be7deb2fa7c53/packages/compiler/test/expression_parser/utils/span.ts
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-
}
15+
const FILE_NAME = 'test.html';
16+
17+
type ParseContext = {
18+
text: string;
19+
file: ParseSourceFile;
20+
start: ParseLocation;
21+
end: ParseLocation;
22+
sourceSpan: ParseSourceSpan;
23+
};
2224

23-
return parseSourceSpan;
25+
// https://github.com/angular/angular/blob/5e9707dc84e6590ec8c9d41e7d3be7deb2fa7c53/packages/compiler/test/expression_parser/utils/span.ts
26+
function getParseSourceSpan(text: string): ParseContext {
27+
const file = new ParseSourceFile(text, FILE_NAME);
28+
const start = new ParseLocation(file, 0, 0, 0);
29+
const end = start.moveBy(text.length);
30+
const sourceSpan = new ParseSourceSpan(start, end);
31+
return {
32+
text,
33+
file,
34+
start,
35+
end,
36+
sourceSpan,
37+
};
2438
}
2539

2640
let parser: Parser;
@@ -53,15 +67,50 @@ function extractComments(text: string) {
5367

5468
function throwErrors<
5569
ResultType extends ASTWithSource | TemplateBindingParseResult,
56-
>(result: ResultType) {
70+
>(
71+
parseResult: ParseContext & {
72+
result: ResultType;
73+
comments: CommentLine[];
74+
},
75+
) {
76+
const { result } = parseResult;
77+
5778
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-
);
79+
const [originalError] = result.errors;
80+
81+
/* c8 ignore next 3 @preserve */
82+
if (!(originalError instanceof ParseError)) {
83+
throw originalError;
84+
}
85+
86+
let { message } = originalError;
87+
{
88+
const match = message.match(/ in .*?@\d+:\d+$/);
89+
if (match) {
90+
message = message.slice(0, match.index);
91+
}
92+
}
93+
94+
let location = parseResult.start;
95+
{
96+
const match = message.match(/at column (?<index>\d+)/);
97+
if (match) {
98+
message = message.slice(0, match.index);
99+
location = location.moveBy(Number(match.groups!.index));
100+
}
101+
}
102+
103+
const error = new SyntaxError(message.trim(), { cause: originalError });
104+
Object.assign({
105+
cause: originalError,
106+
location,
107+
span: originalError.span,
108+
});
109+
110+
throw error;
62111
}
63112

64-
return result;
113+
return parseResult;
65114
}
66115

67116
const createAstParser =
@@ -72,24 +121,32 @@ const createAstParser =
72121
| 'parseAction'
73122
| 'parseInterpolationExpression',
74123
) =>
75-
(text: string) => ({
76-
result: throwErrors<ASTWithSource>(
77-
getParser()[name](text, getParseSourceSpan(), 0),
78-
),
79-
text,
80-
comments: extractComments(text),
81-
});
124+
(text: string) => {
125+
const context = getParseSourceSpan(text);
126+
return throwErrors<ASTWithSource>({
127+
...context,
128+
result: getParser()[name](text, context.sourceSpan, 0),
129+
comments: extractComments(text),
130+
});
131+
};
82132

83133
export const parseAction = createAstParser('parseAction');
84134
export const parseBinding = createAstParser('parseBinding');
85135
export const parseSimpleBinding = createAstParser('parseSimpleBinding');
86136
export const parseInterpolationExpression = createAstParser(
87137
'parseInterpolationExpression',
88138
);
89-
export const parseTemplateBindings = (text: string) => ({
90-
result: throwErrors<TemplateBindingParseResult>(
91-
getParser().parseTemplateBindings('', text, getParseSourceSpan(), 0, 0),
92-
),
93-
text,
94-
comments: [],
95-
});
139+
export const parseTemplateBindings = (text: string) => {
140+
const context = getParseSourceSpan(text);
141+
return throwErrors<TemplateBindingParseResult>({
142+
...context,
143+
result: getParser().parseTemplateBindings(
144+
'',
145+
text,
146+
context.sourceSpan,
147+
0,
148+
0,
149+
),
150+
comments: [],
151+
});
152+
};

tests/error.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1-
import { parseBinding } from '../src/index.js';
1+
import { parseBinding, parseTemplateBindings } from '../src/index.js';
22

33
test('error message', () => {
4-
expect(() => parseBinding('a b c')).toThrowErrorMatchingInlineSnapshot(
5-
`[SyntaxError: Unexpected token 'b']`,
4+
expect(() => parseBinding('a\nb c')).toThrowErrorMatchingInlineSnapshot(
5+
`[SyntaxError: Parser Error: Unexpected token 'b']`,
6+
);
7+
expect(() => parseBinding('---')).toThrowErrorMatchingInlineSnapshot(
8+
`[SyntaxError: Parser Error: Unexpected end of expression: --- at the end of the expression [---]]`,
9+
);
10+
expect(() =>
11+
parseTemplateBindings('---\n'),
12+
).toThrowErrorMatchingInlineSnapshot(
13+
`
14+
[SyntaxError: Parser Error: Unexpected end of expression: ---
15+
at the end of the expression [---
16+
]]
17+
`,
618
);
719
});

0 commit comments

Comments
 (0)