mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
* feat: configure prettier with pre-commit hook integration Add prettier, lint-staged, and prettier-plugin-tailwindcss at the repo root with husky pre-commit hook integration. Moves husky from gitnexus/ to root package.json for reliable hook installation. - Root package.json with prepare/format/format:check scripts - .prettierrc with endOfLine:lf and tailwindStylesheet for TW v4 - .prettierignore excluding fixtures, vendor, generated, *.d.ts, *.md - .gitattributes enforcing LF line endings for Windows consistency - Pre-commit hook uses direct node_modules/.bin/ paths (no npx) * style: apply prettier formatting to entire codebase One-time bulk format. No logic changes. Use .git-blame-ignore-revs to skip this commit in git blame. * chore: add .git-blame-ignore-revs for prettier format commit * perf: pre-commit hook runs only tests related to staged files Use vitest --related to scope test execution to tests that import the changed files, instead of running the full suite on every commit. * perf: remove vitest from pre-commit hook, keep in CI only Pre-commit now runs lint-staged + tsc only. Tests run in CI (ci-tests.yml) where they belong — keeps commits fast. * ci: add prettier format check to quality workflow PRs will now fail if code isn't formatted with prettier.
222 lines
9.1 KiB
TypeScript
222 lines
9.1 KiB
TypeScript
/**
|
|
* Unit tests for proximity-based Python import resolution.
|
|
*
|
|
* When two files share the same bare name (e.g. user.py in two different
|
|
* directories), suffixResolve alone picks whichever was indexed first.
|
|
* resolvePythonImport addresses this by checking the importer's own directory
|
|
* first, mirroring Python's sys.path resolution order.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
buildSuffixIndex,
|
|
suffixResolve,
|
|
} from '../../src/core/ingestion/import-resolvers/utils.js';
|
|
import { resolvePythonImportInternal } from '../../src/core/ingestion/import-resolvers/python.js';
|
|
import { resolveImportPath } from '../../src/core/ingestion/import-resolvers/standard.js';
|
|
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeCtx(files: string[]) {
|
|
const normalized = files.map((f) => f.replace(/\\/g, '/'));
|
|
const allFilesSet = new Set(files);
|
|
const index = buildSuffixIndex(normalized, files);
|
|
const cache = new Map<string, string | null>();
|
|
return { files, normalized, allFilesSet, index, cache };
|
|
}
|
|
|
|
/** Simulate the full dispatch: resolvePythonImport first, then suffixResolve fallback. */
|
|
function resolvePython(
|
|
currentFile: string,
|
|
importPath: string,
|
|
ctx: ReturnType<typeof makeCtx>,
|
|
): string | null {
|
|
const proximity = resolvePythonImportInternal(currentFile, importPath, ctx.allFilesSet);
|
|
if (proximity) return proximity;
|
|
if (importPath.startsWith('.')) return null;
|
|
const pathLike = importPath.replace(/\./g, '/');
|
|
const parts = pathLike.split('/').filter(Boolean);
|
|
return suffixResolve(parts, ctx.normalized, ctx.files, ctx.index);
|
|
}
|
|
|
|
/** For non-Python languages, delegate directly to standard resolveImportPath. */
|
|
function resolve(
|
|
currentFile: string,
|
|
importPath: string,
|
|
language: SupportedLanguages,
|
|
ctx: ReturnType<typeof makeCtx>,
|
|
): string | null {
|
|
return resolveImportPath(
|
|
currentFile,
|
|
importPath,
|
|
ctx.allFilesSet,
|
|
ctx.files,
|
|
ctx.normalized,
|
|
ctx.cache,
|
|
language,
|
|
null,
|
|
ctx.index,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Python proximity resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('resolvePythonImport — proximity-based resolution for Python', () => {
|
|
it('resolves bare import to same-directory file when multiple files share the name', () => {
|
|
const ctx = makeCtx([
|
|
'app/models/user.py', // indexed first — would win without proximity
|
|
'app/services/user.py',
|
|
'app/services/auth.py',
|
|
]);
|
|
|
|
const result = resolvePython('app/services/auth.py', 'user', ctx);
|
|
expect(result).toBe('app/services/user.py');
|
|
});
|
|
|
|
it('falls back to suffix index when no same-directory match exists', () => {
|
|
const ctx = makeCtx(['app/models/user.py', 'app/services/auth.py']);
|
|
|
|
const result = resolvePython('app/services/auth.py', 'user', ctx);
|
|
expect(result).toBe('app/models/user.py');
|
|
});
|
|
|
|
it('handles importer at repo root (no directory) without crashing', () => {
|
|
const ctx = makeCtx(['user.py', 'auth.py']);
|
|
|
|
// importerDir is '' — proximity skipped, suffix fallback used
|
|
const result = resolvePython('auth.py', 'user', ctx);
|
|
expect(result).toBe('user.py');
|
|
});
|
|
|
|
it('does not apply proximity for multi-segment imports (dotted paths)', () => {
|
|
const ctx = makeCtx(['app/models/utils/helpers.py', 'app/services/auth.py']);
|
|
|
|
const result = resolvePython('app/services/auth.py', 'utils.helpers', ctx);
|
|
expect(result).toBe('app/models/utils/helpers.py');
|
|
});
|
|
|
|
it('resolves same-directory package (user/__init__.py) via proximity', () => {
|
|
const ctx = makeCtx([
|
|
'app/models/user/__init__.py', // indexed first — would win without proximity
|
|
'app/services/user/__init__.py',
|
|
'app/services/auth.py',
|
|
]);
|
|
|
|
const result = resolvePython('app/services/auth.py', 'user', ctx);
|
|
expect(result).toBe('app/services/user/__init__.py');
|
|
});
|
|
|
|
it('falls back to suffixResolve for __init__.py when no same-directory package exists', () => {
|
|
const ctx = makeCtx(['app/models/__init__.py', 'app/services/auth.py']);
|
|
|
|
const result = resolvePython('app/services/auth.py', 'models', ctx);
|
|
expect(result).toBe('app/models/__init__.py');
|
|
});
|
|
|
|
it('handles Windows-style backslash paths in currentFile without crashing', () => {
|
|
const ctx = makeCtx(['app/services/user.py', 'app/services/auth.py']);
|
|
|
|
const result = resolvePython('app\\services\\auth.py', 'user', ctx);
|
|
expect(result).toBe('app/services/user.py');
|
|
});
|
|
|
|
it('resolves PEP 328 relative import (.user) to same-directory file', () => {
|
|
const ctx = makeCtx(['app/services/user.py', 'app/services/auth.py']);
|
|
|
|
const result = resolvePython('app/services/auth.py', '.user', ctx);
|
|
expect(result).toBe('app/services/user.py');
|
|
});
|
|
|
|
it('returns null when relative import dots exceed directory depth (PEP 328 over-traversal)', () => {
|
|
// auth.py is at depth 1 (one directory: 'app').
|
|
// '...user' has 3 dots → 2 upward hops required, but only 1 directory level exists.
|
|
// CPython raises ImportError; we return null.
|
|
const ctx = makeCtx(['app/auth.py', 'user.py']);
|
|
|
|
const result = resolvePython('app/auth.py', '...user', ctx);
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Namespace packages (PEP 420) — directory with no __init__.py
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('resolvePythonImport — namespace packages (no __init__.py)', () => {
|
|
// user/ exists as a namespace package: no __init__.py, only submodules.
|
|
const files = [
|
|
'app/services/auth.py',
|
|
'app/services/user/model.py', // user/ has no __init__.py
|
|
'app/services/user/queries.py',
|
|
];
|
|
|
|
it('bare import of namespace package returns null (no file to resolve to)', () => {
|
|
// `import user` — proximity finds neither user.py nor user/__init__.py.
|
|
// suffixResolve also finds nothing because no file is literally named "user".
|
|
// This is expected: CPython itself sets user.__file__ = None for namespace packages.
|
|
const ctx = makeCtx(files);
|
|
const result = resolvePython('app/services/auth.py', 'user', ctx);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('submodule form resolves correctly via suffixResolve fallback', () => {
|
|
// `import user.model` — multi-segment, proximity skipped, suffixResolve finds user/model.py.
|
|
const ctx = makeCtx(files);
|
|
const result = resolvePython('app/services/auth.py', 'user.model', ctx);
|
|
expect(result).toBe('app/services/user/model.py');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Ruby: bare require does NOT use proximity
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('resolveImportPath — Ruby bare require does not use proximity', () => {
|
|
it('returns first-indexed file for bare require (Ruby $LOAD_PATH excludes current directory)', () => {
|
|
const ctx = makeCtx([
|
|
'lib/core/helpers.rb', // indexed first
|
|
'lib/utils/helpers.rb',
|
|
'lib/utils/formatter.rb',
|
|
]);
|
|
|
|
// Ruby bare `require 'helpers'` searches $LOAD_PATH — current directory not included.
|
|
// No proximity bias; first-indexed file is returned, same as before.
|
|
const result = resolve('lib/utils/formatter.rb', 'helpers', SupportedLanguages.Ruby, ctx);
|
|
expect(result).toBe('lib/core/helpers.rb');
|
|
});
|
|
|
|
it('resolves require_relative (dot-prefixed) to same-directory file via generic relative resolver', () => {
|
|
const ctx = makeCtx(['lib/utils/helpers.rb', 'lib/utils/formatter.rb']);
|
|
|
|
// require_relative arrives as "./<path>" — caught by generic relative resolver, not proximity
|
|
const result = resolve('lib/utils/formatter.rb', './helpers', SupportedLanguages.Ruby, ctx);
|
|
expect(result).toBe('lib/utils/helpers.rb');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Other languages: no proximity applied
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('resolveImportPath — no proximity for Java or TypeScript', () => {
|
|
it('Java: fully-qualified import resolves to the correct file via unique suffix', () => {
|
|
const ctx = makeCtx(['src/com/a/User.java', 'src/com/b/User.java', 'src/com/b/Service.java']);
|
|
|
|
// "com.b.User" → "com/b/User" → unique suffix; no ambiguity
|
|
const result = resolve('src/com/b/Service.java', 'com.b.User', SupportedLanguages.Java, ctx);
|
|
expect(result).toBe('src/com/b/User.java');
|
|
});
|
|
|
|
it('TypeScript: relative import resolves via generic relative resolver', () => {
|
|
const ctx = makeCtx(['src/services/user.ts', 'src/services/auth.ts', 'src/models/user.ts']);
|
|
|
|
// "./user" is explicit relative — resolved before proximity is checked
|
|
const result = resolve('src/services/auth.ts', './user', SupportedLanguages.TypeScript, ctx);
|
|
expect(result).toBe('src/services/user.ts');
|
|
});
|
|
});
|