1//! Analyze the crate
2use anyhow::{Context, Result};
3use serde::{Deserialize, Serialize};
4
5use crate::data_model::{Crate, Enum, Function, Module, Struct};
6
7pub fn analyze_crate(path: &str) -> Result<AnalysisResult> {
8 // make the path absolute
9 // TODO we use dunce to canonicalize the path because otherwise there is issues with python's os.path.relpath on windows, but maybe we should fix this on the Python side
10 let path =
11 dunce::canonicalize(path).context(format!("Error resolving crate path: {}", path))?;
12 // check the path is a directory
13 if !path.is_dir() {
14 return Err(anyhow::anyhow!(format!(
15 "Crate path is not a directory: {}",
16 path.to_string_lossy()
17 )));
18 }
19 // check if Cargo.toml exists
20 let cargo_toml_path = path.join("Cargo.toml");
21 if !cargo_toml_path.exists() {
22 return Err(anyhow::anyhow!(format!(
23 "Cargo.toml does not exist in: {}",
24 path.to_string_lossy()
25 )));
26 }
27
28 // read the Cargo.toml and initialize the Crate struct
29 let contents = std::fs::read_to_string(&cargo_toml_path)?;
30 let cargo_toml: CargoToml = toml::from_str(&contents).context(format!(
31 "Error parsing: {}",
32 cargo_toml_path.to_string_lossy()
33 ))?;
34
35 // check whether the crate is a library or binary
36 let (crate_name, to_root) = if let Some(lib) = cargo_toml.lib {
37 if cargo_toml.bin.is_some() {
38 return Err(anyhow::anyhow!(format!(
39 "Both lib and bin sections in: {}",
40 path.to_string_lossy()
41 )));
42 }
43 (
44 lib.name.unwrap_or(cargo_toml.package.name),
45 lib.path.unwrap_or("src/lib.rs".to_string()),
46 )
47 } else if let Some(bin) = cargo_toml.bin {
48 (
49 bin.name.unwrap_or(cargo_toml.package.name),
50 bin.path.unwrap_or("src/main.rs".to_string()),
51 )
52 } else {
53 return Err(anyhow::anyhow!(format!(
54 "No lib or bin section in: {}",
55 path.to_string_lossy()
56 )));
57 };
58
59 let mut result = AnalysisResult::new(Crate {
60 name: crate_name,
61 version: cargo_toml.package.version.clone(),
62 });
63
64 // check existence of the root module
65 let root_module = path.join(to_root);
66 if !root_module.exists() {
67 return Ok(result);
68 }
69
70 // read the top-level module
71 let content = std::fs::read_to_string(&root_module)?;
72 let (module, structs, enums, functions) =
73 Module::parse(Some(&root_module), &[&result.crate_.name], &content).context(format!(
74 "Error parsing module {}",
75 root_module.to_string_lossy()
76 ))?;
77 let mut modules_to_read = module
78 .declarations
79 .iter()
80 .map(|s| {
81 (
82 root_module.parent().unwrap().to_path_buf(),
83 s.to_string(),
84 vec![result.crate_.name.clone()],
85 )
86 })
87 .collect::<Vec<_>>();
88
89 result.modules.push(module);
90 result.structs.extend(structs);
91 result.enums.extend(enums);
92 result.functions.extend(functions);
93
94 // recursively find/read the public sub-modules
95 let mut read_modules = vec![];
96 while let Some((parent_dir, module_name, parent)) = modules_to_read.pop() {
97 let (module_path, submodule_dir) =
98 if parent_dir.join(&module_name).with_extension("rs").exists() {
99 (
100 parent_dir.join(&module_name).with_extension("rs"),
101 parent_dir.join(&module_name),
102 )
103 } else if parent_dir.join(&module_name).join("mod.rs").exists() {
104 (
105 parent_dir.join(&module_name).join("mod.rs"),
106 parent_dir.to_path_buf(),
107 )
108 } else {
109 // TODO warn about missing module?
110 continue;
111 };
112
113 if read_modules.contains(&module_path) {
114 continue;
115 }
116 read_modules.push(module_path.clone());
117
118 let content = std::fs::read_to_string(&module_path)?;
119 let path: Vec<String> = [&parent[..], &[module_name]].concat();
120 let (module, structs, enums, functions) = Module::parse(
121 Some(&module_path),
122 &path.iter().map(|s| s.as_str()).collect::<Vec<&str>>(),
123 &content,
124 )
125 .context(format!(
126 "Error parsing module {}",
127 module_path.to_string_lossy()
128 ))?;
129 modules_to_read.extend(
130 module
131 .declarations
132 .iter()
133 .map(|s| (submodule_dir.clone(), s.to_string(), path.clone()))
134 .collect::<Vec<_>>(),
135 );
136 result.modules.push(module);
137 result.structs.extend(structs);
138 result.enums.extend(enums);
139 result.functions.extend(functions);
140 }
141
142 Ok(result)
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146/// Result from a crate analysis
147pub struct AnalysisResult {
148 pub crate_: Crate,
149 pub modules: Vec<Module>,
150 pub structs: Vec<Struct>,
151 pub enums: Vec<Enum>,
152 pub functions: Vec<Function>,
153}
154
155impl AnalysisResult {
156 pub fn new(crate_: Crate) -> Self {
157 Self {
158 crate_,
159 modules: vec![],
160 structs: vec![],
161 enums: vec![],
162 functions: vec![],
163 }
164 }
165}
166
167#[derive(Debug, Deserialize)]
168struct CargoToml {
169 package: Package,
170 bin: Option<Bin>,
171 lib: Option<Lib>,
172}
173
174#[derive(Debug, Deserialize)]
175struct Package {
176 name: String,
177 version: String,
178}
179
180#[derive(Debug, Deserialize)]
181struct Lib {
182 name: Option<String>,
183 path: Option<String>,
184}
185
186#[derive(Debug, Deserialize)]
187struct Bin {
188 name: Option<String>,
189 path: Option<String>,
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use insta::assert_yaml_snapshot;
196
197 #[test]
198 fn test_analyze_crate() -> Result<()> {
199 // Create a temporary directory for the dummy crate
200 let temp_dir = tempfile::tempdir()?;
201 let temp_dir_path = temp_dir.path();
202
203 // Create a dummy Cargo.toml file
204 let cargo_toml_path = temp_dir_path.join("Cargo.toml");
205 std::fs::write(
206 cargo_toml_path,
207 r#"
208 [package]
209 name = "my_crate"
210 version = "0.1.0"
211
212 [lib]
213 "#,
214 )?;
215
216 // Create a dummy lib.rs file
217 let lib_rs_path = temp_dir_path.join("src").join("lib.rs");
218 std::fs::create_dir_all(lib_rs_path.parent().unwrap())?;
219 std::fs::write(
220 &lib_rs_path,
221 r#"
222 //! The crate docstring
223 pub mod my_module;
224 "#,
225 )?;
226
227 // Create a dummy module file
228 let dummy_module_path = temp_dir_path.join("src").join("my_module.rs");
229 std::fs::create_dir_all(dummy_module_path.parent().unwrap())?;
230 std::fs::write(
231 &dummy_module_path,
232 r#"
233 //! The module docstring
234 pub mod my_submodule;
235 /// The struct1 docstring
236 pub struct DummyStruct1;
237 /// The enum1 docstring
238 pub enum DummyEnum1 {}
239 "#,
240 )?;
241
242 // Create a dummy sub-module file
243 let dummy_module_path = temp_dir_path
244 .join("src")
245 .join("my_module")
246 .join("my_submodule.rs");
247 std::fs::create_dir_all(dummy_module_path.parent().unwrap())?;
248 std::fs::write(
249 &dummy_module_path,
250 r#"
251 //! The sub-module docstring
252 /// The struct2 docstring
253 pub struct DummyStruct2;
254 /// The enum2 docstring
255 pub enum DummyEnum2 {}
256 "#,
257 )?;
258
259 // Analyze the dummy crate
260 let mut result = analyze_crate(temp_dir_path.to_str().unwrap())?;
261
262 // Remove the file paths for snapshot testing, as they are non-deterministic
263 for module in result.modules.iter_mut() {
264 module.file = None;
265 }
266
267 assert_yaml_snapshot!(result, @r###"
268 ---
269 crate_:
270 name: my_crate
271 version: 0.1.0
272 modules:
273 - file: ~
274 path:
275 - my_crate
276 docstring: The crate docstring
277 declarations:
278 - my_module
279 - file: ~
280 path:
281 - my_crate
282 - my_module
283 docstring: The module docstring
284 declarations:
285 - my_submodule
286 - file: ~
287 path:
288 - my_crate
289 - my_module
290 - my_submodule
291 docstring: The sub-module docstring
292 declarations: []
293 structs:
294 - path:
295 - my_crate
296 - my_module
297 - DummyStruct1
298 docstring: The struct1 docstring
299 fields: []
300 - path:
301 - my_crate
302 - my_module
303 - my_submodule
304 - DummyStruct2
305 docstring: The struct2 docstring
306 fields: []
307 enums:
308 - path:
309 - my_crate
310 - my_module
311 - DummyEnum1
312 docstring: The enum1 docstring
313 variants: []
314 - path:
315 - my_crate
316 - my_module
317 - my_submodule
318 - DummyEnum2
319 docstring: The enum2 docstring
320 variants: []
321 functions: []
322 "###);
323
324 Ok(())
325 }
326}