import { describe, it, expect } from 'vitest'; import { inferCallForm, extractReceiverName, } from '../../src/core/ingestion/utils/call-analysis.js'; import type { SyntaxNode } from '../../src/core/ingestion/utils/ast-helpers.js'; import { createSymbolTable } from '../../src/core/ingestion/model/symbol-table.js'; import Parser from 'tree-sitter'; import TypeScript from 'tree-sitter-typescript'; import Python from 'tree-sitter-python'; import Java from 'tree-sitter-java'; import CSharp from 'tree-sitter-c-sharp'; import Kotlin from 'tree-sitter-kotlin'; import Go from 'tree-sitter-go'; import Rust from 'tree-sitter-rust'; import CPP from 'tree-sitter-cpp'; import PHP from 'tree-sitter-php'; import { SupportedLanguages } from '../../src/config/supported-languages.js'; import { getProvider } from '../../src/core/ingestion/languages/index.js'; /** * Helper: parse code, run the language query, and return all @call captures * as { callNode, nameNode } pairs. */ function extractCallCaptures( parser: Parser, code: string, language: string, ): Array<{ callNode: SyntaxNode; nameNode: SyntaxNode; calledName: string }> { const provider = getProvider(language as SupportedLanguages); const queryStr = provider.treeSitterQueries; if (!queryStr) throw new Error(`No query for ${language}`); const tree = parser.parse(code); const lang = parser.getLanguage(); const query = new Parser.Query(lang, queryStr); const matches = query.matches(tree.rootNode); const results: Array<{ callNode: SyntaxNode; nameNode: SyntaxNode; calledName: string }> = []; for (const match of matches) { const captureMap: Record = {}; for (const c of match.captures) { captureMap[c.name] = c.node; } if (captureMap['call'] && captureMap['call.name']) { results.push({ callNode: captureMap['call'], nameNode: captureMap['call.name'], calledName: captureMap['call.name'].text, }); } } return results; } describe('inferCallForm', () => { const parser = new Parser(); describe('TypeScript', () => { it('detects free call', () => { parser.setLanguage(TypeScript.typescript); const captures = extractCallCaptures(parser, 'doStuff()', SupportedLanguages.TypeScript); const match = captures.find((c) => c.calledName === 'doStuff'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free'); }); it('detects member call', () => { parser.setLanguage(TypeScript.typescript); const captures = extractCallCaptures(parser, 'user.save()', SupportedLanguages.TypeScript); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member'); }); }); describe('Python', () => { it('detects free call', () => { parser.setLanguage(Python); const captures = extractCallCaptures(parser, 'print_result()', SupportedLanguages.Python); const match = captures.find((c) => c.calledName === 'print_result'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free'); }); it('detects member call', () => { parser.setLanguage(Python); const captures = extractCallCaptures(parser, 'self.save()', SupportedLanguages.Python); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member'); }); }); describe('Java', () => { it('detects free call (no object)', () => { parser.setLanguage(Java); const code = `class Foo { void run() { doStuff(); } }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Java); const match = captures.find((c) => c.calledName === 'doStuff'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free'); }); it('detects member call (with object)', () => { parser.setLanguage(Java); const code = `class Foo { void run() { user.save(); } }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Java); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member'); }); }); describe('C#', () => { it('detects free call', () => { parser.setLanguage(CSharp); const code = `class Foo { void Run() { DoStuff(); } }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.CSharp); const match = captures.find((c) => c.calledName === 'DoStuff'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free'); }); it('detects member call', () => { parser.setLanguage(CSharp); const code = `class Foo { void Run() { user.Save(); } }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.CSharp); const match = captures.find((c) => c.calledName === 'Save'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member'); }); }); describe('Go', () => { it('detects free call', () => { parser.setLanguage(Go); const code = `package main\nfunc main() { doStuff() }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Go); const match = captures.find((c) => c.calledName === 'doStuff'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free'); }); it('detects member call via selector', () => { parser.setLanguage(Go); const code = `package main\nfunc main() { user.Save() }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Go); const match = captures.find((c) => c.calledName === 'Save'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member'); }); }); describe('Rust', () => { it('detects free call', () => { parser.setLanguage(Rust); const code = `fn main() { do_stuff(); }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Rust); const match = captures.find((c) => c.calledName === 'do_stuff'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free'); }); it('detects member call via field_expression', () => { parser.setLanguage(Rust); const code = `fn main() { user.save(); }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Rust); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member'); }); it('detects scoped call as free (Foo::new)', () => { parser.setLanguage(Rust); const code = `fn main() { Foo::new(); }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Rust); const match = captures.find((c) => c.calledName === 'new'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free'); }); }); describe('C++', () => { it('detects free call', () => { parser.setLanguage(CPP); const code = `void main() { doStuff(); }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.CPlusPlus); const match = captures.find((c) => c.calledName === 'doStuff'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free'); }); it('detects member call via field_expression', () => { parser.setLanguage(CPP); const code = `void main() { obj.run(); }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.CPlusPlus); const match = captures.find((c) => c.calledName === 'run'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member'); }); }); describe('PHP', () => { it('detects free function call', () => { parser.setLanguage(PHP.php); const code = ``; const captures = extractCallCaptures(parser, code, SupportedLanguages.PHP); const match = captures.find((c) => c.calledName === 'doStuff'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free'); }); it('detects member call', () => { parser.setLanguage(PHP.php); const code = `save(); ?>`; const captures = extractCallCaptures(parser, code, SupportedLanguages.PHP); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member'); }); it('detects static call as member', () => { parser.setLanguage(PHP.php); const code = ``; const captures = extractCallCaptures(parser, code, SupportedLanguages.PHP); const match = captures.find((c) => c.calledName === 'bar'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member'); }); }); describe('Kotlin', () => { it('detects free call', () => { parser.setLanguage(Kotlin); const code = `fun main() { doStuff() }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Kotlin); const match = captures.find((c) => c.calledName === 'doStuff'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free'); }); it('detects member call via navigation_expression', () => { parser.setLanguage(Kotlin); const code = `fun main() { user.save() }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Kotlin); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member'); }); it('Foo() is a free call (constructor_invocation only in heritage context)', () => { parser.setLanguage(Kotlin); const code = `fun main() { val x = Foo() }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Kotlin); const match = captures.find((c) => c.calledName === 'Foo'); expect(match).toBeDefined(); // Kotlin Foo() is syntactically a call_expression, not constructor_invocation // Constructor discrimination happens in Phase 2 via symbol kind matching expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free'); }); it('detects constructor_invocation in heritage delegation as constructor', () => { parser.setLanguage(Kotlin); const code = `open class Base\nclass Derived : Base()`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Kotlin); const match = captures.find((c) => c.calledName === 'Base'); // constructor_invocation is captured by heritage queries, not call queries // If it happens to be captured, it should be 'constructor' if (match) { expect(inferCallForm(match.callNode, match.nameNode)).toBe('constructor'); } }); }); }); describe('extractReceiverName', () => { const parser = new Parser(); describe('TypeScript', () => { it('extracts simple identifier receiver', () => { parser.setLanguage(TypeScript.typescript); const captures = extractCallCaptures(parser, 'user.save()', SupportedLanguages.TypeScript); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBe('user'); }); it('extracts "this" as receiver', () => { parser.setLanguage(TypeScript.typescript); const code = `class Foo { run() { this.save(); } }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.TypeScript); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBe('this'); }); it('returns undefined for chained call receiver', () => { parser.setLanguage(TypeScript.typescript); const captures = extractCallCaptures( parser, 'getUser().save()', SupportedLanguages.TypeScript, ); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBeUndefined(); }); it('returns undefined for free call', () => { parser.setLanguage(TypeScript.typescript); const captures = extractCallCaptures(parser, 'doStuff()', SupportedLanguages.TypeScript); const match = captures.find((c) => c.calledName === 'doStuff'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBeUndefined(); }); it('extracts receiver from optional chain call user?.save()', () => { parser.setLanguage(TypeScript.typescript); const captures = extractCallCaptures(parser, 'user?.save()', SupportedLanguages.TypeScript); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBe('user'); }); it('extracts "this" from optional chain call this?.save()', () => { parser.setLanguage(TypeScript.typescript); const code = `class Foo { run() { this?.save(); } }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.TypeScript); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBe('this'); }); }); describe('Python', () => { it('extracts simple identifier receiver', () => { parser.setLanguage(Python); const captures = extractCallCaptures(parser, 'user.save()', SupportedLanguages.Python); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBe('user'); }); it('extracts "self" as receiver', () => { parser.setLanguage(Python); const captures = extractCallCaptures(parser, 'self.save()', SupportedLanguages.Python); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBe('self'); }); }); describe('Java', () => { it('extracts receiver from method_invocation', () => { parser.setLanguage(Java); const code = `class Foo { void run() { user.save(); } }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Java); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBe('user'); }); }); describe('Go', () => { it('extracts receiver from selector_expression', () => { parser.setLanguage(Go); const code = `package main\nfunc main() { user.Save() }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Go); const match = captures.find((c) => c.calledName === 'Save'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBe('user'); }); }); describe('Rust', () => { it('extracts receiver from field_expression', () => { parser.setLanguage(Rust); const code = `fn main() { user.save(); }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Rust); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBe('user'); }); }); describe('C#', () => { it('extracts receiver from member_access_expression', () => { parser.setLanguage(CSharp); const code = `class Foo { void Run() { user.Save(); } }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.CSharp); const match = captures.find((c) => c.calledName === 'Save'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBe('user'); }); it('captures null-conditional user?.Save() and extracts receiver', () => { parser.setLanguage(CSharp); const code = `class Foo { void Run() { user?.Save(); } }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.CSharp); const match = captures.find((c) => c.calledName === 'Save'); // C# conditional_access_expression (user?.Save()) is now captured via member_binding_expression expect(match).toBeDefined(); expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member'); expect(extractReceiverName(match!.nameNode)).toBe('user'); }); }); describe('Kotlin', () => { it('extracts receiver from navigation_expression', () => { parser.setLanguage(Kotlin); const code = `fun main() { user.save() }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Kotlin); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBe('user'); }); it('extracts receiver from safe navigation user?.save()', () => { parser.setLanguage(Kotlin); const code = `fun main() { user?.save() }`; const captures = extractCallCaptures(parser, code, SupportedLanguages.Kotlin); const match = captures.find((c) => c.calledName === 'save'); expect(match).toBeDefined(); expect(extractReceiverName(match!.nameNode)).toBe('user'); }); }); }); describe('ownerId on SymbolDefinition', () => { it('is set for Method symbols via symbolTable.add()', () => { const st = createSymbolTable(); st.add('src/foo.ts', 'save', 'Method:src/foo.ts:save', 'Method', { parameterCount: 1, ownerId: 'Class:src/foo.ts:User', }); const def = st.lookupExactFull('src/foo.ts', 'save'); expect(def).toBeDefined(); expect(def!.ownerId).toBe('Class:src/foo.ts:User'); expect(def!.parameterCount).toBe(1); }); it('is undefined for Function symbols (no owner)', () => { const st = createSymbolTable(); st.add('src/foo.ts', 'helper', 'Function:src/foo.ts:helper', 'Function'); const def = st.lookupExactFull('src/foo.ts', 'helper'); expect(def).toBeDefined(); expect(def!.ownerId).toBeUndefined(); }); it('propagates ownerId through a free Function registration', () => { // Post-A4 Unit 4, Method is no longer in FREE_CALLABLE_TYPES so this test // exercises ownerId propagation through the free-callable index using // a Function label. Method-with-ownerId propagation is covered via // methodsByName in method-registry.test.ts. const st = createSymbolTable(); st.add('src/foo.ts', 'save', 'Function:src/foo.ts:save', 'Function', { ownerId: 'Class:src/foo.ts:User', }); const defs = st.lookupCallableByName('save'); expect(defs).toHaveLength(1); expect(defs[0].ownerId).toBe('Class:src/foo.ts:User'); }); });