Skip to content

feat: Rust language support (.rs) - #372

Open
bitnom wants to merge 1 commit into
trailhq:mainfrom
Apothic-AI:rust-support-rebase
Open

bitnom wants to merge 1 commit into
trailhq:mainfrom
Apothic-AI:rust-support-rebase

Conversation

@bitnom

@bitnom bitnom commented Sep 13, 2026

Copy link
Copy Markdown

Tier-1 graph extraction for Rust via tree-sitter-rust 0.24.0 (the tree-sitter org's own grammar), rebased onto current main as a depth-tier language.rs leaves the generic tags.scm registry (mirroring PHP's move in #157) rather than stacking on #58/#40, so this PR merges independently.

What

  • Definitions: functions, structs, enums, traits (→ interface), type aliases, unions, methods with impl-block owners (generics-stripped: impl Cache<T> owns as Cache), macro_rules! in its own ! namespace, and module/constant nodes (mod mints a module node, const/static mint constants — the initializer is the body, never the signature). Inline modules contribute scope segments (file.rs#imp.open), so cfg-gated sibling mods stay distinct and resolvable.
  • Visibility: pub/pub(crate)/pub(super) → exported; pub(self) and plain items → not; trait-impl methods inherit the trait contract (exported); #[macro_export] macros exported; everything inside #[cfg(test)] mods unexported (#[cfg(not(test))] is correctly NOT matched), and function-body nesting cannot launder a pub into an export.
  • Calls: bare calls, method calls with typed-receiver inference (typed let, constructor-let T::new(), typed params, self → enclosing impl), Type::method / Self::method static calls, turbofish unwrapped, tuple-index calls guarded. Scoped calls (crate::worker::run()) resolve through the module tree and are restricted to the resolved file's candidates — never stripped to a bare name.
  • Macros: invocations and definitions live in their own namespace — names carry the ! (log_it!), so a macro can never wire to a same-named function (Rust's namespaces are distinct). Std/prelude macros are skipped at extraction; path-qualified forms (std::println!) are recognized too.
  • Imports: use paths (all forms: scoped, grouped per-leaf, renames, wildcards) and mod declarations resolve through the real module tree — crate::/super::/self::, both x.rs and x/mod.rs layouts, the parser-file rule (src/parser.rs's children live in src/parser/), inline-mod super consumption (a use super::* inside #[cfg(test)] mod tests correctly refers to its own file), and integration-test crate roots (tests/*.rs anchor crate:: to themselves, not the library).
  • Workspaces: each Cargo.toml's [package] name (section-aware parse; [[bin]] names ignored) maps cross-crate imports (foo_bar::…crates/foo-bar/), including dependency { package = "…" } aliases; duplicate package names are treated as ambiguous and dropped rather than guessed, and crate matching is deterministic regardless of enumeration order.
  • Domain isolation: Rust names resolve in a Rust-only domain — separate global-name and method-owner indexes — so a Rust symbol never links to a same-named symbol from another language, in either direction.
  • Ambiguity posture: a type with several trait impls of the same method name (From<A>/From<B> both giving Frame.from; Display/Debug both giving fmt) drops the member call as ambiguous instead of guessing first-in-file. Drop-don't-guess throughout, matching map clusters one entry per file instead of per directory #35's resolution philosophy.

Evidence

  • Suite: 1244 passed / 0 failed (1 pre-existing skip; 1245 total) — zero regressions outside Rust, with the old breadth-tier .rs tests updated to the depth-tier contract rather than deleted where they guard build/check agreement.
  • 27 focused Rust tests (extraction, bindings, resolution, plus regression pins for crate-root anchoring, inline-mod calls, mod markers, cross-language isolation, CRLF, >32 KB chunk-boundary parsing, malformed-input tolerance, and double-build byte-determinism).
  • tsc --noEmit clean; git diff --check clean.
  • Grammar: tree-sitter-rust 0.24.0 with an npm overrides pin (tree-sitter: $tree-sitter), verified loading on the repo's tree-sitter runtime — 0.24.0 also parses &raw const/&raw mut natively, closing the parse gap 0.23 had.

Known limitations (deliberate; all fail toward dropped edges, never wrong ones)

  • Macro token-tree bodies are opaque — assert_eq!(compute(), 3) contributes no compute call edge (no expansion).
  • UFCS calls (<T as Tr>::method()) drop.
  • #[path = "…"] mod declarations drop (resolving conventionally would wire the wrong file).
  • Cross-file impl Trait for Type heritage is dropped (trait-default-method resolution needs the type defined in the same file).
  • impl blocks inside function bodies mint unowned functions.
  • Files nested deeper than one level under tests/ don't anchor crate:: (statically ambiguous — each test binary is its own crate).
  • pub use re-export chains and proc-macro expansion are unsupported.

Port of PR trailhq#59 rebaked onto the current breadth-tier architecture.

Extraction (extract.ts, bindings.ts): functions, structs, enums, traits
(interface), type aliases, unions, impl-owned methods with generic-strip
ownership, macro_rules! in its own `!` namespace, inline-module scope
segments, module and constant nodes. pub/pub(crate)/pub(super) and
#[macro_export] drive exports; #[cfg(test)] suppresses them through the
whole module subtree, and function-body nesting cannot launder a pub
into an export. Macro signatures stop before their token trees; scoped
calls carry the leaf name; use forms keep full per-leaf paths; inline
modules consume same-file self/super prefixes.

Resolution (resolve.ts): a module-tree resolver for crate::/self::/
super:: paths — x.rs and x/mod.rs layouts, the parser-file stem-dir
rule, bodyless mod imports, integration-test crate roots under tests/
benches/examples, and path-inferred roots when no Cargo.toml exists.
Cargo [package] names map workspace imports (hyphen/underscore-folded,
{ package = "..." } aliases honored); duplicates stay raw. Scoped calls
resolve only against the resolved file's functions, never a bare-name
fallback. Bare names resolve in a Rust-only domain, and duplicate
same-file trait impls drop as ambiguous while a single impl heritage
resolves trait default methods.

Routing: .rs leaves the breadth registry (GENERIC_LANGS) for the depth
tier, mirroring PHP's move in trailhq#157; grammar is tree-sitter-rust 0.24.0
with a $tree-sitter override pin, which also parses &raw const/mut.
@trailhq-graft

trailhq-graft Bot commented Sep 13, 2026

Copy link
Copy Markdown

🌱 graft blast radius

1 area changed → 8 areas can be affected. 33 dependent symbols, depth 2.
Tests: 1 area updated its tests.
Tag: @anirudhkumar-nanonets — 6 of 9 areas · @shhdwi — 7 of 9 areas · @Frankie-Xu — Build Context

flowchart TB
  A0(("Graph Freshness<br/>12 symbols"))
  A1(("Code Review Pipeline<br/>10 symbols"))
  A2(("CLI Entry Point<br/>5 symbols"))
  A3(("MCP Tool Integration<br/>2 symbols"))
  A4(("Viewer Build Script<br/>1 symbol"))
  AX(("3 smaller areas<br/>3 symbols"))
  classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
  class A0,A1,A2,A3,A4 reached;
  classDef tail fill:#EEF2F3,stroke:#9AA4A9,stroke-width:1px,color:#3A4247;
  class AX tail;
Loading
Can be affected Symbols Nearest hop Reached from
Graph Freshness 12 src/graph/check.ts:L58-L164 checkGraph — calls, depth 1 Graph Construction
Code Review Pipeline 10 src/app/brain-build.ts:L251-L358 readRepository — calls, depth 1 Graph Construction
CLI Entry Point 5 src/engine.ts:L91-L101 graph — calls, depth 1 Graph Construction
MCP Tool Integration 2 src/mcp/tools.ts:L216-L244 callTool — calls, depth 2 Graph Construction
Viewer Build Script 1 scripts/build-viewer.mjs:L1-L45 build-viewer.mjs — calls, depth 2 Graph Construction, test/graph-invariants.test.ts
Build Context 1 src/context/build.ts:L1-L468 build.ts — imports, depth 2 Graph Construction
2 smaller areas 2 LSP Graph Enrichment, Synchronous Execution see below
Who knows this code — 4 people across 9 areas
Area Who knows it
Graph Construction · changed @anirudhkumar-nanonets — 21 commits, last 19d ago · @shhdwi — 19 commits, last 30d ago
Graph Freshness · affected @anirudhkumar-nanonets — 16 commits, last 17d ago · @shhdwi — 9 commits, last 1mo ago
Code Review Pipeline · affected @anirudhkumar-nanonets — 12 commits, last 2d ago
CLI Entry Point · affected @anirudhkumar-nanonets — 41 commits, last 3d ago · @shhdwi — 24 commits, last 1mo ago
MCP Tool Integration · affected @shhdwi — 14 commits, last 1mo ago · @anirudhkumar-nanonets — 7 commits, last 11d ago
Viewer Build Script · affected @shhdwi — 2 commits, last 1mo ago
Build Context · affected @anirudhkumar-nanonets — 4 commits, last 24d ago · @Frankie-Xu — 2 commits, last 13d ago
LSP Graph Enrichment · affected @shhdwi — 2 commits, last 1mo ago
…1 further area

Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no @ has no GitHub handle in its commit email — tag them by hand, or add a .mailmap entry. A suggestion from history, not a CODEOWNERS rule.

All 33 dependent symbols, grouped by area

Graph Freshness — 12 symbols in 7 files

  • src/graph/check.ts:L58-L164 — checkGraph (calls, depth 1)
    95: // check` report every `.vue` node as `removed` right after a clean build (#236)
  • src/graph/check.ts:L1-L221 — check.ts (imports, depth 1)
    5: * Tier-1 extraction and diffs the fresh node set against the committed graph by
  • src/graph/container.ts:L151-L208 — extractContainer (calls, depth 1)
    153: const rawEdges: RawEdge[] = [];
  • src/graph/container.ts:L1-L209 — container.ts (imports, depth 1)
    8: * one opaque `raw_text` node, so the cards would come out empty.
  • src/graph/refresh.ts:L150-L227 — ensureFreshGraph (calls, depth 1)
    208: // `graphOnly`: write the graph, the ask sidecar and the fingerprint, nothing
  • src/graph/source-files.ts:L1-L121 — source-files.ts (imports, depth 1)
    9: import { statSync } from "node:fs";
  • src/graph/fingerprint.ts:L1-L197 — fingerprint.ts (imports, depth 2)
    5: * unchanged path: a walk + one `stat` per source file, no reads, no parsing.
  • src/graph/refresh.ts:L235-L261 — ensureFreshChildren (calls, depth 2)
  • src/graph/refresh.ts:L1-L274 — refresh.ts (imports, depth 2)
    34: import { existsSync } from "node:fs";
  • src/graph/workspace-cli.ts:L49-L70 — buildChild (calls, depth 2)
  • src/graph/workspace.ts:L630-L655 — federateCheck (calls, depth 2)
  • src/graph/workspace.ts:L1-L725 — workspace.ts (imports, depth 2)
    24: import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";

Code Review Pipeline — 10 symbols in 6 files

  • src/app/brain-build.ts:L251-L358 — readRepository (calls, depth 1)
    259: // pull-request walk, the digest — and without the elapsed time on each, the
  • src/app/review.ts:L45-L99 — reviewPullRequest (calls, depth 1)
    55: await buildGraph(checkout.dir);
  • src/app/brain-build-worker.ts:L1-L83 — brain-build-worker.ts (calls, depth 2)
    10: * pull-request walk — and none of it can reach the server's event loop.
  • src/app/brain-build-worker.ts:L29-L32 — DoneMessage (references, depth 2)
  • src/app/brain-build.ts:L237-L239 — buildRepoIntoBrain (calls, depth 2)
  • src/app/brain-build.ts:L1-L428 — brain-build.ts (imports, depth 2)
    14: import { buildGraph } from "../graph/build.js";
  • src/app/review-process.ts:L179-L183 — childReviewer (references, depth 2)
  • src/app/review-worker.ts:L67-L87 — run (calls, depth 2)
  • src/app/review.ts:L1-L174 — review.ts (imports, depth 2)
    14: import { buildGraph } from "../graph/build.js";
  • src/app/server.ts:L34-L46 — AppSeams (references, depth 2)

CLI Entry Point — 5 symbols in 2 files

  • src/engine.ts:L91-L101 — graph (calls, depth 1)
    92: return buildGraph(dir, {
  • src/cli.ts:L168-L178 — refreshBefore (calls, depth 2)
  • src/cli.ts:L1-L1407 — cli.ts (imports, depth 2)
    1: #!/usr/bin/env node
  • src/engine.ts:L82-L84 — checkGraph (calls, depth 2)
  • src/engine.ts:L1-L161 — engine.ts (imports, depth 2)
    21: import { buildGraph, type GraphBuildOptions, type GraphBuildResult } from "./graph/build.js";

MCP Tool Integration — 2 symbols in 1 file

  • src/mcp/tools.ts:L216-L244 — callTool (calls, depth 2)
  • src/mcp/tools.ts:L1-L330 — tools.ts (imports, depth 2)
    11: import { contextDirFor } from '../context/node-file.js';

Viewer Build Script — 1 symbol in 1 file

  • scripts/build-viewer.mjs:L1-L45 — build-viewer.mjs (calls, depth 2)
    7: import { mkdirSync, copyFileSync, readdirSync } from "node:fs";

Build Context — 1 symbol in 1 file

  • src/context/build.ts:L1-L468 — build.ts (imports, depth 2)
    7: * 3. Synthesize a CURATED node set from the labeled summaries (the synthesizer

LSP Graph Enrichment — 1 symbol in 1 file

  • src/graph/lsp/enrich.ts:L1-L141 — enrich.ts (imports, depth 1)
    5: * receiver typing at all). For each function/method node we ask the language

Synchronous Execution — 1 symbol in 1 file

  • src/claude/sync-run.ts:L19-L33 — runSync (calls, depth 2)
Test signal per changed area — 1 ✓

Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.

  • Graph Construction — 3 of 46 reached · 4 test files changed here: test/generic-extract.test.ts, test/graph-invariants.test.ts, test/graph-rust-resolve.test.ts, test/graph-rust.test.ts
    • not reached: defName, collectAliases, visit, handleRust, rustConstructorType, rustContextName, rustTypeName, readCargoCrates, …35 more
38 test suites also reference this code

49 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.

  • test/ask-index.test.ts
  • test/ask.test.ts
  • test/container-extract.test.ts
  • test/context-only-dir.test.ts
  • test/context.test.ts
  • test/covers.test.ts
  • test/generic-node24-probe.ts
  • test/graph-bindings.test.ts
  • test/graph-cross-language.test.ts
  • test/graph-enrich-pending.test.ts
  • test/graph-extract-dedup.test.ts
  • test/graph-go.test.ts
  • test/graph-incremental.test.ts
  • test/graph-java.test.ts
  • test/graph-languages.test.ts
  • test/graph-php.test.ts
  • test/graph-posix-paths.test.ts
  • test/graph-python.test.ts
  • test/graph-r-classes.test.ts
  • test/graph-r-phase3.test.ts
  • …18 more

⚠️ 3 changed files not in the graph (CHANGELOG.md, package-lock.json, package.json) — no parser claims the extension, or the index predates the file.

graft blast · origin/main...HEAD · depth 2 · 13 changed files

Open the interactive graph → — click an area to see its dependent symbols at file:line.

github-actions Bot added a commit that referenced this pull request Sep 13, 2026
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.

1 participant