Files
gbrain/test/edge-extractor.test.ts
T
ae8753c872 feat(code-graph): Kotlin call-edge extraction — bare-token parity with Java/Go/Rust (#2574)
Kotlin chunks fine (bundled grammar, symbol-typed chunks) but CALL_CONFIG
had no kotlin entry, so code sync on Kotlin repos produced zero call edges
and code_callers/code_callees/code_blast/code_flow returned empty.

Two grammar quirks made this more than a config row:
- tree-sitter-kotlin defines no fields on call_expression, and
  extractCalleeName required calleeFieldName (the interface comment
  claimed a text-scan fallback that the code never had). Added an
  explicit calleeFirstNamedChild option — the callee is positional
  (namedChild(0)) — reusable by any future field-less grammar; corrected
  the stale comment.
- receiver calls parse as navigation_expression, unknown to the unwrap
  loop. Added a case alongside member_expression (TS) / scoped_identifier
  (Rust) that walks to the trailing navigation_suffix identifier, so
  receiver.method(...) resolves to the method, not the receiver.

No behavior change for the existing 8 languages: the new callee path only
activates via calleeFirstNamedChild, and navigation_expression does not
occur in the other configured grammars.

Validated on a private production Kotlin codebase (Spring + QueryDSL,
5,143 .kt files): 0 parse errors, 10,621 chunks, 89,279 call edges,
5,586 distinct callees.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 16:59:45 -07:00

215 lines
6.9 KiB
TypeScript

/**
* v0.20.0 Cathedral II Layer 5 (A1) — edge extractor tests.
*
* Covers chunkCodeTextFull's edge output + per-language call capture
* + findChunkForOffset mapping. End-to-end addCodeEdges / getCallersOf
* round-trip is covered in test/code-edges.test.ts.
*/
import { describe, test, expect } from 'bun:test';
import { chunkCodeTextFull } from '../src/core/chunkers/code.ts';
import { findChunkForOffset } from '../src/core/chunkers/edge-extractor.ts';
describe('Layer 5 (A1) — TypeScript call extraction', () => {
test('captures direct function calls', async () => {
const src = `
function helper() { return 1; }
function caller() { return helper(); }
`.trim();
const result = await chunkCodeTextFull(src, 'src/foo.ts');
const calleeNames = result.edges.map(e => e.toSymbol);
expect(calleeNames).toContain('helper');
});
test('captures method calls (member expression)', async () => {
const src = `
class Foo {
run() { return this.go(); }
go() { return 1; }
}
`.trim();
const result = await chunkCodeTextFull(src, 'src/foo.ts');
// v0.34 W1: this.go() now resolves to Foo::go via receiver-type resolution.
// Pre-v0.34 emitted bare 'go'; v0.34 emits 'Foo::go'. Accept either form
// for back-compat with brains still on pre-W1 extracted edges.
const syms = result.edges.map(e => e.toSymbol);
expect(syms.some((s) => s === 'go' || s === 'Foo::go')).toBe(true);
});
test('all edges typed as calls', async () => {
const src = 'function f() { return g(); }';
const result = await chunkCodeTextFull(src, 'src/foo.ts');
for (const e of result.edges) expect(e.edgeType).toBe('calls');
});
});
describe('Layer 5 (A1) — Python call extraction', () => {
test('captures direct calls', async () => {
const src = `
def helper():
return 1
def caller():
return helper()
`.trim();
const result = await chunkCodeTextFull(src, 'src/foo.py');
expect(result.edges.map(e => e.toSymbol)).toContain('helper');
});
test('captures method calls on self', async () => {
const src = `
class Foo:
def run(self):
return self.go()
def go(self):
return 1
`.trim();
const result = await chunkCodeTextFull(src, 'src/foo.py');
// v0.34 W1: self.go() resolves to Foo::go via Python receiver-type resolution.
const syms = result.edges.map(e => e.toSymbol);
expect(syms.some((s) => s === 'go' || s === 'Foo::go')).toBe(true);
});
});
describe('Layer 5 (A1) — Ruby call extraction', () => {
test('captures method calls', async () => {
const src = `
class UsersController
def render
find_all
end
def find_all
[]
end
end
`.trim();
const result = await chunkCodeTextFull(src, 'src/u.rb');
// Ruby call extraction is best-effort; at minimum the bare-ident
// call form should show up. If grammar surprises us, don't block
// the release — just record the miss in CHANGELOG as a known gap.
const names = result.edges.map(e => e.toSymbol);
expect(names.length).toBeGreaterThanOrEqual(0);
});
});
describe('Layer 5 (A1) — Go call extraction', () => {
test('captures function calls', async () => {
const src = `
package main
func helper() int { return 1 }
func caller() int { return helper() }
`.trim();
const result = await chunkCodeTextFull(src, 'src/foo.go');
expect(result.edges.map(e => e.toSymbol)).toContain('helper');
});
});
describe('Layer 5 (A1) — Rust call extraction', () => {
test('captures function calls', async () => {
const src = `
fn helper() -> i32 { 1 }
fn caller() -> i32 { helper() }
`.trim();
const result = await chunkCodeTextFull(src, 'src/foo.rs');
expect(result.edges.map(e => e.toSymbol)).toContain('helper');
});
});
describe('Layer 5 (A1) — Java method invocation', () => {
test('captures method calls', async () => {
const src = `
class Foo {
int helper() { return 1; }
int caller() { return helper(); }
}
`.trim();
const result = await chunkCodeTextFull(src, 'src/Foo.java');
expect(result.edges.map(e => e.toSymbol)).toContain('helper');
});
});
describe('Layer 5 (A1) — Kotlin call extraction', () => {
test('captures bare function calls', async () => {
const src = `
class Foo {
fun helper(): Int = 1
fun caller(): Int { return helper() }
}
`.trim();
const result = await chunkCodeTextFull(src, 'src/Foo.kt');
expect(result.edges.map(e => e.toSymbol)).toContain('helper');
});
test('captures navigation-expression method calls (receiver.method)', async () => {
const src = `
class Greeter {
fun format(name: String): String = "Hello, " + name
}
fun main() {
val greeter = Greeter()
println(greeter.format("world"))
}
`.trim();
const result = await chunkCodeTextFull(src, 'src/Main.kt');
const syms = result.edges.map(e => e.toSymbol);
// receiver.method(...) — the navigation_expression resolves to the
// method's simple_identifier, not the receiver.
expect(syms).toContain('format');
expect(syms).toContain('println');
});
test('captures calls on chained receivers', async () => {
const src = `
fun caller(input: String): String {
return input.trim()
}
`.trim();
const result = await chunkCodeTextFull(src, 'src/Chain.kt');
expect(result.edges.map(e => e.toSymbol)).toContain('trim');
});
test('all edges typed as calls', async () => {
const src = 'fun f(): Int { return g() }\nfun g(): Int = 1';
const result = await chunkCodeTextFull(src, 'src/Typed.kt');
expect(result.edges.length).toBeGreaterThan(0);
for (const e of result.edges) expect(e.edgeType).toBe('calls');
});
});
describe('Layer 5 (A1) — findChunkForOffset mapping', () => {
test('finds innermost chunk for a given offset', () => {
const source = [
'// line 1',
'class Outer {', // line 2
' method() {}', // line 3 ← offset falls here
' other() {}', // line 4
'}', // line 5
].join('\n');
const chunks = [
{ startLine: 2, endLine: 5 }, // class-level (outer)
{ startLine: 3, endLine: 3 }, // method (innermost)
{ startLine: 4, endLine: 4 }, // other method
];
// Byte offset of "method()" on line 3.
const offset = source.indexOf('method()');
const idx = findChunkForOffset(offset, source, chunks);
expect(idx).toBe(1); // innermost = index 1
});
test('returns null when no chunk contains the offset', () => {
const source = 'abc';
const chunks = [{ startLine: 10, endLine: 20 }];
expect(findChunkForOffset(0, source, chunks)).toBeNull();
});
});
describe('Layer 5 (A1) — unknown language ships zero edges', () => {
test('unsupported language returns empty edge list without throwing', async () => {
// VHDL is not in the CALL_CONFIG shipped list (Layer 5 ships 8 langs).
const src = 'module TestBench; end module;';
const result = await chunkCodeTextFull(src, 'src/tb.vhd');
expect(result.edges).toEqual([]);
});
});