/** * Integration tests for HAS_METHOD edge extraction. * * These tests exercise findEnclosingClassId against real tree-sitter ASTs * produced by the actual parser pipeline (loadParser + loadLanguage + queries). * Unlike the unit tests that test findEnclosingClassId in isolation with simple * snippets, these focus on multi-class files, interface vs class disambiguation, * and cross-language pipeline correctness. */ import { describe, it, expect, beforeAll } from 'vitest'; import Parser from 'tree-sitter'; import { loadParser, loadLanguage } from '../../src/core/tree-sitter/parser-loader.js'; import { SupportedLanguages } from '../../src/config/supported-languages.js'; import { getProvider } from '../../src/core/ingestion/languages/index.js'; import { findEnclosingClassId, getDefinitionNodeFromCaptures, } from '../../src/core/ingestion/utils/ast-helpers.js'; let parser: Parser; beforeAll(async () => { parser = await loadParser(); }); /** Parse code with given language, run definition queries, return matched definitions with their enclosing class IDs. */ function parseAndExtractMethods( code: string, lang: SupportedLanguages, filePath: string, ): { name: string; defType: string; enclosingClassId: string | null }[] { const tree = parser.parse(code); const provider = getProvider(lang); const query = new Parser.Query(parser.getLanguage(), provider.treeSitterQueries); const matches = query.matches(tree.rootNode); const results: { name: string; defType: string; enclosingClassId: string | null }[] = []; for (const match of matches) { const captureMap: Record = {}; let nameNode: any = null; for (const capture of match.captures) { captureMap[capture.name] = capture.node; if (capture.name === 'name') { nameNode = capture.node; } } const defNode = getDefinitionNodeFromCaptures(captureMap); if (!defNode || !nameNode) continue; const defType = Object.keys(captureMap).find((k) => k.startsWith('definition.')) || 'unknown'; const enclosingClassId = findEnclosingClassId(nameNode, filePath); results.push({ name: nameNode.text, defType, enclosingClassId, }); } return results; } describe('HAS_METHOD integration — C#: class with interface', () => { beforeAll(async () => { await loadLanguage(SupportedLanguages.CSharp); }); it('methods link to correct owner (interface vs class)', () => { const code = ` interface IRepository { void FindById(int id); void Save(object entity); } class SqlRepository { public void FindById(int id) {} public void Save(object entity) {} private void Connect() {} } `; const results = parseAndExtractMethods(code, SupportedLanguages.CSharp, 'src/Repo.cs'); // Interface methods should be enclosed by the interface const ifaceFindById = results.find( (r) => r.name === 'FindById' && r.enclosingClassId?.startsWith('Interface:'), ); expect(ifaceFindById).toBeDefined(); expect(ifaceFindById!.enclosingClassId).toBe('Interface:src/Repo.cs:IRepository'); const ifaceSave = results.find( (r) => r.name === 'Save' && r.enclosingClassId?.startsWith('Interface:'), ); expect(ifaceSave).toBeDefined(); expect(ifaceSave!.enclosingClassId).toBe('Interface:src/Repo.cs:IRepository'); // Class methods should be enclosed by the class const classFindById = results.find( (r) => r.name === 'FindById' && r.enclosingClassId?.startsWith('Class:'), ); expect(classFindById).toBeDefined(); expect(classFindById!.enclosingClassId).toBe('Class:src/Repo.cs:SqlRepository'); const classConnect = results.find((r) => r.name === 'Connect'); expect(classConnect).toBeDefined(); expect(classConnect!.enclosingClassId).toBe('Class:src/Repo.cs:SqlRepository'); }); it('class/interface name captures point to their own container (self-referential)', () => { const code = ` interface IService { void Execute(); } class ServiceImpl { public void Execute() {} } `; const results = parseAndExtractMethods(code, SupportedLanguages.CSharp, 'src/Service.cs'); // The name node for IService sits inside the interface_declaration, so // findEnclosingClassId returns the interface itself. This is expected — // the pipeline uses defType (definition.interface vs definition.method) to // distinguish container declarations from methods, not enclosingClassId. const ifaceDecl = results.find((r) => r.name === 'IService'); expect(ifaceDecl).toBeDefined(); expect(ifaceDecl!.defType).toBe('definition.interface'); const classDecl = results.find((r) => r.name === 'ServiceImpl'); expect(classDecl).toBeDefined(); expect(classDecl!.defType).toBe('definition.class'); // Methods should still correctly reference their container const execMethods = results.filter((r) => r.name === 'Execute'); expect(execMethods.length).toBe(2); expect( execMethods.some((r) => r.enclosingClassId === 'Interface:src/Service.cs:IService'), ).toBe(true); expect(execMethods.some((r) => r.enclosingClassId === 'Class:src/Service.cs:ServiceImpl')).toBe( true, ); }); }); describe('HAS_METHOD integration — Rust: impl + trait', () => { beforeAll(async () => { await loadLanguage(SupportedLanguages.Rust); }); it('methods link to impl vs trait nodes', () => { const code = ` trait Drawable { fn draw(&self); fn resize(&self, w: u32, h: u32); } struct Circle { radius: f64, } impl Circle { fn new(radius: f64) -> Circle { Circle { radius } } fn area(&self) -> f64 { 3.14 * self.radius * self.radius } } `; const results = parseAndExtractMethods(code, SupportedLanguages.Rust, 'src/shapes.rs'); // Trait methods should be enclosed by the trait const traitDraw = results.find((r) => r.name === 'draw'); if (traitDraw) { expect(traitDraw.enclosingClassId).toBe('Trait:src/shapes.rs:Drawable'); } const traitResize = results.find((r) => r.name === 'resize'); if (traitResize) { expect(traitResize.enclosingClassId).toBe('Trait:src/shapes.rs:Drawable'); } // Impl methods should be enclosed by the impl block const implNew = results.find((r) => r.name === 'new'); if (implNew) { expect(implNew.enclosingClassId).toBe('Impl:src/shapes.rs:Circle'); } const implArea = results.find((r) => r.name === 'area'); if (implArea) { expect(implArea.enclosingClassId).toBe('Impl:src/shapes.rs:Circle'); } }); it('standalone functions do not get HAS_METHOD', () => { const code = ` fn helper() -> bool { true } struct Foo; impl Foo { fn bar(&self) {} } `; const results = parseAndExtractMethods(code, SupportedLanguages.Rust, 'src/lib.rs'); const helper = results.find((r) => r.name === 'helper'); expect(helper).toBeDefined(); expect(helper!.enclosingClassId).toBeNull(); const bar = results.find((r) => r.name === 'bar'); if (bar) { expect(bar.enclosingClassId).toBe('Impl:src/lib.rs:Foo'); } }); }); describe('HAS_METHOD integration — Python: class methods vs standalone functions', () => { beforeAll(async () => { await loadLanguage(SupportedLanguages.Python); }); it('methods link to class, standalone functions get null', () => { const code = ` def standalone_helper(): return 42 class Calculator: def __init__(self): self.value = 0 def add(self, x): self.value += x return self def result(self): return self.value def another_standalone(): pass `; const results = parseAndExtractMethods(code, SupportedLanguages.Python, 'src/calc.py'); // Standalone functions should not be enclosed const standaloneHelper = results.find((r) => r.name === 'standalone_helper'); expect(standaloneHelper).toBeDefined(); expect(standaloneHelper!.enclosingClassId).toBeNull(); const anotherStandalone = results.find((r) => r.name === 'another_standalone'); expect(anotherStandalone).toBeDefined(); expect(anotherStandalone!.enclosingClassId).toBeNull(); // Class methods should be enclosed const init = results.find((r) => r.name === '__init__'); expect(init).toBeDefined(); expect(init!.enclosingClassId).toBe('Class:src/calc.py:Calculator'); const add = results.find((r) => r.name === 'add'); expect(add).toBeDefined(); expect(add!.enclosingClassId).toBe('Class:src/calc.py:Calculator'); const resultMethod = results.find((r) => r.name === 'result'); expect(resultMethod).toBeDefined(); expect(resultMethod!.enclosingClassId).toBe('Class:src/calc.py:Calculator'); }); }); describe('HAS_METHOD integration — Multiple classes in one file', () => { describe('TypeScript', () => { beforeAll(async () => { await loadLanguage(SupportedLanguages.TypeScript, 'multi.ts'); }); it('methods associate with their owning class', () => { const code = ` class UserService { findUser(id: number) { return null; } deleteUser(id: number) {} } class OrderService { createOrder(data: any) { return data; } cancelOrder(id: number) {} } function topLevelUtil() { return true; } `; const results = parseAndExtractMethods( code, SupportedLanguages.TypeScript, 'src/services.ts', ); // UserService methods const findUser = results.find((r) => r.name === 'findUser'); expect(findUser).toBeDefined(); expect(findUser!.enclosingClassId).toBe('Class:src/services.ts:UserService'); const deleteUser = results.find((r) => r.name === 'deleteUser'); expect(deleteUser).toBeDefined(); expect(deleteUser!.enclosingClassId).toBe('Class:src/services.ts:UserService'); // OrderService methods const createOrder = results.find((r) => r.name === 'createOrder'); expect(createOrder).toBeDefined(); expect(createOrder!.enclosingClassId).toBe('Class:src/services.ts:OrderService'); const cancelOrder = results.find((r) => r.name === 'cancelOrder'); expect(cancelOrder).toBeDefined(); expect(cancelOrder!.enclosingClassId).toBe('Class:src/services.ts:OrderService'); // Top-level function const topLevelUtil = results.find((r) => r.name === 'topLevelUtil'); expect(topLevelUtil).toBeDefined(); expect(topLevelUtil!.enclosingClassId).toBeNull(); }); }); describe('Java', () => { beforeAll(async () => { await loadLanguage(SupportedLanguages.Java); }); it('methods associate with their owning class', () => { const code = ` class Logger { public void info(String msg) {} public void error(String msg) {} } class Formatter { public String format(String template) { return template; } private String escape(String input) { return input; } } `; const results = parseAndExtractMethods( code, SupportedLanguages.Java, 'src/util/Logging.java', ); const info = results.find((r) => r.name === 'info'); expect(info).toBeDefined(); expect(info!.enclosingClassId).toBe('Class:src/util/Logging.java:Logger'); const error = results.find((r) => r.name === 'error'); expect(error).toBeDefined(); expect(error!.enclosingClassId).toBe('Class:src/util/Logging.java:Logger'); const format = results.find((r) => r.name === 'format'); expect(format).toBeDefined(); expect(format!.enclosingClassId).toBe('Class:src/util/Logging.java:Formatter'); const escape = results.find((r) => r.name === 'escape'); expect(escape).toBeDefined(); expect(escape!.enclosingClassId).toBe('Class:src/util/Logging.java:Formatter'); }); }); }); describe('HAS_METHOD integration — Java: class with interface', () => { beforeAll(async () => { await loadLanguage(SupportedLanguages.Java); }); it('methods link to correct owner (interface vs class)', () => { const code = ` interface Validator { boolean validate(Object input); String getMessage(); } class EmailValidator { public boolean validate(Object input) { return true; } public String getMessage() { return "invalid email"; } private boolean checkFormat(String email) { return true; } } `; const results = parseAndExtractMethods( code, SupportedLanguages.Java, 'src/validation/Validator.java', ); // Interface methods const ifaceValidate = results.find( (r) => r.name === 'validate' && r.enclosingClassId?.startsWith('Interface:'), ); expect(ifaceValidate).toBeDefined(); expect(ifaceValidate!.enclosingClassId).toBe( 'Interface:src/validation/Validator.java:Validator', ); const ifaceGetMessage = results.find( (r) => r.name === 'getMessage' && r.enclosingClassId?.startsWith('Interface:'), ); expect(ifaceGetMessage).toBeDefined(); expect(ifaceGetMessage!.enclosingClassId).toBe( 'Interface:src/validation/Validator.java:Validator', ); // Class methods const classValidate = results.find( (r) => r.name === 'validate' && r.enclosingClassId?.startsWith('Class:'), ); expect(classValidate).toBeDefined(); expect(classValidate!.enclosingClassId).toBe( 'Class:src/validation/Validator.java:EmailValidator', ); const classCheckFormat = results.find((r) => r.name === 'checkFormat'); expect(classCheckFormat).toBeDefined(); expect(classCheckFormat!.enclosingClassId).toBe( 'Class:src/validation/Validator.java:EmailValidator', ); }); it('class/interface declarations are captured with correct defType', () => { const code = ` interface Repository { void save(Object entity); } class UserRepository { public void save(Object entity) {} } `; const results = parseAndExtractMethods(code, SupportedLanguages.Java, 'src/repo/Repo.java'); // The pipeline distinguishes containers from methods via defType, not enclosingClassId const repoDecl = results.find((r) => r.name === 'Repository'); expect(repoDecl).toBeDefined(); expect(repoDecl!.defType).toBe('definition.interface'); const userRepoDecl = results.find((r) => r.name === 'UserRepository'); expect(userRepoDecl).toBeDefined(); expect(userRepoDecl!.defType).toBe('definition.class'); // Methods associate correctly const saveMethods = results.filter((r) => r.name === 'save'); expect(saveMethods.length).toBe(2); expect( saveMethods.some((r) => r.enclosingClassId === 'Interface:src/repo/Repo.java:Repository'), ).toBe(true); expect( saveMethods.some((r) => r.enclosingClassId === 'Class:src/repo/Repo.java:UserRepository'), ).toBe(true); }); }); describe('HAS_METHOD integration — C++ class methods', () => { beforeAll(async () => { await loadLanguage(SupportedLanguages.CPlusPlus); }); it('inline methods link to their owning class_specifier', () => { const code = ` class Stack { public: void push(int val) { data[top++] = val; } int pop() { return data[--top]; } int size() { return top; } private: int data[100]; int top; }; class Queue { public: void enqueue(int val) {} int dequeue() { return 0; } }; `; const results = parseAndExtractMethods(code, SupportedLanguages.CPlusPlus, 'src/containers.h'); // Stack methods const push = results.find((r) => r.name === 'push'); if (push) { expect(push.enclosingClassId).toBe('Class:src/containers.h:Stack'); } const pop = results.find((r) => r.name === 'pop'); if (pop) { expect(pop.enclosingClassId).toBe('Class:src/containers.h:Stack'); } const size = results.find((r) => r.name === 'size'); if (size) { expect(size.enclosingClassId).toBe('Class:src/containers.h:Stack'); } // Queue methods const enqueue = results.find((r) => r.name === 'enqueue'); if (enqueue) { expect(enqueue.enclosingClassId).toBe('Class:src/containers.h:Queue'); } const dequeue = results.find((r) => r.name === 'dequeue'); if (dequeue) { expect(dequeue.enclosingClassId).toBe('Class:src/containers.h:Queue'); } }); it('free functions have null enclosingClassId', () => { const code = ` void freeFunction() {} class Foo { public: void method() {} }; `; const results = parseAndExtractMethods(code, SupportedLanguages.CPlusPlus, 'src/mixed.cpp'); const freeFn = results.find((r) => r.name === 'freeFunction'); if (freeFn) { expect(freeFn.enclosingClassId).toBeNull(); } const method = results.find((r) => r.name === 'method'); if (method) { expect(method.enclosingClassId).toBe('Class:src/mixed.cpp:Foo'); } }); }); describe('HAS_METHOD integration — C++ virtual/static/constructor inline methods', () => { beforeAll(async () => { await loadLanguage(SupportedLanguages.CPlusPlus); }); it('virtual, override, static, and constructor methods are captured from inline class body', () => { const code = ` class Shape { public: Shape() {} virtual ~Shape() {} virtual double area() = 0; static Shape* create(); }; class Circle : public Shape { public: Circle(double r) : radius(r) {} double area() override { return 3.14 * radius * radius; } private: double radius; }; `; const results = parseAndExtractMethods(code, SupportedLanguages.CPlusPlus, 'src/shapes.h'); // Shape methods const shapeCtor = results.find( (r) => r.name === 'Shape' && r.enclosingClassId === 'Class:src/shapes.h:Shape', ); expect(shapeCtor).toBeDefined(); const shapeDtor = results.find((r) => r.name === '~Shape'); expect(shapeDtor).toBeDefined(); expect(shapeDtor!.enclosingClassId).toBe('Class:src/shapes.h:Shape'); const area = results.find( (r) => r.name === 'area' && r.enclosingClassId === 'Class:src/shapes.h:Shape', ); expect(area).toBeDefined(); const create = results.find((r) => r.name === 'create'); expect(create).toBeDefined(); expect(create!.enclosingClassId).toBe('Class:src/shapes.h:Shape'); // Circle methods const circleCtor = results.find( (r) => r.name === 'Circle' && r.enclosingClassId === 'Class:src/shapes.h:Circle', ); expect(circleCtor).toBeDefined(); const circleArea = results.find( (r) => r.name === 'area' && r.enclosingClassId === 'Class:src/shapes.h:Circle', ); expect(circleArea).toBeDefined(); }); }); describe('HAS_METHOD integration — TypeScript: abstract, interface, and #private methods', () => { beforeAll(async () => { await loadLanguage(SupportedLanguages.TypeScript, 'methods.ts'); }); it('abstract method signatures are captured and link to abstract class', () => { const code = ` abstract class Shape { abstract area(): number; describe(): string { return "shape"; } } `; const results = parseAndExtractMethods(code, SupportedLanguages.TypeScript, 'src/shapes.ts'); const area = results.find((r) => r.name === 'area' && r.defType === 'definition.method'); expect(area).toBeDefined(); expect(area!.enclosingClassId).toBe('Class:src/shapes.ts:Shape'); const describe = results.find( (r) => r.name === 'describe' && r.defType === 'definition.method', ); expect(describe).toBeDefined(); expect(describe!.enclosingClassId).toBe('Class:src/shapes.ts:Shape'); }); it('interface method signatures are captured and link to interface', () => { const code = ` interface Printable { print(format: string): void; getLabel(): string; } `; const results = parseAndExtractMethods(code, SupportedLanguages.TypeScript, 'src/printable.ts'); const print = results.find((r) => r.name === 'print' && r.defType === 'definition.method'); expect(print).toBeDefined(); expect(print!.enclosingClassId).toBe('Interface:src/printable.ts:Printable'); const getLabel = results.find( (r) => r.name === 'getLabel' && r.defType === 'definition.method', ); expect(getLabel).toBeDefined(); expect(getLabel!.enclosingClassId).toBe('Interface:src/printable.ts:Printable'); }); it('ES2022 #private methods are captured and link to class', () => { const code = ` class Vault { #decrypt(data: string): string { return data; } read(): string { return this.#decrypt("x"); } } `; const results = parseAndExtractMethods(code, SupportedLanguages.TypeScript, 'src/vault.ts'); const decrypt = results.find((r) => r.name === '#decrypt'); expect(decrypt).toBeDefined(); expect(decrypt!.defType).toBe('definition.method'); expect(decrypt!.enclosingClassId).toBe('Class:src/vault.ts:Vault'); const read = results.find((r) => r.name === 'read'); expect(read).toBeDefined(); expect(read!.enclosingClassId).toBe('Class:src/vault.ts:Vault'); }); }); describe('HAS_METHOD integration — JavaScript: ES2022 #private methods', () => { beforeAll(async () => { await loadLanguage(SupportedLanguages.JavaScript); }); it('#private methods are captured and link to class', () => { const code = ` class Encapsulated { #internal() { return 42; } expose() { return this.#internal(); } } `; const results = parseAndExtractMethods( code, SupportedLanguages.JavaScript, 'src/encapsulated.js', ); const internal = results.find((r) => r.name === '#internal'); expect(internal).toBeDefined(); expect(internal!.defType).toBe('definition.method'); expect(internal!.enclosingClassId).toBe('Class:src/encapsulated.js:Encapsulated'); }); }); describe('HAS_METHOD integration — C# struct and record', () => { beforeAll(async () => { await loadLanguage(SupportedLanguages.CSharp); }); it('struct methods link to struct, record methods link to record', () => { const code = ` struct Vector2 { public float Length() { return 0; } public Vector2 Normalize() { return this; } } record Person { public string GetFullName() { return ""; } } `; const results = parseAndExtractMethods(code, SupportedLanguages.CSharp, 'src/Types.cs'); const length = results.find((r) => r.name === 'Length'); if (length) { expect(length.enclosingClassId).toBe('Struct:src/Types.cs:Vector2'); } const normalize = results.find((r) => r.name === 'Normalize'); if (normalize) { expect(normalize.enclosingClassId).toBe('Struct:src/Types.cs:Vector2'); } const getFullName = results.find((r) => r.name === 'GetFullName'); if (getFullName) { expect(getFullName.enclosingClassId).toBe('Record:src/Types.cs:Person'); } }); });