Skip to content

fix(scanner): Resolve literal Rust includes - #125

Merged
JordanCoin merged 1 commit into
JordanCoin:mainfrom
reneleonhardt:fix/rust-literal-includes
Aug 13, 2026
Merged

fix(scanner): Resolve literal Rust includes#125
JordanCoin merged 1 commit into
JordanCoin:mainfrom
reneleonhardt:fix/rust-literal-includes

Conversation

@reneleonhardt

@reneleonhardt reneleonhardt commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Record direct Rust source dependencies introduced by literal include! calls. Paths are decoded with the existing Rust literal parser, resolved relative to the including file, and accepted only when the path itself is indexed exactly once (a bindings.rs.in template must not fabricate a bindings.rs target). Dynamic, missing, external, non-Rust, ambiguous, and self targets remain unresolved.

Type of change

  • Bug fix
  • New feature
  • New language support
  • Documentation
  • Other (describe below)

Checklist

  • I've tested this locally with go build && ./codemap .
  • I've read CONTRIBUTING.md; this does not add a new language.
  • Documentation is unchanged because this corrects existing Rust scanning.

Additional notes

Maintainer review addressed: the resolver requires the indexed file itself, so a gitignored, build-time-generated bindings.rs beside bindings.rs.in no longer fabricates an edge, and a real file keeps its edge when a template shares its key. Extraction and conservative-resolution tests, go vet ./..., the full suite, and the scanner race suite are GREEN on macOS Go 1.26.5.

Worktrunk's alternate git-wt binary uses include!("main.rs"); this change adds the missing src/git_wt.rs to src/main.rs dependency edge. The inspected GitButler, Codanna, and Worktrunk corpora contain no other literal source includes, so this is intentionally presented as a narrow correctness fix.

include_str! and include_bytes! are not matched; resolving their non-Rust assets remains a documented follow-up.

This branch targets main and contains no generic proc-macro, Cargo-topology, configuration, or non-Rust changes.

Developed with carefully directed, manually reviewed AI assistance.

@JordanCoin

Copy link
Copy Markdown
Owner

Reviewed. The resolution logic is right and the subtle part is correct — include paths resolve relative to the file containing the macro, not the crate root, and I verified that transitively (src/nested/raw.rs including sibling.rs resolves to src/nested/sibling.rs). concat!(env!("OUT_DIR"), …) correctly produces no edge. Windows handling is right too (filepath.FromSlash before the join).

One blocker: it can fabricate an edge to a file that does not exist.

scanner/rustgraph.go:504-510 — returns the index key, not the indexed file

target := resolveRustExplicitModule(root, declaringFile, literal)
if target == "" || len(idx.byExact[target]) != 1 {
    return ""
}
return target

idx.byExact is double-indexed — scanner/filegraph.go:248-250 registers every file under both its full path and its extension-stripped path:

idx.byExact[path] = append(idx.byExact[path], path)
noExt := strings.TrimSuffix(path, filepath.Ext(path))
idx.byExact[noExt] = append(idx.byExact[noExt], path)

So src/bindings.rs.in registers under the key src/bindings.rs. The length check passes, and the function returns the key.

Reproduced on a crate containing only src/main.rs (with include!("bindings.rs");) and src/bindings.rs.in — there is no src/bindings.rs on disk:

$ codemap --importers src/bindings.rs
📍 File: src/bindings.rs
   Imported by 1 file(s)
   • src/main.rs

$ 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

origin/main correctly reports No files import src/bindings.rs, so this is introduced here. It's realistic rather than contrived: a foo.rs.in template beside a build-time-generated, gitignored foo.rs is a normal codegen layout — present in the include, absent from the index.

The codebase already has the right guard in two places — resolveRustModuleDeclaration (rustgraph.go:461-468) uses the exact fileCounts map, and #126's askama resolver checks files[0] != target. Same shape here:

files := idx.byExact[target]
if len(files) != 1 || files[0] != target {
    return ""
}

Related, same root cause

When both src/bindings.rs and src/bindings.rs.in exist, byExact["src/bindings.rs"] has length 2 and the != 1 check drops a correct edge. That direction is conservative so it's not a blocker, but note the fix above doesn't repair it — exact paths are unique by construction, so the length check isn't doing useful work. The fileCounts-style exact map handles both cases.

Smaller

include_str! and include_bytes! produce no reference at all — only include! is matched. That matches the PR title so it's a gap rather than a lie, but they're considerably more common in real crates and resolve identically, so worth a follow-up.

Nit: rustinclude_test.go constructs two scanners; the one that actually does the ScanDirectory is never Close()d while the redundant Available() one is.

Merge note

No signature changes and no symbol or rule-id collisions with #124/#126/#127 — I checked, rust-include-imports / rust-askama-template-imports / rust-cargo-rerun-imports are distinct, and the new function names are disjoint. But all four edit the same m.RuleID disjunction and the same two switches, so expect textual conflicts on the 2nd–4th merge. Those are loud, not silent — just worth building and testing the integrated tree, since CI only ever sees each PR against main.

Record source dependencies from literal include!() calls, resolved relative
to the including file. Only a single indexed in-repo .rs target is accepted;
dynamic, external, non-Rust, ambiguous, and self-includes stay unresolved.

Co-Authored-By: GPT-5.6 Sol <codex@openai.com>
@reneleonhardt
reneleonhardt marked this pull request as draft August 13, 2026 08:02
@reneleonhardt
reneleonhardt force-pushed the fix/rust-literal-includes branch from 92fa8ba to 0608490 Compare August 13, 2026 08:21
@reneleonhardt

reneleonhardt commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — the blocker writeup was exactly right, and the fix uses the guard you suggested.

Confirmed

  • resolveRustInclude now resolves only when the indexed file itself is the target: byExact[target] must contain target exactly once. A lone bindings.rs.in no longer fabricates an edge (--importers src/bindings.rs → "No files import").
  • Because a plain files[0] != target check stays conservative when both files exist, I used the fileCounts-style exact count you pointed to instead — so the real bindings.rs keeps its edge when the template shares its key.
  • Test nit fixed: the extraction test uses one scanner and closes it.

Also changed

  • Rebased onto current main (now includes feat(scanner): Resolve Askama template dependencies #126/fix(mcp): Align context and status surfaces #121). The three textual conflicts merged cleanly; no rule-id or symbol collisions, and the integrated tree is green — askama and include edges coexist on the same file.
  • include_str!/include_bytes!: left unmatched, per the original design scope and your follow-up framing; the follow-up is now stated explicitly in the PR body.
  • Tests: TestResolveRustIncludeRequiresOneIndexedFile replaced with TestResolveRustIncludeRequiresRealIndexedFile (phantom-template and coexistence cases).
  • Housekeeping: tightened the new comment, the commit message, and the PR body.

Optional, untouched here: the askama resolver shares the same byExact double-keying, so it could take the same exact-count guard if you ever want to tighten that conservative case too.

@reneleonhardt
reneleonhardt marked this pull request as ready for review August 13, 2026 08:35

@JordanCoin JordanCoin left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified fixed, both directions.

Phantom edge gone — a crate with include!("bindings.rs") where only src/bindings.rs.in exists now reports importer_count: 0 instead of asserting an importer for a file that isn't on disk. Real includes still resolve. And the exact-count guard also repairs the inverse case the old len != 1 check broke: with both bindings.rs and bindings.rs.in present, the correct edge now resolves rather than being dropped. Nice that TestResolveRustIncludeRequiresRealIndexedFile pins it.

Note the underlying index defect is still open as #130byExact conflating exact and extension-stripped keys is what made this reachable. Your guard is correct regardless; fixing #130 would make it belt-and-braces rather than the only thing standing between us and a phantom edge.

@JordanCoin
JordanCoin merged commit d51bf14 into JordanCoin:main Aug 13, 2026
12 checks passed
@reneleonhardt
reneleonhardt deleted the fix/rust-literal-includes branch August 13, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants