GitNexus/gitnexus/test/unit/import-resolution.test.ts
Gergo Magyar 76ed0fa53b fix: address PR #409 review findings (P0-P3) and simplify import resolution API
Bug fixes (P0):
- Narrow Go /cmd/ entry-point detection to only match /main.go
- Fix Rust scoped grouped imports (use crate::models::{User, Repo}) resolution
- Filter PHP use function/use const from class-type namedImportMap bindings

Improvements (P1):
- Add C# resolveStandard fallback when .csproj discovery fails
- Change preprocessImportPath return type to string | null with caller guards
- Add Q_SIGNALS/Q_SLOTS (standard plural Qt macros)
- Fix stale "11 supported languages" comment → 13

API simplification (P2):
- Replace buildImportResolvers() factory with const importResolvers table
- Move configs onto ResolveCtx (extends ImportResolutionContext)
- Eliminate tsconfigPaths parameter threading through 6 non-TS resolvers
- Split utils.ts (1,476 lines) into ast-helpers.ts + call-analysis.ts + utils.ts
- Consolidate findChild/findChildByType into single source of truth
- Match multi-file import bindings to files by basename for namedImportMap

Cleanup (P3):
- EMPTY_INDEX returns shared frozen empty array instead of allocating per call
- Type appendKotlinWildcard parameter as SyntaxNode instead of any
- Document call-routing validation requirement on CallRouter type

Tests:
- Add unit tests for preprocessImportPath (13 tests)
- Add integration tests: Rust scoped multi-file, PHP use function/const,
  C# without .csproj, Go cmd/ helper scoring (14 tests, 4 fixtures)

All 3579 tests pass.
2026-03-21 14:15:15 +00:00

149 lines
6.8 KiB
TypeScript

/**
* Unit tests for import-resolution.ts
*
* Coverage notes:
* - `preprocessImportPath` is tested directly below (no tree-sitter required for most paths).
* - Rust scoped grouped import logic (`resolveRustImportDispatch`) requires a live file system
* and ResolveCtx — that path is covered by test/integration/resolvers/rust.test.ts.
* - PHP `use function` / `use const` filtering (via `extractPhpNamedBindings`) requires
* tree-sitter PHP nodes — covered by test/integration/resolvers/php.test.ts.
*/
import { describe, it, expect } from 'vitest';
import { preprocessImportPath } from '../../src/core/ingestion/import-resolution.js';
import { SupportedLanguages } from '../../src/config/supported-languages.js';
// ---------------------------------------------------------------------------
// Minimal SyntaxNode stub — only the fields preprocessImportPath touches.
// For non-Kotlin languages preprocessImportPath never reads the node, so an
// empty stub satisfies the type requirement without loading tree-sitter.
// ---------------------------------------------------------------------------
function makeNode(overrides: Partial<{ childCount: number; child: (i: number) => any }> = {}): any {
return {
childCount: overrides.childCount ?? 0,
child: overrides.child ?? (() => null),
};
}
// ---------------------------------------------------------------------------
// preprocessImportPath — universal cleaning behaviour
// ---------------------------------------------------------------------------
describe('preprocessImportPath', () => {
describe('quote and bracket stripping', () => {
it('strips double quotes from a bare module path', () => {
const node = makeNode();
expect(preprocessImportPath('"foo"', node, SupportedLanguages.TypeScript)).toBe('foo');
});
it('strips single quotes from a bare module path', () => {
const node = makeNode();
expect(preprocessImportPath("'bar/baz'", node, SupportedLanguages.JavaScript)).toBe('bar/baz');
});
it('strips angle brackets from a C-style include path', () => {
const node = makeNode();
expect(preprocessImportPath('<stdio.h>', node, SupportedLanguages.C)).toBe('stdio.h');
});
it('strips mixed quote and angle bracket characters', () => {
const node = makeNode();
// Pathological input — all stripped characters removed
expect(preprocessImportPath('"<hello>"', node, SupportedLanguages.TypeScript)).toBe('hello');
});
});
describe('null returns for invalid inputs', () => {
it('returns null for an empty string (after cleaning)', () => {
const node = makeNode();
// Only quote characters — cleaned result is empty string
expect(preprocessImportPath('""', node, SupportedLanguages.TypeScript)).toBeNull();
});
it('returns null for a string containing control characters', () => {
const node = makeNode();
// \x01 is a control character that passes the length check but fails the regex guard
expect(preprocessImportPath('foo\x01bar', node, SupportedLanguages.Rust)).toBeNull();
});
it('returns null for a string containing a null byte', () => {
const node = makeNode();
expect(preprocessImportPath('foo\x00bar', node, SupportedLanguages.Go)).toBeNull();
});
it('returns null for a path exceeding 2048 characters', () => {
const node = makeNode();
const longPath = 'a'.repeat(2049);
expect(preprocessImportPath(longPath, node, SupportedLanguages.Python)).toBeNull();
});
it('accepts a path of exactly 2048 characters', () => {
const node = makeNode();
const maxPath = 'a'.repeat(2048);
expect(preprocessImportPath(maxPath, node, SupportedLanguages.Python)).toBe(maxPath);
});
});
describe('Kotlin wildcard pass-through', () => {
it('delegates to appendKotlinWildcard when language is Kotlin — no wildcard child', () => {
// Node with no children -> appendKotlinWildcard returns the path unchanged
const node = makeNode({ childCount: 0 });
const result = preprocessImportPath('com.example.models', node, SupportedLanguages.Kotlin);
// Without a wildcard_import child the path is returned as-is
expect(result).toBe('com.example.models');
});
it('delegates to appendKotlinWildcard when language is Kotlin — wildcard_import child present', () => {
// Simulate a node that has a wildcard_import child at index 0
const wildcardChild = { type: 'wildcard_import' };
const node = makeNode({
childCount: 1,
child: (i: number) => (i === 0 ? wildcardChild : null),
});
const result = preprocessImportPath('com.example.models', node, SupportedLanguages.Kotlin);
// appendKotlinWildcard appends .* when the wildcard_import child is found
expect(result).toBe('com.example.models.*');
});
});
describe('non-Kotlin languages are returned unchanged (after cleaning)', () => {
it('returns the cleaned path for Rust without modification', () => {
const node = makeNode();
expect(preprocessImportPath('"crate::models"', node, SupportedLanguages.Rust)).toBe('crate::models');
});
it('returns the cleaned path for PHP without modification', () => {
const node = makeNode();
expect(preprocessImportPath('"App\\\\Models\\\\User"', node, SupportedLanguages.PHP)).toBe('App\\\\Models\\\\User');
});
});
});
// ---------------------------------------------------------------------------
// Rust scoped grouped import logic (resolveRustImportDispatch)
// ---------------------------------------------------------------------------
// The dispatch function requires a live ResolveCtx with file lists — unit
// testing it without a file system would duplicate the integration fixtures.
// The following comment documents what the integration tests verify:
//
// test/integration/resolvers/rust.test.ts covers:
// - Top-level grouped: use {crate::a, crate::b}
// - Scoped grouped: use crate::models::{User, Repo}
// - Alias stripping: use crate::models::{User, Repo as R} -> resolves User + Repo
// - Prefix fallback: when no individual items resolve, resolves the prefix path
//
// The ::{ detection and alias-stripping logic lives in resolveRustImportDispatch()
// at import-resolution.ts lines 328-344.
// ---------------------------------------------------------------------------
// PHP use function / use const filtering (extractPhpNamedBindings)
// ---------------------------------------------------------------------------
// extractPhpNamedBindings requires live tree-sitter PHP SyntaxNode objects.
// The filtering of `use function` and `use const` declarations is covered by:
//
// test/integration/resolvers/php.test.ts
//
// which runs the full ingestion pipeline over PHP fixture repositories and
// asserts that function/const use-declarations do not produce spurious IMPORTS
// edges to non-existent class files.