Skip to content

Commit f8c6579

Browse files
authored
feat(rust): add Rust language extractor (#85)
* Add Rust AST extraction tests and Rust extractor detection tests - Implemented comprehensive tests for Rust AST extraction in `rust_ast_test.go`, covering various Rust constructs such as structs, enums, traits, type aliases, constants, and method calls. - Added tests for handling dependencies, including internal and external dependencies, and cross-crate workspace dependencies. - Created utility functions for writing temporary Rust repositories for testing purposes in `rust_test.go`. - Established detection tests for identifying Rust projects with and without `Cargo.toml`, including support for subdirectory monorepos. * feat: add Rust support to architecture and documentation * feat(rust): add support for struct literal instantiation tracking in AST extraction * feat(rust): implement Axum route extraction and add corresponding tests * feat(rust): enhance Rust extractor with Axum route detection and update cache version * feat(rust): update cache version to v112 and enhance AST extraction with macro argument handling * feat(rust): enhance Rust AST extraction with function reference tracking and macro argument handling * feat(rust): enhance AST extraction with scoped function references and sibling file submodule handling * feat(rust): update cache version to v114 and enhance AST extraction with Drop and Future method overrides * improve(rust): enchance Rust support to extractors and enhance AST instantiation tracking * feat(rust): enhance AST extraction with scoped variant handling and update cache version to v119 * feat(rust): update cache version to v113 * feat(rust): update cache version to v114 and enhance AST extraction for test functions and array literals
1 parent dd9900c commit f8c6579

24 files changed

Lines changed: 3638 additions & 10 deletions

File tree

ARCHITECTURE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ Repository
114114
115115
116116
┌──────────────────┐ Go · Java · Kotlin · JS/TS · Vue · Svelte · Python ·
117-
│ Extractors │ Swift · Ruby · C/C++ · PHP · OpenAPI (source → facts)
117+
│ Extractors │ Swift · Ruby · Rust · C/C++ · PHP · OpenAPI (source → facts)
118118
└──────────────────┘
119119
120120
@@ -482,6 +482,7 @@ extractors:
482482
- typescript
483483
- swift
484484
- ruby
485+
- rust
485486
explainers:
486487
- cycles
487488
- layers
@@ -506,7 +507,7 @@ The bundled [`mcp-arch.yaml`](mcp-arch.yaml) ships a much fuller `ignore` list (
506507
|-------|-------------|---------|
507508
| `repo` | Repository root path | `"."` |
508509
| `ignore` | Glob patterns for files/dirs to skip | vendor, node_modules, .git, tests, build dirs, minified JS (`*.min.js`/`*.bundle.js`), docs, config data, … |
509-
| `extractors` | Enabled extractors | `["cpp", "go", "grpc", "java", "kotlin", "openapi", "php", "python", "typescript", "swift", "ruby"]` |
510+
| `extractors` | Enabled extractors | `["cpp", "go", "grpc", "java", "kotlin", "openapi", "php", "python", "typescript", "swift", "ruby", "rust"]` |
510511
| `explainers` | Enabled explainers | `["cycles", "layers", "crossrepo", "coverage", "unused-routes", "god-class", "hotspots", "dependency-depth", "exported-surface", "complexity-outliers"]` |
511512
| `renderers` | Enabled renderers | `["llm_context"]` |
512513
| `output.dir` | Output directory for artifacts | `".enola"` |
@@ -531,6 +532,7 @@ Each extractor is detected by characteristic project files and then parses what
531532
| Svelte | tree-sitter | `package.json` with `svelte` dependency, or `svelte.config.js/ts/mjs` / `@sveltejs/kit` for SvelteKit |
532533
| Swift | tree-sitter | `Package.swift`, `.xcodeproj`, or `.xcworkspace` present |
533534
| Ruby | tree-sitter | `Gemfile` present |
535+
| Rust | tree-sitter | `Cargo.toml` present (root or up to 3 levels deep) |
534536
| C/C++ | tree-sitter | a C source (`.c`) or C++ source (`.cpp`/`.cc`/`.cxx`/`.hpp`/...) present, or a build file (`CMakeLists.txt`/`Makefile`/`meson.build`/`*.vcxproj`) plus any header |
535537
| PHP | tree-sitter | `composer.json`, a WordPress bootstrap file (`wp-load.php`/`wp-settings.php`/`wp-config.php`), or any `.php` source within 3 directory levels |
536538
| OpenAPI | YAML/JSON scanner| any file containing `openapi:` or `swagger:` |
@@ -554,6 +556,8 @@ Each extractor is detected by characteristic project files and then parses what
554556

555557
**Swift** (tree-sitter) emits symbol facts for classes, structs, enums, protocols, and extensions plus their methods, initializers, and properties, named `<targetDir>.<Type>.<member>` — where `<targetDir>` is the file's resolved SPM/XcodeGen *target* module (parsed from `Package.swift` and `project.yml`), not its leaf directory. Members declared inside a type are classified `symbol_kind: method`; free functions stay `function`. It walks bodies for the call graph: same-type `self.`/`self?.` dispatch, member calls on any receiver (`coordinator?.show()`, `delegate?.tap()`), and cross-`extension` calls all become `calls` edges — emitted as bare short names at walk time (extraction is parallel-per-file) and bound in a serial post-pass against a project-wide method index (unique name → the qualified `dir.Type.method`, ambiguous → the bare name still matched by short name, unmatched → dropped so stdlib/framework calls don't create phantom edges). A further post-pass resolves **inherited-method calls** — a subclass or protocol conformer calling a base-class / protocol-extension method — by walking the caller type's supertype chain (from the `implements` edges) nearest-first and rewriting the otherwise-dangling call target to the declaring ancestor's method fact (`dir.DataModel.runRequest`), so class/protocol hierarchies are traversable for impact analysis, dead-code, and the performs_io closure. `Foo()` → `instantiates`, constructor/property DI → `injects`, SwiftUI `View`→`ViewModel` → `depends_on`, and custom-operator usage (`a <- b`, but not stdlib operators like `+`/`<=`) → a `calls` edge to the operator. Top-level calls in `#!/usr/bin/swift` scripts are captured via a file-scope reference fact. Like the other AST extractors, it walks function/method bodies — and also computed-property getters and `willSet`/`didSet` observers — for the standard complexity metrics `cyclomatic`, `loop_depth`, `loop_count`, `calls_in_loop`, and `recursive_self`, which the enterprise `analyze_performance` tool consumes; syntactic `for`/`while`/`repeat-while` and iterator closures (`map`/`forEach`/`filter`/…) count as loops, but **constant-bounded loops do not add scaling depth** — a literal integer range (`for i in 0..<10`), a literal-bound `stride(...)`, or an iterator over an array/dictionary literal or ALL-CAPS constant (`STOP_CHARS.forEach`) runs a fixed number of times, so it never inflates a genuine O(n) into a false O(n²)/O(n³). A method whose body invokes a network/file I/O primitive (`URLSession`/`dataTask`/`.data(for:)`, Alamofire `request`/`download`/`upload`, `Data(contentsOf:)`) is tagged `io_direct`; a serial post-pass then propagates that up the call graph into a transitive `performs_io` prop — crossing ambiguous kept-bare member-call edges by expanding them through the method-name index (bounded) rather than mutating the graph — so the enterprise `analyze_performance` tool can flag a per-iteration network N+1 (a loop calling a method that transitively hits the network) even when the I/O sits behind wrapper layers. It is **iOS-aware**: SwiftUI views (`View`/`App`/`Scene`), UIKit (`UIViewController`/`UIView` subclasses), Combine view models (`ObservableObject`, `@Observable`), architectural roles (Repositories, Use Cases, Coordinators, Services, DI containers), and `@MainActor`. *Limitation:* the vendored tree-sitter-swift grammar cannot parse a few advanced constructs — notably a tuple-type metatype `(A, B).self` (e.g. `withTaskGroup(of: (UUID, Result<T, Error>).self)`) — and its error recovery then flattens the whole enclosing type to file scope, so that file's type node is lost and its methods surface as top-level `function` symbols (~3% of files in a large iOS codebase). Dead-code detection stays accurate on these — a member call whose method was flattened falls back to resolving against the top-level function of that name (a rare same-name collision biases toward a missed lead, never a false accusation) — but the type's coupling/impact edges are degraded for the affected file until the construct is removed or the grammar gains support.
556558

559+
**Rust** (tree-sitter) targets a Cargo workspace or single crate. Every `Cargo.toml` in the repo is scanned up front (a minimal line-based `[package]`/`name` scan, not a full TOML parser) into a crate-name → crate-directory index, so cross-crate `use` paths resolve without a second parsing pass. It emits symbol facts for `fn`/`struct`/`enum`/`trait`/`type`/`const`/`static` items, qualifying nested `mod { }` blocks and `impl`/`trait` bodies into the name (`<dir>.<mod>.<Type>.<method>`); a function/method inside an `impl` or `trait` block is a `method`, everything else a `function`, and an impl-block method with no `self` parameter is tagged `static`. Because `impl Trait for Type` is a separate top-level item from `Type`'s own declaration — frequently in a different file — the resulting `implements` edge is attached to `Type`'s existing symbol fact by a small post-pass over the merged fact set (`applyImplements`) rather than emitted as a second, otherwise-empty fact that would double-count `Type` in symbol-kind stats. `use` declarations (including brace-expanded lists like `use std::{fmt, collections::HashMap}`) become `dependency` facts classified `internal`/`external`/`stdlib`: `self::`/`super::`/`crate::` paths and other in-workspace crate names resolve to the real submodule directory when one is known (trying progressively shorter suffixes against the known module-directory set, mirroring Python's import resolution) and fall back to the crate/module root otherwise; `std`/`core`/`alloc` are `stdlib`. Call resolution is deliberately conservative: a bare call resolves against a sibling method in the enclosing `impl`/`trait` block or a same-directory top-level function; `self.method()`/`Self::method()` resolve to the enclosing type's sibling method; any other receiver or path form (`recv.method()`, `Type::method()` for a different type) falls back to a bare short-name `calls` edge so dead-code matching still sees it used, without guessing a canonical target it can't verify. It also computes `cyclomatic` per function/method body (`if`, `match` arm, `while`/`for`/`loop`, the `?` operator, and `&&`/`||` each add one). Both construction forms emit `instantiates`: a tuple-call (`Foo()`) as part of ordinary call handling, and a named struct literal (`Foo { field: value }`, including functional-update syntax `Foo { field: value, ..base }`) via its own `struct_expression` handler, since it isn't a `call_expression` in the grammar. It also detects Axum routes: a `.route("/path", get(handler))` builder call (including chained verbs like `get(a).post(b)`) emits one `route` fact per (path, method) pair, matched structurally rather than gated on an `axum` dependency check. *Scope:* `.route_service(...)` and `.nest(...)` sub-router prefixes aren't handled (see [`axum.go`](internal/extractors/rustextractor/axum.go)), Actix/Rocket have no route awareness yet, and macros are treated as opaque (no expansion, no edges through them).
560+
557561
**OpenAPI** scans for spec files independently of the main walker (so it finds them even when `*.yaml`/`*.json` are globally ignored), confirming candidates by an `openapi:`/`swagger:` key. It emits one `route` per operation enriched with method, `operationId`, summary, tags, and a spec back-reference; specs under an `openapi/client/` directory are marked `role:"client"`. Gateway extensions (`x-gateway-config`, `x-gateway-capabilities`) are parsed into props.
558562

559563
**gRPC** models Protocol Buffers services the same way HTTP endpoints are modeled, so a gRPC surface answers the same cross-repo and unused-endpoint questions as a REST one. A small dependency-free proto3 scanner (comment-stripped, brace-depth aware — the same class of parser as OpenAPI's) reads each `.proto` and emits, for every `rpc`, a **server-role `route`** whose `Name` is the gRPC wire path `/pkg.Service/Method` (e.g. `/users.v1.UserService/GetUser`) with `method:"POST"`, `framework:"grpc"`, `source:"grpc-proto"`, `type:"grpc"`, and `rpc_service`/`rpc_method`/`streaming` props — the exact path+method a gRPC-web client hits over HTTP, so these flow through the cross-repo linker's normalized path+method matching and the `unused-routes` explainer with no linker special-casing. Each service also emits an `interface` symbol, each RPC a `method` symbol (`has_method`-linked to its service), and each message a `struct`/`enum` symbol, and proto `import`s become `dependency` facts — so proto participates in `traverse`, `find_path`, and `impact_analysis`. **Client-side detection** covers both TypeScript and Go. In the **TypeScript** extractor a repo-wide pre-pass resolves generated stubs — `@protobuf-ts` (`new ServiceType(...)`), connect-es (`typeName`), and **classic grpc-web** (where it derives the service and methods from the `MethodDescriptor`/`rpcCall` `/pkg.Service/Method` path literals) — into a service→method map, then per-file it binds `new XxxServiceClient(...)` variables (including typed constructor-injected fields) and emits a **client-role `route`** (`source:"ts-grpc-client"`) for each `client.method(...)` **call site** — only for methods actually called, so an RPC the frontend never invokes correctly surfaces as unmatched by clients. The **Go** extractor does the same for grpc-go consumers: because a Go call site (`client.GetUser(ctx, req)`) carries no wire path, a repo-wide pre-pass reads the authoritative `/pkg.Service/Method` from the *generated* code — the concrete client's `Invoke`/`NewStream` string literal (grpc-go, unary + streaming) or the `…Procedure` const (connect-go) — and builds a client-interface→method→path index. Per-file, it reuses the Go extractor's own receiver/field/local-variable type resolution (`resolveChain`) so a client is recognized whether it's a **local variable**, an **inline construction**, a **struct field** (`s.users.GetUser(...)` — dependency injection), or a **package-level var**, emitting a **client-role `route`** (`source:"go-grpc-client"`) per call site. Both **grpc-go** and **connect-go** consumers are covered. On the TypeScript side, **connect-es** consumers using `createClient(Service, transport)` / `createPromiseClient(...)` are detected alongside the `new XxxClient(...)` form. Cross-repo gRPC edges are tagged `via:"grpc"`. **Go handler binding:** a post-extraction pass connects each gRPC server route to the Go method that serves it via a `handled_by` edge (route → `pkg.Type.Method`) and a `handler` prop, so `impact_analysis`/`find_path` traverse from the RPC to its implementation (and, through the cross-repo edges, on to its clients). The bridge is the `protoc-gen-go-grpc` forward-compatibility convention — a server impl embeds `Unimplemented<Service>Server`, which the Go extractor already records as an `implements` edge, so the service short name matches the route's `rpc_service` with no new Go parsing; ambiguous or non-embedding impls are left unbound. *Scope:* client detection targets protoc-gen-go-grpc, connect-go, `@protobuf-ts`, connect-es, and classic grpc-web generated stubs; hand-rolled clients that bypass the generated stubs are not recognized (a namespaced commonjs grpc-web constructor, `new proto.pkg.XxxClient(...)`, binds only best-effort).

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ GitHub Copilot / VS Code (add to `.vscode/mcp.json`):
4646
4747
Done. Your agent now has a precise structural map of your code. For configuration options, multi-repo setup, and what to ask next, see [Quick start](#quick-start) below.
4848

49-
**Supported languages:** Go · JavaScript · TypeScript · Python · Java · Kotlin · Swift · Ruby · C · C++ · PHP · Vue · Svelte · OpenAPI · gRPC — with framework awareness (Next.js, Nuxt, SvelteKit, FastAPI, Django, Spring, Rails, Laravel, Symfony, SwiftUI, Jetpack Compose, WordPress, …)
49+
**Supported languages:** Go · JavaScript · TypeScript · Python · Java · Kotlin · Swift · Ruby · Rust · C · C++ · PHP · Vue · Svelte · OpenAPI · gRPC — with framework awareness (Next.js, Nuxt, SvelteKit, FastAPI, Django, Spring, Rails, Laravel, Symfony, SwiftUI, Jetpack Compose, WordPress, …)
5050

5151
---
5252

@@ -296,14 +296,15 @@ When you snapshot a *different* repo without `append`, enola assumes you're exte
296296
| Kotlin | `build.gradle(.kts)` with Kotlin/Android (Compose / Hilt / Room aware) |
297297
| Swift | `Package.swift`, `.xcodeproj`, `.xcworkspace` (SwiftUI / UIKit aware) |
298298
| Ruby | `Gemfile` (Rails / ActiveRecord / Packwerk aware) |
299+
| Rust | `Cargo.toml` (workspace or single crate; crate/module/`impl`/trait aware; Axum route DSL aware) |
299300
| C / C++ | `.c`/`.h` (tree-sitter-c) or `.cpp`/`.hpp`/… (tree-sitter-cpp), or `CMakeLists.txt`/`Makefile` + header (per-fact `language`, header/source method merging, namespaces, templates) |
300301
| PHP | `composer.json`, WordPress markers, or any `.php` source (WordPress / Laravel / Symfony route + outbound HTTP-client aware) |
301302
| OpenAPI | any spec with an `openapi:` / `swagger:` key |
302303
| gRPC | any `.proto` file (proto services → routes; TypeScript gRPC-web client calls detected) |
303304

304305
Framework- and platform-specific detection for each language is described in **[ARCHITECTURE.md → Supported languages](ARCHITECTURE.md#supported-languages)**.
305306

306-
> Python, Ruby, and PHP are parsed with tree-sitter and contribute call and dependency edges to the graph, so `traverse`, `find_path`, and `impact_analysis` reach into them — not just modules and routes.
307+
> Python, Ruby, PHP, and Rust are parsed with tree-sitter and contribute call and dependency edges to the graph, so `traverse`, `find_path`, and `impact_analysis` reach into them — not just modules and routes.
307308
308309
---
309310

examples/full.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# - typescript (detection: tsconfig.json or package.json with TypeScript)
1212
# - swift (detection: Package.swift, .xcodeproj, or .xcworkspace)
1313
# - ruby (detection: Gemfile)
14+
# - rust (detection: Cargo.toml)
1415
# - cpp (detection: .cpp/.hpp/... or CMakeLists.txt/Makefile + header)
1516
# - php (detection: composer.json, WordPress markers, or any .php source)
1617

@@ -104,6 +105,7 @@ extractors:
104105
- typescript
105106
- swift
106107
- ruby
108+
- rust
107109
- php
108110
explainers:
109111
- cycles

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/tree-sitter/tree-sitter-php v0.22.9-0.20240819002312-a552625b56c1
1414
github.com/tree-sitter/tree-sitter-python v0.23.6
1515
github.com/tree-sitter/tree-sitter-ruby v0.21.1-0.20240818211811-7dbc1e2d0e2d
16+
github.com/tree-sitter/tree-sitter-rust v0.23.3
1617
github.com/tree-sitter/tree-sitter-typescript v0.23.2
1718
gopkg.in/yaml.v3 v3.0.1
1819
)

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ github.com/tree-sitter/tree-sitter-python v0.23.6 h1:qHnWFR5WhtMQpxBZRwiaU5Hk/29
4848
github.com/tree-sitter/tree-sitter-python v0.23.6/go.mod h1:cpdthSy/Yoa28aJFBscFHlGiU+cnSiSh1kuDVtI8YeM=
4949
github.com/tree-sitter/tree-sitter-ruby v0.21.1-0.20240818211811-7dbc1e2d0e2d h1:fcYCvoXdcP1uRQYXqJHRy6Hec+uKScQdKVtMwK9JeCI=
5050
github.com/tree-sitter/tree-sitter-ruby v0.21.1-0.20240818211811-7dbc1e2d0e2d/go.mod h1:T1nShQ4v5AJtozZ8YyAS4uzUtDAJj/iv4YfwXSbUHzg=
51-
github.com/tree-sitter/tree-sitter-rust v0.21.3-0.20240818005432-2b43eafe6447 h1:o9alBu1J/WjrcTKEthYtXmdkDc5OVXD+PqlvnEZ0Lzc=
52-
github.com/tree-sitter/tree-sitter-rust v0.21.3-0.20240818005432-2b43eafe6447/go.mod h1:1Oh95COkkTn6Ezp0vcMbvfhRP5gLeqqljR0BYnBzWvc=
51+
github.com/tree-sitter/tree-sitter-rust v0.23.3 h1:v41N2Dx5ZEzouqeQJ0IIH2cUwVw/iepwrUoE9D56IL4=
52+
github.com/tree-sitter/tree-sitter-rust v0.23.3/go.mod h1:hfeGWic9BAfgTrc7Xf6FaOAguCFJRo3RBbs7QJ6D7MI=
5353
github.com/tree-sitter/tree-sitter-typescript v0.23.2 h1:/Odvphn18PniVixb9e97X0DbNVsU6Qocv9mfkyzdXwU=
5454
github.com/tree-sitter/tree-sitter-typescript v0.23.2/go.mod h1:zjzMXT/Ulffel2xfOcAkQQkiAkmgnbtPGlFQw/5X4xA=
5555
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=

0 commit comments

Comments
 (0)