Skip to content

Commit 5ce456a

Browse files
committed
fix: improve SyntaxError messages to include identifier name (issue #143)
When acorn reports "Unexpected token" for code like `async fetch('/url')` or `foo bar`, rewrite the message to "Unexpected identifier 'fetch'" / "Unexpected identifier 'bar'", matching Node.js/browser behavior. https://claude.ai/code/session_012YWgUtQyEApgN1EQAntvHo
1 parent d7006ac commit 5ce456a

File tree

2 files changed

+67
-1
lines changed

2 files changed

+67
-1
lines changed

src/index.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,33 @@ export interface SvalOptions {
1818

1919
const latestVer = 15
2020

21+
function improveSyntaxError(err: SyntaxError & { pos?: number }, code: string): SyntaxError {
22+
if (typeof err.pos !== 'number' || !err.message.startsWith('Unexpected token')) return err
23+
const pos = err.pos
24+
const ch = pos < code.length ? code[pos] : undefined
25+
26+
let ident: string | null = null
27+
28+
if (ch !== undefined && /[a-zA-Z_$]/.test(ch)) {
29+
// error position is at the start of an identifier
30+
const m = code.slice(pos).match(/^[a-zA-Z_$][a-zA-Z0-9_$]*/)
31+
if (m) ident = m[0]
32+
} else if (ch === undefined || ch === '(') {
33+
// end of input or '(' — look backwards for a preceding identifier
34+
let end = pos
35+
while (end > 0 && /\s/.test(code[end - 1])) end--
36+
if (end > 0 && /[a-zA-Z0-9_$]/.test(code[end - 1])) {
37+
let start = end
38+
while (start > 0 && /[a-zA-Z0-9_$]/.test(code[start - 1])) start--
39+
const candidate = code.slice(start, end)
40+
if (/^[a-zA-Z_$]/.test(candidate)) ident = candidate
41+
}
42+
}
43+
44+
if (ident) return new SyntaxError(`Unexpected identifier '${ident}'`)
45+
return err
46+
}
47+
2148
class Sval {
2249
static version: string = PkgJson.version
2350

@@ -83,7 +110,11 @@ class Sval {
83110
if (typeof parser === 'function') {
84111
return parser(code, this.options)
85112
}
86-
return parse(code, this.options)
113+
try {
114+
return parse(code, this.options)
115+
} catch (err) {
116+
throw improveSyntaxError(err as SyntaxError & { pos?: number }, code)
117+
}
87118
}
88119

89120
run(code: string | Node) {

tests/expression.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,4 +571,39 @@ describe('testing src/expression.ts', () => {
571571
expect(interp3.exports.err).toBeInstanceOf(TypeError)
572572
expect(interp3.exports.err.message).toBe('foo[0] is not a function')
573573
})
574+
575+
it('should include identifier name in SyntaxError for invalid async usage (issue #143)', () => {
576+
// async followed by identifier that is not a valid async arrow → Unexpected identifier 'name'
577+
const sval1 = new Sval()
578+
let err1: any
579+
try {
580+
sval1.run("async fetch('/url')")
581+
} catch (e) {
582+
err1 = e
583+
}
584+
expect(err1).toBeInstanceOf(SyntaxError)
585+
expect(err1.message).toBe("Unexpected identifier 'fetch'")
586+
587+
// async followed by bare identifier (no call, just end of input)
588+
const sval2 = new Sval()
589+
let err2: any
590+
try {
591+
sval2.run('async foo')
592+
} catch (e) {
593+
err2 = e
594+
}
595+
expect(err2).toBeInstanceOf(SyntaxError)
596+
expect(err2.message).toBe("Unexpected identifier 'foo'")
597+
598+
// two identifiers in a row (not async-specific)
599+
const sval3 = new Sval()
600+
let err3: any
601+
try {
602+
sval3.run('foo bar')
603+
} catch (e) {
604+
err3 = e
605+
}
606+
expect(err3).toBeInstanceOf(SyntaxError)
607+
expect(err3.message).toBe("Unexpected identifier 'bar'")
608+
})
574609
})

0 commit comments

Comments
 (0)