GitNexus/gitnexus/test/integration/has-method.test.ts
Gergő Magyar 7999b6ba7b
refactor: SICP-informed LanguageProvider architecture (#488)
* refactor: SICP-informed LanguageProvider architecture for ingestion pipeline

Consolidate 16 scattered dispatch surfaces into a single LanguageProvider
Strategy interface per language. Processors are now fully language-agnostic —
zero SupportedLanguages.X enum access, zero dispatch table imports.

Architecture (5-layer DAG, zero circular dependencies):
  L0: Capability modules (dispatch tables, single source of truth)
  L1: LanguageProvider interface + createLanguageProvider factory
  L2: 13 per-language provider files (Strategy objects)
  L3: Registry with satisfies Record<SL, LP> + pre-built lookup maps
  L4: Processors (language-agnostic, all behavior via provider.*)

Key changes:
- Add LanguageProvider interface with 15 properties (6 required, 9 optional)
- Create 13 provider files in languages/ + php-helpers.ts
- Migrate all processors to getProvider(language) — cached once per scope
- Replace heritage if-checks with provider.interfaceNamePattern/heritageDefaultEdge
- Replace MRO switch(language) with switch(provider.mroStrategy)
- Replace isNodeExported with provider.exportChecker
- Move PHP description extraction behind provider.descriptionExtractor
- Move Swift implicit imports behind provider.implicitImportWirer
- Move PHP route detection behind provider.isRouteFile
- Move Kotlin wildcard append behind provider.importPathPreprocessor
- Remove deprecated TypeEnvironment.env, add fileScope()/allScopes()
- De-export TypeEnv type (module-private)
- Pre-build extensionMap, WILDCARD_LANGUAGES, SYNTHESIS_LANGUAGES at load
- Remove dead entryPointPatterns/frameworkPatterns from interface
- Derive createLanguageProvider config type via Pick/Partial/Omit
- Tighten callback types from any to SyntaxNode
- Migrate 270+ test call sites from .env to TypeEnvironment API

Adding a new language: 3 files (enum + provider + registry line).
No processor file touched. Ever.

* refactor: clean architecture for LanguageProvider with O(1) AST cache

Address all PR #488 review comments and achieve pristine SICP layer separation:

Interface redesign:
- Split LanguageProvider into Config (input) + Provider (runtime with defaults)
- Rename createLanguageProvider → defineLanguage with explicit DEFAULTS constant
- Add MroStrategy, ImportSemantics named type aliases for better IDE tooltips
- Tighten labelOverride signature: string|null → NodeLabel|null (compile-time safety)
- Tighten descriptionExtractor nodeLabel: string → NodeLabel
- Un-export LanguageProviderConfig (internal to defineLanguage)

CI fixes (all 4 failures resolved):
- isNodeExported: add null guard for unknown languages
- preprocessImportPath tests: pass getProvider() instead of raw enum
- MRO tests: update expected strings to match language-agnostic prefixes

Code deduplication:
- Extract findDescendant/extractStringContent to ast-helpers.ts (single source of truth)
- Unify Kotlin method detection: remove duplicate from extractFunctionName,
  use provider.labelOverride as single source of truth via findEnclosingFunctionId
- extractFunctionName return type: string → NodeLabel

Performance (O(1) AST node access):
- Add per-file Map-based memoization in parse-worker for parent-chain walks
- Cache enclosingClassId, enclosingFunctionId, exportStatus per SyntaxNode
- Clear caches before each file parse (not after — handles parse failures)

Architecture (pristine languages/ folder):
- Move php-helpers.ts → helpers/php.ts (L0 capability, not L2 config)
- Create helpers/swift.ts from extracted Swift provider logic
- Extract cppLabelOverride AST walk → isCppInsideClassOrStruct in ast-helpers.ts
- Extract isPhpRouteFile → helpers/php.ts
- All 13 provider files are now pure configuration — zero implementation logic
- Ruby: remove no-op namedBindingExtractor assignment (undefined from dispatch table)

* refactor: eliminate LANGUAGE_QUERIES, typeConfigs, namedBindingExtractors dispatch tables

Phase 1 of L0 dispatch table elimination. Providers now import capabilities
directly instead of indexing into redundant Record<SL, T> dispatch tables:

- LANGUAGE_QUERIES: providers import named query constants directly
  (TYPESCRIPT_QUERIES, PYTHON_QUERIES, etc.). Table kept in tree-sitter-queries.ts
  for call-processor.ts dynamic lookup + test consumers.

- typeConfigs: providers import from individual type-extractor files
  (typescriptConfig from typescript.ts, javaTypeConfig from jvm.ts, etc.).
  Dispatch table fully removed from type-extractors/index.ts.

- namedBindingExtractors: providers import extractors directly from
  named-binding-extraction.ts (extractTsNamedBindings, etc.).
  Dispatch table fully removed from import-resolution.ts.

Net: -48 LOC of dispatch table indirection. L3 satisfies Record<SL, LP>
remains the single exhaustiveness check.

* refactor: eliminate exportCheckers, callRouters, importResolvers dispatch tables

Phase 2 of L0 dispatch table elimination. All 6 dispatch tables are now gone:

- exportCheckers: individual checkers exported directly (tsExportChecker,
  pythonExportChecker, etc.). isNodeExported uses a local checkersByLanguage
  map to avoid circular dependency with languages/index.ts.

- callRouters: table removed. Providers import noRouting or routeRubyCall
  directly. noRouting now exported. Dead import removed from call-processor.ts.

- importResolvers: resolver functions exported with clean names
  (resolveTypescriptImport, resolveJavaImport, etc.). Inline lambdas
  extracted to named exports. Dispatch functions renamed from *Dispatch
  suffix to clean resolve*Import pattern.

Combined with Phase 1, all 6 L0 dispatch tables have been eliminated.
L3 satisfies Record<SL, LanguageProvider> is the single exhaustiveness check.
Providers are now fully self-contained — each imports its capabilities directly.

* perf+refactor: type-env caching, sequential fallback caching, utils.ts split

Phase 3 — performance optimizations and barrel cleanup:

Type-env parent-walk caching:
- Memoize findEnclosingClassName and findEnclosingParentClassName with
  per-file Map<SyntaxNode, string|undefined> caches
- Eliminates O(n*m) repeated child scanning in extractParentClassFromNode
- Caches cleared in buildTypeEnv before each file's walk phase

Sequential fallback caching:
- Add classIdCache + exportCache Maps to parsing-processor.ts
- Mirrors the O(1) memoization pattern from parse-worker.ts
- Both paths now have identical caching for parent-chain walks

Split utils.ts barrel into focused modules:
- noise-filter.ts: BUILT_IN_NAMES + isBuiltInOrNoise (167 LOC)
- language-detection.ts: getLanguageFromFilename (58 LOC)
- utils.ts slimmed to re-exports + yieldToEventLoop + isVerboseIngestionEnabled
- Backward compatible — existing imports from utils.ts still work

* refactor: rename resolvers/ → import-resolvers/, restructure tests per-concern

Directory renames (git mv — history preserved):
- src/core/ingestion/resolvers/ → import-resolvers/ (10 files)
- test/unit/call-routing.test.ts → call-routing/ruby.test.ts
- test/unit/named-binding-extraction.test.ts → named-bindings/csharp.test.ts
- test/unit/import-resolution.test.ts → import-resolution/preprocessing.test.ts

All 11 import paths updated to reference new import-resolvers/ location.
Test imports updated for new subdirectory depth.

Note: test/integration/resolvers/ NOT renamed — those tests cover the full
ingestion pipeline per-language, not just import resolution.

* refactor: eliminate utils.ts barrel — all 33 consumers now import directly

Migrated 65 import sites across 33 files to import from the focused source
module instead of the utils.ts barrel:

- ast-helpers.js: SyntaxNode, extractFunctionName, findEnclosingClassId, etc.
- call-analysis.js: inferCallForm, extractReceiverName, countCallArguments, etc.
- noise-filter.js: BUILT_IN_NAMES, isBuiltInOrNoise
- language-detection.js: getLanguageFromFilename

utils.ts reduced to 2 original functions only:
- yieldToEventLoop
- isVerboseIngestionEnabled

Zero re-exports remain. Every import is now direct to its source module.

* refactor: create utils/ folder, move all shared utilities, delete utils.ts barrel

Final phase of module structure migration:

- git mv ast-helpers.ts, call-analysis.ts, noise-filter.ts,
  language-detection.ts → utils/ subdirectory (history preserved)
- Extract yieldToEventLoop → utils/event-loop.ts
- Extract isVerboseIngestionEnabled → utils/verbose.ts
- Delete utils.ts (zero re-exports, zero functions remain)
- Update 38 import paths across source and test files

The ingestion/ root is now clean — only processors, capability modules,
and the pipeline orchestrator live at the top level. All shared utilities
are in utils/, all language-specific helpers in helpers/, all import
resolvers in import-resolvers/.

* refactor: move findChild from import-resolvers/utils.ts to utils/ast-helpers.ts

findChild is a generic AST helper (find first named child by type) — it
belongs with the other AST traversal utilities, not in the import resolver
module. 4 consumers updated to import from utils/ast-helpers.js.

* refactor: split named-binding-extraction.ts into per-language files

Rename named-binding-extraction.ts → named-binding-processor.ts (git mv,
history preserved), keeping only walkBindingChain for re-export chain resolution.

7 per-language extractor functions moved to named-bindings/ subdirectory:
- named-bindings/typescript.ts (extractTsNamedBindings — TS + JS)
- named-bindings/python.ts (extractPythonNamedBindings)
- named-bindings/kotlin.ts (extractKotlinNamedBindings)
- named-bindings/rust.ts (extractRustNamedBindings + collectRustBindings)
- named-bindings/php.ts (extractPhpNamedBindings)
- named-bindings/csharp.ts (extractCsharpNamedBindings)
- named-bindings/java.ts (extractJavaNamedBindings)

Each provider now imports its binding extractor from the per-language file.

* refactor: eliminate import-resolution.ts — distribute to natural homes

Split per-language resolvers into import-resolvers/ per-language files and
eliminate the import-resolution.ts catch-all module entirely:

Per-language resolvers moved to import-resolvers/:
- standard.ts: resolveStandard, resolveJavascriptImport, resolveTypescriptImport,
  resolveCImport, resolveCppImport
- jvm.ts: resolveJavaImport, resolveKotlinImport
- go.ts: resolveGoImport
- csharp.ts: resolveCSharpImport (helper renamed to Internal)
- php.ts, python.ts, ruby.ts, rust.ts: same pattern
- swift.ts: new file for resolveSwiftImport

Types distributed to their concern directories:
- import-resolvers/types.ts: ImportResult, ImportConfigs, ResolveCtx, ImportResolverFn
- named-bindings/types.ts: NamedBinding, NamedBindingExtractorFn

preprocessImportPath moved to import-processor.ts (its primary consumer).

import-resolution.ts deleted — zero catch-all modules remain.

* refactor: tighten SPR — eliminate re-exports, dead code, type holes, and redundant patterns

12 review findings resolved across the ingestion layer:

Type safety:
- CallRouter callNode: any → SyntaxNode (closes type hole)
- CaptureMap type alias replaces Record<string, any>
- providersWithImplicitWiring filter now type-narrowed (removes ! assertions)
- Ruby exportChecker: unnecessary as-cast removed, named export created

Architecture:
- Circular type dependency eliminated (ImportResolutionContext moved to types.ts)
- LANGUAGE_QUERIES residual dispatch replaced with provider.treeSitterQueries
- noRouting sentinel deleted — callRouter now properly optional on 12 providers
- All 6 re-exports from import-processor/pipeline/languages eliminated

Pattern cleanup:
- Dead checkersByLanguage table + isNodeExported removed from export-detection
- 4 duplicated config interfaces consolidated to language-config.ts
- extractCsharpNamedBindings → extractCSharpNamedBindings (casing consistency)

Simplification:
- import-resolvers/index.ts barrel deleted (dead re-exports)
- helpers/ inlined into languages/ (php.ts, swift.ts) — 1 directory removed

Verified: tsc --noEmit clean, 3837 tests pass, 0 failures.

* refactor: address review — remove LANGUAGE_QUERIES table, type-extractors barrel, fix Windows timeout

Review comment fixes (github.com/abhigyanpatwari/GitNexus/pull/488#issuecomment-4117817648):

1. LANGUAGE_QUERIES dispatch table removed from tree-sitter-queries.ts
   — 5 test files migrated to getProvider(lang).treeSitterQueries
   — eliminates last parallel dispatch surface

2. type-extractors/index.ts barrel deleted
   — type-env.ts now imports TYPED_PARAMETER_TYPES from shared.js directly

3. Windows CI timeout fix: afterAll cleanup hook in test-indexed-db.ts
   now passes explicit 120s timeout to prevent KuzuDB C++ destructor
   hang from hitting vitest's default 30s testTimeout on Windows

Verified: tsc --noEmit clean, 3835 tests pass, 0 failures.

* refactor: eliminate chained getProvider property access — assign to variable first

All getProvider(lang).property calls now follow the pattern:
  const provider = getProvider(language);
  const x = provider.property;

5 source files + 4 test files updated (~35 occurrences).
This ensures consistent provider variable usage and avoids
repeated lookups in hot paths.

* refactor: remove last 4 re-exports from import-resolvers, fix stale CaptureMap comment

- Remove `export type { TsconfigPaths }` from standard.ts
- Remove `export type { GoModuleConfig }` from go.ts
- Remove `export type { ComposerConfig }` from php.ts
- Remove `export type { CSharpProjectConfig }` from csharp.ts
  All 4 types are canonically defined in language-config.ts;
  zero consumers imported via the resolver re-exports.

- Fix stale CaptureMap JSDoc: said "Uses any" but type is SyntaxNode | undefined
2026-03-24 13:42:39 +00:00

532 lines
17 KiB
TypeScript

/**
* 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,
DEFINITION_CAPTURE_KEYS,
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<string, any> = {};
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# 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');
}
});
});