mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +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
434 lines
15 KiB
TypeScript
434 lines
15 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import Parser from 'tree-sitter';
|
|
import TypeScript from 'tree-sitter-typescript';
|
|
import Python from 'tree-sitter-python';
|
|
import Java from 'tree-sitter-java';
|
|
import CPP from 'tree-sitter-cpp';
|
|
import Rust from 'tree-sitter-rust';
|
|
import CSharp from 'tree-sitter-c-sharp';
|
|
import Go from 'tree-sitter-go';
|
|
import { findEnclosingClassId, CLASS_CONTAINER_TYPES, CONTAINER_TYPE_TO_LABEL } from '../../src/core/ingestion/utils/ast-helpers.js';
|
|
|
|
function parseCode(language: any, code: string): Parser.Tree {
|
|
const parser = new Parser();
|
|
parser.setLanguage(language);
|
|
return parser.parse(code);
|
|
}
|
|
|
|
/** Find the first descendant node matching a predicate (BFS). */
|
|
function findNode(root: Parser.SyntaxNode, predicate: (n: Parser.SyntaxNode) => boolean): Parser.SyntaxNode | null {
|
|
const queue: Parser.SyntaxNode[] = [root];
|
|
while (queue.length > 0) {
|
|
const node = queue.shift()!;
|
|
if (predicate(node)) return node;
|
|
for (let i = 0; i < node.childCount; i++) {
|
|
queue.push(node.child(i)!);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
describe('CLASS_CONTAINER_TYPES', () => {
|
|
it('contains expected class-like AST node types', () => {
|
|
expect(CLASS_CONTAINER_TYPES.has('class_declaration')).toBe(true);
|
|
expect(CLASS_CONTAINER_TYPES.has('interface_declaration')).toBe(true);
|
|
expect(CLASS_CONTAINER_TYPES.has('struct_declaration')).toBe(true);
|
|
expect(CLASS_CONTAINER_TYPES.has('impl_item')).toBe(true);
|
|
expect(CLASS_CONTAINER_TYPES.has('class_specifier')).toBe(true);
|
|
expect(CLASS_CONTAINER_TYPES.has('class_definition')).toBe(true);
|
|
});
|
|
|
|
it('does not contain function types', () => {
|
|
expect(CLASS_CONTAINER_TYPES.has('function_declaration')).toBe(false);
|
|
expect(CLASS_CONTAINER_TYPES.has('function_definition')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('CONTAINER_TYPE_TO_LABEL', () => {
|
|
it('maps class-like types to correct labels', () => {
|
|
expect(CONTAINER_TYPE_TO_LABEL['class_declaration']).toBe('Class');
|
|
expect(CONTAINER_TYPE_TO_LABEL['interface_declaration']).toBe('Interface');
|
|
expect(CONTAINER_TYPE_TO_LABEL['struct_declaration']).toBe('Struct');
|
|
expect(CONTAINER_TYPE_TO_LABEL['impl_item']).toBe('Impl');
|
|
expect(CONTAINER_TYPE_TO_LABEL['trait_item']).toBe('Trait');
|
|
expect(CONTAINER_TYPE_TO_LABEL['record_declaration']).toBe('Record');
|
|
expect(CONTAINER_TYPE_TO_LABEL['protocol_declaration']).toBe('Interface');
|
|
});
|
|
});
|
|
|
|
describe('findEnclosingClassId', () => {
|
|
const filePath = 'test/example.ts';
|
|
|
|
describe('TypeScript', () => {
|
|
it('finds enclosing class for a method', () => {
|
|
const tree = parseCode(TypeScript.typescript, `
|
|
class MyService {
|
|
getData() {
|
|
return 42;
|
|
}
|
|
}
|
|
`);
|
|
// Find the method_definition node for getData
|
|
const methodNode = findNode(tree.rootNode, n => n.type === 'method_definition');
|
|
expect(methodNode).not.toBeNull();
|
|
|
|
// Find the identifier 'getData' inside the method
|
|
const nameNode = findNode(methodNode!, n => n.type === 'property_identifier' && n.text === 'getData');
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, filePath);
|
|
expect(result).toBe('Class:test/example.ts:MyService');
|
|
});
|
|
|
|
it('finds enclosing interface for a method signature', () => {
|
|
const tree = parseCode(TypeScript.typescript, `
|
|
interface MyInterface {
|
|
doSomething(): void;
|
|
}
|
|
`);
|
|
// In TS, interface methods are method_signature nodes — find method name
|
|
const nameNode = findNode(tree.rootNode, n => n.type === 'property_identifier' && n.text === 'doSomething');
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, filePath);
|
|
expect(result).toBe('Interface:test/example.ts:MyInterface');
|
|
}
|
|
});
|
|
|
|
it('returns null for a top-level function', () => {
|
|
const tree = parseCode(TypeScript.typescript, `
|
|
function topLevel() {
|
|
return 1;
|
|
}
|
|
`);
|
|
const nameNode = findNode(tree.rootNode, n => n.type === 'identifier' && n.text === 'topLevel');
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, filePath);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('returns null when node has no parent', () => {
|
|
// Root node's parent is null
|
|
const tree = parseCode(TypeScript.typescript, 'const x = 1;');
|
|
const result = findEnclosingClassId(tree.rootNode, filePath);
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Python', () => {
|
|
it('finds enclosing class for a method', () => {
|
|
const tree = parseCode(Python, `
|
|
class Calculator:
|
|
def add(self, a, b):
|
|
return a + b
|
|
`);
|
|
const methodNode = findNode(tree.rootNode, n => n.type === 'function_definition');
|
|
expect(methodNode).not.toBeNull();
|
|
|
|
const nameNode = findNode(methodNode!, n => n.type === 'identifier' && n.text === 'add');
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/calc.py');
|
|
expect(result).toBe('Class:test/calc.py:Calculator');
|
|
});
|
|
});
|
|
|
|
describe('Java', () => {
|
|
it('finds enclosing class for a method', () => {
|
|
const tree = parseCode(Java, `
|
|
class UserService {
|
|
public void save(User user) {}
|
|
}
|
|
`);
|
|
const methodNode = findNode(tree.rootNode, n => n.type === 'method_declaration');
|
|
expect(methodNode).not.toBeNull();
|
|
|
|
const nameNode = findNode(methodNode!, n => n.type === 'identifier' && n.text === 'save');
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/UserService.java');
|
|
expect(result).toBe('Class:test/UserService.java:UserService');
|
|
});
|
|
|
|
it('finds enclosing interface for a method', () => {
|
|
const tree = parseCode(Java, `
|
|
interface Repository {
|
|
void findById(int id);
|
|
}
|
|
`);
|
|
const methodNode = findNode(tree.rootNode, n => n.type === 'method_declaration');
|
|
expect(methodNode).not.toBeNull();
|
|
|
|
const nameNode = findNode(methodNode!, n => n.type === 'identifier' && n.text === 'findById');
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/Repository.java');
|
|
expect(result).toBe('Interface:test/Repository.java:Repository');
|
|
});
|
|
});
|
|
|
|
describe('C++', () => {
|
|
it('finds enclosing class_specifier for a method', () => {
|
|
const tree = parseCode(CPP, `
|
|
class Vector {
|
|
public:
|
|
int size() { return _size; }
|
|
private:
|
|
int _size;
|
|
};
|
|
`);
|
|
// In C++ tree-sitter, the class is a class_specifier
|
|
const classNode = findNode(tree.rootNode, n => n.type === 'class_specifier');
|
|
expect(classNode).not.toBeNull();
|
|
|
|
// Find a function_definition inside the class
|
|
const funcDef = findNode(classNode!, n => n.type === 'function_definition');
|
|
expect(funcDef).not.toBeNull();
|
|
|
|
const nameNode = findNode(funcDef!, n => n.type === 'identifier' && n.text === 'size');
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, 'test/vector.h');
|
|
expect(result).toBe('Class:test/vector.h:Vector');
|
|
}
|
|
});
|
|
|
|
it('finds enclosing struct_specifier for a method', () => {
|
|
const tree = parseCode(CPP, `
|
|
struct Point {
|
|
double distance() { return 0; }
|
|
};
|
|
`);
|
|
const funcDef = findNode(tree.rootNode, n => n.type === 'function_definition');
|
|
if (funcDef) {
|
|
const nameNode = findNode(funcDef, n => n.type === 'identifier' && n.text === 'distance');
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, 'test/point.h');
|
|
expect(result).toBe('Struct:test/point.h:Point');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Rust', () => {
|
|
it('finds enclosing impl_item for a method', () => {
|
|
const tree = parseCode(Rust, `
|
|
struct Counter {
|
|
count: u32,
|
|
}
|
|
impl Counter {
|
|
fn increment(&mut self) {
|
|
self.count += 1;
|
|
}
|
|
}
|
|
`);
|
|
const implNode = findNode(tree.rootNode, n => n.type === 'impl_item');
|
|
expect(implNode).not.toBeNull();
|
|
|
|
const funcItem = findNode(implNode!, n => n.type === 'function_item');
|
|
expect(funcItem).not.toBeNull();
|
|
|
|
const nameNode = findNode(funcItem!, n => n.type === 'identifier' && n.text === 'increment');
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/counter.rs');
|
|
expect(result).toBe('Impl:test/counter.rs:Counter');
|
|
});
|
|
|
|
it('picks struct name (not trait name) for impl Trait for Struct', () => {
|
|
const tree = parseCode(Rust, `
|
|
struct MyStruct {
|
|
value: i32,
|
|
}
|
|
|
|
trait MyTrait {
|
|
fn do_something(&self);
|
|
}
|
|
|
|
impl MyTrait for MyStruct {
|
|
fn do_something(&self) {
|
|
println!("{}", self.value);
|
|
}
|
|
}
|
|
`);
|
|
// Find the impl_item that has a `for` keyword (impl Trait for Struct)
|
|
const implNode = findNode(tree.rootNode, n =>
|
|
n.type === 'impl_item' && n.children?.some((c: any) => c.text === 'for')
|
|
);
|
|
expect(implNode).not.toBeNull();
|
|
|
|
const funcItem = findNode(implNode!, n => n.type === 'function_item');
|
|
expect(funcItem).not.toBeNull();
|
|
|
|
const nameNode = findNode(funcItem!, n => n.type === 'identifier' && n.text === 'do_something');
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/my_struct.rs');
|
|
// Should resolve to MyStruct (the implementing type), NOT MyTrait
|
|
expect(result).toBe('Impl:test/my_struct.rs:MyStruct');
|
|
});
|
|
|
|
it('still picks struct name for plain impl Struct (no trait)', () => {
|
|
const tree = parseCode(Rust, `
|
|
struct Counter {
|
|
count: u32,
|
|
}
|
|
impl Counter {
|
|
fn increment(&mut self) {
|
|
self.count += 1;
|
|
}
|
|
}
|
|
`);
|
|
const implNode = findNode(tree.rootNode, n => n.type === 'impl_item');
|
|
expect(implNode).not.toBeNull();
|
|
|
|
const funcItem = findNode(implNode!, n => n.type === 'function_item');
|
|
expect(funcItem).not.toBeNull();
|
|
|
|
const nameNode = findNode(funcItem!, n => n.type === 'identifier' && n.text === 'increment');
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/counter.rs');
|
|
expect(result).toBe('Impl:test/counter.rs:Counter');
|
|
});
|
|
|
|
it('finds enclosing trait_item for a method', () => {
|
|
const tree = parseCode(Rust, `
|
|
trait Drawable {
|
|
fn draw(&self);
|
|
}
|
|
`);
|
|
const traitNode = findNode(tree.rootNode, n => n.type === 'trait_item');
|
|
expect(traitNode).not.toBeNull();
|
|
|
|
const funcItem = findNode(traitNode!, n => n.type === 'function_signature_item' || n.type === 'function_item');
|
|
if (funcItem) {
|
|
const nameNode = findNode(funcItem, n => n.type === 'identifier' && n.text === 'draw');
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, 'test/draw.rs');
|
|
expect(result).toBe('Trait:test/draw.rs:Drawable');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('C#', () => {
|
|
it('finds enclosing class for a method', () => {
|
|
const tree = parseCode(CSharp, `
|
|
class OrderService {
|
|
public void Process() {}
|
|
}
|
|
`);
|
|
const methodNode = findNode(tree.rootNode, n => n.type === 'method_declaration');
|
|
expect(methodNode).not.toBeNull();
|
|
|
|
const nameNode = findNode(methodNode!, n => n.type === 'identifier' && n.text === 'Process');
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/OrderService.cs');
|
|
expect(result).toBe('Class:test/OrderService.cs:OrderService');
|
|
});
|
|
|
|
it('finds enclosing record for a method', () => {
|
|
const tree = parseCode(CSharp, `
|
|
record Person {
|
|
public string GetName() { return ""; }
|
|
}
|
|
`);
|
|
const methodNode = findNode(tree.rootNode, n => n.type === 'method_declaration');
|
|
if (methodNode) {
|
|
const nameNode = findNode(methodNode, n => n.type === 'identifier' && n.text === 'GetName');
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, 'test/Person.cs');
|
|
expect(result).toBe('Record:test/Person.cs:Person');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('finds enclosing struct for a method', () => {
|
|
const tree = parseCode(CSharp, `
|
|
struct Vector2 {
|
|
public float Length() { return 0; }
|
|
}
|
|
`);
|
|
const methodNode = findNode(tree.rootNode, n => n.type === 'method_declaration');
|
|
if (methodNode) {
|
|
const nameNode = findNode(methodNode, n => n.type === 'identifier' && n.text === 'Length');
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, 'test/Vector2.cs');
|
|
expect(result).toBe('Struct:test/Vector2.cs:Vector2');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Go', () => {
|
|
it('returns receiver struct ID for Go methods', () => {
|
|
// Go methods have receiver parameter: func (s *Server) Start() {}
|
|
// findEnclosingClassId extracts the receiver type to link method → struct
|
|
const tree = parseCode(Go, `
|
|
package main
|
|
|
|
type Server struct {
|
|
port int
|
|
}
|
|
|
|
func (s *Server) Start() {}
|
|
`);
|
|
const methodNode = findNode(tree.rootNode, n => n.type === 'method_declaration');
|
|
expect(methodNode).not.toBeNull();
|
|
|
|
const nameNode = findNode(methodNode!, n => n.type === 'field_identifier' && n.text === 'Start');
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, 'test/server.go');
|
|
expect(result).not.toBeNull();
|
|
// Should generate a Struct ID for "Server"
|
|
expect(result).toContain('Server');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('handles nested classes — returns innermost enclosing class', () => {
|
|
const tree = parseCode(TypeScript.typescript, `
|
|
class Outer {
|
|
inner = class Inner {
|
|
doWork() {}
|
|
}
|
|
}
|
|
`);
|
|
const methodNode = findNode(tree.rootNode, n => n.type === 'method_definition');
|
|
if (methodNode) {
|
|
const nameNode = findNode(methodNode, n => n.type === 'property_identifier' && n.text === 'doWork');
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, filePath);
|
|
// Should find the innermost class (Inner, which is a class node)
|
|
expect(result).not.toBeNull();
|
|
// The result should reference the inner class, not the outer
|
|
}
|
|
}
|
|
});
|
|
|
|
it('returns null for a node without parent', () => {
|
|
// Simulate a node with null parent
|
|
const fakeNode = { parent: null };
|
|
const result = findEnclosingClassId(fakeNode, filePath);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('skips containers without a name node', () => {
|
|
// Simulate AST nodes where the class container has no name
|
|
const fakeClassNode = {
|
|
type: 'class_declaration',
|
|
childForFieldName: () => null,
|
|
children: [],
|
|
parent: null,
|
|
};
|
|
const fakeChild = {
|
|
parent: fakeClassNode,
|
|
};
|
|
const result = findEnclosingClassId(fakeChild, filePath);
|
|
// The class has no name, so should return null
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
});
|