cargo_files_core/
parser.rs

1use crate::Error;
2use std::collections::HashSet;
3use std::fs;
4use std::path::{Path, PathBuf};
5use syn::visit::Visit;
6use syn::{Expr, ExprLit, ItemMod, Lit, Meta};
7
8#[derive(Default, Debug)]
9struct ModVisitor {
10    modules: Vec<Module>,
11    stack: Vec<PathComponent>,
12}
13
14impl<'ast> Visit<'ast> for ModVisitor {
15    fn visit_item_mod(&mut self, item: &'ast ItemMod) {
16        // Parse any #[path = "bla.rs"] declaration.
17        let mut path = None;
18        for attr in &item.attrs {
19            let Meta::NameValue(meta) = &attr.meta else {
20                continue;
21            };
22
23            let Some(attr_ident) = attr.path().get_ident() else {
24                continue;
25            };
26            if attr_ident != "path" {
27                continue;
28            }
29
30            let Expr::Lit(ExprLit {
31                lit: Lit::Str(lit), ..
32            }) = &meta.value
33            else {
34                continue;
35            };
36            path = Some(lit.value());
37            break;
38        }
39
40        self.stack.push(PathComponent {
41            name: item.ident.to_string(),
42            path,
43        });
44
45        // AFAIK mod foobar {} blocks don't contribute a file
46        if item.content.is_none() {
47            self.modules.push(Module {
48                parts: self.stack.clone(),
49            });
50        }
51
52        syn::visit::visit_item_mod(self, item);
53        self.stack.pop().expect("should be balanced");
54    }
55}
56
57#[derive(Clone, Debug)]
58struct PathComponent {
59    name: String,
60
61    /// An optional path override, set using the #[path = "..."] attribute.
62    path: Option<String>,
63}
64
65#[derive(Debug)]
66struct Module {
67    /// The collection of path components that make up this module definition.
68    ///
69    /// The source code:
70    ///
71    /// #[path = "a"]
72    /// mod a_mod {
73    ///   #[path = "b.rs"]
74    ///   mod b_mod;
75    /// }
76    ///
77    /// would give rise to a single Module, having two parts.
78    parts: Vec<PathComponent>,
79}
80
81impl Module {
82    /// Return the source file corresponding to this module.
83    fn resolve(self, root_path: &Path, source_file_path: &Path) -> Result<PathBuf, Error> {
84        assert!(!self.parts.is_empty());
85
86        let source_file_directory = source_file_path
87            .parent()
88            .ok_or(Error::NoParent)?
89            .to_path_buf();
90
91        let mut base_resolution_path = resolve_base_resolution_path(root_path, source_file_path)?;
92        let (final_part, head) = self.parts.split_last().unwrap();
93
94        // Handle parent module paths
95        for (i, component) in head.iter().enumerate() {
96            if let Some(path) = component.path.as_deref() {
97                assert!(!path.ends_with(".rs")); // should be a _folder_
98                if i == 0 {
99                    // Special-case: A top-level module definition having a path attribute
100                    // is always resolved relative to the source file.
101                    base_resolution_path = source_file_directory.join(path);
102                } else {
103                    base_resolution_path.push(path);
104                }
105            } else {
106                base_resolution_path.push(&component.name);
107            }
108        }
109
110        // Handle the actual module definition path
111        if let Some(path) = final_part.path.as_deref() {
112            if head.is_empty() {
113                // There are no parent modules, so this is actually a top-level module definition.
114                base_resolution_path = source_file_directory.join(path);
115            } else {
116                base_resolution_path.push(path);
117            }
118            return if base_resolution_path.exists() {
119                Ok(base_resolution_path)
120            } else {
121                Err(Error::ModuleNotFound)
122            };
123        }
124
125        // Look for a new-style module {name}.rs
126        base_resolution_path.push(format!("{}.rs", final_part.name));
127        if base_resolution_path.exists() {
128            return Ok(base_resolution_path);
129        }
130
131        // Look for an old-style module {name}/mod.rs
132        base_resolution_path.pop();
133        base_resolution_path.extend([&final_part.name, "mod.rs"]);
134        if base_resolution_path.exists() {
135            return Ok(base_resolution_path);
136        }
137
138        Err(Error::ModuleNotFound)
139    }
140}
141
142fn resolve_base_resolution_path(
143    root_path: &Path,
144    source_file_path: &Path,
145) -> Result<PathBuf, Error> {
146    let base_name = source_file_path.file_stem().ok_or(Error::NoStem)?;
147    let is_mod_rs = (root_path == source_file_path) || base_name == "mod";
148
149    let mut source_file_directory = source_file_path
150        .parent()
151        .ok_or(Error::NoParent)?
152        .to_path_buf();
153
154    // If this is a mod.rs-like file, then paths are resolved relative to the source file's
155    // parent directory. If it isn't, then we need to resolve from one level deeper.
156    if !is_mod_rs {
157        source_file_directory.push(base_name);
158    }
159
160    Ok(source_file_directory)
161}
162
163pub fn extract_crate_files(
164    root_path: &Path,
165    path: &Path,
166    acc: &mut HashSet<PathBuf>,
167) -> Result<(), Error> {
168    acc.insert(path.to_path_buf());
169    let source = fs::read_to_string(path).map_err(|e| Error::FileError(path.to_path_buf(), e))?;
170
171    // Extract all the mod definitions in the given file
172    let file = syn::parse_file(&source)?;
173    let mut visitor = ModVisitor::default();
174    visitor.visit_file(&file);
175
176    for module in visitor.modules {
177        let module_path = module.resolve(root_path, path)?;
178        let canonical_module_path = dunce::canonicalize(&module_path).unwrap_or(module_path);
179        extract_crate_files(root_path, &canonical_module_path, acc)?;
180        acc.insert(canonical_module_path);
181    }
182
183    Ok(())
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    fn assert_unique_module_parts(source: &str, parts: Vec<(&str, Option<&str>)>) {
191        let file = syn::parse_file(source).unwrap();
192        let mut visitor = ModVisitor::default();
193        visitor.visit_file(&file);
194
195        assert_eq!(visitor.modules.len(), 1);
196
197        let module = visitor.modules.pop().unwrap();
198        assert_eq!(module.parts.len(), parts.len());
199
200        for (path_component, (expected_name, expected_path)) in module.parts.into_iter().zip(parts)
201        {
202            assert_eq!(path_component.name, expected_name);
203            assert_eq!(path_component.path, expected_path.map(String::from));
204        }
205    }
206
207    #[test]
208    fn test_path_attribute_parsing() {
209        let source = r#"
210        #[path = "apple.rs"]
211        mod banana;
212        "#;
213
214        assert_unique_module_parts(source, vec![("banana", Some("apple.rs"))]);
215    }
216
217    #[test]
218    fn test_nested_mod_parsing() {
219        let source = r#"
220        mod a {
221            mod b {
222                mod c {
223                    mod d;
224                }
225            }
226        }
227        "#;
228
229        assert_unique_module_parts(
230            source,
231            vec![("a", None), ("b", None), ("c", None), ("d", None)],
232        );
233    }
234
235    #[test]
236    fn test_nested_mod_parsing_with_path_attribute() {
237        let source = r#"
238        mod a {
239            mod b {
240                #[path = "putty.rs"]
241                mod c;
242            }
243        }
244        "#;
245
246        assert_unique_module_parts(
247            source,
248            vec![("a", None), ("b", None), ("c", Some("putty.rs"))],
249        );
250    }
251
252    #[test]
253    fn test_docstring_on_module_ignored() {
254        // Regression test for #7
255        let source = r#"
256        ///
257        mod intern;
258        "#;
259
260        assert_unique_module_parts(source, vec![("intern", None)]);
261    }
262
263    #[test]
264    fn test_path_and_nested_path() {
265        let source = r#"
266        #[path = "abc"]
267        mod thread {
268            #[path = "tls.rs"]
269            mod data;
270        }
271        "#;
272
273        assert_unique_module_parts(
274            source,
275            vec![("thread", Some("abc")), ("data", Some("tls.rs"))],
276        );
277    }
278}