mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* feat: add Method Resolution Order (MRO) with language-specific rules
Implement full MRO computation for multi-language inheritance hierarchies:
- HAS_METHOD edges: Class→Method ownership edges emitted during parsing
(both worker pool and sequential fallback paths)
- Method signatures: extract parameterCount and returnType from AST nodes
- C# heritage fix: distinguish EXTENDS vs IMPLEMENTS for base_list captures
using symbol table lookup + I[A-Z] naming heuristic fallback
- MRO processor (Phase 4.5): walks inheritance DAG, detects method-name
collisions across parents, applies language-specific resolution:
- C++: leftmost base class in declaration order wins
- C#/Java: class method wins over interface default
- Python: C3 linearization with cycle detection
- Rust: no auto-resolution (requires qualified syntax)
- Default: first definition in BFS order wins
- OVERRIDES edges emitted for resolved method collisions
- KuzuDB schema: Method table extended with parameterCount/returnType;
dedicated CSV writer and COPY query for 10-column Method rows
- MCP tools: updated Cypher examples for HAS_METHOD, OVERRIDES, diamond
72 tests across 5 test files covering MRO resolution, HAS_METHOD edges,
method signature extraction, C# heritage resolution, and integration
tests across C#/Rust/Python/TS/Java/C++.
* feat: add scope-based symbol resolution replacing raw lookupFuzzy
Introduces a shared 3-tier resolveSymbol function used by both
heritage-processor and call-processor:
1. Same-file (lookupExactFull — authoritative)
2. Import-scoped (filtered by ImportMap — high confidence)
3. Global fuzzy (first match — low confidence fallback)
Adds lookupExactFull to SymbolTable returning full SymbolDefinition
with type info needed for heritage Class/Interface disambiguation.
* refactor: tighten symbol resolution — Tier 3 refuses ambiguous matches
- lookupExactFull now O(1) via direct SymbolDefinition storage in fileIndex
(shared object references with globalIndex — zero additional memory)
- Added resolveSymbolInternal() preserving { definition, tier, candidateCount }
for test assertions and logging
- Tier 3 now returns null when multiple global candidates exist instead of
arbitrary allDefs[0] — a wrong edge is worse than no edge
- call-processor: renamed fuzzy-global → unique-global, removed dead branch
- 12 new tests: tier assertions, ambiguous refusal per language family,
heritage false-positive guard, O(1) shared reference verification
* fix: critical language support bugs in import resolution and MRO
Phase 5 critical fixes from all-language analysis:
- Python: add relative_import query capture (PEP 328) — `.models`, `..utils`
were silently dropped, producing zero ImportMap entries
- Rust: extract prefix from grouped imports `crate::module::{A, B}` — brace
groups previously failed resolution entirely
- Swift: use normalizedFileList for Windows path compatibility in module
import resolution (matches Go's resolveGoPackage pattern)
- MRO: fix c_sharp → csharp language name mismatch (enum is 'csharp'),
add Kotlin to C#/Java resolution rules (class method wins over interface)
* feat: add strict multi-language integration tests + fix C/C++ import resolution
Add 32 integration tests across 6 language fixtures (TypeScript, C#, C++,
Java, Python, Rust) with exact toBe/toEqual assertions validating heritage
edges, import resolution, and trait implementations.
Fix C/C++ import resolution bug where dot-to-slash conversion mangled
include paths (e.g. "animal.h" became "animal/h"). Now skips conversion
for C/C++ languages which use actual file paths in #include directives.
* fix: language-gate heritage heuristic, add Swift extension heritage, handle Rust grouped imports
- Gate I[A-Z] naming heuristic to C#/Java only (was firing for all languages)
- Swift unresolved types default to IMPLEMENTS (protocol conformance is the norm)
- Add tree-sitter query for Swift extension protocol conformance (extension Foo: Protocol)
- Handle Rust top-level grouped imports (use {crate::a, crate::b}) in both import loops
- Add 4 new heritage-processor tests (TypeScript refusal, Swift default, Swift Tier 1)
* feat: add Go struct embedding heritage + PackageMap optimization
Add Go struct embedding detection (anonymous fields → EXTENDS edges) via
new tree-sitter heritage query with named-field filtering in both
parse-worker and heritage-processor paths.
Implement PackageMap optimization for Go cross-package resolution:
replace O(N) file-level ImportMap expansion with directory-level suffix
matching (Tier 2b in symbol resolver). Graph IMPORTS edges are preserved
via addImportGraphEdge split.
Remove overly broad @definition.type from GO_QUERIES that was
double-matching structs/interfaces as TypeAlias nodes, breaking Tier 3
unique-global resolution.
Add Go fixture (go-pkg) with Admin→User embedding, cross-package calls,
and 7 integration tests covering structs, functions, imports, calls,
and heritage edges.
* test: add Kotlin heritage integration tests
Adds a kotlin-heritage fixture and 7 integration tests validating
class inheritance, interface implementation, JVM-style import
resolution, and symbol-table-driven EXTENDS/IMPLEMENTS disambiguation
via Kotlin delegation specifiers.
* feat: extract resolvers, add PHP tests, ambiguous tests for all languages
- Extract language-specific resolvers from import-processor.ts into
resolvers/ directory (P7): jvm, go, csharp, php, rust, standard, utils
- import-processor.ts reduced from 1412 to 711 lines (50% reduction)
- Add comprehensive PHP integration tests: PSR-4 imports, traits, enums,
heritage edges, method calls, MRO overrides
- Add ambiguous symbol resolution tests for all 9 languages verifying
correct disambiguation via import chains
- Split monolithic lang-resolution.test.ts (1080 lines) into 9 per-language
files under test/integration/resolvers/ with shared helpers
* feat: update integration tests to include resolver tests for multiple languages
* fix: address code review — schema gap, Rust impl name, Property OVERRIDES
Bugs fixed:
- Add 13 missing FROM/TO pairs in RELATION_SCHEMA for HAS_METHOD edges
(Class/Interface/Struct/Trait/Impl/Record to Method/Constructor/Property)
- Fix findEnclosingClassId to pick implementing type for Rust
impl Trait for Struct blocks (was picking trait name)
- Exclude Property nodes from MRO OVERRIDES collision detection
- Change MRO language fallback from typescript to unknown
Tests added:
- Unit: Property OVERRIDES exclusion (2 tests), Rust impl Trait for
Struct name resolution (2 tests), schema HAS_METHOD pair coverage
- Integration: no OVERRIDES targets Property nodes across all 9 languages
- PHP fixture: added shared $status property to both traits to create
real collision scenario for Property OVERRIDES exclusion test
Documentation:
- OVERRIDES edge direction (Class to Method), Go return type gap,
BFS first-reach heuristic limitation
* feat: harden CALLS-edge resolution — Phase 0 validation
- Fix same-file confidence (0.85 → 0.95) to correctly outrank import-scoped (0.9)
- Fix Tier 1 overload preservation: use globalIndex filter instead of fileIndex lookup
- Add callable-kind guard: refuse CALLS edges to Interface and Enum symbols
- Fix Kotlin countCallArguments: handle call_suffix → value_arguments nesting
- Fix Kotlin extractFunctionName: add simple_identifier to fallback search
- Strictly type findParameterList and countCallArguments (remove all `any`)
- Add arity-based call resolution integration tests for 9 languages
- Add unit regression tests for Interface/Enum CALLS refusal
* chore: remove C# build artifacts from fixtures
* feat: add call-form discrimination and ownerId to symbol table (Phase 1)
Add inferCallForm() and extractReceiverName() to distinguish free/member/constructor
calls at the AST level across all 9 languages. Add ownerId field to SymbolDefinition
linking Method/Constructor/Property to their owning class. Includes 36 unit tests
and member-call integration tests for all 9 languages (132 tests, 0 failures).
* feat: constructor/struct-literal resolution across all languages (Phase 2)
Add constructor discrimination to CALLS-edge resolution: new Foo(),
User{...} struct literals, and C# primary constructors now resolve to
Constructor/Class/Struct/Record nodes instead of being filtered out.
Queries: new_expression (C++), object_creation_expression (PHP),
composite_literal (Go), struct_expression (Rust), primary constructor
and implicit_object_creation_expression (C#).
Relaxes global tier in collectTieredCandidates to pass all candidates
through filterCallableCandidates, allowing kind/arity narrowing to
disambiguate at lower confidence.
* feat: receiver-constrained resolution with integration tests for all 9 languages
Add receiver-type filtering (Phase 3): when a member call like `user.save()`
has a known receiver type from TypeEnv, filter candidates by ownerId to
disambiguate methods with the same name across different classes.
Key changes:
- call-processor: build per-file TypeEnv, pass receiverTypeName to resolveCallTarget
- parse-worker: extract receiverTypeName from TypeEnv in worker thread
- resolveCallTarget: new step D filters by ownerId matching receiver type
- utils: extractReceiverName supports C++ field_expression (argument field)
- utils: findEnclosingClassId extracts Go method receiver types
- type-env: handle Go qualified_type, Kotlin user_type/variable_declaration
- parse-worker + parsing-processor: Function added to needsOwner for
Kotlin/Rust/Python class methods captured as Function nodes
Integration tests added for receiver-constrained resolution across all 9
languages: TypeScript, Java, Python, Go, Rust, C++, C#, Kotlin, PHP.
* feat: NamedImportMap, scoped TypeEnv, broadened signatures + TS rest-param variadic fix
Address all 4 PR #238 review items:
1. Remove redundant lookupFuzzy in processRoutesFromExtracted
2. Add NamedImportMap for TS/Python symbol-level import tracking (Tier 2a)
3. Make TypeEnv scope-aware (Map<scopeKey, Map<varName, type>>) to fix
non-deterministic receiver resolution across functions
4. Broaden extractMethodSignature: Go/Rust/C++ return types, variadic
detection for Go/Java/Python/C++/Kotlin/TypeScript rest params
Discovered and fixed: TS rest params (...args) were not detected as
variadic — added rest_pattern detection inside required_parameter nodes.
Integration tests added: scoped receiver, named import disambiguation,
and variadic call resolution for both TypeScript and Python.
* fix: alias import resolution, Go multi-assign TypeEnv, dead code removal
- NamedImportMap now stores {sourcePath, exportedName} so aliased imports
(import { User as U }) resolve U → User in the source file
- Named binding check moved before empty-allDefs early return in both
call-processor and symbol-resolver, fixing constructor calls via aliases
- Go extractFromGoShortVarDeclaration iterates all LHS/RHS pairs for
multi-assignment (user, repo := User{}, Repo{}) instead of only first
- Remove unused TYPED_DECLARATION_TYPES set (TYPED_PARAMETER_TYPES kept)
- Integration tests for both fixes (go-multi-assign, typescript-alias-imports)
* feat: alias import extraction for Kotlin, Rust, PHP, C# + integration tests
Add named import alias extraction to both pipeline paths
(import-processor.ts and parse-worker.ts) for Kotlin, Rust, PHP,
and C#. Add integration test fixtures and tests for all 5 languages
(Python alias extraction already worked, just needed the test).
Each test verifies: class detection, member call resolution through
aliases to correct target files, and IMPORTS edge emission.
* refactor: use SupportedLanguages enum everywhere instead of raw strings
Replace all raw language string literals and `language: string` types
with the SupportedLanguages enum across 10 files. This ensures
compile-time safety for language dispatch and eliminates dead
`language === 'tsx'` checks (tsx maps to TypeScript in the enum).
* fix: tier-ordering bug, re-export chains, PHP grouped imports, Java named imports
- Fix collectTieredCandidates tier-ordering: same-file now checked before
named bindings, preventing imports from shadowing local definitions
(matches resolveSymbolInternal priority order)
- Add re-export chain resolution for TypeScript/JavaScript barrel files:
export { X } from './base' and export type { X } from './base' now
followed up to 5 hops through NamedImportMap
- Fix PHP grouped import alias extraction: use App\Models\{User, Repo as R}
now correctly handled in both parse-worker and import-processor
- Add Java NamedImportMap support: import com.example.models.User now
records User as a named binding for precise disambiguation
- Add 16 new integration tests across TypeScript, PHP, and Java resolvers
(220 total resolver tests, all passing)
* refactor: consolidate alias extraction + add variadic/constructor/shadow integration tests
- Extract shared named-binding-extraction.ts from duplicate logic in
import-processor.ts and parse-worker.ts (net -200 lines)
- Deduplicate appendKotlinWildcard (now imported from resolvers/index.ts)
- Add integration tests: constructor calls (Kotlin, Python), variadic
resolution (Go, Java, C#, C++, Kotlin), re-export chains (Python),
local definition shadowing (Python, Go)
- Add TODO(stack-graph) for TypeEnv scope key collision
- 225 integration tests passing (was 223)
* fix: PHP non-aliased imports, Python node identity, re-export chain dedup + local-shadow tests
- PHP flat non-aliased imports (use App\Models\User) now stored in NamedImportMap
- PHP grouped non-aliased imports ({User} in {User, Repo as R}) now stored in NamedImportMap
- Python: replace non-public child.id with child.startIndex for node identity
- Extract shared walkBindingChain() from symbol-resolver and call-processor
- Add PHP variadic resolution fixture + test (variadic_parameter already covers PHP)
- Add local-shadow integration tests for Java, C#, Kotlin, Rust, PHP, C++ (6 languages)
* feat: Rust non-aliased use bindings, Kotlin non-aliased imports, re-export chain resolution
Extend NamedImportMap coverage for Rust and Kotlin non-aliased imports:
- Rust: rename collectUseAsClauses → collectRustBindings, extract terminal
scoped_identifier (use crate::models::User) and identifier in use_list
(use crate::models::{User, Repo}) into NamedImportMap. This also enables
pub use re-export chain following via walkBindingChain.
- Kotlin: extend extractKotlinNamedBindings to handle non-aliased imports
(import com.example.User), skipping wildcard imports.
- Add rust-reexport-chain fixture + 3 integration tests verifying Handler{}
resolves through mod.rs pub use to handler.rs.
- Add Kotlin heritage + constructor-calls reason assertions for non-aliased
import-resolved resolution.
- Add C# heritage test documenting namespace import tier behavior.
* fix: skip Kotlin lowercase member imports in NamedImportMap
Member imports like `import util.OneArg.writeAudit` (lowercase last
segment) must not populate NamedImportMap — same-named function imports
from different classes collide, breaking arity-based disambiguation.
Apply the same guard Java already uses: skip lowercase last segments.
* fix: skip spurious path-prefix bindings in Rust grouped imports
collectRustBindings was extracting the path segment (e.g. "models") from
`use crate::models::{User, Repo}` as a spurious NamedImportMap entry.
Skip scoped_identifier nodes that are direct children of scoped_use_list
since they are path prefixes, not importable symbols.
Adds rust-grouped-imports fixture and 4 integration tests verifying both
symbols resolve correctly and no spurious binding leaks through.
* fix: use startIndex in TypeEnv scope key to prevent same-name method collision
Two methods named identically in different classes within the same file
previously shared a scope key, causing non-deterministic type resolution.
Now keys use funcName@startIndex for uniqueness.
Also adds tests documenting destructuring assignment extraction gap.
* test: document C# namespace-level import limitation in named binding extraction
* test: document same-arity overload discrimination limitation in call processor
* perf: parallelize calls/heritage/routes processing in worker path
Worker path now runs processCallsFromExtracted, processHeritageFromExtracted,
and processRoutesFromExtracted via Promise.all instead of sequentially.
Safe because all three only read shared state and write via addRelationship's
dedup guard. Sequential fallback path stays sequential (shared LRU astCache).
Also fixes Rust collectRustBindings spurious path-prefix bindings for 3+ level
grouped imports, and adds @param JSDoc for walkBindingChain's allDefs invariant.
* docs: improve Promise.all safety comment and walkBindingChain JSDoc
Clarify that the parallelization safety comes from disjoint relationship
types + idempotent id-keyed Maps, not from lack of shared state (the
graph is shared). Strengthen allDefs JSDoc to describe silent-miss
consequence of passing pre-filtered results.
* refactor: extract language-specific processing into modular dispatch tables
Phase 1: Extract type binding logic from type-env.ts (635→125 LOC) into
type-extractors/ directory with per-language files and Record<SupportedLanguages,
LanguageTypeConfig> + satisfies dispatch.
Phase 2: Extract 5 config loaders from import-processor.ts into
language-config.ts (removed ~196 LOC of inline loaders).
Phase 3: Convert export-detection.ts switch/case to exhaustive
Record<SupportedLanguages, ExportChecker> + satisfies dispatch table,
fix node: any → SyntaxNode.
Also adds language feature matrix to README.
All 1146 unit tests and 433 integration tests pass.
* refactor: extract type binding logic into type-extractors/ directory (Phase 1)
Extract per-language type extraction from type-env.ts (635→125 LOC) into
type-extractors/ with Record<SupportedLanguages, LanguageTypeConfig> + satisfies
dispatch. 9 per-language files, shared helpers, and barrel index.
* refactor: extract config loaders to language-config.ts (Phase 2)
Move 5 language-specific config loaders and their type interfaces from
import-processor.ts into standalone language-config.ts module.
531 lines
17 KiB
TypeScript
531 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 { LANGUAGE_QUERIES } from '../../src/core/ingestion/tree-sitter-queries.js';
|
|
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
|
import {
|
|
findEnclosingClassId,
|
|
DEFINITION_CAPTURE_KEYS,
|
|
getDefinitionNodeFromCaptures,
|
|
} from '../../src/core/ingestion/utils.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 query = new Parser.Query(parser.getLanguage(), LANGUAGE_QUERIES[lang]);
|
|
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');
|
|
}
|
|
});
|
|
});
|