diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml
new file mode 100644
index 0000000..665985f
--- /dev/null
+++ b/.github/workflows/pr-check.yml
@@ -0,0 +1,35 @@
+name: PR Checks
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, ready_for_review]
+ paths:
+ - 'parser-Rust/**'
+ - '.github/workflows/pr-check.yml'
+
+jobs:
+ rust-parser:
+ name: Rust Parser Checks
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: parser-Rust
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ components: rustfmt
+
+ - name: Cache cargo artifacts
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: parser-Rust
+
+ - name: Check formatting
+ run: cargo fmt -- --check
+
+ - name: Run tests
+ run: cargo test --all-targets
diff --git a/README.md b/README.md
index ff59850..f202ee8 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,9 @@ YASA-UAST is an intermediate representation structure for multi-language program
## parser-Python
- Python language parser for UAST, parsing Python code into UAST.
+## parser-Rust
+- Rust language parser for UAST, parsing Rust code into UAST.
+
## Join Us
Welcome to submit issues if you encounter any problems!
@@ -58,4 +61,4 @@ YASA - Making code analysis more precise, easier, and smarter.
## Contact Us
[Official Website](https://cybersec.antgroup.com/)
-
\ No newline at end of file
+
diff --git a/README_ZH.md b/README_ZH.md
index c2f199a..eb403e7 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -35,6 +35,9 @@ UAST(Unified Abstract Syntax Tree)是一种面向多语言程序分析的中
## parser-Python
- UAST Python 语言的解析器,将 Python 语言代码解析为 UAST
+## parser-Rust
+- UAST Rust 语言解析器,将 Rust 语言代码解析为 UAST
+
# 加入我们
遇到问题欢迎提交issue!
diff --git a/parser-Rust/Cargo.lock b/parser-Rust/Cargo.lock
new file mode 100644
index 0000000..378c916
--- /dev/null
+++ b/parser-Rust/Cargo.lock
@@ -0,0 +1,506 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "getrandom"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.182"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
+dependencies = [
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "uast4rust"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "syn",
+ "tempfile",
+ "toml",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "wasip2"
+version = "1.0.2+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/parser-Rust/Cargo.toml b/parser-Rust/Cargo.toml
new file mode 100644
index 0000000..bc33f51
--- /dev/null
+++ b/parser-Rust/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "uast4rust"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+syn = { version = "2.0", features = ["full"] }
+toml = "0.8"
+
+[dev-dependencies]
+tempfile = "3.10"
diff --git a/parser-Rust/README.md b/parser-Rust/README.md
new file mode 100644
index 0000000..cd52ce5
--- /dev/null
+++ b/parser-Rust/README.md
@@ -0,0 +1,60 @@
+# uast4rust
+
+`uast4rust` is the Rust parser binary for YASA-UAST.
+
+## Build
+
+```bash
+cargo build --release
+```
+
+## Usage
+
+Single-file mode:
+
+```bash
+./target/release/uast4rust -rootDir /path/to/file.rs -output /path/to/output.json -single
+```
+
+Project mode:
+
+```bash
+./target/release/uast4rust -rootDir /path/to/project -output /path/to/output.json
+```
+
+## Output Contract
+
+Top-level JSON fields:
+
+- `packageInfo`
+- `moduleName`
+- `cargoTomlPath`
+- `numOfCargoToml`
+
+`CompileUnit.language` is always `"rust"` for this parser.
+
+## Coverage (Current)
+
+Single-file lowering includes:
+
+- function and struct definitions
+- variable declarations and assignments (including compound assignments)
+- call/member expressions
+- control flow: `if`, `match`, `for`, `while`, `loop`, `break`, `continue`, `return`
+
+Project mode includes:
+
+- recursive Cargo manifest discovery
+- stable package tree output (`packageInfo.subs`)
+- hidden directory skipping during discovery
+
+## Limitations
+
+This parser currently focuses on parser-first coverage. Some Rust constructs are intentionally not lowered yet (for example macro-heavy forms and advanced type/trait semantics).
+
+## Validate
+
+```bash
+cargo fmt -- --check
+cargo test
+```
diff --git a/parser-Rust/src/lib.rs b/parser-Rust/src/lib.rs
new file mode 100644
index 0000000..2f9b199
--- /dev/null
+++ b/parser-Rust/src/lib.rs
@@ -0,0 +1,1373 @@
+mod model;
+
+pub use model::{CompileUnit, NodeInfo, Output, PackagePathInfo, LANGUAGE};
+use serde_json::{json, Value};
+use std::collections::BTreeMap;
+use std::env;
+use std::ffi::OsStr;
+use std::fmt;
+use std::fs;
+use std::path::{Path, PathBuf};
+use syn::{
+ Arm, BinOp, Block, Expr, ExprAssign, ExprBinary, ExprBlock, ExprBreak, ExprCall, ExprContinue,
+ ExprField, ExprForLoop, ExprIf, ExprLit, ExprLoop, ExprMatch, ExprMethodCall, ExprPath,
+ ExprRange, ExprReference, ExprReturn, ExprTuple, ExprUnary, ExprWhile, Field, Fields, FnArg,
+ Item, ItemFn, ItemStruct, Lit, Local, Pat, PatTuple, RangeLimits, ReturnType, Stmt, Type, UnOp,
+ Visibility,
+};
+
+const SINGLE_FILE_PACKAGE_NAME: &str = "__single__";
+const SINGLE_FILE_MODULE_NAME: &str = "__single_module__";
+const UNKNOWN_MODULE_NAME: &str = "__unknown_module__";
+const CARGO_TOML_FILE_NAME: &str = "Cargo.toml";
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct CliArgs {
+ pub root_dir: PathBuf,
+ pub output: PathBuf,
+ pub single: bool,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ParseArgsError {
+ HelpRequested,
+ Message(String),
+}
+
+impl fmt::Display for ParseArgsError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::HelpRequested => write!(f, "{}", usage()),
+ Self::Message(msg) => write!(f, "{msg}"),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum RunError {
+ InvalidInputPath {
+ mode: &'static str,
+ path: PathBuf,
+ },
+ OutputWouldOverwriteSource {
+ source: PathBuf,
+ output: PathBuf,
+ },
+ Io {
+ context: String,
+ source: std::io::Error,
+ },
+ Serialize {
+ source: serde_json::Error,
+ },
+ ParseSource {
+ path: PathBuf,
+ source: syn::Error,
+ },
+}
+
+impl fmt::Display for RunError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::InvalidInputPath { mode, path } => {
+ write!(f, "{} mode expects a valid path: {}", mode, path.display())
+ }
+ Self::OutputWouldOverwriteSource { source, output } => write!(
+ f,
+ "refusing to overwrite source file in single mode: source={} output={}",
+ source.display(),
+ output.display()
+ ),
+ Self::Io { context, source } => write!(f, "{context}: {source}"),
+ Self::Serialize { source } => write!(f, "failed to serialize output json: {source}"),
+ Self::ParseSource { path, source } => {
+ write!(
+ f,
+ "failed to parse rust source {}: {}",
+ path.display(),
+ source
+ )
+ }
+ }
+ }
+}
+
+impl std::error::Error for RunError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ Self::InvalidInputPath { .. } => None,
+ Self::OutputWouldOverwriteSource { .. } => None,
+ Self::Io { source, .. } => Some(source),
+ Self::Serialize { source } => Some(source),
+ Self::ParseSource { source, .. } => Some(source),
+ }
+ }
+}
+
+pub fn usage() -> &'static str {
+ "Usage: uast4rust -rootDir -output [-single]"
+}
+
+pub fn parse_args_from(args: I) -> Result
+where
+ I: IntoIterator- ,
+ S: Into,
+{
+ let mut root_dir: Option = None;
+ let mut output: Option = None;
+ let mut single = false;
+ let mut iter = args.into_iter().map(Into::into);
+
+ while let Some(arg) = iter.next() {
+ match arg.as_str() {
+ "-rootDir" | "--rootDir" => {
+ let value = next_arg_value(&mut iter, &arg)?;
+ root_dir = Some(PathBuf::from(value));
+ }
+ "-output" | "--output" => {
+ let value = next_arg_value(&mut iter, &arg)?;
+ output = Some(PathBuf::from(value));
+ }
+ "-single" | "--single" => {
+ single = true;
+ }
+ "-h" | "--help" => {
+ return Err(ParseArgsError::HelpRequested);
+ }
+ unknown => {
+ return Err(ParseArgsError::Message(format!(
+ "Unknown argument: {unknown}"
+ )));
+ }
+ }
+ }
+
+ let root_dir = root_dir.ok_or_else(|| {
+ ParseArgsError::Message(format!("Missing required argument: -rootDir\n{}", usage()))
+ })?;
+ let output = output.ok_or_else(|| {
+ ParseArgsError::Message(format!("Missing required argument: -output\n{}", usage()))
+ })?;
+
+ Ok(CliArgs {
+ root_dir,
+ output,
+ single,
+ })
+}
+
+fn next_arg_value(iter: &mut I, flag: &str) -> Result
+where
+ I: Iterator
- ,
+{
+ let value = iter
+ .next()
+ .ok_or_else(|| ParseArgsError::Message(format!("Missing value for '{}'", flag)))?;
+ if is_known_flag(&value) {
+ return Err(ParseArgsError::Message(format!(
+ "Missing value for '{}'",
+ flag
+ )));
+ }
+ Ok(value)
+}
+
+fn is_known_flag(value: &str) -> bool {
+ matches!(
+ value,
+ "-rootDir"
+ | "--rootDir"
+ | "-output"
+ | "--output"
+ | "-single"
+ | "--single"
+ | "-h"
+ | "--help"
+ )
+}
+
+pub fn parse_args() -> Result {
+ parse_args_from(env::args().skip(1))
+}
+
+pub fn run(cli: &CliArgs) -> Result<(), RunError> {
+ if cli.single {
+ parse_single_file(&cli.root_dir, &cli.output)
+ } else {
+ parse_project(&cli.root_dir, &cli.output)
+ }
+}
+
+fn parse_single_file(file: &Path, output: &Path) -> Result<(), RunError> {
+ if !file.is_file() {
+ return Err(RunError::InvalidInputPath {
+ mode: "single",
+ path: file.to_path_buf(),
+ });
+ }
+
+ let source = fs::read_to_string(file).map_err(|source| RunError::Io {
+ context: format!("failed to read file {}", file.display()),
+ source,
+ })?;
+
+ let source_canonical = fs::canonicalize(file).map_err(|source| RunError::Io {
+ context: format!("failed to canonicalize source file {}", file.display()),
+ source,
+ })?;
+ let output_canonical = fs::canonicalize(output).unwrap_or_else(|_| output.to_path_buf());
+ if source_canonical == output_canonical {
+ return Err(RunError::OutputWouldOverwriteSource {
+ source: source_canonical,
+ output: output.to_path_buf(),
+ });
+ }
+
+ let file_key = file.to_string_lossy().to_string();
+ let parsed_file = syn::parse_file(&source).map_err(|source| RunError::ParseSource {
+ path: file.to_path_buf(),
+ source,
+ })?;
+ let compile_unit = lower_single_file_uast(&parsed_file, file_key.clone());
+ let mut files = BTreeMap::new();
+ files.insert(
+ file_key.clone(),
+ NodeInfo {
+ node: compile_unit,
+ package_name: SINGLE_FILE_PACKAGE_NAME.to_string(),
+ },
+ );
+
+ let output_model = Output {
+ package_info: PackagePathInfo {
+ path_name: "/".to_string(),
+ files,
+ subs: BTreeMap::new(),
+ },
+ module_name: SINGLE_FILE_MODULE_NAME.to_string(),
+ cargo_toml_path: String::new(),
+ num_of_cargo_toml: 0,
+ };
+
+ write_output(output, &output_model)
+}
+
+fn parse_project(root_dir: &Path, output: &Path) -> Result<(), RunError> {
+ if !root_dir.is_dir() {
+ return Err(RunError::InvalidInputPath {
+ mode: "project",
+ path: root_dir.to_path_buf(),
+ });
+ }
+
+ let mut manifests = discover_cargo_toml(root_dir)?;
+ manifests.sort_by(|a, b| a.rel_path.cmp(&b.rel_path));
+
+ let module_name = manifests
+ .iter()
+ .find_map(|m| m.package_name.clone())
+ .unwrap_or_else(|| UNKNOWN_MODULE_NAME.to_string());
+ let cargo_toml_path = manifests
+ .first()
+ .map(|m| m.rel_path.clone())
+ .unwrap_or_default();
+ let num_of_cargo_toml = manifests.len();
+ let package_info = build_package_info(&manifests);
+
+ let output_model = Output {
+ package_info,
+ module_name,
+ cargo_toml_path,
+ num_of_cargo_toml,
+ };
+
+ write_output(output, &output_model)
+}
+
+fn lower_single_file_uast(file: &syn::File, uri: String) -> CompileUnit {
+ let mut unit = CompileUnit::empty(uri);
+ unit.body = file.items.iter().filter_map(lower_item).collect();
+ unit
+}
+
+fn lower_item(item: &Item) -> Option {
+ match item {
+ Item::Fn(item_fn) => Some(lower_function(item_fn)),
+ Item::Struct(item_struct) => Some(lower_struct(item_struct)),
+ _ => None,
+ }
+}
+
+fn lower_function(item_fn: &ItemFn) -> Value {
+ let parameters = item_fn
+ .sig
+ .inputs
+ .iter()
+ .filter_map(lower_fn_arg)
+ .collect::>();
+ let return_type = lower_return_type(&item_fn.sig.output);
+ let body = lower_block(&item_fn.block);
+
+ json!({
+ "type": "FunctionDefinition",
+ "id": identifier(item_fn.sig.ident.to_string()),
+ "parameters": parameters,
+ "returnType": return_type,
+ "body": body,
+ "modifiers": function_modifiers(item_fn),
+ })
+}
+
+fn function_modifiers(item_fn: &ItemFn) -> Vec {
+ let mut modifiers = Vec::new();
+ if matches!(item_fn.vis, Visibility::Public(_)) {
+ modifiers.push("pub".to_string());
+ }
+ if item_fn.sig.constness.is_some() {
+ modifiers.push("const".to_string());
+ }
+ if item_fn.sig.asyncness.is_some() {
+ modifiers.push("async".to_string());
+ }
+ if item_fn.sig.unsafety.is_some() {
+ modifiers.push("unsafe".to_string());
+ }
+ modifiers
+}
+
+fn lower_struct(item_struct: &ItemStruct) -> Value {
+ let body = match &item_struct.fields {
+ Fields::Named(named) => named
+ .named
+ .iter()
+ .map(lower_struct_field)
+ .collect::>(),
+ Fields::Unnamed(unnamed) => unnamed
+ .unnamed
+ .iter()
+ .enumerate()
+ .map(|(idx, field)| lower_unnamed_struct_field(idx, field))
+ .collect::>(),
+ Fields::Unit => Vec::new(),
+ };
+
+ json!({
+ "type": "ClassDefinition",
+ "id": identifier(item_struct.ident.to_string()),
+ "body": body,
+ "supers": [],
+ })
+}
+
+fn lower_struct_field(field: &Field) -> Value {
+ let name = field
+ .ident
+ .as_ref()
+ .expect("fields in a named struct should have an identifier")
+ .to_string();
+ variable_declaration(identifier(name), None, lower_type(&field.ty), false, false)
+}
+
+fn lower_unnamed_struct_field(index: usize, field: &Field) -> Value {
+ variable_declaration(
+ identifier(format!("_{index}")),
+ None,
+ lower_type(&field.ty),
+ false,
+ false,
+ )
+}
+
+fn lower_fn_arg(arg: &FnArg) -> Option {
+ match arg {
+ FnArg::Typed(pat_ty) => {
+ let name = extract_binding_name(&pat_ty.pat)?;
+ Some(variable_declaration(
+ identifier(name),
+ None,
+ lower_type(&pat_ty.ty),
+ false,
+ false,
+ ))
+ }
+ FnArg::Receiver(_) => None,
+ }
+}
+
+fn lower_stmt(stmt: &Stmt) -> Option {
+ match stmt {
+ Stmt::Local(local) => lower_local(local),
+ Stmt::Item(item) => lower_item(item),
+ Stmt::Expr(expr, _) => lower_expr(expr),
+ Stmt::Macro(_) => None,
+ }
+}
+
+fn lower_local(local: &Local) -> Option {
+ match &local.pat {
+ Pat::Tuple(tuple_pat) => lower_tuple_pattern_local(local, tuple_pat),
+ _ => lower_single_pattern_local(local),
+ }
+}
+
+fn lower_single_pattern_local(local: &Local) -> Option {
+ let (name, explicit_type) = extract_binding_name_and_type(&local.pat)?;
+ let init = local
+ .init
+ .as_ref()
+ .and_then(|local_init| lower_expr(&local_init.expr));
+ if name == "_" {
+ return init;
+ }
+ let var_type = explicit_type.map(lower_type).unwrap_or_else(dynamic_type);
+ let cloned = init.is_some();
+ Some(variable_declaration(
+ identifier(name),
+ init,
+ var_type,
+ cloned,
+ false,
+ ))
+}
+
+fn lower_tuple_pattern_local(local: &Local, tuple_pat: &PatTuple) -> Option {
+ let tuple_init_elements =
+ local
+ .init
+ .as_ref()
+ .and_then(|local_init| match local_init.expr.as_ref() {
+ Expr::Tuple(expr_tuple) => Some(&expr_tuple.elems),
+ _ => None,
+ });
+ let tuple_init_expr = local
+ .init
+ .as_ref()
+ .and_then(|local_init| lower_expr(&local_init.expr));
+
+ let mut declarations = Vec::new();
+ for (idx, pat) in tuple_pat.elems.iter().enumerate() {
+ let Some((name, explicit_type)) = extract_binding_name_and_type(pat) else {
+ continue;
+ };
+ if name == "_" {
+ continue;
+ }
+ let init = tuple_init_elements
+ .as_ref()
+ .and_then(|elems| elems.iter().nth(idx))
+ .and_then(lower_expr)
+ .or_else(|| {
+ tuple_init_expr
+ .as_ref()
+ .map(|expr| member_access(expr.clone(), literal_number(idx as i64), true))
+ });
+ let var_type = explicit_type.map(lower_type).unwrap_or_else(dynamic_type);
+ let cloned = init.is_some();
+ declarations.push(variable_declaration(
+ identifier(name),
+ init,
+ var_type,
+ cloned,
+ false,
+ ));
+ }
+
+ if declarations.is_empty() {
+ return local
+ .init
+ .as_ref()
+ .and_then(|local_init| lower_expr(&local_init.expr));
+ }
+ if declarations.len() == 1 {
+ return declarations.into_iter().next();
+ }
+ Some(sequence(declarations))
+}
+
+fn lower_expr(expr: &Expr) -> Option {
+ match expr {
+ Expr::Path(expr_path) => Some(lower_path(expr_path)),
+ Expr::Call(expr_call) => lower_call(expr_call),
+ Expr::MethodCall(expr_method_call) => lower_method_call(expr_method_call),
+ Expr::Field(expr_field) => lower_field_access(expr_field),
+ Expr::Assign(expr_assign) => lower_assignment(expr_assign),
+ Expr::Binary(expr_binary) => lower_binary(expr_binary),
+ Expr::Unary(expr_unary) => lower_unary(expr_unary),
+ Expr::Reference(expr_reference) => lower_reference(expr_reference),
+ Expr::Tuple(expr_tuple) => Some(lower_tuple(expr_tuple)),
+ Expr::Range(expr_range) => lower_range_expr(expr_range),
+ Expr::If(expr_if) => lower_if(expr_if),
+ Expr::Match(expr_match) => lower_match(expr_match),
+ Expr::ForLoop(expr_for_loop) => lower_for_loop(expr_for_loop),
+ Expr::While(expr_while) => lower_while(expr_while),
+ Expr::Loop(expr_loop) => lower_loop(expr_loop),
+ Expr::Break(expr_break) => Some(lower_break(expr_break)),
+ Expr::Continue(expr_continue) => Some(lower_continue(expr_continue)),
+ Expr::Block(expr_block) => Some(lower_block(&expr_block.block)),
+ Expr::Return(expr_return) => Some(lower_return(expr_return)),
+ Expr::Lit(expr_lit) => Some(lower_literal(expr_lit)),
+ Expr::Paren(expr_paren) => lower_expr(&expr_paren.expr),
+ Expr::Group(expr_group) => lower_expr(&expr_group.expr),
+ _ => None,
+ }
+}
+
+fn lower_path(expr_path: &ExprPath) -> Value {
+ lower_syn_path(&expr_path.path)
+}
+
+fn lower_syn_path(path: &syn::Path) -> Value {
+ let mut segments = path.segments.iter();
+ let first = segments
+ .next()
+ .expect("path should have at least one segment");
+
+ let mut value = identifier(first.ident.to_string());
+ for segment in segments {
+ value = member_access(value, identifier(segment.ident.to_string()), false);
+ }
+ value
+}
+
+fn lower_call(expr_call: &ExprCall) -> Option {
+ let callee = lower_expr(&expr_call.func)?;
+ let arguments = expr_call
+ .args
+ .iter()
+ .map(|arg| lower_expr(arg).unwrap_or(Value::Null))
+ .collect::>();
+ Some(json!({
+ "type": "CallExpression",
+ "callee": callee,
+ "arguments": arguments,
+ }))
+}
+
+fn lower_method_call(expr_method_call: &ExprMethodCall) -> Option {
+ let object = lower_expr(&expr_method_call.receiver)?;
+ let callee = member_access(
+ object,
+ identifier(expr_method_call.method.to_string()),
+ false,
+ );
+ let arguments = expr_method_call
+ .args
+ .iter()
+ .map(|arg| lower_expr(arg).unwrap_or(Value::Null))
+ .collect::>();
+ Some(json!({
+ "type": "CallExpression",
+ "callee": callee,
+ "arguments": arguments,
+ }))
+}
+
+fn lower_field_access(expr_field: &ExprField) -> Option {
+ let object = lower_expr(&expr_field.base)?;
+ Some(match &expr_field.member {
+ syn::Member::Named(ident) => member_access(object, identifier(ident.to_string()), false),
+ syn::Member::Unnamed(index) => {
+ member_access(object, literal_number(index.index as i64), true)
+ }
+ })
+}
+
+fn lower_assignment(expr_assign: &ExprAssign) -> Option {
+ let left = lower_expr(&expr_assign.left)?;
+ let right = lower_expr(&expr_assign.right)?;
+ Some(json!({
+ "type": "AssignmentExpression",
+ "left": left,
+ "right": right,
+ "operator": "=",
+ "cloned": true,
+ }))
+}
+
+fn lower_binary(expr_binary: &ExprBinary) -> Option {
+ let left = lower_expr(&expr_binary.left)?;
+ let right = lower_expr(&expr_binary.right)?;
+ match &expr_binary.op {
+ BinOp::Add(_) => Some(binary_expression("+", left, right)),
+ BinOp::Sub(_) => Some(binary_expression("-", left, right)),
+ BinOp::Mul(_) => Some(binary_expression("*", left, right)),
+ BinOp::Div(_) => Some(binary_expression("/", left, right)),
+ BinOp::Rem(_) => Some(binary_expression("%", left, right)),
+ BinOp::And(_) => Some(binary_expression("&&", left, right)),
+ BinOp::Or(_) => Some(binary_expression("||", left, right)),
+ BinOp::BitXor(_) => Some(binary_expression("^", left, right)),
+ BinOp::BitAnd(_) => Some(binary_expression("&", left, right)),
+ BinOp::BitOr(_) => Some(binary_expression("|", left, right)),
+ BinOp::Shl(_) => Some(binary_expression("<<", left, right)),
+ BinOp::Shr(_) => Some(binary_expression(">>", left, right)),
+ BinOp::Eq(_) => Some(binary_expression("==", left, right)),
+ BinOp::Lt(_) => Some(binary_expression("<", left, right)),
+ BinOp::Le(_) => Some(binary_expression("<=", left, right)),
+ BinOp::Ne(_) => Some(binary_expression("!=", left, right)),
+ BinOp::Ge(_) => Some(binary_expression(">=", left, right)),
+ BinOp::Gt(_) => Some(binary_expression(">", left, right)),
+ BinOp::AddAssign(_) => Some(assignment_expression("+=", left, right)),
+ BinOp::SubAssign(_) => Some(assignment_expression("-=", left, right)),
+ BinOp::MulAssign(_) => Some(assignment_expression("*=", left, right)),
+ BinOp::DivAssign(_) => Some(assignment_expression("/=", left, right)),
+ BinOp::RemAssign(_) => Some(assignment_expression("%=", left, right)),
+ BinOp::BitXorAssign(_) => Some(assignment_expression("^=", left, right)),
+ BinOp::BitAndAssign(_) => Some(assignment_expression("&=", left, right)),
+ BinOp::BitOrAssign(_) => Some(assignment_expression("|=", left, right)),
+ BinOp::ShlAssign(_) => Some(assignment_expression("<<=", left, right)),
+ BinOp::ShrAssign(_) => Some(assignment_expression(">>=", left, right)),
+ _ => None,
+ }
+}
+
+fn lower_unary(expr_unary: &ExprUnary) -> Option {
+ let argument = lower_expr(&expr_unary.expr)?;
+ match &expr_unary.op {
+ UnOp::Neg(_) => Some(unary_expression("-", argument)),
+ UnOp::Not(_) => Some(unary_expression("!", argument)),
+ UnOp::Deref(_) => Some(json!({
+ "type": "DereferenceExpression",
+ "argument": argument,
+ })),
+ _ => None,
+ }
+}
+
+fn lower_reference(expr_reference: &ExprReference) -> Option {
+ let argument = lower_expr(&expr_reference.expr)?;
+ Some(json!({
+ "type": "ReferenceExpression",
+ "argument": argument,
+ }))
+}
+
+fn lower_tuple(expr_tuple: &ExprTuple) -> Value {
+ let elements = expr_tuple
+ .elems
+ .iter()
+ .filter_map(lower_expr)
+ .collect::>();
+ json!({
+ "type": "TupleExpression",
+ "elements": elements,
+ "modifiable": false,
+ })
+}
+
+fn lower_range_expr(expr_range: &ExprRange) -> Option {
+ let start = expr_range
+ .start
+ .as_ref()
+ .and_then(|expr| lower_expr(expr))
+ .unwrap_or_else(noop);
+ let end = expr_range
+ .end
+ .as_ref()
+ .and_then(|expr| lower_expr(expr))
+ .unwrap_or_else(noop);
+ let inclusive = matches!(expr_range.limits, RangeLimits::Closed(_));
+
+ Some(json!({
+ "type": "TupleExpression",
+ "elements": [start, end, literal_boolean(inclusive)],
+ "modifiable": false,
+ }))
+}
+
+fn lower_if(expr_if: &ExprIf) -> Option {
+ let test = lower_expr(&expr_if.cond)?;
+ let consequent = lower_block(&expr_if.then_branch);
+ let alternative = expr_if
+ .else_branch
+ .as_ref()
+ .and_then(|(_, else_expr)| lower_if_alternative(else_expr))
+ .unwrap_or(Value::Null);
+ Some(json!({
+ "type": "IfStatement",
+ "test": test,
+ "consequent": consequent,
+ "alternative": alternative,
+ }))
+}
+
+fn lower_if_alternative(else_expr: &Expr) -> Option {
+ match else_expr {
+ Expr::If(expr_if) => lower_if(expr_if),
+ Expr::Block(ExprBlock { block, .. }) => Some(lower_block(block)),
+ _ => lower_expr(else_expr),
+ }
+}
+
+fn lower_match(expr_match: &ExprMatch) -> Option {
+ let discriminant = lower_expr(&expr_match.expr)?;
+ if expr_match.arms.iter().any(|arm| arm.guard.is_some()) {
+ return lower_match_with_guards(&discriminant, &expr_match.arms);
+ }
+
+ let mut cases = Vec::new();
+ for arm in &expr_match.arms {
+ let body = lower_expr(arm.body.as_ref()).unwrap_or_else(empty_scoped_statement);
+ let tests = lower_match_arm_tests(&arm.pat);
+ for test in tests {
+ cases.push(case_clause(test, body.clone()));
+ }
+ }
+ Some(json!({
+ "type": "SwitchStatement",
+ "discriminant": discriminant,
+ "cases": cases,
+ }))
+}
+
+fn lower_match_with_guards(discriminant: &Value, arms: &[Arm]) -> Option {
+ let mut alternative = Value::Null;
+ for arm in arms.iter().rev() {
+ let consequent = lower_expr(arm.body.as_ref())
+ .map(ensure_scoped_statement)
+ .unwrap_or_else(empty_scoped_statement);
+ let condition = lower_match_arm_condition(
+ discriminant,
+ &arm.pat,
+ arm.guard.as_ref().map(|(_, guard)| guard.as_ref()),
+ )?;
+ alternative = match condition {
+ Some(test) => json!({
+ "type": "IfStatement",
+ "test": test,
+ "consequent": consequent,
+ "alternative": alternative,
+ }),
+ None => consequent,
+ };
+ }
+ if alternative.is_null() {
+ return Some(empty_scoped_statement());
+ }
+ Some(alternative)
+}
+
+fn lower_match_arm_condition(
+ discriminant: &Value,
+ pat: &Pat,
+ guard: Option<&Expr>,
+) -> Option