GitNexus/gitnexus/test/unit/scope-resolution/python/python-import-probe-count.test.ts
Gergő Magyar d57f570dff
refactor(ingestion): colocate import resolution rules (#3670)
* refactor(ingestion): remove dormant import resolvers

Remove the unused LanguageProvider importResolver contract, factory and strategy chain. Keep active scope resolver helpers and their semantic tests.

Verified 5,309 corpus edges unchanged, 7,411 focused tests, typechecks, build and resolver benchmarks. Full local suite is still running; graph analysis remains partial on deleted and comment-only regions.

* refactor: colocate language import helpers

Keep import policy beside language resolvers and share only generic
indexes. Pass the existing ordered candidates explicitly so lookup
behavior and cache identities stay unchanged.

* fix: defer MCP imports and narrow Python paths

* fix(python): prefer packages in relative imports

* test(python): pin package-first import lookup

* test(python): use a valid parent-relative import

---------

Co-authored-by: Gergely Magyar <magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-10-22 03:49:35 +00:00

259 lines
11 KiB
TypeScript

/**
* Gate for the two probe-count defects in Python import resolution: the
* duplicated tail in `resolvePythonImportTarget`, and the missing O(1) proof of
* absence in front of `resolvePythonImportInternal`'s bare-import walk.
*
* ## What is being counted, and why not `CountingSet`
*
* `test/helpers/counting-file-set.ts` counts full TRAVERSALS of the file set.
* Neither defect here traverses it even once: both are made of `Set.has`
* probes, so the house instrument reads the same number before and after and
* cannot see either. This file counts the probes themselves — the one quantity
* both defects move — with a local `Set` subclass. Deterministic: the count is
* a function of the corpus and the spelling, never of wall time, and the same
* run reports the same number on any machine.
*
* ## Defect 1 — the duplicated tail (`named` / `alias` paid twice)
*
* `resolvePythonImportTarget` probes the package first with
* `targetIncludesImportedName: true`. That recursion differs from the outer
* frame in exactly one field, whose only effect is to skip the branch, so it
* runs the outer frame's whole tail — `resolvePythonImportInternal`, the
* relative gate, `hasRepoCandidate`, `resolveAbsoluteFromFiles` — on identical
* inputs. When it returned null the code FELL THROUGH and ran all of it again.
* Measured at four directory components: 24 probes, of which 12 were
* byte-identical repeats.
*
* The gate is that `from x import y` and `import x as y` issue exactly the
* probes `import x` issues. Stated as absolute numbers rather than as
* `named === namespace`, because an equality alone also passes if BOTH kinds
* start paying twice.
*
* ## Defect 2 — no proof of absence in front of the walk
*
* The bare walk probed `<ancestor>/<seg>.py` and `<ancestor>/<seg>/__init__.py`
* at every step from the importer's directory to the workspace root, for every
* single-segment import — including `import os`, `import sys` and every other
* distribution the repo does not vendor, where every probe is guaranteed to
* miss. `pythonSegmentAbsent` answers "no file anywhere can have either shape"
* in two Map lookups on the index the dotted tiers already build.
*
* The gate is that a provably-absent segment costs the SAME at depth 16 as at
* depth 1, paired with the control that a segment which survives the proof
* still walks and still costs more with depth — otherwise a resolver that
* simply stopped working would post a perfect flat line.
*/
import { describe, expect, it } from 'vitest';
import type { ParsedImport } from 'gitnexus-shared';
import { pythonScopeResolver } from '../../../../src/core/ingestion/languages/python/scope-resolver.js';
import { resolvePythonImportInternal } from '../../../../src/core/ingestion/languages/python/import-helpers.js';
import {
NO_PARSED_FILES,
pythonNamedImport,
pythonNamespaceImport,
} from '../../../helpers/counting-file-set.js';
const { resolveImportTarget } = pythonScopeResolver;
/** Counts `has` probes. `instanceof Set` still holds, which the adapter's
* structural narrowing needs. */
class ProbeCountingSet extends Set<string> {
probes = 0;
override has(value: string): boolean {
this.probes++;
return super.has(value);
}
}
/** `import x as y`. The third kind, and the only one of the three that is not
* shared with the memo guards — it reaches the same package-attribute probe
* `pythonNamedImport` does, and both arms below assert they cost the same. */
const aliasImport = (targetRaw: string): ParsedImport => ({
kind: 'alias',
localName: 'w',
importedName: 'Widget',
alias: 'w',
targetRaw,
});
const DEPTHS: readonly number[] = [1, 2, 4, 8, 16];
/**
* An importer `depth` directory components down.
*
* `far/away/probe.py` is out of the importer's ancestry, so a `probe` walk runs
* to the end and misses — and it makes `probe` a known basename, so the absence
* proof passes it through. `vendor/thing.py` makes `vendor/` a root directory
* prefix, so `hasRepoCandidate('vendor')` passes on its check (2) and the
* dotted target below reaches `resolveAbsoluteFromFiles` instead of being
* gated out.
*/
function corpus(depth: number): { files: readonly string[]; fromFile: string } {
const fromFile = `${Array.from({ length: depth }, (_, i) => `d${i}`).join('/')}/mod.py`;
return {
files: [fromFile, 'zz/keep.py', 'far/away/probe.py', 'vendor/thing.py'],
fromFile,
};
}
function probeCount(
mkImport: (targetRaw: string) => ParsedImport,
depth: number,
targetRaw: string,
): { probes: number; result: string | readonly string[] | null } {
const { files, fromFile } = corpus(depth);
const set = new ProbeCountingSet(files);
const result = resolveImportTarget(targetRaw, fromFile, set, undefined, {
parsedFiles: NO_PARSED_FILES,
parsedImport: mkImport(targetRaw),
});
return { probes: set.probes, result };
}
/** Exists as a basename, so the absence proof passes it through to the walk. */
const PRESENT_TARGET = 'probe';
const PRESENT_RESULT = 'far/away/probe.py';
/** No file has basename `ghostmod.py` and no directory is named `ghostmod`. */
const ABSENT_TARGET = 'ghostmod';
/**
* `2 + 2 x depth` probes in the bare walk (proximity, then two per ancestor
* step including the workspace root), then `2 + depth` in the dotted tier below
* it (two direct root probes, then one per ancestor — only the module form,
* because no `probe/__init__.py` exists anywhere). `4 + 3 x depth`.
*/
const PRESENT_PROBES: readonly number[] = [7, 10, 16, 28, 52];
/** Two: the dotted tier's direct workspace-root probes. The bare walk issues
* NONE — it is retired before the proximity check. */
const ABSENT_PROBES = 2;
/**
* A DOTTED target that passes `hasRepoCandidate` (its leading segment `vendor`
* is a root directory prefix), reaches `resolveAbsoluteFromFiles`, walks the
* whole ancestor chain and still resolves to nothing — because the only
* `probe.py` in the workspace does not end with `/vendor/probe.py`.
*
* This is the shape the duplicated tail actually costs on, and the reason the
* single-segment arm above cannot see it: a single-segment target that survives
* the absence proof is always answered by the suffix fallback, so its
* `packageTarget` is never null and the fallthrough never fires. A dotted one
* can miss, and missing is precisely when the old code ran the tail again.
*/
const DOTTED_TARGET = 'vendor.probe';
/** `2 + depth`: two direct root probes, then one module probe per ancestor. */
const DOTTED_NAMESPACE_PROBES: readonly number[] = [3, 4, 6, 10, 18];
/**
* `named`/`alias` legitimately add TWO — the submodule probe for
* `vendor.probe.Widget`, a different target with its own direct root checks.
* What they must NOT add is a third component: another whole copy of the
* package tail. With the fallthrough restored these read [8, 10, 14, 22, 38].
*/
const DOTTED_SUBMODULE_PROBES: readonly number[] = [5, 6, 8, 12, 20];
describe('Python import probe count', () => {
it.each([
{ kind: 'import x (namespace)', mkImport: pythonNamespaceImport },
{ kind: 'from x import y (named)', mkImport: pythonNamedImport },
{ kind: 'import x as y (alias)', mkImport: aliasImport },
])('costs the same for every import KIND — single-segment, resolving — $kind', ({ mkImport }) => {
const counted = DEPTHS.map((depth) => probeCount(mkImport, depth, PRESENT_TARGET));
expect(counted.map((c) => c.probes)).toEqual(PRESENT_PROBES);
// Non-vacuity: a probe count is equally flattering to a resolver that
// resolves nothing.
expect(counted.map((c) => c.result)).toEqual(DEPTHS.map(() => PRESENT_RESULT));
});
it.each([
{
kind: 'import x (namespace)',
mkImport: pythonNamespaceImport,
expected: DOTTED_NAMESPACE_PROBES,
},
{
kind: 'from x import y (named)',
mkImport: pythonNamedImport,
expected: DOTTED_SUBMODULE_PROBES,
},
{ kind: 'import x as y (alias)', mkImport: aliasImport, expected: DOTTED_SUBMODULE_PROBES },
])(
'runs the package tail ONCE for a dotted target that misses — $kind',
({ mkImport, expected }) => {
const counted = DEPTHS.map((depth) => probeCount(mkImport, depth, DOTTED_TARGET));
// The duplicated-tail gate. Restoring the fallthrough adds a second copy of
// the namespace column to the two submodule rows.
expect(counted.map((c) => c.probes)).toEqual(expected);
expect(counted.map((c) => c.result)).toEqual(DEPTHS.map(() => null));
},
);
it.each([
{ kind: 'import x (namespace)', mkImport: pythonNamespaceImport },
{ kind: 'from x import y (named)', mkImport: pythonNamedImport },
{ kind: 'import x as y (alias)', mkImport: aliasImport },
])('retires a provably absent segment in a CONSTANT probe count — $kind', ({ mkImport }) => {
const counted = DEPTHS.map((depth) => probeCount(mkImport, depth, ABSENT_TARGET));
// The gate: flat in depth. Without the proof of absence this is
// `4 + 4 x depth` for a miss, i.e. 8 at depth 1 and 68 at depth 16.
expect(counted.map((c) => c.probes)).toEqual(DEPTHS.map(() => ABSENT_PROBES));
expect(counted.map((c) => c.result)).toEqual(DEPTHS.map(() => null));
});
it('the counter can see depth — the flat line above is the proof, not the instrument', () => {
// Control for the arm above: the same instrument, the same corpus, the same
// depths, one different spelling — and the count triples across the range.
// So a flat line means the walk was skipped, not that nothing is counted.
const present = DEPTHS.map(
(depth) => probeCount(pythonNamedImport, depth, PRESENT_TARGET).probes,
);
expect(present).toEqual(PRESENT_PROBES);
expect(new Set(present).size).toBe(DEPTHS.length);
});
/**
* The two inputs `pythonSegmentAbsent` refuses to answer for. Both must keep
* probing exactly as before; a proof of absence that fires on either would
* silently stop resolving real files.
*/
it.each([
{
why: 'the EMPTY segment, module form — basename `.py` is indexed normally',
files: ['a/b/.py', 'a/b/mod.py'],
fromFile: 'a/b/mod.py',
importPath: '',
expected: 'a/b/.py',
},
{
// THE reason the empty-segment carve-out exists. The probe for an empty
// segment is `<prefix>/__init__.py`, whose parent directory name is empty
// — exactly the case the `byInitParent` build skips. So the bucket cannot
// witness this file, and its absence is not proof of the file's absence.
why: 'the EMPTY segment, package form under a doubled separator — `byInitParent` skips it',
files: ['a//__init__.py', 'a/b/mod.py'],
fromFile: 'a/b/mod.py',
importPath: '',
expected: 'a//__init__.py',
},
{
why: 'the EMPTY segment, package form at the filesystem root',
files: ['/__init__.py', 'a/b/mod.py'],
fromFile: 'a/b/mod.py',
importPath: '',
expected: '/__init__.py',
},
{
why: 'a segment carrying a BACKSLASH — the buckets are keyed on normalized paths',
files: ['a\\b.py', 'x/mod.py'],
fromFile: 'x/mod.py',
importPath: 'a\\b',
expected: 'a\\b.py',
},
])('still resolves what the proof of absence cannot rule out — $why', (row) => {
expect(resolvePythonImportInternal(row.fromFile, row.importPath, new Set(row.files))).toBe(
row.expected,
);
});
});