mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +00:00
* 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
461 lines
18 KiB
TypeScript
461 lines
18 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { computeMRO } from '../../src/core/ingestion/mro-processor.js';
|
|
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
|
|
import type { KnowledgeGraph } from '../../src/core/graph/types.js';
|
|
import { generateId } from '../../src/lib/utils.js';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function addClass(graph: KnowledgeGraph, name: string, language: string, label: 'Class' | 'Interface' | 'Struct' | 'Trait' = 'Class') {
|
|
const id = generateId(label, name);
|
|
graph.addNode({
|
|
id,
|
|
label,
|
|
properties: { name, filePath: `src/${name}.ts`, language },
|
|
});
|
|
return id;
|
|
}
|
|
|
|
function addMethod(graph: KnowledgeGraph, className: string, methodName: string, classLabel: 'Class' | 'Interface' | 'Struct' | 'Trait' = 'Class') {
|
|
const classId = generateId(classLabel, className);
|
|
const methodId = generateId('Method', `${className}.${methodName}`);
|
|
graph.addNode({
|
|
id: methodId,
|
|
label: 'Method',
|
|
properties: { name: methodName, filePath: `src/${className}.ts` },
|
|
});
|
|
graph.addRelationship({
|
|
id: generateId('HAS_METHOD', `${classId}->${methodId}`),
|
|
sourceId: classId,
|
|
targetId: methodId,
|
|
type: 'HAS_METHOD',
|
|
confidence: 1.0,
|
|
reason: '',
|
|
});
|
|
return methodId;
|
|
}
|
|
|
|
function addExtends(graph: KnowledgeGraph, childName: string, parentName: string, childLabel: 'Class' | 'Struct' = 'Class', parentLabel: 'Class' | 'Interface' | 'Trait' = 'Class') {
|
|
const childId = generateId(childLabel, childName);
|
|
const parentId = generateId(parentLabel, parentName);
|
|
graph.addRelationship({
|
|
id: generateId('EXTENDS', `${childId}->${parentId}`),
|
|
sourceId: childId,
|
|
targetId: parentId,
|
|
type: 'EXTENDS',
|
|
confidence: 1.0,
|
|
reason: '',
|
|
});
|
|
}
|
|
|
|
function addImplements(graph: KnowledgeGraph, childName: string, parentName: string, childLabel: 'Class' | 'Struct' = 'Class', parentLabel: 'Interface' | 'Trait' = 'Interface') {
|
|
const childId = generateId(childLabel, childName);
|
|
const parentId = generateId(parentLabel, parentName);
|
|
graph.addRelationship({
|
|
id: generateId('IMPLEMENTS', `${childId}->${parentId}`),
|
|
sourceId: childId,
|
|
targetId: parentId,
|
|
type: 'IMPLEMENTS',
|
|
confidence: 1.0,
|
|
reason: '',
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('computeMRO', () => {
|
|
|
|
// ---- C++ diamond --------------------------------------------------------
|
|
describe('C++ diamond inheritance', () => {
|
|
it('leftmost base wins when both B and C override foo', () => {
|
|
// Diamond: A <- B, A <- C, B <- D, C <- D
|
|
const graph = createKnowledgeGraph();
|
|
const aId = addClass(graph, 'A', 'cpp');
|
|
const bId = addClass(graph, 'B', 'cpp');
|
|
const cId = addClass(graph, 'C', 'cpp');
|
|
const dId = addClass(graph, 'D', 'cpp');
|
|
|
|
addExtends(graph, 'B', 'A');
|
|
addExtends(graph, 'C', 'A');
|
|
addExtends(graph, 'D', 'B'); // B is leftmost
|
|
addExtends(graph, 'D', 'C');
|
|
|
|
// A has foo, B overrides foo, C overrides foo
|
|
addMethod(graph, 'A', 'foo');
|
|
const bFoo = addMethod(graph, 'B', 'foo');
|
|
const cFoo = addMethod(graph, 'C', 'foo');
|
|
|
|
const result = computeMRO(graph);
|
|
|
|
// D should have an entry with ambiguity on foo
|
|
const dEntry = result.entries.find(e => e.className === 'D');
|
|
expect(dEntry).toBeDefined();
|
|
expect(dEntry!.language).toBe('cpp');
|
|
|
|
const fooAmbiguity = dEntry!.ambiguities.find(a => a.methodName === 'foo');
|
|
expect(fooAmbiguity).toBeDefined();
|
|
expect(fooAmbiguity!.definedIn.length).toBeGreaterThanOrEqual(2);
|
|
|
|
// Leftmost base (B) wins
|
|
expect(fooAmbiguity!.resolvedTo).toBe(bFoo);
|
|
expect(fooAmbiguity!.reason).toContain('leftmost base');
|
|
expect(fooAmbiguity!.reason).toContain('B');
|
|
|
|
// OVERRIDES edge emitted
|
|
expect(result.overrideEdges).toBeGreaterThanOrEqual(1);
|
|
const overrides = graph.relationships.filter(r => r.type === 'OVERRIDES');
|
|
expect(overrides.some(r => r.sourceId === dId && r.targetId === bFoo)).toBe(true);
|
|
});
|
|
|
|
it('no ambiguity when foo only in A (diamond no override)', () => {
|
|
// Diamond: A <- B, A <- C, B <- D, C <- D, but only A has foo
|
|
const graph = createKnowledgeGraph();
|
|
addClass(graph, 'A', 'cpp');
|
|
addClass(graph, 'B', 'cpp');
|
|
addClass(graph, 'C', 'cpp');
|
|
addClass(graph, 'D', 'cpp');
|
|
|
|
addExtends(graph, 'B', 'A');
|
|
addExtends(graph, 'C', 'A');
|
|
addExtends(graph, 'D', 'B');
|
|
addExtends(graph, 'D', 'C');
|
|
|
|
// Only A has foo
|
|
addMethod(graph, 'A', 'foo');
|
|
|
|
const result = computeMRO(graph);
|
|
|
|
const dEntry = result.entries.find(e => e.className === 'D');
|
|
expect(dEntry).toBeDefined();
|
|
// A::foo appears only once across ancestors — no collision
|
|
// (B and C don't have their own foo, the duplicate is A::foo seen through both paths)
|
|
const fooAmbiguity = dEntry!.ambiguities.find(a => a.methodName === 'foo');
|
|
expect(fooAmbiguity).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---- C# class + interface -----------------------------------------------
|
|
describe('C# class + interface', () => {
|
|
it('class method beats interface default', () => {
|
|
const graph = createKnowledgeGraph();
|
|
const classId = addClass(graph, 'MyClass', 'csharp');
|
|
const baseId = addClass(graph, 'BaseClass', 'csharp');
|
|
const ifaceId = addClass(graph, 'IDoSomething', 'csharp', 'Interface');
|
|
|
|
addExtends(graph, 'MyClass', 'BaseClass');
|
|
addImplements(graph, 'MyClass', 'IDoSomething');
|
|
|
|
const baseDoIt = addMethod(graph, 'BaseClass', 'doIt');
|
|
const ifaceDoIt = addMethod(graph, 'IDoSomething', 'doIt', 'Interface');
|
|
|
|
const result = computeMRO(graph);
|
|
|
|
const entry = result.entries.find(e => e.className === 'MyClass');
|
|
expect(entry).toBeDefined();
|
|
|
|
const doItAmbiguity = entry!.ambiguities.find(a => a.methodName === 'doIt');
|
|
expect(doItAmbiguity).toBeDefined();
|
|
// Class method wins
|
|
expect(doItAmbiguity!.resolvedTo).toBe(baseDoIt);
|
|
expect(doItAmbiguity!.reason).toContain('class method wins');
|
|
});
|
|
|
|
it('multiple interface methods with same name are ambiguous', () => {
|
|
const graph = createKnowledgeGraph();
|
|
addClass(graph, 'MyClass', 'csharp');
|
|
addClass(graph, 'IFoo', 'csharp', 'Interface');
|
|
addClass(graph, 'IBar', 'csharp', 'Interface');
|
|
|
|
addImplements(graph, 'MyClass', 'IFoo');
|
|
addImplements(graph, 'MyClass', 'IBar');
|
|
|
|
addMethod(graph, 'IFoo', 'process', 'Interface');
|
|
addMethod(graph, 'IBar', 'process', 'Interface');
|
|
|
|
const result = computeMRO(graph);
|
|
|
|
const entry = result.entries.find(e => e.className === 'MyClass');
|
|
expect(entry).toBeDefined();
|
|
|
|
const processAmbiguity = entry!.ambiguities.find(a => a.methodName === 'process');
|
|
expect(processAmbiguity).toBeDefined();
|
|
expect(processAmbiguity!.resolvedTo).toBeNull();
|
|
expect(processAmbiguity!.reason).toContain('ambiguous');
|
|
expect(result.ambiguityCount).toBeGreaterThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
// ---- Python C3 ----------------------------------------------------------
|
|
describe('Python C3 linearization', () => {
|
|
it('C3 order determines winner in diamond with overrides', () => {
|
|
// Diamond: A <- B, A <- C, B <- D, C <- D
|
|
// class D(B, C) → C3 MRO: B, C, A
|
|
const graph = createKnowledgeGraph();
|
|
addClass(graph, 'A', 'python');
|
|
addClass(graph, 'B', 'python');
|
|
addClass(graph, 'C', 'python');
|
|
const dId = addClass(graph, 'D', 'python');
|
|
|
|
addExtends(graph, 'B', 'A');
|
|
addExtends(graph, 'C', 'A');
|
|
addExtends(graph, 'D', 'B'); // B first → leftmost in C3
|
|
addExtends(graph, 'D', 'C');
|
|
|
|
addMethod(graph, 'A', 'foo');
|
|
const bFoo = addMethod(graph, 'B', 'foo');
|
|
addMethod(graph, 'C', 'foo');
|
|
|
|
const result = computeMRO(graph);
|
|
|
|
const dEntry = result.entries.find(e => e.className === 'D');
|
|
expect(dEntry).toBeDefined();
|
|
|
|
const fooAmbiguity = dEntry!.ambiguities.find(a => a.methodName === 'foo');
|
|
expect(fooAmbiguity).toBeDefined();
|
|
// C3 linearization for D(B, C): B comes first
|
|
expect(fooAmbiguity!.resolvedTo).toBe(bFoo);
|
|
expect(fooAmbiguity!.reason).toContain('C3 MRO');
|
|
});
|
|
});
|
|
|
|
// ---- Java class + interface ---------------------------------------------
|
|
describe('Java class + interface', () => {
|
|
it('class method beats interface default', () => {
|
|
const graph = createKnowledgeGraph();
|
|
addClass(graph, 'Service', 'java');
|
|
addClass(graph, 'BaseService', 'java');
|
|
addClass(graph, 'Runnable', 'java', 'Interface');
|
|
|
|
addExtends(graph, 'Service', 'BaseService');
|
|
addImplements(graph, 'Service', 'Runnable');
|
|
|
|
const baseRun = addMethod(graph, 'BaseService', 'run');
|
|
addMethod(graph, 'Runnable', 'run', 'Interface');
|
|
|
|
const result = computeMRO(graph);
|
|
|
|
const entry = result.entries.find(e => e.className === 'Service');
|
|
expect(entry).toBeDefined();
|
|
|
|
const runAmbiguity = entry!.ambiguities.find(a => a.methodName === 'run');
|
|
expect(runAmbiguity).toBeDefined();
|
|
expect(runAmbiguity!.resolvedTo).toBe(baseRun);
|
|
expect(runAmbiguity!.reason).toContain('class method wins');
|
|
});
|
|
});
|
|
|
|
// ---- Rust trait conflicts -----------------------------------------------
|
|
describe('Rust trait conflicts', () => {
|
|
it('trait conflicts result in null resolution with qualified syntax reason', () => {
|
|
const graph = createKnowledgeGraph();
|
|
addClass(graph, 'MyStruct', 'rust', 'Struct');
|
|
addClass(graph, 'TraitA', 'rust', 'Trait');
|
|
addClass(graph, 'TraitB', 'rust', 'Trait');
|
|
|
|
addImplements(graph, 'MyStruct', 'TraitA', 'Struct', 'Trait');
|
|
addImplements(graph, 'MyStruct', 'TraitB', 'Struct', 'Trait');
|
|
|
|
addMethod(graph, 'TraitA', 'execute', 'Trait');
|
|
addMethod(graph, 'TraitB', 'execute', 'Trait');
|
|
|
|
const result = computeMRO(graph);
|
|
|
|
const entry = result.entries.find(e => e.className === 'MyStruct');
|
|
expect(entry).toBeDefined();
|
|
|
|
const execAmbiguity = entry!.ambiguities.find(a => a.methodName === 'execute');
|
|
expect(execAmbiguity).toBeDefined();
|
|
expect(execAmbiguity!.resolvedTo).toBeNull();
|
|
expect(execAmbiguity!.reason).toContain('qualified syntax');
|
|
expect(result.ambiguityCount).toBeGreaterThanOrEqual(1);
|
|
|
|
// No OVERRIDES edge emitted for Rust ambiguity
|
|
const overrides = graph.relationships.filter(
|
|
r => r.type === 'OVERRIDES' && r.sourceId === generateId('Struct', 'MyStruct')
|
|
);
|
|
expect(overrides).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ---- Property collisions don't trigger OVERRIDES ------------------------
|
|
describe('Property nodes excluded from OVERRIDES', () => {
|
|
it('property name collision across parents does not emit OVERRIDES edge', () => {
|
|
const graph = createKnowledgeGraph();
|
|
const parentA = addClass(graph, 'ParentA', 'typescript');
|
|
const parentB = addClass(graph, 'ParentB', 'typescript');
|
|
const child = addClass(graph, 'Child', 'typescript');
|
|
|
|
addExtends(graph, 'Child', 'ParentA');
|
|
addExtends(graph, 'Child', 'ParentB');
|
|
|
|
// Add Property nodes (same name 'name') to both parents via HAS_PROPERTY
|
|
const propA = generateId('Property', 'ParentA.name');
|
|
graph.addNode({ id: propA, label: 'Property', properties: { name: 'name', filePath: 'src/ParentA.ts' } });
|
|
graph.addRelationship({
|
|
id: generateId('HAS_PROPERTY', `${parentA}->${propA}`),
|
|
sourceId: parentA, targetId: propA, type: 'HAS_PROPERTY', confidence: 1.0, reason: '',
|
|
});
|
|
|
|
const propB = generateId('Property', 'ParentB.name');
|
|
graph.addNode({ id: propB, label: 'Property', properties: { name: 'name', filePath: 'src/ParentB.ts' } });
|
|
graph.addRelationship({
|
|
id: generateId('HAS_PROPERTY', `${parentB}->${propB}`),
|
|
sourceId: parentB, targetId: propB, type: 'HAS_PROPERTY', confidence: 1.0, reason: '',
|
|
});
|
|
|
|
const result = computeMRO(graph);
|
|
|
|
// No OVERRIDES edge should be emitted for properties
|
|
const overrides = graph.relationships.filter(r => r.type === 'OVERRIDES');
|
|
expect(overrides).toHaveLength(0);
|
|
expect(result.overrideEdges).toBe(0);
|
|
});
|
|
|
|
it('method collision still triggers OVERRIDES even when properties also collide', () => {
|
|
const graph = createKnowledgeGraph();
|
|
const parentA = addClass(graph, 'PA', 'cpp');
|
|
const parentB = addClass(graph, 'PB', 'cpp');
|
|
addClass(graph, 'Ch', 'cpp');
|
|
|
|
addExtends(graph, 'Ch', 'PA');
|
|
addExtends(graph, 'Ch', 'PB');
|
|
|
|
// Method collision (should trigger OVERRIDES)
|
|
const methodA = addMethod(graph, 'PA', 'doWork');
|
|
addMethod(graph, 'PB', 'doWork');
|
|
|
|
// Property collision (should NOT trigger OVERRIDES — properties use HAS_PROPERTY, not HAS_METHOD)
|
|
const propA = generateId('Property', 'PA.id');
|
|
graph.addNode({ id: propA, label: 'Property', properties: { name: 'id', filePath: 'src/PA.ts' } });
|
|
graph.addRelationship({
|
|
id: generateId('HAS_PROPERTY', `${parentA}->${propA}`),
|
|
sourceId: parentA, targetId: propA, type: 'HAS_PROPERTY', confidence: 1.0, reason: '',
|
|
});
|
|
|
|
const propB = generateId('Property', 'PB.id');
|
|
graph.addNode({ id: propB, label: 'Property', properties: { name: 'id', filePath: 'src/PB.ts' } });
|
|
graph.addRelationship({
|
|
id: generateId('HAS_PROPERTY', `${parentB}->${propB}`),
|
|
sourceId: parentB, targetId: propB, type: 'HAS_PROPERTY', confidence: 1.0, reason: '',
|
|
});
|
|
|
|
const result = computeMRO(graph);
|
|
|
|
// Only 1 OVERRIDES edge (for the method, not the property)
|
|
const overrides = graph.relationships.filter(r => r.type === 'OVERRIDES');
|
|
expect(overrides).toHaveLength(1);
|
|
expect(overrides[0].targetId).toBe(methodA); // leftmost base wins for C++
|
|
expect(result.overrideEdges).toBe(1);
|
|
});
|
|
});
|
|
|
|
// ---- No ambiguity: single parent ----------------------------------------
|
|
describe('single parent, no ambiguity', () => {
|
|
it('single parent with unique methods produces no ambiguities', () => {
|
|
const graph = createKnowledgeGraph();
|
|
addClass(graph, 'Parent', 'typescript');
|
|
addClass(graph, 'Child', 'typescript');
|
|
|
|
addExtends(graph, 'Child', 'Parent');
|
|
|
|
addMethod(graph, 'Parent', 'foo');
|
|
addMethod(graph, 'Parent', 'bar');
|
|
|
|
const result = computeMRO(graph);
|
|
|
|
const entry = result.entries.find(e => e.className === 'Child');
|
|
expect(entry).toBeDefined();
|
|
expect(entry!.ambiguities).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ---- No parents: standalone class not in entries ------------------------
|
|
describe('standalone class', () => {
|
|
it('class with no parents is not included in entries', () => {
|
|
const graph = createKnowledgeGraph();
|
|
addClass(graph, 'Standalone', 'typescript');
|
|
addMethod(graph, 'Standalone', 'doStuff');
|
|
|
|
const result = computeMRO(graph);
|
|
|
|
const entry = result.entries.find(e => e.className === 'Standalone');
|
|
expect(entry).toBeUndefined();
|
|
expect(result.overrideEdges).toBe(0);
|
|
expect(result.ambiguityCount).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ---- Own method shadows ancestor ----------------------------------------
|
|
describe('own method shadows ancestor', () => {
|
|
it('class defining its own method suppresses ambiguity', () => {
|
|
const graph = createKnowledgeGraph();
|
|
addClass(graph, 'Base1', 'cpp');
|
|
addClass(graph, 'Base2', 'cpp');
|
|
addClass(graph, 'Child', 'cpp');
|
|
|
|
addExtends(graph, 'Child', 'Base1');
|
|
addExtends(graph, 'Child', 'Base2');
|
|
|
|
addMethod(graph, 'Base1', 'foo');
|
|
addMethod(graph, 'Base2', 'foo');
|
|
addMethod(graph, 'Child', 'foo'); // own method
|
|
|
|
const result = computeMRO(graph);
|
|
|
|
const entry = result.entries.find(e => e.className === 'Child');
|
|
expect(entry).toBeDefined();
|
|
// No ambiguity because Child defines its own foo
|
|
const fooAmbiguity = entry!.ambiguities.find(a => a.methodName === 'foo');
|
|
expect(fooAmbiguity).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---- Empty graph --------------------------------------------------------
|
|
describe('empty graph', () => {
|
|
it('returns empty result for graph with no classes', () => {
|
|
const graph = createKnowledgeGraph();
|
|
const result = computeMRO(graph);
|
|
expect(result.entries).toHaveLength(0);
|
|
expect(result.overrideEdges).toBe(0);
|
|
expect(result.ambiguityCount).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ---- Cyclic inheritance (P1 fix) ----------------------------------------
|
|
describe('cyclic inheritance', () => {
|
|
it('does not stack overflow on cyclic Python hierarchy', () => {
|
|
// A extends B, B extends A — cyclic
|
|
const graph = createKnowledgeGraph();
|
|
addClass(graph, 'A', 'python');
|
|
addClass(graph, 'B', 'python');
|
|
addExtends(graph, 'A', 'B');
|
|
addExtends(graph, 'B', 'A');
|
|
addMethod(graph, 'A', 'foo');
|
|
addMethod(graph, 'B', 'foo');
|
|
|
|
// Should NOT throw — c3Linearize returns null, falls back to BFS
|
|
const result = computeMRO(graph);
|
|
expect(result).toBeDefined();
|
|
// Both A and B have parents, so both get entries
|
|
expect(result.entries.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('handles 3-node cycle gracefully', () => {
|
|
// A → B → C → A
|
|
const graph = createKnowledgeGraph();
|
|
addClass(graph, 'X', 'python');
|
|
addClass(graph, 'Y', 'python');
|
|
addClass(graph, 'Z', 'python');
|
|
addExtends(graph, 'X', 'Y');
|
|
addExtends(graph, 'Y', 'Z');
|
|
addExtends(graph, 'Z', 'X');
|
|
|
|
const result = computeMRO(graph);
|
|
expect(result).toBeDefined();
|
|
});
|
|
});
|
|
});
|