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(®ex).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}