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:
Two fixes, independent:
- Defect B — skip the second insert when
noExt == path, or slices.Compact the 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 name byExact promises 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.
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:Defect A — phantom keys (causes false positives)
A file named
src/bindings.rs.inhasExt == ".in", sonoExt == "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 targetwill therefore return a path to a nonexistent file. This is the mechanism behind the fabricated edge in #125:foo.rs.inbeside a build-time-generated, gitignoredfoo.rsis 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.Extreturns""andnoExt == path, so the same path is appended twice under the same key:app/VERSION""app/VERSIONMakefile""Makefileapp/schema.proto".proto"app/schemaEvery extension-less file has
len(byExact[p]) == 2, so the samelen(...) != 1idiom rejects it forever. #127 found this concretely:cargoprovesapp/VERSIONis a declared build-script input, and codemap reportsNo files import app/VERSION. It hitsVERSION,Makefile,Dockerfile,LICENSE, and extension-less config — all commonrerun-if-changedtargets.Why fixing it centrally is worth more than three local patches
Three open PRs touch this idiom:
resolveRustInclude— hits defect A, produces a fabricated edge (blocker).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).resolveRustBuildScriptInput— hits defect B, silently drops real inputs.Two fixes, independent:
noExt == path, orslices.Compactthe slice. Purely additive, no behavior change for extensioned files.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.