Repository navigation
buildFileIndex: byExact double-indexing causes both phantom keys and unresolvable extension-less files #130
Description
Pre-existing on main. One three-line block produces two opposite defects, and it is the shared root cause behind findings in #125, #126 and #127.
The code
scanner/filegraph.go:248-250:
idx.byExact[path] = append(idx.byExact[path], path)
noExt := strings.TrimSuffix(path, filepath.Ext(path))
idx.byExact[noExt] = append(idx.byExact[noExt], path)Defect A — phantom keys (causes false positives)
A file named src/bindings.rs.in has Ext == ".in", so noExt == "src/bindings.rs". The index now contains a key for a path that may not exist on disk, with exactly one entry.
Any resolver using the common idiom if len(idx.byExact[target]) != 1 { return "" }; return target will therefore return a path to a nonexistent file. This is the mechanism behind the fabricated edge in #125:
$ codemap --json --importers src/bindings.rs
{"file":"src/bindings.rs","importers":["src/main.rs"],"importer_count":1}
$ ls src/bindings.rs
No such file or directory
foo.rs.in beside a build-time-generated, gitignored foo.rs is a normal codegen layout, so this is reachable in ordinary repos.
Defect B — extension-less files are permanently unresolvable (causes false negatives)
When a path has no extension, filepath.Ext returns "" and noExt == path, so the same path is appended twice under the same key:
| path | ext | noExt | noExt == path |
|---|---|---|---|
app/VERSION |
"" |
app/VERSION |
true |
Makefile |
"" |
Makefile |
true |
app/schema.proto |
".proto" |
app/schema |
false |
Every extension-less file has len(byExact[p]) == 2, so the same len(...) != 1 idiom rejects it forever. #127 found this concretely: cargo proves app/VERSION is a declared build-script input, and codemap reports No files import app/VERSION. It hits VERSION, Makefile, Dockerfile, LICENSE, and extension-less config — all common rerun-if-changed targets.
Why fixing it centrally is worth more than three local patches
Three open PRs touch this idiom:
- fix(scanner): Resolve literal Rust includes #125
resolveRustInclude— hits defect A, produces a fabricated edge (blocker). - feat(scanner): Resolve Askama template dependencies #126
resolveRustAskamaTemplate— already guards correctly withfiles[0] != target, so it is immune to A. Worth noting the correct pattern already exists in-tree, also inresolveRustModuleDeclaration(rustgraph.go:461-468, via the exactfileCountsmap). - feat(scanner): Track Cargo build-script inputs #127
resolveRustBuildScriptInput— hits defect B, silently drops real inputs.
Two fixes, independent:
- Defect B — skip the second insert when
noExt == path, orslices.Compactthe slice. Purely additive, no behavior change for extensioned files. - Defect A — either stop conflating "exact path" and "extension-stripped path" in one map (give fuzzy matching its own index), or require every consumer to verify
files[0] == target. The first is better: the current namebyExactpromises something the map does not deliver, which is how three resolvers ended up trusting it.
Fixing (1) makes #127's finding disappear; fixing (2) makes #125's blocker disappear and removes the trap for the next resolver.
Activity
fix(scanner): stop rust-use imports leak and fix byExact exact-count
JordanCoin commentedon Oct 7, 2026
Verified on
main:buildFileIndexContextnow keepsbyExactas an inventory count (map[string]uint32, scanner/filegraph.go:33 and :416) and never inserts an extension-stripped key, so neither the phantomsrc/bindings.rskey (defect A) nor the double insert for extension-less paths (defect B) can occur; the resolvers compare the count to 1 (lines 777 to 791). This landed with the compact index rewrite in #182. Closing; reopen with the #125/#127 fixtures if either edge reappears.Generated by Claude Code