diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/c-cpp.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/c-cpp.ts new file mode 100644 index 000000000..bcfddee0b --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/c-cpp.ts @@ -0,0 +1,18 @@ +/** + * C / C++ import resolution configs. + * Both use standard resolution for #include directives. + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { ImportResolutionConfig } from '../types.js'; +import { createStandardStrategy } from '../standard.js'; + +export const cImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.C, + strategies: [createStandardStrategy(SupportedLanguages.C)], +}; + +export const cppImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.CPlusPlus, + strategies: [createStandardStrategy(SupportedLanguages.CPlusPlus)], +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/csharp.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/csharp.ts new file mode 100644 index 000000000..8fcb6f94d --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/csharp.ts @@ -0,0 +1,14 @@ +/** + * C# import resolution config. + * Namespace-based strategy via .csproj configs, then standard fallback. + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { ImportResolutionConfig } from '../types.js'; +import { createStandardStrategy } from '../standard.js'; +import { csharpNamespaceStrategy } from '../csharp.js'; + +export const csharpImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.CSharp, + strategies: [csharpNamespaceStrategy, createStandardStrategy(SupportedLanguages.CSharp)], +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/dart.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/dart.ts new file mode 100644 index 000000000..a8e5c4166 --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/dart.ts @@ -0,0 +1,13 @@ +/** + * Dart import resolution config. + * SDK/package strategy first, then relative import strategy (with ./ prepending). + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { ImportResolutionConfig } from '../types.js'; +import { dartPackageStrategy, dartRelativeStrategy } from '../dart.js'; + +export const dartImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.Dart, + strategies: [dartPackageStrategy, dartRelativeStrategy], +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/go.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/go.ts new file mode 100644 index 000000000..7b294159c --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/go.ts @@ -0,0 +1,14 @@ +/** + * Go import resolution config. + * Go-specific package strategy (go.mod), then standard fallback. + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { ImportResolutionConfig } from '../types.js'; +import { createStandardStrategy } from '../standard.js'; +import { goPackageStrategy } from '../go.js'; + +export const goImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.Go, + strategies: [goPackageStrategy, createStandardStrategy(SupportedLanguages.Go)], +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/jvm.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/jvm.ts new file mode 100644 index 000000000..67a3a7824 --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/jvm.ts @@ -0,0 +1,19 @@ +/** + * Java / Kotlin import resolution configs. + * JVM-specific wildcard/member strategy, then standard fallback. + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { ImportResolutionConfig } from '../types.js'; +import { createStandardStrategy } from '../standard.js'; +import { javaJvmStrategy, kotlinJvmStrategy } from '../jvm.js'; + +export const javaImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.Java, + strategies: [javaJvmStrategy, createStandardStrategy(SupportedLanguages.Java)], +}; + +export const kotlinImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.Kotlin, + strategies: [kotlinJvmStrategy, createStandardStrategy(SupportedLanguages.Kotlin)], +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/php.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/php.ts new file mode 100644 index 000000000..bd84ef34d --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/php.ts @@ -0,0 +1,13 @@ +/** + * PHP import resolution config. + * PSR-4 strategy via composer.json — no standard fallback (PSR-4 includes its own suffix matching). + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { ImportResolutionConfig } from '../types.js'; +import { phpPsr4Strategy } from '../php.js'; + +export const phpImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.PHP, + strategies: [phpPsr4Strategy], +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/python.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/python.ts new file mode 100644 index 000000000..bb7e87e64 --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/python.ts @@ -0,0 +1,14 @@ +/** + * Python import resolution config. + * PEP 328 relative + proximity-based strategy, then standard fallback. + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { ImportResolutionConfig } from '../types.js'; +import { createStandardStrategy } from '../standard.js'; +import { pythonImportStrategy } from '../python.js'; + +export const pythonImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.Python, + strategies: [pythonImportStrategy, createStandardStrategy(SupportedLanguages.Python)], +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/ruby.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/ruby.ts new file mode 100644 index 000000000..f02f27a7c --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/ruby.ts @@ -0,0 +1,13 @@ +/** + * Ruby import resolution config. + * Require/require_relative suffix matching — no standard fallback. + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { ImportResolutionConfig } from '../types.js'; +import { rubyRequireStrategy } from '../ruby.js'; + +export const rubyImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.Ruby, + strategies: [rubyRequireStrategy], +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/rust.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/rust.ts new file mode 100644 index 000000000..ba9c9995c --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/rust.ts @@ -0,0 +1,14 @@ +/** + * Rust import resolution config. + * Rust module strategy (grouped imports, crate/super/self paths), then standard fallback. + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { ImportResolutionConfig } from '../types.js'; +import { createStandardStrategy } from '../standard.js'; +import { rustModuleStrategy } from '../rust.js'; + +export const rustImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.Rust, + strategies: [rustModuleStrategy, createStandardStrategy(SupportedLanguages.Rust)], +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/swift.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/swift.ts new file mode 100644 index 000000000..a1af57c64 --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/swift.ts @@ -0,0 +1,13 @@ +/** + * Swift import resolution config. + * Package.swift target map strategy — no standard fallback (unresolved = external framework). + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { ImportResolutionConfig } from '../types.js'; +import { swiftPackageStrategy } from '../swift.js'; + +export const swiftImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.Swift, + strategies: [swiftPackageStrategy], +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/typescript-javascript.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/typescript-javascript.ts new file mode 100644 index 000000000..5869e3397 --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/typescript-javascript.ts @@ -0,0 +1,24 @@ +/** + * TypeScript / JavaScript / Vue import resolution configs. + * All use standard resolution — TS/JS with tsconfig path aliases, + * Vue delegates to TypeScript's resolver. + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { ImportResolutionConfig } from '../types.js'; +import { createStandardStrategy } from '../standard.js'; + +export const typescriptImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.TypeScript, + strategies: [createStandardStrategy(SupportedLanguages.TypeScript)], +}; + +export const javascriptImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.JavaScript, + strategies: [createStandardStrategy(SupportedLanguages.JavaScript)], +}; + +export const vueImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.Vue, + strategies: [createStandardStrategy(SupportedLanguages.TypeScript)], +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/csharp.ts b/gitnexus/src/core/ingestion/import-resolvers/csharp.ts index 33cc9c828..fc815aab0 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/csharp.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/csharp.ts @@ -6,7 +6,7 @@ import type { SuffixIndex } from './utils.js'; import { suffixResolve } from './utils.js'; import { SupportedLanguages } from 'gitnexus-shared'; -import type { ImportResult, ResolveCtx } from './types.js'; +import type { ImportResult, ImportResolverStrategy, ResolveCtx } from './types.js'; import { resolveStandard } from './standard.js'; import type { CSharpProjectConfig } from '../language-config.js'; @@ -127,12 +127,12 @@ export function resolveCSharpNamespaceDir( return null; } -/** C#: namespace-based resolution via .csproj configs, with suffix-match fallback. */ -export function resolveCSharpImport( - rawImportPath: string, - filePath: string, - ctx: ResolveCtx, -): ImportResult { +/** C# namespace-based resolution strategy via .csproj configs. */ +export const csharpNamespaceStrategy: ImportResolverStrategy = ( + rawImportPath, + _filePath, + ctx, +) => { const csharpConfigs = ctx.configs.csharpConfigs; if (csharpConfigs.length > 0) { const resolvedFiles = resolveCSharpImportInternal( @@ -150,5 +150,15 @@ export function resolveCSharpImport( } if (resolvedFiles.length > 0) return { kind: 'files', files: resolvedFiles }; } - return resolveStandard(rawImportPath, filePath, ctx, SupportedLanguages.CSharp); + return null; +}; + +/** C#: namespace-based resolution via .csproj configs, with suffix-match fallback. */ +export function resolveCSharpImport( + rawImportPath: string, + filePath: string, + ctx: ResolveCtx, +): ImportResult { + return csharpNamespaceStrategy(rawImportPath, filePath, ctx) + ?? resolveStandard(rawImportPath, filePath, ctx, SupportedLanguages.CSharp); } diff --git a/gitnexus/src/core/ingestion/import-resolvers/dart.ts b/gitnexus/src/core/ingestion/import-resolvers/dart.ts index 2a3cf3f6b..1a5bf8a9c 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/dart.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/dart.ts @@ -4,10 +4,63 @@ * SDK imports (dart:*) and external packages are skipped. */ -import type { ImportResult, ResolveCtx } from './types.js'; +import type { ImportResult, ImportResolverStrategy, ResolveCtx } from './types.js'; import { resolveStandard } from './standard.js'; import { SupportedLanguages } from 'gitnexus-shared'; +/** + * Dart SDK and package: import strategy. + * Absorbs dart: SDK imports and external packages (returns empty result to stop chain). + * Returns null for relative imports to let the next strategy handle them. + */ +export const dartPackageStrategy: ImportResolverStrategy = ( + rawImportPath, + _filePath, + ctx, +) => { + // Strip surrounding quotes from configurable_uri capture + const stripped = rawImportPath.replace(/^['"]|['"]$/g, ''); + + // Skip dart: SDK imports (dart:async, dart:io, etc.) + if (stripped.startsWith('dart:')) return { kind: 'files', files: [] }; + + // Local package: imports → resolve to lib/ + if (stripped.startsWith('package:')) { + const slashIdx = stripped.indexOf('/'); + if (slashIdx === -1) return { kind: 'files', files: [] }; + const relPath = stripped.slice(slashIdx + 1); + const candidates = [`lib/${relPath}`, relPath]; + const files: string[] = []; + for (const candidate of candidates) { + for (const fp of ctx.allFileList) { + if (fp.endsWith('/' + candidate) || fp === candidate) { + files.push(fp); + break; + } + } + if (files.length > 0) break; + } + if (files.length > 0) return { kind: 'files', files }; + return { kind: 'files', files: [] }; // external package + } + + return null; +}; + +/** + * Dart relative import strategy — prepends "./" for bare relative paths, + * then delegates to standard resolution. + */ +export const dartRelativeStrategy: ImportResolverStrategy = ( + rawImportPath, + filePath, + ctx, +) => { + const stripped = rawImportPath.replace(/^['"]|['"]$/g, ''); + const relPath = stripped.startsWith('.') ? stripped : './' + stripped; + return resolveStandard(relPath, filePath, ctx, SupportedLanguages.Dart); +}; + export function resolveDartImport( rawImportPath: string, filePath: string, diff --git a/gitnexus/src/core/ingestion/import-resolvers/go.ts b/gitnexus/src/core/ingestion/import-resolvers/go.ts index 1b1eb3ebc..c4fc5f945 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/go.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/go.ts @@ -4,7 +4,7 @@ */ import { SupportedLanguages } from 'gitnexus-shared'; -import type { ImportResult, ResolveCtx } from './types.js'; +import type { ImportResult, ImportResolverStrategy, ResolveCtx } from './types.js'; import { resolveStandard } from './standard.js'; import type { GoModuleConfig } from '../language-config.js'; @@ -57,12 +57,12 @@ export function resolveGoPackage( return matches; } -/** Go: package-level imports via go.mod module path. */ -export function resolveGoImport( - rawImportPath: string, - filePath: string, - ctx: ResolveCtx, -): ImportResult { +/** Go-specific package resolution strategy — resolves go.mod-based package imports. */ +export const goPackageStrategy: ImportResolverStrategy = ( + rawImportPath, + _filePath, + ctx, +) => { const goModule = ctx.configs.goModule; if (goModule && rawImportPath.startsWith(goModule.modulePath)) { const pkgSuffix = resolveGoPackageDir(rawImportPath, goModule); @@ -79,5 +79,15 @@ export function resolveGoImport( } // Fall through if no files found (package might be external) } - return resolveStandard(rawImportPath, filePath, ctx, SupportedLanguages.Go); + return null; +}; + +/** Go: package-level imports via go.mod module path. */ +export function resolveGoImport( + rawImportPath: string, + filePath: string, + ctx: ResolveCtx, +): ImportResult { + return goPackageStrategy(rawImportPath, filePath, ctx) + ?? resolveStandard(rawImportPath, filePath, ctx, SupportedLanguages.Go); } diff --git a/gitnexus/src/core/ingestion/import-resolvers/jvm.ts b/gitnexus/src/core/ingestion/import-resolvers/jvm.ts index 2dd7636ab..447cdf52d 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/jvm.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/jvm.ts @@ -6,7 +6,7 @@ import type { SuffixIndex } from './utils.js'; import type { SyntaxNode } from '../utils/ast-helpers.js'; import { SupportedLanguages } from 'gitnexus-shared'; -import type { ImportResult, ResolveCtx } from './types.js'; +import type { ImportResult, ImportResolverStrategy, ResolveCtx } from './types.js'; import { resolveStandard } from './standard.js'; /** Kotlin file extensions for JVM resolver reuse */ @@ -126,12 +126,12 @@ export function resolveJvmMemberImport( return null; } -/** Java: JVM wildcard -> member import -> standard fallthrough */ -export function resolveJavaImport( - rawImportPath: string, - filePath: string, - ctx: ResolveCtx, -): ImportResult { +/** Java JVM resolution strategy — wildcard and member import resolution. */ +export const javaJvmStrategy: ImportResolverStrategy = ( + rawImportPath, + _filePath, + ctx, +) => { if (rawImportPath.endsWith('.*')) { const matchedFiles = resolveJvmWildcard( rawImportPath, @@ -151,18 +151,27 @@ export function resolveJavaImport( ); if (memberResolved) return { kind: 'files', files: [memberResolved] }; } - return resolveStandard(rawImportPath, filePath, ctx, SupportedLanguages.Java); -} + return null; +}; -/** - * Kotlin: JVM wildcard/member with Java-interop fallback -> top-level function imports -> standard. - * Kotlin can import from .kt/.kts files OR from .java files (Java interop). - */ -export function resolveKotlinImport( +/** Java: JVM wildcard -> member import -> standard fallthrough */ +export function resolveJavaImport( rawImportPath: string, filePath: string, ctx: ResolveCtx, ): ImportResult { + return javaJvmStrategy(rawImportPath, filePath, ctx) + ?? resolveStandard(rawImportPath, filePath, ctx, SupportedLanguages.Java); +} + +/** + * Kotlin JVM resolution strategy — wildcard/member with Java-interop + top-level function imports. + */ +export const kotlinJvmStrategy: ImportResolverStrategy = ( + rawImportPath, + _filePath, + ctx, +) => { if (rawImportPath.endsWith('.*')) { const matchedFiles = resolveJvmWildcard( rawImportPath, @@ -228,5 +237,18 @@ export function resolveKotlinImport( if (dirFiles.length > 0) return { kind: 'files', files: dirFiles }; } } - return resolveStandard(rawImportPath, filePath, ctx, SupportedLanguages.Kotlin); + return null; +}; + +/** + * Kotlin: JVM wildcard/member with Java-interop fallback -> top-level function imports -> standard. + * Kotlin can import from .kt/.kts files OR from .java files (Java interop). + */ +export function resolveKotlinImport( + rawImportPath: string, + filePath: string, + ctx: ResolveCtx, +): ImportResult { + return kotlinJvmStrategy(rawImportPath, filePath, ctx) + ?? resolveStandard(rawImportPath, filePath, ctx, SupportedLanguages.Kotlin); } diff --git a/gitnexus/src/core/ingestion/import-resolvers/php.ts b/gitnexus/src/core/ingestion/import-resolvers/php.ts index 20517bbed..4c9a5432f 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/php.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/php.ts @@ -5,7 +5,7 @@ import type { SuffixIndex } from './utils.js'; import { suffixResolve } from './utils.js'; -import type { ImportResult, ResolveCtx } from './types.js'; +import type { ImportResult, ImportResolverStrategy, ResolveCtx } from './types.js'; import type { ComposerConfig } from '../language-config.js'; /** Get or compute the sorted PSR-4 entries (cached after first call). */ @@ -92,12 +92,12 @@ export function resolvePhpImportInternal( return suffixResolve(pathParts, normalizedFileList, allFileList, index); } -/** PHP: namespace-based resolution via composer.json PSR-4. */ -export function resolvePhpImport( - rawImportPath: string, - _filePath: string, - ctx: ResolveCtx, -): ImportResult { +/** PHP PSR-4 resolution strategy via composer.json autoload mappings. */ +export const phpPsr4Strategy: ImportResolverStrategy = ( + rawImportPath, + _filePath, + ctx, +) => { const resolved = resolvePhpImportInternal( rawImportPath, ctx.configs.composerConfig, @@ -107,4 +107,13 @@ export function resolvePhpImport( ctx.index, ); return resolved ? { kind: 'files', files: [resolved] } : null; +}; + +/** PHP: namespace-based resolution via composer.json PSR-4. */ +export function resolvePhpImport( + rawImportPath: string, + _filePath: string, + ctx: ResolveCtx, +): ImportResult { + return phpPsr4Strategy(rawImportPath, _filePath, ctx); } diff --git a/gitnexus/src/core/ingestion/import-resolvers/python.ts b/gitnexus/src/core/ingestion/import-resolvers/python.ts index 4e9c4bbb3..8d81c88af 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/python.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/python.ts @@ -5,7 +5,7 @@ import { tryResolveWithExtensions } from './utils.js'; import { SupportedLanguages } from 'gitnexus-shared'; -import type { ImportResult, ResolveCtx } from './types.js'; +import type { ImportResult, ImportResolverStrategy, ResolveCtx } from './types.js'; import { resolveStandard } from './standard.js'; /** @@ -75,6 +75,26 @@ export function resolvePythonImportInternal( return null; } +/** + * Python import resolution strategy — PEP 328 relative + proximity-based bare imports. + * Returns null to continue chain for non-relative imports. + * Absorbs unresolved relative imports (returns empty result to stop the chain). + */ +export const pythonImportStrategy: ImportResolverStrategy = ( + rawImportPath, + filePath, + ctx, +) => { + const resolved = resolvePythonImportInternal(filePath, rawImportPath, ctx.allFilePaths); + if (resolved) { + ctx.resolveCache.set(`${filePath}::${rawImportPath}`, resolved); + return { kind: 'files', files: [resolved] }; + } + // PEP 328: unresolved relative imports should not fall through to suffix matching + if (rawImportPath.startsWith('.')) return { kind: 'files', files: [] }; + return null; +}; + /** * Python: relative imports (PEP 328) + proximity-based bare imports. * Falls through to standard suffix resolution when proximity finds no match. diff --git a/gitnexus/src/core/ingestion/import-resolvers/resolver-factory.ts b/gitnexus/src/core/ingestion/import-resolvers/resolver-factory.ts new file mode 100644 index 000000000..4f00a559f --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/resolver-factory.ts @@ -0,0 +1,30 @@ +/** + * Import resolver factory — creates a composable import resolver from + * an ordered list of strategies. + * + * Mirrors the method-extractors/generic.ts and call-extractors/generic.ts + * pattern: declare a config per language, produce a runtime resolver via factory. + * + * Each strategy is tried in order. The first non-null result wins. + * A result with an empty `files` array is treated as "handled but unresolved" + * (stops the chain without producing import edges). + */ + +import type { ImportResolverFn, ImportResolutionConfig } from './types.js'; + +/** + * Create an ImportResolverFn from a declarative config. + * + * Chains strategies in declaration order — first non-null result wins. + * Returns null only if every strategy returns null. + */ +export function createImportResolver(config: ImportResolutionConfig): ImportResolverFn { + const { strategies } = config; + return (rawImportPath, filePath, ctx) => { + for (const strategy of strategies) { + const result = strategy(rawImportPath, filePath, ctx); + if (result) return result; + } + return null; + }; +} diff --git a/gitnexus/src/core/ingestion/import-resolvers/ruby.ts b/gitnexus/src/core/ingestion/import-resolvers/ruby.ts index b8ada4cf4..fa2508ae2 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/ruby.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/ruby.ts @@ -5,7 +5,7 @@ import type { SuffixIndex } from './utils.js'; import { suffixResolve } from './utils.js'; -import type { ImportResult, ResolveCtx } from './types.js'; +import type { ImportResult, ImportResolverStrategy, ResolveCtx } from './types.js'; /** * Resolve a Ruby require/require_relative path to a matching .rb file (low-level helper). @@ -23,12 +23,12 @@ export function resolveRubyImportInternal( return suffixResolve(pathParts, normalizedFileList, allFileList, index); } -/** Ruby: require / require_relative. */ -export function resolveRubyImport( - rawImportPath: string, - _filePath: string, - ctx: ResolveCtx, -): ImportResult { +/** Ruby require/require_relative resolution strategy. */ +export const rubyRequireStrategy: ImportResolverStrategy = ( + rawImportPath, + _filePath, + ctx, +) => { const resolved = resolveRubyImportInternal( rawImportPath, ctx.normalizedFileList, @@ -36,4 +36,13 @@ export function resolveRubyImport( ctx.index, ); return resolved ? { kind: 'files', files: [resolved] } : null; +}; + +/** Ruby: require / require_relative. */ +export function resolveRubyImport( + rawImportPath: string, + _filePath: string, + ctx: ResolveCtx, +): ImportResult { + return rubyRequireStrategy(rawImportPath, _filePath, ctx); } diff --git a/gitnexus/src/core/ingestion/import-resolvers/rust.ts b/gitnexus/src/core/ingestion/import-resolvers/rust.ts index 632e19352..006d0b438 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/rust.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/rust.ts @@ -4,7 +4,7 @@ */ import { SupportedLanguages } from 'gitnexus-shared'; -import type { ImportResult, ResolveCtx } from './types.js'; +import type { ImportResult, ImportResolverStrategy, ResolveCtx } from './types.js'; import { resolveStandard } from './standard.js'; /** @@ -85,12 +85,12 @@ export function tryRustModulePath(modulePath: string, allFiles: Set): st return null; } -/** Rust: expand grouped imports: use {crate::a, crate::b} and use crate::models::{User, Repo}. */ -export function resolveRustImport( - rawImportPath: string, - filePath: string, - ctx: ResolveCtx, -): ImportResult { +/** Rust module resolution strategy — handles grouped imports and crate/super/self paths. */ +export const rustModuleStrategy: ImportResolverStrategy = ( + rawImportPath, + filePath, + ctx, +) => { // Top-level grouped: use {crate::a, crate::b} if (rawImportPath.startsWith('{') && rawImportPath.endsWith('}')) { const inner = rawImportPath.slice(1, -1); @@ -128,5 +128,15 @@ export function resolveRustImport( if (prefixResult) return { kind: 'files', files: [prefixResult] }; } - return resolveStandard(rawImportPath, filePath, ctx, SupportedLanguages.Rust); + return null; +}; + +/** Rust: expand grouped imports: use {crate::a, crate::b} and use crate::models::{User, Repo}. */ +export function resolveRustImport( + rawImportPath: string, + filePath: string, + ctx: ResolveCtx, +): ImportResult { + return rustModuleStrategy(rawImportPath, filePath, ctx) + ?? resolveStandard(rawImportPath, filePath, ctx, SupportedLanguages.Rust); } diff --git a/gitnexus/src/core/ingestion/import-resolvers/standard.ts b/gitnexus/src/core/ingestion/import-resolvers/standard.ts index a29810a2f..7aac0e191 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/standard.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/standard.ts @@ -8,7 +8,7 @@ import type { SuffixIndex } from './utils.js'; import { tryResolveWithExtensions, suffixResolve } from './utils.js'; import { resolveRustImportInternal } from './rust.js'; import { SupportedLanguages } from 'gitnexus-shared'; -import type { ImportResult, ImportResolverFn, ResolveCtx } from './types.js'; +import type { ImportResult, ImportResolverFn, ImportResolverStrategy, ResolveCtx } from './types.js'; import type { TsconfigPaths } from '../language-config.js'; /** Max entries in the resolve cache. Beyond this, entries are evicted. @@ -189,3 +189,12 @@ export const resolveCImport: ImportResolverFn = (raw, fp, ctx) => /** C++: standard single-file resolution for #include directives. */ export const resolveCppImport: ImportResolverFn = (raw, fp, ctx) => resolveStandard(raw, fp, ctx, SupportedLanguages.CPlusPlus); + +// ============================================================================ +// Strategy factory — composable hook for ImportResolutionConfig +// ============================================================================ + +/** Create a reusable standard-resolution strategy for a given language. */ +export function createStandardStrategy(language: SupportedLanguages): ImportResolverStrategy { + return (raw, fp, ctx) => resolveStandard(raw, fp, ctx, language); +} diff --git a/gitnexus/src/core/ingestion/import-resolvers/swift.ts b/gitnexus/src/core/ingestion/import-resolvers/swift.ts index 7ccc10458..24da4d2bd 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/swift.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/swift.ts @@ -3,14 +3,14 @@ * Handles module imports via Package.swift target map. */ -import type { ImportResult, ResolveCtx } from './types.js'; +import type { ImportResult, ImportResolverStrategy, ResolveCtx } from './types.js'; -/** Swift: module imports via Package.swift target map. */ -export function resolveSwiftImport( - rawImportPath: string, - _filePath: string, - ctx: ResolveCtx, -): ImportResult { +/** Swift Package.swift target map resolution strategy. */ +export const swiftPackageStrategy: ImportResolverStrategy = ( + rawImportPath, + _filePath, + ctx, +) => { const swiftPackageConfig = ctx.configs.swiftPackageConfig; if (swiftPackageConfig) { const targetDir = swiftPackageConfig.targets.get(rawImportPath); @@ -29,4 +29,13 @@ export function resolveSwiftImport( } } return null; // External framework (Foundation, UIKit, etc.) +}; + +/** Swift: module imports via Package.swift target map. */ +export function resolveSwiftImport( + rawImportPath: string, + _filePath: string, + ctx: ResolveCtx, +): ImportResult { + return swiftPackageStrategy(rawImportPath, _filePath, ctx); } diff --git a/gitnexus/src/core/ingestion/import-resolvers/types.ts b/gitnexus/src/core/ingestion/import-resolvers/types.ts index 3e68c38b2..f079c0879 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/types.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/types.ts @@ -12,6 +12,7 @@ import type { } from '../language-config.js'; import type { SwiftPackageConfig } from '../language-config.js'; import type { SuffixIndex } from './utils.js'; +import type { SupportedLanguages } from 'gitnexus-shared'; /** * Result of resolving an import via language-specific dispatch. @@ -53,3 +54,22 @@ export type ImportResolverFn = ( filePath: string, resolveCtx: ResolveCtx, ) => ImportResult; + +/** + * A single import resolution strategy — one step in a composable chain. + * Same signature as ImportResolverFn. Returns null to let the next strategy + * in the chain try; returns a result (even with empty files) to stop the chain. + */ +export type ImportResolverStrategy = ImportResolverFn; + +/** + * Declarative config for composable import resolution — mirrors the + * MethodExtractionConfig / CallExtractionConfig pattern. + * + * Each language declares an ordered list of strategies to try. + * The factory (`createImportResolver`) chains them: first non-null result wins. + */ +export interface ImportResolutionConfig { + readonly language: SupportedLanguages; + readonly strategies: readonly ImportResolverStrategy[]; +} diff --git a/gitnexus/src/core/ingestion/languages/c-cpp.ts b/gitnexus/src/core/ingestion/languages/c-cpp.ts index b823cefcc..88827e26e 100644 --- a/gitnexus/src/core/ingestion/languages/c-cpp.ts +++ b/gitnexus/src/core/ingestion/languages/c-cpp.ts @@ -15,6 +15,8 @@ import { defineLanguage } from '../language-provider.js'; import { typeConfig as cCppConfig } from '../type-extractors/c-cpp.js'; import { cCppExportChecker } from '../export-detection.js'; import { resolveCImport, resolveCppImport } from '../import-resolvers/standard.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { cImportConfig, cppImportConfig } from '../import-resolvers/configs/c-cpp.js'; import { C_QUERIES, CPP_QUERIES } from '../tree-sitter-queries.js'; /** @@ -318,7 +320,7 @@ export const cProvider = defineLanguage({ treeSitterQueries: C_QUERIES, typeConfig: cCppConfig, exportChecker: cCppExportChecker, - importResolver: resolveCImport, + importResolver: createImportResolver(cImportConfig), importSemantics: 'wildcard-transitive', callExtractor: createCallExtractor(cCallConfig), fieldExtractor: createFieldExtractor(cFieldConfig), @@ -338,7 +340,7 @@ export const cppProvider = defineLanguage({ treeSitterQueries: CPP_QUERIES, typeConfig: cCppConfig, exportChecker: cCppExportChecker, - importResolver: resolveCppImport, + importResolver: createImportResolver(cppImportConfig), importSemantics: 'wildcard-transitive', mroStrategy: 'leftmost-base', callExtractor: createCallExtractor(cppCallConfig), diff --git a/gitnexus/src/core/ingestion/languages/csharp.ts b/gitnexus/src/core/ingestion/languages/csharp.ts index d7330fc87..00062d65b 100644 --- a/gitnexus/src/core/ingestion/languages/csharp.ts +++ b/gitnexus/src/core/ingestion/languages/csharp.ts @@ -13,6 +13,8 @@ import { defineLanguage } from '../language-provider.js'; import { typeConfig as csharpConfig } from '../type-extractors/csharp.js'; import { csharpExportChecker } from '../export-detection.js'; import { resolveCSharpImport } from '../import-resolvers/csharp.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { csharpImportConfig } from '../import-resolvers/configs/csharp.js'; import { extractCSharpNamedBindings } from '../named-bindings/csharp.js'; import { CSHARP_QUERIES } from '../tree-sitter-queries.js'; import { createCallExtractor } from '../call-extractors/generic.js'; @@ -125,7 +127,7 @@ export const csharpProvider = defineLanguage({ treeSitterQueries: CSHARP_QUERIES, typeConfig: csharpConfig, exportChecker: csharpExportChecker, - importResolver: resolveCSharpImport, + importResolver: createImportResolver(csharpImportConfig), namedBindingExtractor: extractCSharpNamedBindings, interfaceNamePattern: /^I[A-Z]/, mroStrategy: 'implements-split', diff --git a/gitnexus/src/core/ingestion/languages/dart.ts b/gitnexus/src/core/ingestion/languages/dart.ts index 1deb488b8..82ed9bd7f 100644 --- a/gitnexus/src/core/ingestion/languages/dart.ts +++ b/gitnexus/src/core/ingestion/languages/dart.ts @@ -20,6 +20,8 @@ import { defineLanguage } from '../language-provider.js'; import { typeConfig as dartConfig } from '../type-extractors/dart.js'; import { dartExportChecker } from '../export-detection.js'; import { resolveDartImport } from '../import-resolvers/dart.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { dartImportConfig } from '../import-resolvers/configs/dart.js'; import { DART_QUERIES } from '../tree-sitter-queries.js'; import { createFieldExtractor } from '../field-extractors/generic.js'; import { dartConfig as dartFieldConfig } from '../field-extractors/configs/dart.js'; @@ -94,7 +96,7 @@ export const dartProvider = defineLanguage({ treeSitterQueries: DART_QUERIES, typeConfig: dartConfig, exportChecker: dartExportChecker, - importResolver: resolveDartImport, + importResolver: createImportResolver(dartImportConfig), importSemantics: 'wildcard-leaf', callExtractor: createCallExtractor(dartCallConfig), fieldExtractor: createFieldExtractor(dartFieldConfig), diff --git a/gitnexus/src/core/ingestion/languages/go.ts b/gitnexus/src/core/ingestion/languages/go.ts index ae92ff995..ed42a6290 100644 --- a/gitnexus/src/core/ingestion/languages/go.ts +++ b/gitnexus/src/core/ingestion/languages/go.ts @@ -16,6 +16,8 @@ import { defineLanguage } from '../language-provider.js'; import { typeConfig as goConfig } from '../type-extractors/go.js'; import { goExportChecker } from '../export-detection.js'; import { resolveGoImport } from '../import-resolvers/go.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { goImportConfig } from '../import-resolvers/configs/go.js'; import { GO_QUERIES } from '../tree-sitter-queries.js'; import { createFieldExtractor } from '../field-extractors/generic.js'; import { goConfig as goFieldConfig } from '../field-extractors/configs/go.js'; @@ -32,7 +34,7 @@ export const goProvider = defineLanguage({ treeSitterQueries: GO_QUERIES, typeConfig: goConfig, exportChecker: goExportChecker, - importResolver: resolveGoImport, + importResolver: createImportResolver(goImportConfig), importSemantics: 'wildcard-leaf', callExtractor: createCallExtractor(goCallConfig), fieldExtractor: createFieldExtractor(goFieldConfig), diff --git a/gitnexus/src/core/ingestion/languages/java.ts b/gitnexus/src/core/ingestion/languages/java.ts index b8446b76a..b078d9120 100644 --- a/gitnexus/src/core/ingestion/languages/java.ts +++ b/gitnexus/src/core/ingestion/languages/java.ts @@ -14,6 +14,8 @@ import { defineLanguage } from '../language-provider.js'; import { javaTypeConfig } from '../type-extractors/jvm.js'; import { javaExportChecker } from '../export-detection.js'; import { resolveJavaImport } from '../import-resolvers/jvm.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { javaImportConfig } from '../import-resolvers/configs/jvm.js'; import { extractJavaNamedBindings } from '../named-bindings/java.js'; import { JAVA_QUERIES } from '../tree-sitter-queries.js'; import { createCallExtractor } from '../call-extractors/generic.js'; @@ -31,7 +33,7 @@ export const javaProvider = defineLanguage({ treeSitterQueries: JAVA_QUERIES, typeConfig: javaTypeConfig, exportChecker: javaExportChecker, - importResolver: resolveJavaImport, + importResolver: createImportResolver(javaImportConfig), namedBindingExtractor: extractJavaNamedBindings, interfaceNamePattern: /^I[A-Z]/, mroStrategy: 'implements-split', diff --git a/gitnexus/src/core/ingestion/languages/kotlin.ts b/gitnexus/src/core/ingestion/languages/kotlin.ts index 7ab962cc5..b12c1a84d 100644 --- a/gitnexus/src/core/ingestion/languages/kotlin.ts +++ b/gitnexus/src/core/ingestion/languages/kotlin.ts @@ -14,6 +14,8 @@ import { defineLanguage } from '../language-provider.js'; import { kotlinTypeConfig } from '../type-extractors/jvm.js'; import { kotlinExportChecker } from '../export-detection.js'; import { resolveKotlinImport } from '../import-resolvers/jvm.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { kotlinImportConfig } from '../import-resolvers/configs/jvm.js'; import { extractKotlinNamedBindings } from '../named-bindings/kotlin.js'; import { appendKotlinWildcard } from '../import-resolvers/jvm.js'; import { KOTLIN_QUERIES } from '../tree-sitter-queries.js'; @@ -106,7 +108,7 @@ export const kotlinProvider = defineLanguage({ treeSitterQueries: KOTLIN_QUERIES, typeConfig: kotlinTypeConfig, exportChecker: kotlinExportChecker, - importResolver: resolveKotlinImport, + importResolver: createImportResolver(kotlinImportConfig), namedBindingExtractor: extractKotlinNamedBindings, importPathPreprocessor: appendKotlinWildcard, mroStrategy: 'implements-split', diff --git a/gitnexus/src/core/ingestion/languages/php.ts b/gitnexus/src/core/ingestion/languages/php.ts index e3af7c62d..8913cb1ad 100644 --- a/gitnexus/src/core/ingestion/languages/php.ts +++ b/gitnexus/src/core/ingestion/languages/php.ts @@ -13,6 +13,8 @@ import { defineLanguage } from '../language-provider.js'; import { typeConfig as phpConfig } from '../type-extractors/php.js'; import { phpExportChecker } from '../export-detection.js'; import { resolvePhpImport } from '../import-resolvers/php.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { phpImportConfig } from '../import-resolvers/configs/php.js'; import { extractPhpNamedBindings } from '../named-bindings/php.js'; import { PHP_QUERIES } from '../tree-sitter-queries.js'; import { findDescendant, extractStringContent, type SyntaxNode } from '../utils/ast-helpers.js'; @@ -240,7 +242,7 @@ export const phpProvider = defineLanguage({ treeSitterQueries: PHP_QUERIES, typeConfig: phpConfig, exportChecker: phpExportChecker, - importResolver: resolvePhpImport, + importResolver: createImportResolver(phpImportConfig), namedBindingExtractor: extractPhpNamedBindings, callExtractor: createCallExtractor(phpCallConfig), fieldExtractor: createFieldExtractor(phpFieldConfig), diff --git a/gitnexus/src/core/ingestion/languages/python.ts b/gitnexus/src/core/ingestion/languages/python.ts index 1e4f7e8c8..4871a6eb2 100644 --- a/gitnexus/src/core/ingestion/languages/python.ts +++ b/gitnexus/src/core/ingestion/languages/python.ts @@ -17,6 +17,8 @@ import { defineLanguage } from '../language-provider.js'; import { typeConfig as pythonConfig } from '../type-extractors/python.js'; import { pythonExportChecker } from '../export-detection.js'; import { resolvePythonImport } from '../import-resolvers/python.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { pythonImportConfig } from '../import-resolvers/configs/python.js'; import { extractPythonNamedBindings } from '../named-bindings/python.js'; import { PYTHON_QUERIES } from '../tree-sitter-queries.js'; import { createFieldExtractor } from '../field-extractors/generic.js'; @@ -64,7 +66,7 @@ export const pythonProvider = defineLanguage({ treeSitterQueries: PYTHON_QUERIES, typeConfig: pythonConfig, exportChecker: pythonExportChecker, - importResolver: resolvePythonImport, + importResolver: createImportResolver(pythonImportConfig), namedBindingExtractor: extractPythonNamedBindings, importSemantics: 'namespace', mroStrategy: 'c3', diff --git a/gitnexus/src/core/ingestion/languages/ruby.ts b/gitnexus/src/core/ingestion/languages/ruby.ts index 6f3fe4488..8f4c96a46 100644 --- a/gitnexus/src/core/ingestion/languages/ruby.ts +++ b/gitnexus/src/core/ingestion/languages/ruby.ts @@ -17,6 +17,8 @@ import { typeConfig as rubyConfig } from '../type-extractors/ruby.js'; import { routeRubyCall } from '../call-routing.js'; import { rubyExportChecker } from '../export-detection.js'; import { resolveRubyImport } from '../import-resolvers/ruby.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { rubyImportConfig } from '../import-resolvers/configs/ruby.js'; import { RUBY_QUERIES } from '../tree-sitter-queries.js'; import { createFieldExtractor } from '../field-extractors/generic.js'; import { rubyConfig as rubyFieldConfig } from '../field-extractors/configs/ruby.js'; @@ -110,7 +112,7 @@ export const rubyProvider = defineLanguage({ treeSitterQueries: RUBY_QUERIES, typeConfig: rubyConfig, exportChecker: rubyExportChecker, - importResolver: resolveRubyImport, + importResolver: createImportResolver(rubyImportConfig), callRouter: routeRubyCall, importSemantics: 'wildcard-leaf', callExtractor: createCallExtractor(rubyCallConfig), diff --git a/gitnexus/src/core/ingestion/languages/rust.ts b/gitnexus/src/core/ingestion/languages/rust.ts index 21586d692..bcd93398f 100644 --- a/gitnexus/src/core/ingestion/languages/rust.ts +++ b/gitnexus/src/core/ingestion/languages/rust.ts @@ -19,6 +19,8 @@ import type { SyntaxNode } from '../utils/ast-helpers.js'; import { typeConfig as rustConfig } from '../type-extractors/rust.js'; import { rustExportChecker } from '../export-detection.js'; import { resolveRustImport } from '../import-resolvers/rust.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { rustImportConfig } from '../import-resolvers/configs/rust.js'; import { extractRustNamedBindings } from '../named-bindings/rust.js'; import { RUST_QUERIES } from '../tree-sitter-queries.js'; import { createFieldExtractor } from '../field-extractors/generic.js'; @@ -122,7 +124,7 @@ export const rustProvider = defineLanguage({ treeSitterQueries: RUST_QUERIES, typeConfig: rustConfig, exportChecker: rustExportChecker, - importResolver: resolveRustImport, + importResolver: createImportResolver(rustImportConfig), namedBindingExtractor: extractRustNamedBindings, mroStrategy: 'qualified-syntax', callExtractor: createCallExtractor(rustCallConfig), diff --git a/gitnexus/src/core/ingestion/languages/swift.ts b/gitnexus/src/core/ingestion/languages/swift.ts index a3139897c..97788ef42 100644 --- a/gitnexus/src/core/ingestion/languages/swift.ts +++ b/gitnexus/src/core/ingestion/languages/swift.ts @@ -18,6 +18,8 @@ import { defineLanguage } from '../language-provider.js'; import { typeConfig as swiftConfig } from '../type-extractors/swift.js'; import { swiftExportChecker } from '../export-detection.js'; import { resolveSwiftImport } from '../import-resolvers/swift.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { swiftImportConfig } from '../import-resolvers/configs/swift.js'; import { SWIFT_QUERIES } from '../tree-sitter-queries.js'; import type { SwiftPackageConfig } from '../language-config.js'; import type { SyntaxNode } from '../utils/ast-helpers.js'; @@ -242,7 +244,7 @@ export const swiftProvider = defineLanguage({ treeSitterQueries: SWIFT_QUERIES, typeConfig: swiftConfig, exportChecker: swiftExportChecker, - importResolver: resolveSwiftImport, + importResolver: createImportResolver(swiftImportConfig), importSemantics: 'wildcard-leaf', heritageDefaultEdge: 'IMPLEMENTS', callExtractor: createCallExtractor(swiftCallConfig), diff --git a/gitnexus/src/core/ingestion/languages/typescript.ts b/gitnexus/src/core/ingestion/languages/typescript.ts index 69b4ca84c..05c2353a5 100644 --- a/gitnexus/src/core/ingestion/languages/typescript.ts +++ b/gitnexus/src/core/ingestion/languages/typescript.ts @@ -19,6 +19,11 @@ import type { SyntaxNode } from '../utils/ast-helpers.js'; import { typeConfig as typescriptConfig } from '../type-extractors/typescript.js'; import { tsExportChecker } from '../export-detection.js'; import { resolveTypescriptImport, resolveJavascriptImport } from '../import-resolvers/standard.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { + typescriptImportConfig, + javascriptImportConfig, +} from '../import-resolvers/configs/typescript-javascript.js'; import { extractTsNamedBindings } from '../named-bindings/typescript.js'; import { TYPESCRIPT_QUERIES, JAVASCRIPT_QUERIES } from '../tree-sitter-queries.js'; import { typescriptFieldExtractor } from '../field-extractors/typescript.js'; @@ -168,7 +173,7 @@ export const typescriptProvider = defineLanguage({ treeSitterQueries: TYPESCRIPT_QUERIES, typeConfig: typescriptConfig, exportChecker: tsExportChecker, - importResolver: resolveTypescriptImport, + importResolver: createImportResolver(typescriptImportConfig), namedBindingExtractor: extractTsNamedBindings, callExtractor: createCallExtractor(typescriptCallConfig), fieldExtractor: typescriptFieldExtractor, @@ -187,7 +192,7 @@ export const javascriptProvider = defineLanguage({ treeSitterQueries: JAVASCRIPT_QUERIES, typeConfig: typescriptConfig, exportChecker: tsExportChecker, - importResolver: resolveJavascriptImport, + importResolver: createImportResolver(javascriptImportConfig), namedBindingExtractor: extractTsNamedBindings, callExtractor: createCallExtractor(javascriptCallConfig), fieldExtractor: createFieldExtractor(javascriptConfig), diff --git a/gitnexus/src/core/ingestion/languages/vue.ts b/gitnexus/src/core/ingestion/languages/vue.ts index 4164f6b02..8303c62f7 100644 --- a/gitnexus/src/core/ingestion/languages/vue.ts +++ b/gitnexus/src/core/ingestion/languages/vue.ts @@ -18,6 +18,8 @@ import { defineLanguage } from '../language-provider.js'; import { typeConfig as typescriptConfig } from '../type-extractors/typescript.js'; import { tsExportChecker } from '../export-detection.js'; import { resolveVueImport } from '../import-resolvers/vue.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { vueImportConfig } from '../import-resolvers/configs/typescript-javascript.js'; import { extractTsNamedBindings } from '../named-bindings/typescript.js'; import { TYPESCRIPT_QUERIES } from '../tree-sitter-queries.js'; import { typescriptFieldExtractor } from '../field-extractors/typescript.js'; @@ -69,7 +71,7 @@ export const vueProvider = defineLanguage({ treeSitterQueries: TYPESCRIPT_QUERIES, typeConfig: typescriptConfig, exportChecker: tsExportChecker, - importResolver: resolveVueImport, + importResolver: createImportResolver(vueImportConfig), namedBindingExtractor: extractTsNamedBindings, callExtractor: createCallExtractor(typescriptCallConfig), fieldExtractor: typescriptFieldExtractor, diff --git a/gitnexus/test/unit/import-resolver-factory.test.ts b/gitnexus/test/unit/import-resolver-factory.test.ts new file mode 100644 index 000000000..7c3dee563 --- /dev/null +++ b/gitnexus/test/unit/import-resolver-factory.test.ts @@ -0,0 +1,293 @@ +/** + * Unit tests for the import resolver factory and per-strategy composition. + * + * Validates that: + * - createImportResolver chains strategies in order + * - First non-null result wins + * - Empty files array stops the chain (absorbing sentinel) + * - All per-language configs produce valid resolvers + */ + +import { describe, it, expect } from 'vitest'; +import { createImportResolver } from '../../src/core/ingestion/import-resolvers/resolver-factory.js'; +import { createStandardStrategy } from '../../src/core/ingestion/import-resolvers/standard.js'; +import type { + ImportResolutionConfig, + ImportResolverStrategy, + ResolveCtx, +} from '../../src/core/ingestion/import-resolvers/types.js'; +import { SupportedLanguages } from 'gitnexus-shared'; +import { buildSuffixIndex } from '../../src/core/ingestion/import-resolvers/utils.js'; + +// ── Per-language strategy imports ────────────────────────────────────── +import { goPackageStrategy } from '../../src/core/ingestion/import-resolvers/go.js'; +import { + javaJvmStrategy, + kotlinJvmStrategy, +} from '../../src/core/ingestion/import-resolvers/jvm.js'; +import { rustModuleStrategy } from '../../src/core/ingestion/import-resolvers/rust.js'; +import { pythonImportStrategy } from '../../src/core/ingestion/import-resolvers/python.js'; +import { csharpNamespaceStrategy } from '../../src/core/ingestion/import-resolvers/csharp.js'; +import { phpPsr4Strategy } from '../../src/core/ingestion/import-resolvers/php.js'; +import { swiftPackageStrategy } from '../../src/core/ingestion/import-resolvers/swift.js'; +import { + dartPackageStrategy, + dartRelativeStrategy, +} from '../../src/core/ingestion/import-resolvers/dart.js'; +import { rubyRequireStrategy } from '../../src/core/ingestion/import-resolvers/ruby.js'; + +// ── Per-language config imports ──────────────────────────────────────── +import { + typescriptImportConfig, + javascriptImportConfig, + vueImportConfig, +} from '../../src/core/ingestion/import-resolvers/configs/typescript-javascript.js'; +import { + cImportConfig, + cppImportConfig, +} from '../../src/core/ingestion/import-resolvers/configs/c-cpp.js'; +import { goImportConfig } from '../../src/core/ingestion/import-resolvers/configs/go.js'; +import { + javaImportConfig, + kotlinImportConfig, +} from '../../src/core/ingestion/import-resolvers/configs/jvm.js'; +import { pythonImportConfig } from '../../src/core/ingestion/import-resolvers/configs/python.js'; +import { rustImportConfig } from '../../src/core/ingestion/import-resolvers/configs/rust.js'; +import { csharpImportConfig } from '../../src/core/ingestion/import-resolvers/configs/csharp.js'; +import { phpImportConfig } from '../../src/core/ingestion/import-resolvers/configs/php.js'; +import { swiftImportConfig } from '../../src/core/ingestion/import-resolvers/configs/swift.js'; +import { dartImportConfig } from '../../src/core/ingestion/import-resolvers/configs/dart.js'; +import { rubyImportConfig } from '../../src/core/ingestion/import-resolvers/configs/ruby.js'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function makeCtx(files: string[]): ResolveCtx { + const allFileList = files; + const normalizedFileList = files.map((p) => p.replace(/\\/g, '/')); + const allFilePaths = new Set(allFileList); + const index = buildSuffixIndex(normalizedFileList, allFileList); + return { + allFilePaths, + allFileList, + normalizedFileList, + index, + resolveCache: new Map(), + configs: { + tsconfigPaths: null, + goModule: null, + composerConfig: null, + swiftPackageConfig: null, + csharpConfigs: [], + }, + }; +} + +// --------------------------------------------------------------------------- +// createImportResolver — factory behavior +// --------------------------------------------------------------------------- + +describe('createImportResolver', () => { + it('returns null when no strategies match', () => { + const config: ImportResolutionConfig = { + language: SupportedLanguages.TypeScript, + strategies: [], + }; + const resolver = createImportResolver(config); + expect(resolver('./foo', 'src/index.ts', makeCtx([]))).toBeNull(); + }); + + it('returns first non-null result from strategy chain', () => { + const strategyA: ImportResolverStrategy = () => null; + const strategyB: ImportResolverStrategy = () => ({ kind: 'files', files: ['b.ts'] }); + const strategyC: ImportResolverStrategy = () => ({ kind: 'files', files: ['c.ts'] }); + + const resolver = createImportResolver({ + language: SupportedLanguages.TypeScript, + strategies: [strategyA, strategyB, strategyC], + }); + + const result = resolver('./foo', 'src/index.ts', makeCtx(['b.ts', 'c.ts'])); + expect(result).toEqual({ kind: 'files', files: ['b.ts'] }); + }); + + it('stops chain when strategy returns result with empty files (absorbing sentinel)', () => { + const absorber: ImportResolverStrategy = () => ({ kind: 'files', files: [] }); + const shouldNotRun: ImportResolverStrategy = () => ({ kind: 'files', files: ['should-not.ts'] }); + + const resolver = createImportResolver({ + language: SupportedLanguages.TypeScript, + strategies: [absorber, shouldNotRun], + }); + + const result = resolver('./foo', 'src/index.ts', makeCtx([])); + expect(result).toEqual({ kind: 'files', files: [] }); + }); + + it('passes correct arguments to strategies', () => { + const ctx = makeCtx(['src/utils.ts']); + const captured: { raw: string; fp: string }[] = []; + + const spy: ImportResolverStrategy = (raw, fp) => { + captured.push({ raw, fp }); + return null; + }; + + const resolver = createImportResolver({ + language: SupportedLanguages.TypeScript, + strategies: [spy], + }); + + resolver('./utils', 'src/index.ts', ctx); + expect(captured).toEqual([{ raw: './utils', fp: 'src/index.ts' }]); + }); +}); + +// --------------------------------------------------------------------------- +// Per-language strategies — exported and callable +// --------------------------------------------------------------------------- + +describe('per-language strategy exports', () => { + it('goPackageStrategy is a function', () => { + expect(typeof goPackageStrategy).toBe('function'); + }); + + it('javaJvmStrategy is a function', () => { + expect(typeof javaJvmStrategy).toBe('function'); + }); + + it('kotlinJvmStrategy is a function', () => { + expect(typeof kotlinJvmStrategy).toBe('function'); + }); + + it('rustModuleStrategy is a function', () => { + expect(typeof rustModuleStrategy).toBe('function'); + }); + + it('pythonImportStrategy is a function', () => { + expect(typeof pythonImportStrategy).toBe('function'); + }); + + it('csharpNamespaceStrategy is a function', () => { + expect(typeof csharpNamespaceStrategy).toBe('function'); + }); + + it('phpPsr4Strategy is a function', () => { + expect(typeof phpPsr4Strategy).toBe('function'); + }); + + it('swiftPackageStrategy is a function', () => { + expect(typeof swiftPackageStrategy).toBe('function'); + }); + + it('dartPackageStrategy is a function', () => { + expect(typeof dartPackageStrategy).toBe('function'); + }); + + it('dartRelativeStrategy is a function', () => { + expect(typeof dartRelativeStrategy).toBe('function'); + }); + + it('rubyRequireStrategy is a function', () => { + expect(typeof rubyRequireStrategy).toBe('function'); + }); +}); + +// --------------------------------------------------------------------------- +// Per-language configs — all construct cleanly +// --------------------------------------------------------------------------- + +describe('per-language import configs', () => { + const configs: { name: string; config: ImportResolutionConfig }[] = [ + { name: 'TypeScript', config: typescriptImportConfig }, + { name: 'JavaScript', config: javascriptImportConfig }, + { name: 'Vue', config: vueImportConfig }, + { name: 'C', config: cImportConfig }, + { name: 'C++', config: cppImportConfig }, + { name: 'Go', config: goImportConfig }, + { name: 'Java', config: javaImportConfig }, + { name: 'Kotlin', config: kotlinImportConfig }, + { name: 'Python', config: pythonImportConfig }, + { name: 'Rust', config: rustImportConfig }, + { name: 'C#', config: csharpImportConfig }, + { name: 'PHP', config: phpImportConfig }, + { name: 'Swift', config: swiftImportConfig }, + { name: 'Dart', config: dartImportConfig }, + { name: 'Ruby', config: rubyImportConfig }, + ]; + + for (const { name, config } of configs) { + it(`${name} config has strategies and constructs a resolver`, () => { + expect(config.strategies.length).toBeGreaterThan(0); + expect(() => createImportResolver(config)).not.toThrow(); + }); + } +}); + +// --------------------------------------------------------------------------- +// createStandardStrategy — reusable across languages +// --------------------------------------------------------------------------- + +describe('createStandardStrategy', () => { + it('creates a reusable strategy from a language', () => { + const strategy = createStandardStrategy(SupportedLanguages.TypeScript); + expect(typeof strategy).toBe('function'); + }); + + it('resolves relative imports', () => { + const strategy = createStandardStrategy(SupportedLanguages.TypeScript); + const ctx = makeCtx(['src/utils.ts']); + const result = strategy('./utils', 'src/index.ts', ctx); + expect(result).toEqual({ kind: 'files', files: ['src/utils.ts'] }); + }); + + it('returns null for unresolvable imports', () => { + const strategy = createStandardStrategy(SupportedLanguages.TypeScript); + const ctx = makeCtx([]); + const result = strategy('./nonexistent', 'src/index.ts', ctx); + expect(result).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// Python strategy — absorbs unresolved relative imports +// --------------------------------------------------------------------------- + +describe('pythonImportStrategy', () => { + it('absorbs unresolved relative imports with empty-files sentinel', () => { + const ctx = makeCtx([]); + const result = pythonImportStrategy('.nonexistent', 'src/app.py', ctx); + expect(result).toEqual({ kind: 'files', files: [] }); + }); + + it('returns null for non-relative imports (allows chain to continue)', () => { + const ctx = makeCtx([]); + const result = pythonImportStrategy('os', 'src/app.py', ctx); + expect(result).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// Dart strategies — absorbs SDK / external package imports +// --------------------------------------------------------------------------- + +describe('dartPackageStrategy', () => { + it('absorbs dart: SDK imports', () => { + const ctx = makeCtx([]); + const result = dartPackageStrategy("'dart:async'", 'lib/main.dart', ctx); + expect(result).toEqual({ kind: 'files', files: [] }); + }); + + it('absorbs external package: imports', () => { + const ctx = makeCtx([]); + const result = dartPackageStrategy("'package:http/http.dart'", 'lib/main.dart', ctx); + expect(result).toEqual({ kind: 'files', files: [] }); + }); + + it('returns null for relative imports (chains to dartRelativeStrategy)', () => { + const ctx = makeCtx([]); + const result = dartPackageStrategy("'models.dart'", 'lib/main.dart', ctx); + expect(result).toBeNull(); + }); +});