import { describe, it, expect } from 'vitest'; import { parse } from './parser'; import { buildSymbolTable } from './symbols'; function build(src: string) { const r = parse(src); return { ...r, table: buildSymbolTable(r) }; } describe('symbols — declarations and usages', () => { it('captures let declarations', () => { const { table } = build('let x = 1; x + 1;'); const x = table.allDecls.find((d) => d.name === 'x')!; expect(x.kind).toBe('let'); expect(table.usages.find((u) => u.name === 'x')!.resolved).toBe(x); }); it('records fn signatures for completion detail', () => { const { table } = build('fn process(order, user) { order }'); const fn = table.allDecls.find((d) => d.name === 'process')!; expect(fn.kind).toBe('fn'); expect(fn.signature).toBe('process(order, user)'); }); it('hoists fn declarations: calls above the decl resolve', () => { const { table } = build('greet("world"); fn greet(s) { s }'); const u = table.usages.find((u) => u.name === 'greet')!; expect(u.resolved?.kind).toBe('fn'); }); it('function bodies do not see outer locals', () => { const { table } = build(` let outer = 1; fn f() { outer } `); const outerUse = table.usages.find((u) => u.name === 'outer')!; expect(outerUse.resolved).toBeNull(); }); it('function bodies do see outer fn declarations', () => { const { table } = build(` fn helper() { 1 } fn caller() { helper() } `); const helperUse = table.usages.find((u) => u.name === 'helper' && u.range.start > 30)!; expect(helperUse.resolved?.kind).toBe('fn'); }); it('captures function parameters in their body scope', () => { const { table } = build('fn f(a, b) { a + b }'); const a = table.allDecls.find((d) => d.name === 'a')!; expect(a.kind).toBe('param'); const useOfA = table.usages.find((u) => u.name === 'a')!; expect(useOfA.resolved).toBe(a); }); it('captures for-loop binders', () => { const { table } = build('for item in [1, 2, 3] { item }'); const item = table.allDecls.find((d) => d.name === 'item')!; expect(item.kind).toBe('for'); }); it('respects forward-declaration: cannot use a let before its decl', () => { const { table } = build('x; let x = 1;'); const earlyUse = table.usages.find((u) => u.name === 'x' && u.range.start < 5)!; expect(earlyUse.resolved).toBeNull(); }); }); describe('symbols — object-literal field maps', () => { it('records fields of an object-map literal initializer', () => { const { table } = build('let order = #{ id: 1, total: 5 };'); const order = table.allDecls.find((d) => d.name === 'order')!; expect(order.objectFields).toEqual(['id', 'total']); }); it('objectFieldsOf returns the set after the declaration', () => { const src = 'let order = #{ id: 1 }; order.id'; const { table } = build(src); const afterDecl = src.indexOf('order.id') + 'order.'.length; expect(table.objectFieldsOf('order', afterDecl)).toEqual(['id']); }); }); describe('symbols — completion + navigation helpers', () => { it('scopeCompletions surfaces in-scope locals and hoisted fns', () => { const src = ` let outer = 1; fn process(order) { order } `; const { table } = build(src); const insideFn = src.indexOf('order\n'); const names = table.scopeCompletions(insideFn).map((d) => d.name); expect(names).toContain('order'); expect(names).toContain('process'); // outer is not visible from inside `fn process`. expect(names).not.toContain('outer'); }); it('declOfUsageAt resolves a usage to its declaration', () => { const src = 'fn process(o) { o } process(1)'; const { table } = build(src); const callPos = src.lastIndexOf('process'); const d = table.declOfUsageAt(callPos)!; expect(d.name).toBe('process'); expect(d.kind).toBe('fn'); }); it('usagesOf collects declaration + every reference', () => { const src = 'fn process(o) { o } process(1); process(2);'; const { table } = build(src); const fn = table.allDecls.find((d) => d.name === 'process')!; const all = table.usagesOf(fn); // 1 decl name + 2 call sites = 3 ranges expect(all).toHaveLength(3); }); });