import { describe, expect, it } from "vitest";
import { extractSymbols } from "../src/code-query/symbols.js";
describe("extractSymbols", () => {
it("finds top-level function and class declarations in TS", async () => {
const source = `
export function add(a: number, b: number): number {
return a + b;
}
export class Calculator {
total = 0;
add(n: number): void {
this.total += n;
}
}
`;
const symbols = await extractSymbols("calc.ts", source);
const names = symbols.map((s) => `${s.kind}:${s.name}${s.parent ? `@${s.parent}` : ""}`);
expect(names).toContain("function:add");
expect(names).toContain("class:Calculator");
expect(names).toContain("property:total@Calculator");
expect(names).toContain("method:add@Calculator");
});
it("extracts interfaces, type aliases, and enums", async () => {
const source = `
interface User { id: string; }
type ID = string | number;
enum Color { Red, Green, Blue }
`;
const symbols = await extractSymbols("types.ts", source);
const byKind = (k: string) => symbols.filter((s) => s.kind === k).map((s) => s.name);
expect(byKind("interface")).toEqual(["User"]);
expect(byKind("type")).toEqual(["ID"]);
expect(byKind("enum")).toEqual(["Color"]);
});
it("treats arrow-function variables as functions", async () => {
const source = `
const greet = (name: string): string => "hi " + name;
const noop = function () {};
const value = 42;
`;
const symbols = await extractSymbols("vars.ts", source);
const names = symbols.filter((s) => s.kind === "function").map((s) => s.name);
expect(names).toContain("greet");
expect(names).toContain("noop");
expect(names).not.toContain("value");
});
it("returns symbols in source order with 1-based positions", async () => {
const source = "function a(){}\nfunction b(){}\nfunction c(){}\n";
const symbols = await extractSymbols("order.ts", source);
expect(symbols.map((s) => s.name)).toEqual(["a", "b", "c"]);
expect(symbols[0]!.line).toBe(1);
expect(symbols[1]!.line).toBe(2);
expect(symbols[2]!.line).toBe(3);
expect(symbols.every((s) => s.column === 1)).toBe(true);
});
it("handles TSX with class + JSX method body", async () => {
const source = `
class App {
render() {
return <div>hi</div>;
}
}
`;
const symbols = await extractSymbols("App.tsx", source);
const names = symbols.map((s) => `${s.kind}:${s.name}`);
expect(names).toContain("class:App");
expect(names).toContain("method:render");
});
it("handles JS without interfaces or types", async () => {
const source = `
function hello() {}
class Foo {
bar() {}
}
const g = () => 1;
`;
const symbols = await extractSymbols("a.js", source);
const names = symbols.map((s) => `${s.kind}:${s.name}`);
expect(names).toContain("function:hello");
expect(names).toContain("class:Foo");
expect(names).toContain("method:bar");
expect(names).toContain("function:g");
});
it("returns empty for unsupported language", async () => {
expect(await extractSymbols("file.cpp", "int main(){}")).toEqual([]);
});
it("extracts Python functions and classes; nested functions become methods", async () => {
const source = `
def hello():
return 1
class Greeter:
def greet(self):
return "hi"
def __init__(self):
self.n = 0
`;
const symbols = await extractSymbols("a.py", source);
const named = symbols.map((s) => `${s.kind}:${s.name}${s.parent ? `@${s.parent}` : ""}`);
expect(named).toContain("function:hello");
expect(named).toContain("class:Greeter");
expect(named).toContain("method:greet@Greeter");
expect(named).toContain("method:__init__@Greeter");
});
it("extracts Go funcs, methods, structs, and interfaces", async () => {
const source = `package main
type User struct {
Name string
}
type Greeter interface {
Greet() string
}
func Hello() string { return "hi" }
func (u *User) Greet() string { return "hi " + u.Name }
`;
const symbols = await extractSymbols("a.go", source);
const named = symbols.map((s) => `${s.kind}:${s.name}`);
expect(named).toContain("class:User");
expect(named).toContain("interface:Greeter");
expect(named).toContain("function:Hello");
expect(named).toContain("method:Greet");
});
it("extracts Rust functions, structs, traits, and impl methods", async () => {
const source = `pub struct User { pub name: String }
pub trait Greet { fn greet(&self) -> String; }
pub fn hello() -> i32 { 1 }
impl User {
pub fn name_len(&self) -> usize { self.name.len() }
}
`;
const symbols = await extractSymbols("a.rs", source);
const named = symbols.map((s) => `${s.kind}:${s.name}${s.parent ? `@${s.parent}` : ""}`);
expect(named).toContain("class:User");
expect(named).toContain("interface:Greet");
expect(named).toContain("function:hello");
expect(named).toContain("method:name_len@User");
});
it("extracts Java classes, methods, and fields", async () => {
const source = `public class Calc {
int total = 0;
public int add(int n) { return total + n; }
public Calc() {}
}
`;
const symbols = await extractSymbols("Calc.java", source);
const named = symbols.map((s) => `${s.kind}:${s.name}${s.parent ? `@${s.parent}` : ""}`);
expect(named).toContain("class:Calc");
expect(named).toContain("method:add@Calc");
expect(named).toContain("method:Calc@Calc");
expect(named).toContain("property:total@Calc");
});
it("returns empty for empty source", async () => {
expect(await extractSymbols("empty.ts", "")).toEqual([]);
});
it("survives parse errors and still extracts what came before", async () => {
const source = "function ok() {}\nconst broken = ;\n";
const symbols = await extractSymbols("partial.ts", source);
expect(symbols.map((s) => s.name)).toContain("ok");
});
});