NxtSoftLabs
← All writing

Rust code graphs: CGraph adds its 13th language

August 18, 2026·4 min read

CGraph now extracts Rust. Point it at a .rs tree and you get function, struct, trait, and enum nodes plus CALLS edges — including self.method() member calls resolved to their definitions — in the same deterministic fragment shape as every other language. Rust becomes the thirteenth language CGraph parses with a vendored tree-sitter grammar, joining C, C++, C#, Go, Groovy, Java, JavaScript, TypeScript, Kotlin, Python, Ruby, and Scala.

Rust was the most-requested gap: it's a major language for exactly the coding agents CGraph serves. It also turned out to be a well-scoped add, because it fits the grammar-driven extractor path with no hand-written C++ walker — with one exception worth explaining, and a set of v1 non-goals we'd rather state than hide.

What a Rust file becomes

Take a small program — a struct Engine, a trait Runnable, an impl block whose run calls self.tick(), and a main that builds and runs the engine. Run the extractor:

cgraph --root path/to/rust-project --out out

On that fixture CGraph produces 8 nodes and 10 edges: the file, Engine, Runnable, run, tick, build_engine, and main, wired with contains edges — and three CALLS edges: main → build_engine, main → run, and the member call run → tick, resolved same-file through the field_expression handling. The struct literal Engine { ticks: 0 } correctly produces no call edge; construction isn't a call.

Concretely, the Rust config captures:

  • Functionsfunction_item and function_signature_item (trait method signatures).
  • Typesstruct_item, enum_item, union_item, trait_item, and type_item.
  • Callscall_expression, plus x.method() member calls via field_expression.

The grammar itself is tree-sitter-rust v0.24.2, pinned and vendored like the other twelve — including Rust's external scanner.c, which the grammar needs for raw strings. As with every tree-sitter language CGraph supports, vendoring means no Rust toolchain is required on the machine: you don't need rustc installed to graph a Rust repo, and the same tree yields the same graph every time.

The one Rust-specific piece: resolving Type::method() and turbofish

Twelve languages in, most of extraction is configuration, not code: name the grammar node kinds, and the shared pipeline does the rest. Rust needed one real addition — its own callee-name resolver.

The C++ resolver that other curly-brace languages reuse doesn't descend two shapes Rust puts at a call site: scoped_identifier (Type::method(), module::function()) and generic_function — the turbofish, foo::<T>(). Reusing the C++ resolver would have silently dropped those calls, and a call graph that quietly loses String::from or str::parse::<u64> call sites is worse than none. So rust_callee_name descends scoped_identifier to its trailing name, generic_function to the function under the generics, and field_expression to the method field — always landing on the leaf identifier that names the callee.

What v1 deliberately does not do

Per the blog's standing rule — only claim what's true today — here is what Rust v1 skips, and why, each recorded as a follow-up:

  • use imports. CGraph's import resolver matches /-delimited, path-like specs against project files and deletes any stub that resolves to no file. Rust use paths are ::-delimited and decoupled from file layout (mod.rs, re-exports), so every emitted stub would have been dropped — zero surviving edges. Rather than ship import edges that all silently vanish, v1 emits none; a real Rust module resolver is the top follow-up.
  • impl → type method attribution. impl blocks carry no name, so methods inside are captured as file-contained functions — exactly like Go's methods. Linking them to their type needs a relation handler; deferred.
  • Macros, modules as nodes, and closures as call scopes. All deferred rather than half-done.

This is the same trade the extractor makes everywhere: a deterministic graph of what the parser can see, with the gaps stated instead of papered over.

It flows into everything downstream

Because Rust lands in the standard fragment shape, everything built on the graph works unchanged: impact analysis over your Rust call edges, budgeted context for an agent reading a Rust crate, the MCP tools, the warm daemon with fold-in updates, and the exports. No downstream feature needed to learn what Rust is — that's the point of resolving everything into one graph model.

Try it

Run CGraph on a Rust checkout and open the result:

cgraph --root path/to/rust-project --out out
open out/graph.html

The extractor, the grammar wiring, and the rust_callee_name resolver are all open source — read the code on GitHub. For how the rest of the pipeline turns parsed files into a resolved graph, start with how CGraph extracts a code graph across 11 languages — a title that, as of this post, undercounts by two.