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}