refactor(ingestion): split ImportSemantics into per-strategy hooks

- Add ImportResolverStrategy and ImportResolutionConfig types
- Create createImportResolver factory (resolver-factory.ts)
- Add createStandardStrategy to standard.ts
- Extract per-language strategies from existing resolvers:
  goPackageStrategy, javaJvmStrategy, kotlinJvmStrategy,
  rustModuleStrategy, pythonImportStrategy, csharpNamespaceStrategy,
  phpPsr4Strategy, swiftPackageStrategy, dartPackageStrategy,
  dartRelativeStrategy, rubyRequireStrategy
- Create per-language config files in import-resolvers/configs/
- Update all 15 language providers to use createImportResolver(config)
- Add 38 unit tests for factory and strategy composition
- All 3640+ existing tests pass, tsc --noEmit passes

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c3aec32d-2155-4808-88df-9cd6b2384174

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-16 14:06:08 +00:00 • committed by GitHub
parent 24835341c0
commit 2cc9ecd27f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 780 additions and 78 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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