Skip to content

Commit 05de340

Browse files
authored
Show codeframe with location of fluent parse errors (#25)
1 parent 88e4204 commit 05de340

File tree

2 files changed

+69
-4
lines changed

2 files changed

+69
-4
lines changed

__tests__/frameworks/vite/errors.spec.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,18 @@ describe('Error checking', () => {
3333
await expect(code).rejects.toThrowErrorMatchingInlineSnapshot(`
3434
"Fluent parse errors:
3535
E0003: Expected token: \\"}\\" (2:31)
36-
E0010: Expected one of the variants to be marked as default (*) (9:3)"
36+
1 | # Simple things are simple.
37+
2 | hello-user = Hello, {$userName!
38+
| ^
39+
3 |
40+
4 | # Complex things are possible.
41+
E0010: Expected one of the variants to be marked as default (*) (9:3)
42+
7 | [one] added one photo
43+
8 | [other] added {$photoCount} new photo
44+
9 | }to {$userGender ->
45+
| ^
46+
10 | [male] his stream
47+
11 | [female] her stream"
3748
`)
3849
})
3950

@@ -53,7 +64,18 @@ describe('Error checking', () => {
5364
await expect(code).rejects.toThrowErrorMatchingInlineSnapshot(`
5465
"Fluent parse errors:
5566
E0003: Expected token: \\"}\\" (2:31)
56-
E0010: Expected one of the variants to be marked as default (*) (9:3)"
67+
1 | # Simple things are simple.
68+
2 | hello-user = Hello, {$userName!
69+
| ^
70+
3 |
71+
4 | # Complex things are possible.
72+
E0010: Expected one of the variants to be marked as default (*) (9:3)
73+
7 | [one] added one photo
74+
8 | [other] added {$photoCount} new photo
75+
9 | }to {$userGender ->
76+
| ^
77+
10 | [male] his stream
78+
11 | [female] her stream"
5779
`)
5880
})
5981
})

src/plugins/ftl/parse.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,58 @@
11
import type { Junk } from '@fluent/syntax'
22
import { columnOffset, lineOffset, parse } from '@fluent/syntax'
33

4-
export function getSyntaxErrors(source: string): string | undefined {
4+
function padRight(str: string | number, len: number) {
5+
return str + ' '.repeat(len - String(str).length)
6+
}
7+
8+
const RANGE = 2
9+
10+
/**
11+
* Generate a string that highlights the position of the error in the source
12+
* @param source The source string
13+
* @param line The line number of the error (1-indexed)
14+
* @param column The column number of the error (1-indexed)
15+
* Example:
16+
* | proper-key = Value
17+
* | key-with-error = error {->
18+
* | ^
19+
* | continuation = Value
20+
*/
21+
export function generateCodeFrame(
22+
source: string,
23+
line: number,
24+
column: number,
25+
): string {
26+
const lines = source.split(/\r?\n/)
27+
const start = Math.max(line - RANGE - 1, 0)
28+
const end = Math.min(lines.length, line + RANGE)
29+
30+
const result = []
31+
32+
const lineNumberLength = String(end).length
33+
34+
for (let i = start; i < end; i++) {
35+
result.push(`${padRight(i + 1, lineNumberLength)} | ${lines[i]}`)
36+
37+
if (i + 1 === line)
38+
result.push(`${padRight(' ', lineNumberLength)} | ${' '.repeat(column - 1)}^`)
39+
}
40+
41+
return result.join('\n')
42+
}
43+
44+
export function getSyntaxErrors(
45+
source: string,
46+
): string | undefined {
547
const parsed = parse(source, { withSpans: true })
648
const junks = parsed.body.filter(x => x.type === 'Junk') as Junk[]
749
const errors = junks.map(x => x.annotations).flat()
850
if (errors.length > 0) {
951
const errorsText = errors.map((x) => {
1052
const line = lineOffset(source, x.span.start) + 1
1153
const column = columnOffset(source, x.span.start) + 1
12-
return ` ${x.code}: ${x.message} (${line}:${column})`
54+
return ` ${x.code}: ${x.message} (${line}:${column})
55+
${generateCodeFrame(source, line, column)}`
1356
}).join('\n')
1457

1558
return `Fluent parse errors:\n${errorsText}`

0 commit comments

Comments
 (0)