Skip to content

buildFileIndex: byExact double-indexing causes both phantom keys and unresolvable extension-less files #130

Description

@JordanCoin

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:

  1. Defect B — skip the second insert when noExt == path, or slices.Compact the slice. Purely additive, no behavior change for extensioned files.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions