Files
socraticode/tests/unit/graph-symbols.test.ts
2026-05-04 16:32:51 +08:00

419 lines
12 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
import { Lang } from "@ast-grep/napi";
import { beforeAll, describe, expect, it } from "vitest";
import { ensureDynamicLanguages } from "../../src/services/code-graph.js";
import {
extractSymbolsAndCalls,
rawCallsToUnresolvedEdges,
} from "../../src/services/graph-symbols.js";
beforeAll(() => {
ensureDynamicLanguages();
});
describe("graph-symbols", () => {
describe("TypeScript/JavaScript", () => {
it("extracts function declarations and synthesizes a <module> symbol", () => {
const src = `
function foo() { return 1; }
function bar() { return foo(); }
`;
const out = extractSymbolsAndCalls(src, Lang.TypeScript, ".ts", "src/a.ts");
const names = out.symbols.map((s) => s.name).sort();
expect(names).toContain("<module>");
expect(names).toContain("foo");
expect(names).toContain("bar");
});
it("attributes calls inside a function to that function as caller", () => {
const src = `
function foo() {}
function bar() { foo(); }
`;
const out = extractSymbolsAndCalls(src, Lang.TypeScript, ".ts", "src/b.ts");
const fooCall = out.rawCalls.find((c) => c.calleeName === "foo");
expect(fooCall).toBeDefined();
expect(fooCall?.callerId).toContain("::bar#");
});
it("extracts class methods with qualified names", () => {
const src = `
class Foo {
bar() { return 1; }
baz() { return this.bar(); }
}
`;
const out = extractSymbolsAndCalls(src, Lang.TypeScript, ".ts", "src/c.ts");
const qnames = out.symbols.map((s) => s.qualifiedName);
expect(qnames).toContain("Foo.bar");
expect(qnames).toContain("Foo.baz");
const kinds = out.symbols.filter((s) => s.qualifiedName === "Foo.bar").map((s) => s.kind);
expect(kinds).toContain("method");
});
it("extracts arrow function constants", () => {
const src = `
export const validate = (x: number) => x > 0;
const helper = function () { return 42; };
`;
const out = extractSymbolsAndCalls(src, Lang.TypeScript, ".ts", "src/d.ts");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("validate");
expect(names).toContain("helper");
});
});
describe("Python", () => {
it("extracts def and class symbols", () => {
const src = `
def foo():
return 1
class Bar:
def baz(self):
return foo()
`;
const out = extractSymbolsAndCalls(src, "python" as unknown as Lang, ".py", "app.py");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("foo");
expect(names).toContain("Bar");
expect(names).toContain("baz");
});
});
describe("Go", () => {
it("extracts func declarations", () => {
const src = `
package main
func Foo() int { return 1 }
func Bar() int { return Foo() }
`;
const out = extractSymbolsAndCalls(src, "go" as unknown as Lang, ".go", "main.go");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("Foo");
expect(names).toContain("Bar");
});
});
describe("rawCallsToUnresolvedEdges", () => {
it("converts raw calls to unresolved SymbolEdge objects", () => {
const raw = [
{
callerId: "src/a.ts::foo#1",
calleeName: "bar",
callSite: { file: "src/a.ts", line: 5 },
},
];
const edges = rawCallsToUnresolvedEdges(raw);
expect(edges).toHaveLength(1);
expect(edges[0].confidence).toBe("unresolved");
expect(edges[0].calleeCandidates).toEqual([]);
expect(edges[0].calleeName).toBe("bar");
});
});
describe("Rust", () => {
it("extracts fn and impl methods", () => {
const src = `
fn foo() -> i32 { 1 }
struct S;
impl S {
fn bar(&self) -> i32 { foo() }
}
`;
const out = extractSymbolsAndCalls(src, "rust" as unknown as Lang, ".rs", "lib.rs");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("foo");
expect(names).toContain("bar");
expect(out.symbols.some((s) => s.name === "<module>")).toBe(true);
});
});
describe("Java / Kotlin / Scala (JVM family)", () => {
it("extracts Java class and methods", () => {
const src = `
public class Foo {
public int bar() { return 1; }
public int baz() { return bar(); }
}
`;
const out = extractSymbolsAndCalls(src, "java" as unknown as Lang, ".java", "Foo.java");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("Foo");
expect(names).toContain("bar");
expect(names).toContain("baz");
});
it("prefers the declared Java class name over parameter types in Spring Boot entrypoints", () => {
const src = `
@SpringBootApplication
public class WorkflowFlowableApplication {
public static void main(String[] args) {
SpringApplication.run(WorkflowFlowableApplication.class, args);
}
}
`;
const out = extractSymbolsAndCalls(src, "java" as unknown as Lang, ".java", "WorkflowFlowableApplication.java");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("WorkflowFlowableApplication");
expect(names).not.toContain("String");
expect(names).toContain("main");
});
it("does not treat Java test annotations as method names", () => {
const src = `
class SecurityAuthClientRequireSubjectTest {
@AfterEach
void cleanup() {}
@Test
void requireSubjectThrows() {}
@Test(timeout = 1000)
void fastTest() {}
}
`;
const out = extractSymbolsAndCalls(src, "java" as unknown as Lang, ".java", "SecurityAuthClientRequireSubjectTest.java");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("SecurityAuthClientRequireSubjectTest");
expect(names).toContain("cleanup");
expect(names).toContain("requireSubjectThrows");
expect(names).toContain("fastTest");
expect(names).not.toContain("AfterEach");
expect(names).not.toContain("Test");
});
it("preserves Java declarations when annotations share the same line", () => {
const src = `
class InlineAnnotationTest {
@Test void cleanup() {}
}
`;
const out = extractSymbolsAndCalls(src, "java" as unknown as Lang, ".java", "InlineAnnotationTest.java");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("InlineAnnotationTest");
expect(names).toContain("cleanup");
expect(names).not.toContain("Test");
});
it("extracts Kotlin top-level fun and class methods", () => {
const src = `
fun greet(name: String): String = "Hi"
class Bar {
fun work(): String = greet("x")
}
`;
const out = extractSymbolsAndCalls(src, "kotlin" as unknown as Lang, ".kt", "main.kt");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("greet");
expect(names).toContain("Bar");
expect(names).toContain("work");
});
it("extracts Scala def and class", () => {
const src = `
class Foo {
def bar(): Int = 1
def size: Int = 1
def now = Instant.now()
}
object Main {
def main(args: Array[String]): Unit = println("hi")
}
`;
const out = extractSymbolsAndCalls(src, "scala" as unknown as Lang, ".scala", "Main.scala");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("bar");
expect(names).toContain("size");
expect(names).toContain("now");
expect(names).toContain("main");
});
});
describe("C#", () => {
it("extracts class and methods", () => {
const src = `
namespace App {
public class Foo {
public int Bar() { return 1; }
public int Baz() { return Bar(); }
}
}
`;
const out = extractSymbolsAndCalls(src, "csharp" as unknown as Lang, ".cs", "Foo.cs");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("Foo");
expect(names).toContain("Bar");
expect(names).toContain("Baz");
});
});
describe("C / C++", () => {
it("extracts C function definitions", () => {
const src = `
int add(int a, int b) { return a + b; }
int main(void) {
return add(2, 3);
}
`;
const out = extractSymbolsAndCalls(src, "c" as unknown as Lang, ".c", "main.c");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("add");
expect(names).toContain("main");
});
it("extracts C++ class declarations and free functions", () => {
// Note: inline class methods are `field_declaration` nodes in tree-sitter-cpp,
// not `function_definition`, so the current extractor catches them only
// when defined out-of-line. See language-coverage table in DEVELOPER.md.
const src = `
class Foo {
public:
int bar();
};
int Foo::bar() { return 1; }
int helper() { return 42; }
`;
const out = extractSymbolsAndCalls(src, "cpp" as unknown as Lang, ".cpp", "Foo.cpp");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("Foo");
expect(names).toContain("helper");
// Out-of-line method `Foo::bar` is detected as qualifiedName "Foo::bar".
const qnames = out.symbols.map((s) => s.qualifiedName);
expect(qnames.some((q) => q === "Foo::bar" || q === "bar")).toBe(true);
});
});
describe("Ruby", () => {
it("extracts def and class", () => {
const src = `
def foo
1
end
class Bar
def baz
foo
end
end
`;
const out = extractSymbolsAndCalls(src, "ruby" as unknown as Lang, ".rb", "app.rb");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("foo");
expect(names).toContain("Bar");
expect(names).toContain("baz");
});
});
describe("PHP", () => {
it("extracts function and class methods", () => {
const src = `<?php
function greet($name) {
return "Hi " . $name;
}
class Foo {
public function bar() {
return greet("x");
}
}
`;
const out = extractSymbolsAndCalls(src, "php" as unknown as Lang, ".php", "index.php");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("greet");
expect(names).toContain("Foo");
expect(names).toContain("bar");
});
});
describe("Swift", () => {
it("extracts Swift func and class", () => {
const src = `
func greet(name: String) -> String { return "Hi" }
class Foo {
func bar() -> String { return greet(name: "x") }
}
`;
const out = extractSymbolsAndCalls(src, "swift" as unknown as Lang, ".swift", "App.swift");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("greet");
expect(names).toContain("Foo");
expect(names).toContain("bar");
});
});
describe("Bash", () => {
it("extracts shell function definitions", () => {
const src = `
greet() {
echo "hi $1"
}
main() {
greet "world"
}
`;
const out = extractSymbolsAndCalls(src, "bash" as unknown as Lang, ".sh", "run.sh");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("greet");
expect(names).toContain("main");
});
});
describe("Regex fallback (Dart, Lua, Svelte, Vue, unknown)", () => {
it("handles Dart via regex fallback", () => {
const src = `
String greet(String name) {
return 'Hi $name';
}
class Foo {
int bar() => 1;
}
`;
const out = extractSymbolsAndCalls(src, "dart" as unknown as Lang, ".dart", "main.dart");
// regex fallback should at least produce <module> and not throw
expect(out.symbols.some((s) => s.name === "<module>")).toBe(true);
const names = out.symbols.map((s) => s.name);
// best-effort detection: should find at least one named symbol
expect(names.length).toBeGreaterThanOrEqual(1);
});
it("handles Lua via regex fallback", () => {
const src = `
function greet(name)
return "hi " .. name
end
local function helper()
return greet("x")
end
`;
const out = extractSymbolsAndCalls(src, "lua" as unknown as Lang, ".lua", "init.lua");
expect(out.symbols.some((s) => s.name === "<module>")).toBe(true);
});
it("handles unknown language without throwing", () => {
const src = "some random text\nwith no recognizable structure";
const out = extractSymbolsAndCalls(
src,
"unknown" as unknown as Lang,
".xyz",
"data.xyz",
);
expect(out.symbols.some((s) => s.name === "<module>")).toBe(true);
expect(out.rawCalls).toEqual([]);
});
});
});