bug: tier 1b stem-matches relative import specifiers — require("./utils") from lib/ reaches test/utils.js #57
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
h-dv/code-index#57
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while implementing #56. Blocks #56 part 2 (CommonJS
require()capture) and is very likely producing phantoms in the other languages today.The defect
Tier 1b's file-key reachability arm (
crates/indexer/src/index.rs~1026-1032) treats an import as evidence for a candidate file when the import's module string matches that file's stem:temp.file_keysholds barePath::file_stemvalues (index.rs~879-890). So a relative specifier is matched by stem alone, with the directory it is relative to discarded.Reproduction (measured)
In the pinned
js-expresscorpus repo there are two files with stemutils:lib/utils.jsandtest/utils.js.lib/application.jsrequires./utils— unambiguouslylib/utils.js.With #56 part 2's import capture landed, six references resolve into the test file:
A production file binding into a test file.
fnis a local inapplication.js; the import edge is what madetest/utils.jsreachable.Why this matters beyond JS
The same arm serves every language. Any two files sharing a stem in a repo that uses relative imports can be confused:
from .utils import xwherepkg_a/utils.pyandpkg_b/utils.pyboth existimport "./helpers"withsrc/helpers.tsandtest/helpers.tsrequire __DIR__ . "/../Util.php"The fix therefore has upside beyond unblocking #56, and should be measured across the whole corpus rather than just express.
Suggested direction
Path-resolve a relative specifier against the importing file's directory before matching, instead of comparing stems:
./or../→ resolve to a normalised repo-relative path and require the candidate file's path to equal it (modulo extension and/index.<ext>);express,lodash,crate::x) keep the current stem/package-tail behaviour — they have no directory to resolve against.That is a narrowing, so it can only remove candidates, never add them: this cannot mint a phantom. It can reduce recall where the current stem match happened to be right, so it needs the usual measurement.
Two implementation notes:
imports.modulestores the raw specifier, so resolution can be done in Rust when buildingtemp.file_keys/a new temp table, rather than in SQL (SQLite cannot normalise../paths)../xtox.js|.ts|.tsx|/index.js, Python's.xis package-relative, PHP uses filesystem paths. Handle the ones the corpus covers and leave the rest on the existing behaviour.Acceptance
phantom_count == 0maintained; corpus mutation guard still 0 rebindscorpus_ratchetre-blessed with the reasonlib/application.js -> test/utils.jsbindings are goneSeverity: medium
Latent today (nothing has measured how many current resolutions depend on a wrong same-stem match — worth quantifying first), but it is a hard blocker for #56 part 2, and it is a phantom class rather than a recall gap.
Fixed and released in v0.9.0.
Relative import specifiers are now path-resolved against the importing file's directory instead of stem-matched, so
require("./utils")fromlib/can no longer reachtest/utils.js. Implemented via atemp.import_reltable built in Rust (normalise_rel,relative_import_paths,resolution_candidates).Three things the corpus forced during the fix, each caught by measurement rather than review:
./api.jsmust resolve toapi.ts→ fixed with extension substitution.require("../..")produced an empty base and matched/index.js→ prefix guard added.This was a prerequisite for #56 part 2, which had been reverted for +6 phantoms and re-landed clean once this was in.
Regression cover: three new tests in
crates/indexer/tests/resolver.rs.