cargo_files_core/
parser.rs1use 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 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 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 path: Option<String>,
63}
64
65#[derive(Debug)]
66struct Module {
67 parts: Vec<PathComponent>,
79}
80
81impl Module {
82 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 for (i, component) in head.iter().enumerate() {
96 if let Some(path) = component.path.as_deref() {
97 assert!(!path.ends_with(".rs")); if i == 0 {
99 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 if let Some(path) = final_part.path.as_deref() {
112 if head.is_empty() {
113 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 base_resolution_path.push(format!("{}.rs", final_part.name));
127 if base_resolution_path.exists() {
128 return Ok(base_resolution_path);
129 }
130
131 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 !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 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 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}