refactor(ingestion): strengthen import-resolver tests and document Vue config intent

Address non-blocking follow-ups from PR #886 review:
- Add inline comment to vueImportConfig explaining intentional
  language: Vue / TypeScript-strategy mismatch (Vue SFCs are
  preprocessed into TS upstream of import resolution).
- Replace 11 tautological typeof === 'function' assertions with
  behavioral tests for goPackageStrategy, kotlinJvmStrategy, and
  csharpNamespaceStrategy, including full-chain strategy-order
  guards via createImportResolver(config).
- Apply prettier formatting to sibling configs touched during
  factory introduction.

Test: 37 passed (previously 26), tsc --noEmit clean.
This commit is contained in:
Gergo Magyar 2026-04-16 18:23:12 +01:00
parent 1c5e12e594
commit f4be87fb8d
12 changed files with 134 additions and 102 deletions

View file

@ -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(

View file

@ -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);

View file

@ -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);

View file

@ -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,

View file

@ -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,

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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)],

View file

@ -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'] });
});

View file

@ -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');
});
});