Skip to content

Commit 6214376

Browse files
authored
Merge pull request #147 from Siubaak/claude/fix-issue-143-Z4efM
2 parents b1956c4 + 5ce456a commit 6214376

File tree

3 files changed

+138
-3
lines changed

3 files changed

+138
-3
lines changed

src/evaluate/expression.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,32 @@ export function* ConditionalExpression(node: acorn.ConditionalExpression, scope:
323323
: (yield* evaluate(node.alternate, scope))
324324
}
325325

326+
function getCalleeDesc(node: acorn.Expression | acorn.Super): string {
327+
if (node.type === 'Identifier') {
328+
return (node as acorn.Identifier).name
329+
} else if (node.type === 'MemberExpression') {
330+
const memberNode = node as acorn.MemberExpression
331+
const objDesc = getCalleeDesc(memberNode.object)
332+
if (!memberNode.computed) {
333+
if (memberNode.property.type === 'PrivateIdentifier') {
334+
return `${objDesc}.#${(memberNode.property as acorn.PrivateIdentifier).name}`
335+
}
336+
return `${objDesc}.${(memberNode.property as acorn.Identifier).name}`
337+
}
338+
const prop = memberNode.property as acorn.Expression
339+
if (prop.type === 'Literal') {
340+
return `${objDesc}[${(prop as acorn.Literal).raw}]`
341+
}
342+
if (prop.type === 'Identifier') {
343+
return `${objDesc}[${(prop as acorn.Identifier).name}]`
344+
}
345+
return objDesc
346+
} else if (node.type === 'Super') {
347+
return 'super'
348+
}
349+
return '(intermediate value)'
350+
}
351+
326352
export function* CallExpression(node: acorn.CallExpression, scope: Scope) {
327353
let func: any
328354
let object: any
@@ -371,9 +397,11 @@ export function* CallExpression(node: acorn.CallExpression, scope: Scope) {
371397
}
372398

373399
if (typeof func !== 'function') {
374-
throw new TypeError(`${key} is not a function`)
400+
const calleeDesc = getCalleeDesc(node.callee as acorn.MemberExpression)
401+
throw new TypeError(`${calleeDesc} is not a function`)
375402
} else if (CLSCTOR in func) {
376-
throw new TypeError(`Class constructor ${key} cannot be invoked without 'new'`)
403+
const calleeDesc = getCalleeDesc(node.callee as acorn.MemberExpression)
404+
throw new TypeError(`Class constructor ${calleeDesc} cannot be invoked without 'new'`)
377405
}
378406
} else {
379407
func = yield* evaluate(node.callee, scope)

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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,4 +530,80 @@ describe('testing src/expression.ts', () => {
530530
`)
531531
expect(interpreter.exports.sawAssignmentError).toBe(true)
532532
})
533+
534+
it('should include full property path in TypeError for non-function member calls (issue #143)', () => {
535+
// simple member expression: foo.bar is not a function
536+
const interp1 = new Sval()
537+
interp1.run(`
538+
const foo = { bar: 42 }
539+
try {
540+
foo.bar()
541+
} catch (e) {
542+
exports.err = e
543+
}
544+
`)
545+
expect(interp1.exports.err).toBeInstanceOf(TypeError)
546+
expect(interp1.exports.err.message).toBe('foo.bar is not a function')
547+
548+
// nested member expression: foo.bar.baz is not a function
549+
const interp2 = new Sval()
550+
interp2.run(`
551+
const foo = { bar: { baz: 42 } }
552+
try {
553+
foo.bar.baz()
554+
} catch (e) {
555+
exports.err = e
556+
}
557+
`)
558+
expect(interp2.exports.err).toBeInstanceOf(TypeError)
559+
expect(interp2.exports.err.message).toBe('foo.bar.baz is not a function')
560+
561+
// computed member expression: foo[0] is not a function
562+
const interp3 = new Sval()
563+
interp3.run(`
564+
const foo = [42]
565+
try {
566+
foo[0]()
567+
} catch (e) {
568+
exports.err = e
569+
}
570+
`)
571+
expect(interp3.exports.err).toBeInstanceOf(TypeError)
572+
expect(interp3.exports.err.message).toBe('foo[0] is not a function')
573+
})
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+
})
533609
})

0 commit comments

Comments
 (0)