firedbg_rust_parser/
lib.rs

1//! ## FireDBG Source Parser for Rust
2//!
3//! Based on [`syn`](https://github.com/dtolnay/syn).
4//!
5//! `firedbg-rust-parser` is a Rust source code parser. It can parse a Rust source file, walk the abstract syntax tree of Rust, then produce a list of breakpoints for the debugger to pause the program at the beginning of every function call.
6//!
7//! ### Walking the AST
8//!
9//! We will walk the Rust AST, [`syn::Item`](https://docs.rs/syn/latest/syn/enum.Item.html), and collect all forms of function / method:
10//!
11//! 1. Free standalone function, [`syn::Item::Fn`](https://docs.rs/syn/latest/syn/enum.Item.html#variant.Fn)
12//! 2. Impl function, [`syn::Item::Impl`](https://docs.rs/syn/latest/syn/enum.Item.html#variant.Impl)
13//! 3. Trait default function, [`syn::Item::Trait`](https://docs.rs/syn/latest/syn/enum.Item.html#variant.Trait)
14//! 4. Impl trait function, [`syn::Item::Impl`](https://docs.rs/syn/latest/syn/enum.Item.html#variant.Impl)
15//! 5. Nested function, walking the [`syn::Item`](https://docs.rs/syn/latest/syn/enum.Item.html) recursively
16//! 6. Function defined inside inline module, [`syn::Item::Mod`](https://docs.rs/syn/latest/syn/enum.Item.html#variant.Mod)
17//!
18//! ### Breakable Span
19//!
20//! A span is a region of source code, denoted by a ranged line and column number tuple, along with macro expansion information.
21//! It allows the debugger to set a breakpoint at the correct location. The debugger will set the breakpoint either at the start or the end of the breakable span.
22//!
23//! ```ignore
24//! fn func() -> i32 {
25//! /*                ^-- Start of Breakable Span: (Line 1, Column 19)  */
26//!     let mut i = 0;
27//! /*  ^-- End of Breakable Span: (Line 3, Column 5)  */
28//!     for _ in (1..10) {
29//!         i += 1;
30//!     }
31//!     i
32//! }
33//! ```
34//!
35//! ### Ideas
36//!
37//! The current implementation is rudimentary, but we get exactly what we need. We considered embedding Rust Analyzer, for a few advantages: 1) to get the fully-qualified type names
38//! 2) to traverse the static call graph. The problem is resource usage: we'd end up running the compiler frontend thrice (by cargo, by language server, by firedbg).
39#![cfg_attr(docsrs, feature(doc_cfg))]
40#![deny(
41    missing_debug_implementations,
42    clippy::missing_panics_doc,
43    clippy::unwrap_used,
44    clippy::print_stderr,
45    clippy::print_stdout
46)]
47
48pub mod def;
49pub use def::*;
50
51mod parsing;
52use parsing::*;
53
54pub mod serde;
55
56use anyhow::{Context, Result};
57use glob::glob;
58use std::{
59    io::Read,
60    path::Path,
61    process::{Command, Stdio},
62    time::SystemTime,
63};
64
65pub fn parse_file<T>(path: T) -> Result<Vec<FunctionDef>>
66where
67    T: AsRef<Path>,
68{
69    let path = path.as_ref();
70    let mut file = std::fs::File::open(path)
71        .with_context(|| format!("Fail to open file: `{}`", path.display()))?;
72    let mut source_code = String::new();
73    file.read_to_string(&mut source_code)
74        .with_context(|| format!("Fail to read file: `{}`", path.display()))?;
75    let res = syn::parse_file(&source_code)
76        .with_context(|| format!("Fail to parse file: `{}`", path.display()))?
77        .items
78        .into_iter()
79        .fold(Vec::new(), |mut acc, item| {
80            acc.extend(item.parse());
81            acc
82        });
83    Ok(res)
84}
85
86pub fn parse_directory<T>(directory: T) -> Result<Vec<File>>
87where
88    T: Into<String>,
89{
90    let regex = format!("{}/**/*.rs", directory.into()).replace("//", "/");
91    let mut res = Vec::new();
92    let context = || format!("Invalid glob regex: `{regex}`");
93    for path in glob(&regex).with_context(context)?.filter_map(Result::ok) {
94        let modified = path_created(&path);
95        let file_path = path_to_str(&path).into();
96        let functions = parse_file(&path)
97            .with_context(|| format!("Fail to parse file: `{}`", path.display()))?;
98        res.push(File {
99            path: file_path,
100            functions,
101            crate_name: "".into(),
102            modified,
103        });
104    }
105    Ok(res)
106}
107
108pub fn parse_workspace<T>(directory: T) -> Result<Workspace>
109where
110    T: Into<String>,
111{
112    let dir = directory.into();
113    let dir = dir.trim_end_matches('/');
114
115    let res = Command::new("cargo")
116        .current_dir(dir)
117        .arg("metadata")
118        .arg("--format-version=1")
119        .stderr(Stdio::inherit())
120        .output()?;
121
122    // println!("{:#?}", res);
123
124    if !res.status.success() {
125        panic!("Fail to parse workspace metadata");
126    }
127
128    let workspace_raw =
129        serde_json::from_slice(&res.stdout).context("Fail to deserialize JSON string")?;
130
131    // println!("{:#?}", workspace_raw);
132
133    Ok(parsing::parse_workspace(workspace_raw))
134}
135
136fn path_to_str(path: &Path) -> &str {
137    path.to_str().expect("Failed to convert Path to &str")
138}
139
140fn path_created(path: &Path) -> SystemTime {
141    path.metadata()
142        .expect("No metadata")
143        .created()
144        .expect("No created time")
145}