diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/csharp.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/csharp.ts index 04cc01815..cb5f77145 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/configs/csharp.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/csharp.ts @@ -9,11 +9,7 @@ import { createStandardStrategy } from '../standard.js'; import { resolveCSharpImportInternal, resolveCSharpNamespaceDir } from '../csharp.js'; /** C# namespace-based resolution strategy via .csproj configs. */ -export const csharpNamespaceStrategy: ImportResolverStrategy = ( - rawImportPath, - _filePath, - ctx, -) => { +export const csharpNamespaceStrategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => { const csharpConfigs = ctx.configs.csharpConfigs; if (csharpConfigs.length > 0) { const resolvedFiles = resolveCSharpImportInternal( diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/dart.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/dart.ts index d72a787d7..06dcea98a 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/configs/dart.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/dart.ts @@ -12,11 +12,7 @@ import { resolveStandard } from '../standard.js'; * 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, -) => { +export const dartPackageStrategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => { // Strip surrounding quotes from configurable_uri capture const stripped = rawImportPath.replace(/^['"]|['"]$/g, ''); @@ -50,11 +46,7 @@ export const dartPackageStrategy: ImportResolverStrategy = ( * Dart relative import strategy — prepends "./" for bare relative paths, * then delegates to standard resolution. */ -export const dartRelativeStrategy: ImportResolverStrategy = ( - rawImportPath, - filePath, - ctx, -) => { +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); diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/go.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/go.ts index b22624d66..7eee17988 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/configs/go.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/go.ts @@ -9,11 +9,7 @@ import { createStandardStrategy } from '../standard.js'; import { resolveGoPackageDir, resolveGoPackage } from '../go.js'; /** Go-specific package resolution strategy — resolves go.mod-based package imports. */ -export const goPackageStrategy: ImportResolverStrategy = ( - rawImportPath, - _filePath, - ctx, -) => { +export const goPackageStrategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => { const goModule = ctx.configs.goModule; if (goModule && rawImportPath.startsWith(goModule.modulePath)) { const pkgSuffix = resolveGoPackageDir(rawImportPath, goModule); diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/jvm.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/jvm.ts index 4b94d35b4..47e590fe8 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/configs/jvm.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/jvm.ts @@ -9,11 +9,7 @@ import { createStandardStrategy } from '../standard.js'; import { resolveJvmWildcard, resolveJvmMemberImport, KOTLIN_EXTENSIONS } from '../jvm.js'; /** Java JVM resolution strategy — wildcard and member import resolution. */ -export const javaJvmStrategy: ImportResolverStrategy = ( - rawImportPath, - _filePath, - ctx, -) => { +export const javaJvmStrategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => { if (rawImportPath.endsWith('.*')) { const matchedFiles = resolveJvmWildcard( rawImportPath, @@ -39,11 +35,7 @@ export const javaJvmStrategy: ImportResolverStrategy = ( /** * Kotlin JVM resolution strategy — wildcard/member with Java-interop + top-level function imports. */ -export const kotlinJvmStrategy: ImportResolverStrategy = ( - rawImportPath, - _filePath, - ctx, -) => { +export const kotlinJvmStrategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => { if (rawImportPath.endsWith('.*')) { const matchedFiles = resolveJvmWildcard( rawImportPath, diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/php.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/php.ts index 54b0557e1..5a8446f25 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/configs/php.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/php.ts @@ -8,11 +8,7 @@ import type { ImportResolutionConfig, ImportResolverStrategy } from '../types.js import { resolvePhpImportInternal } from '../php.js'; /** PHP PSR-4 resolution strategy via composer.json autoload mappings. */ -export const phpPsr4Strategy: ImportResolverStrategy = ( - rawImportPath, - _filePath, - ctx, -) => { +export const phpPsr4Strategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => { const resolved = resolvePhpImportInternal( rawImportPath, ctx.configs.composerConfig, diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/python.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/python.ts index 5818c5448..85a17c594 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/configs/python.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/python.ts @@ -13,11 +13,7 @@ import { resolvePythonImportInternal } from '../python.js'; * 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, -) => { +export const pythonImportStrategy: ImportResolverStrategy = (rawImportPath, filePath, ctx) => { const resolved = resolvePythonImportInternal(filePath, rawImportPath, ctx.allFilePaths); if (resolved) { ctx.resolveCache.set(`${filePath}::${rawImportPath}`, resolved); diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/ruby.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/ruby.ts index bacbbe0ea..bf2507f70 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/configs/ruby.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/ruby.ts @@ -8,11 +8,7 @@ import type { ImportResolutionConfig, ImportResolverStrategy } from '../types.js import { suffixResolve } from '../utils.js'; /** Ruby require/require_relative resolution strategy. */ -export const rubyRequireStrategy: ImportResolverStrategy = ( - rawImportPath, - _filePath, - ctx, -) => { +export const rubyRequireStrategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => { const pathParts = rawImportPath.replace(/^\.\//, '').split('/').filter(Boolean); const resolved = suffixResolve(pathParts, ctx.normalizedFileList, ctx.allFileList, ctx.index); return resolved ? { kind: 'files', files: [resolved] } : null; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/rust.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/rust.ts index b9c848c55..daa2173e7 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/configs/rust.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/rust.ts @@ -9,11 +9,7 @@ import { createStandardStrategy } from '../standard.js'; import { resolveRustImportInternal } from '../rust.js'; /** Rust module resolution strategy — handles grouped imports and crate/super/self paths. */ -export const rustModuleStrategy: ImportResolverStrategy = ( - rawImportPath, - filePath, - ctx, -) => { +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); diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/swift.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/swift.ts index 3c26ad9d9..f7d9ec195 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/configs/swift.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/swift.ts @@ -7,11 +7,7 @@ import { SupportedLanguages } from 'gitnexus-shared'; import type { ImportResolutionConfig, ImportResolverStrategy } from '../types.js'; /** Swift Package.swift target map resolution strategy. */ -export const swiftPackageStrategy: ImportResolverStrategy = ( - rawImportPath, - _filePath, - ctx, -) => { +export const swiftPackageStrategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => { const swiftPackageConfig = ctx.configs.swiftPackageConfig; if (swiftPackageConfig) { const targetDir = swiftPackageConfig.targets.get(rawImportPath); diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/typescript-javascript.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/typescript-javascript.ts index 5869e3397..b86b365fd 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/configs/typescript-javascript.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/typescript-javascript.ts @@ -18,6 +18,10 @@ export const javascriptImportConfig: ImportResolutionConfig = { strategies: [createStandardStrategy(SupportedLanguages.JavaScript)], }; +// Vue SFCs are preprocessed into TypeScript upstream of import resolution, +// so the resolver intentionally runs as TypeScript. `language: Vue` here is +// documentation-only metadata (see `ImportResolutionConfig.language` JSDoc +// and ARCHITECTURE.md §Vue); it is not consumed by `createImportResolver`. export const vueImportConfig: ImportResolutionConfig = { language: SupportedLanguages.Vue, strategies: [createStandardStrategy(SupportedLanguages.TypeScript)], diff --git a/gitnexus/test/unit/dart-import-resolver.test.ts b/gitnexus/test/unit/dart-import-resolver.test.ts index 8151f0323..ff5dfdc3e 100644 --- a/gitnexus/test/unit/dart-import-resolver.test.ts +++ b/gitnexus/test/unit/dart-import-resolver.test.ts @@ -61,11 +61,7 @@ describe('dartPackageStrategy', () => { describe('package: imports', () => { it('resolves local package import to lib/', () => { const ctx = makeCtx(['lib/models/user.dart', 'lib/main.dart']); - const result = dartPackageStrategy( - "'package:my_app/models/user.dart'", - 'lib/main.dart', - ctx, - ); + const result = dartPackageStrategy("'package:my_app/models/user.dart'", 'lib/main.dart', ctx); expect(result).toEqual({ kind: 'files', files: ['lib/models/user.dart'] }); }); diff --git a/gitnexus/test/unit/import-resolver-factory.test.ts b/gitnexus/test/unit/import-resolver-factory.test.ts index 791f66158..f79d6f20e 100644 --- a/gitnexus/test/unit/import-resolver-factory.test.ts +++ b/gitnexus/test/unit/import-resolver-factory.test.ts @@ -21,20 +21,10 @@ import { buildSuffixIndex } from '../../src/core/ingestion/import-resolvers/util // ── Per-language strategy imports (from config files) ────────────────── import { goPackageStrategy } from '../../src/core/ingestion/import-resolvers/configs/go.js'; -import { - javaJvmStrategy, - kotlinJvmStrategy, -} from '../../src/core/ingestion/import-resolvers/configs/jvm.js'; -import { rustModuleStrategy } from '../../src/core/ingestion/import-resolvers/configs/rust.js'; +import { kotlinJvmStrategy } from '../../src/core/ingestion/import-resolvers/configs/jvm.js'; import { pythonImportStrategy } from '../../src/core/ingestion/import-resolvers/configs/python.js'; import { csharpNamespaceStrategy } from '../../src/core/ingestion/import-resolvers/configs/csharp.js'; -import { phpPsr4Strategy } from '../../src/core/ingestion/import-resolvers/configs/php.js'; -import { swiftPackageStrategy } from '../../src/core/ingestion/import-resolvers/configs/swift.js'; -import { - dartPackageStrategy, - dartRelativeStrategy, -} from '../../src/core/ingestion/import-resolvers/configs/dart.js'; -import { rubyRequireStrategy } from '../../src/core/ingestion/import-resolvers/configs/ruby.js'; +import { dartPackageStrategy } from '../../src/core/ingestion/import-resolvers/configs/dart.js'; // ── Per-language config imports ──────────────────────────────────────── import { @@ -114,7 +104,10 @@ describe('createImportResolver', () => { 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 shouldNotRun: ImportResolverStrategy = () => ({ + kind: 'files', + files: ['should-not.ts'], + }); const resolver = createImportResolver({ language: SupportedLanguages.TypeScript, @@ -145,52 +138,135 @@ describe('createImportResolver', () => { }); // --------------------------------------------------------------------------- -// Per-language strategies — exported and callable +// Per-language strategies — behavioral coverage +// +// The previous `typeof strategy === 'function'` assertions were tautological: +// TypeScript's `ImportResolverStrategy` type enforces the function shape at +// compile time, so those tests could never fail. Python and Dart already have +// deep behavioral tests below; Go / Kotlin / C# are covered here. The +// remaining strategies (Java, Rust, PHP, Swift, Ruby) are exercised via the +// per-language `createImportResolver(config)` smoke tests and the factory +// behavior suite, which together verify exports, wiring, and composition. // --------------------------------------------------------------------------- -describe('per-language strategy exports', () => { - it('goPackageStrategy is a function', () => { - expect(typeof goPackageStrategy).toBe('function'); +describe('goPackageStrategy', () => { + it('resolves go.mod package imports to a package result with dirSuffix', () => { + const files = ['cmd/server/main.go', 'cmd/server/handler.go']; + const ctx = makeCtx(files); + ctx.configs.goModule = { modulePath: 'example.com/app' }; + + const result = goPackageStrategy('example.com/app/cmd/server', 'main.go', ctx); + // `kind: 'package'` + `dirSuffix` is unique to goPackageStrategy — the + // standard strategy always returns `kind: 'files'`. Asserting this shape + // makes config-level strategy ordering observable via the full-chain test + // in `goImportConfig` below. + expect(result?.kind).toBe('package'); + expect(result?.files).toEqual(expect.arrayContaining(files)); + if (result?.kind === 'package') { + expect(result.dirSuffix).toContain('cmd/server'); + } }); - it('javaJvmStrategy is a function', () => { - expect(typeof javaJvmStrategy).toBe('function'); + it('returns null for imports outside the go module (allows chain to continue)', () => { + const ctx = makeCtx(['vendor/other/pkg/foo.go']); + ctx.configs.goModule = { modulePath: 'example.com/app' }; + const result = goPackageStrategy('github.com/other/pkg', 'main.go', ctx); + expect(result).toBeNull(); }); - it('kotlinJvmStrategy is a function', () => { - expect(typeof kotlinJvmStrategy).toBe('function'); + it('returns null when goModule is not configured', () => { + const ctx = makeCtx(['cmd/server/main.go']); + expect(ctx.configs.goModule).toBeNull(); + const result = goPackageStrategy('example.com/app/cmd/server', 'main.go', ctx); + expect(result).toBeNull(); }); - it('rustModuleStrategy is a function', () => { - expect(typeof rustModuleStrategy).toBe('function'); + it('goImportConfig full chain produces the package-kind result (strategy-order guard)', () => { + const files = ['cmd/server/main.go', 'cmd/server/handler.go']; + const ctx = makeCtx(files); + ctx.configs.goModule = { modulePath: 'example.com/app' }; + + const resolver = createImportResolver(goImportConfig); + const result = resolver('example.com/app/cmd/server', 'main.go', ctx); + // If goPackageStrategy were moved after createStandardStrategy, the + // standard strategy's suffix resolution would return a single file with + // `kind: 'files'` (or null), not `kind: 'package'` with a dirSuffix. + expect(result?.kind).toBe('package'); + }); +}); + +describe('kotlinJvmStrategy', () => { + it('resolves wildcard imports to files in the package directory', () => { + const files = [ + 'src/main/kotlin/com/example/foo/Bar.kt', + 'src/main/kotlin/com/example/foo/Baz.kt', + 'src/main/kotlin/com/example/other/Unrelated.kt', + ]; + const ctx = makeCtx(files); + + const result = kotlinJvmStrategy('com.example.foo.*', 'App.kt', ctx); + expect(result?.kind).toBe('files'); + expect(result?.files).toEqual( + expect.arrayContaining([ + 'src/main/kotlin/com/example/foo/Bar.kt', + 'src/main/kotlin/com/example/foo/Baz.kt', + ]), + ); + expect(result?.files).not.toContain('src/main/kotlin/com/example/other/Unrelated.kt'); }); - it('pythonImportStrategy is a function', () => { - expect(typeof pythonImportStrategy).toBe('function'); + it('returns null for wildcard with no matching files (allows chain to continue)', () => { + const ctx = makeCtx(['src/main/kotlin/com/example/other/Foo.kt']); + const result = kotlinJvmStrategy('com.example.missing.*', 'App.kt', ctx); + expect(result).toBeNull(); }); - it('csharpNamespaceStrategy is a function', () => { - expect(typeof csharpNamespaceStrategy).toBe('function'); + it('kotlinImportConfig full chain resolves wildcard via the JVM strategy', () => { + const files = ['src/main/kotlin/com/example/foo/Bar.kt']; + const ctx = makeCtx(files); + const resolver = createImportResolver(kotlinImportConfig); + const result = resolver('com.example.foo.*', 'App.kt', ctx); + // Standard strategy returns null for `.*` imports (see standard.ts:137), + // so only kotlinJvmStrategy can produce this result. + expect(result).toEqual({ kind: 'files', files }); + }); +}); + +describe('csharpNamespaceStrategy', () => { + it('resolves namespace imports via .csproj root-namespace mapping', () => { + const files = ['src/Services/Auth/AuthService.cs', 'src/Services/Auth/TokenService.cs']; + const ctx = makeCtx(files); + ctx.configs.csharpConfigs = [{ rootNamespace: 'MyCo', projectDir: 'src' }]; + + const result = csharpNamespaceStrategy('MyCo.Services.Auth', 'App.cs', ctx); + // Multi-file namespace resolution produces `kind: 'package'` with + // dirSuffix — unique to csharpNamespaceStrategy; the standard strategy + // always emits `kind: 'files'`. + expect(result?.kind).toBe('package'); + expect(result?.files).toEqual(expect.arrayContaining(files)); + if (result?.kind === 'package') { + expect(result.dirSuffix).toContain('Services/Auth'); + } }); - it('phpPsr4Strategy is a function', () => { - expect(typeof phpPsr4Strategy).toBe('function'); + it('returns null when no csharpConfigs are configured', () => { + const ctx = makeCtx(['src/Services/Auth/AuthService.cs']); + expect(ctx.configs.csharpConfigs).toEqual([]); + const result = csharpNamespaceStrategy('MyCo.Services.Auth', 'App.cs', ctx); + expect(result).toBeNull(); }); - it('swiftPackageStrategy is a function', () => { - expect(typeof swiftPackageStrategy).toBe('function'); - }); + it('csharpImportConfig full chain produces package-kind (strategy-order guard)', () => { + const files = ['src/Services/Auth/AuthService.cs', 'src/Services/Auth/TokenService.cs']; + const ctx = makeCtx(files); + ctx.configs.csharpConfigs = [{ rootNamespace: 'MyCo', projectDir: 'src' }]; - 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'); + const resolver = createImportResolver(csharpImportConfig); + const result = resolver('MyCo.Services.Auth', 'App.cs', ctx); + // If csharpNamespaceStrategy were reordered after createStandardStrategy, + // the result would be `kind: 'files'` (suffix match) or null, never + // `kind: 'package'` with a dirSuffix. + expect(result?.kind).toBe('package'); }); });