mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 19:34:35 +00:00
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>
215 lines
6.9 KiB
TypeScript
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([]);
|
|
});
|
|
});
|