1extern crate proc_macro;
2
3mod cache;
4mod parser;
5mod pathing;
6
7#[cfg(doctest)]
8#[doc = include_str!("../README.md")]
9mod readme_doctests {}
10
11use std::path::Path;
12
13use crate::cache::{
14 clear_line_cache_for_file, file_fingerprint, get_or_parse_file_modules, store_line_result,
15};
16use parser::{parse_file_modules, resolve_module_path_from_lines};
17use pathing::normalize_file_path;
18pub use pathing::{
19 module_path_from_file, module_path_from_file_with_root, module_path_to_file,
20 module_root_from_file,
21};
22
23pub fn get_source_info() -> Option<(String, usize)> {
25 let span = std::panic::catch_unwind(proc_macro::Span::call_site).ok()?;
28 let line_number = span.start().line();
29
30 if let Some(local_file) = span.local_file() {
31 return Some((local_file.to_string_lossy().into_owned(), line_number));
32 }
33
34 let file_path = span.file();
35 if file_path.is_empty() {
36 None
37 } else {
38 Some((file_path, line_number))
39 }
40}
41
42pub fn find_module_path(file_path: &str, line_number: usize) -> Option<String> {
44 let normalized_file_path = normalize_file_path(file_path);
45 let fingerprint = file_fingerprint(&normalized_file_path)?;
46
47 match cache::cached_line_result(&normalized_file_path, line_number, fingerprint) {
48 cache::CacheLookup::Fresh(module_path) => return module_path,
49 cache::CacheLookup::Stale => clear_line_cache_for_file(&normalized_file_path),
52 cache::CacheLookup::Missing => {}
53 }
54
55 let parsed_file = get_or_parse_file_modules(&normalized_file_path, fingerprint)?;
56 let resolved = resolve_module_path_from_lines(
57 &parsed_file.base_module,
58 &parsed_file.line_modules,
59 line_number,
60 );
61
62 store_line_result(
63 &normalized_file_path,
64 line_number,
65 fingerprint,
66 resolved.clone(),
67 );
68
69 resolved
70}
71
72pub fn find_module_path_in_file(
74 file_path: &str,
75 line_number: usize,
76 module_root: &Path,
77) -> Option<String> {
78 let normalized_file_path = normalize_file_path(file_path);
79 let (base_module, line_modules) = parse_file_modules(&normalized_file_path, module_root)?;
80 resolve_module_path_from_lines(&base_module, &line_modules, line_number)
81}
82
83pub fn get_pseudo_module_path() -> String {
85 get_source_info()
86 .and_then(|(file, line)| find_module_path(&file, line))
87 .unwrap_or_else(|| "unknown".to_string())
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use std::fs;
94 use std::path::{Path, PathBuf};
95 use std::thread;
96 use std::time::Duration;
97 use std::time::{SystemTime, UNIX_EPOCH};
98
99 fn unique_temp_dir(label: &str) -> PathBuf {
100 let nanos = SystemTime::now()
101 .duration_since(UNIX_EPOCH)
102 .expect("clock")
103 .as_nanos();
104 let dir = std::env::temp_dir().join(format!("statum_module_path_{label}_{nanos}"));
105 fs::create_dir_all(&dir).expect("create temp dir");
106 dir
107 }
108
109 fn write_file(path: &Path, contents: &str) {
110 if let Some(parent) = path.parent() {
111 fs::create_dir_all(parent).expect("create parent");
112 }
113 fs::write(path, contents).expect("write file");
114 }
115
116 #[test]
117 fn module_path_from_file_handles_lib_mod_and_nested_paths() {
118 assert_eq!(module_path_from_file("/tmp/project/src/lib.rs"), "crate");
119 assert_eq!(module_path_from_file("/tmp/project/src/main.rs"), "crate");
120 assert_eq!(
121 module_path_from_file("/tmp/project/src/foo/bar.rs"),
122 "foo::bar"
123 );
124 assert_eq!(module_path_from_file("/tmp/project/src/foo/mod.rs"), "foo");
125 }
126
127 #[test]
128 fn module_path_to_file_resolves_crate_rs_and_mod_rs() {
129 let crate_dir = unique_temp_dir("to_file");
130 let src = crate_dir.join("src");
131 let lib = src.join("lib.rs");
132 let workflow = src.join("workflow.rs");
133 let worker_mod = src.join("worker").join("mod.rs");
134
135 write_file(&lib, "pub mod workflow; pub mod worker;");
136 write_file(&workflow, "pub fn run() {}");
137 write_file(&worker_mod, "pub fn spawn() {}");
138
139 let current = workflow.to_string_lossy().into_owned();
140 let module_root = src;
141
142 assert_eq!(
143 module_path_to_file("crate", ¤t, &module_root),
144 Some(lib.clone())
145 );
146 assert_eq!(
147 module_path_to_file("crate::workflow", ¤t, &module_root),
148 Some(workflow.clone())
149 );
150 assert_eq!(
151 module_path_to_file("crate::worker", ¤t, &module_root),
152 Some(worker_mod.clone())
153 );
154
155 let _ = fs::remove_dir_all(crate_dir);
156 }
157
158 #[test]
159 fn find_module_path_in_file_resolves_nested_inline_modules() {
160 let crate_dir = unique_temp_dir("nested_mods");
161 let src = crate_dir.join("src");
162 let lib = src.join("lib.rs");
163
164 write_file(
165 &lib,
166 "mod outer {\n mod inner {\n pub fn marker() {}\n }\n}\n",
167 );
168
169 let found = find_module_path_in_file(&lib.to_string_lossy(), 3, &src);
170 assert_eq!(found.as_deref(), Some("outer::inner"));
171
172 let _ = fs::remove_dir_all(crate_dir);
173 }
174
175 #[test]
176 fn find_module_path_in_file_handles_raw_identifier_modules() {
177 let crate_dir = unique_temp_dir("raw_ident_mods");
178 let src = crate_dir.join("src");
179 let lib = src.join("lib.rs");
180
181 write_file(
182 &lib,
183 "#[cfg(any())]\npub(crate) mod r#async {\n pub mod r#type {\n pub fn marker() {}\n }\n}\n",
184 );
185
186 let found = find_module_path_in_file(&lib.to_string_lossy(), 4, &src);
187 assert_eq!(found.as_deref(), Some("r#async::r#type"));
188
189 let _ = fs::remove_dir_all(crate_dir);
190 }
191
192 #[test]
193 fn find_module_path_in_file_separates_sibling_modules_with_similar_shapes() {
194 let crate_dir = unique_temp_dir("sibling_modules");
195 let src = crate_dir.join("src");
196 let lib = src.join("lib.rs");
197
198 write_file(
199 &lib,
200 "mod alpha {\n mod support {\n pub struct Text;\n }\n\n pub enum WorkflowState {\n Draft,\n }\n\n pub struct Row {\n pub status: &'static str,\n }\n}\n\nmod beta {\n mod support {\n pub struct Text;\n }\n\n pub enum WorkflowState {\n Draft,\n }\n\n pub struct Row {\n pub status: &'static str,\n }\n}\n",
201 );
202
203 assert_eq!(
204 find_module_path_in_file(&lib.to_string_lossy(), 6, &src).as_deref(),
205 Some("alpha")
206 );
207 assert_eq!(
208 find_module_path_in_file(&lib.to_string_lossy(), 20, &src).as_deref(),
209 Some("beta")
210 );
211 assert_eq!(
212 find_module_path_in_file(&lib.to_string_lossy(), 19, &src).as_deref(),
213 Some("beta")
214 );
215
216 let _ = fs::remove_dir_all(crate_dir);
217 }
218
219 #[test]
220 fn find_module_path_in_file_ignores_mod_tokens_in_comments_and_raw_strings() {
221 let crate_dir = unique_temp_dir("comments_and_raw_strings");
222 let src = crate_dir.join("src");
223 let lib = src.join("lib.rs");
224
225 write_file(
226 &lib,
227 "const TEMPLATE: &str = r#\"\nmod fake {\n mod nested {}\n}\n\"#;\n\n/* mod ignored {\n mod deeper {}\n} */\n\nmod outer {\n // mod hidden { mod nope {} }\n mod inner {\n pub fn marker() {}\n }\n}\n",
228 );
229
230 let found = find_module_path_in_file(&lib.to_string_lossy(), 14, &src);
231 assert_eq!(found.as_deref(), Some("outer::inner"));
232
233 let _ = fs::remove_dir_all(crate_dir);
234 }
235
236 #[test]
237 fn find_module_path_in_file_ignores_modules_inside_macro_rules_bodies() {
238 let crate_dir = unique_temp_dir("macro_rules_body");
239 let src = crate_dir.join("src");
240 let lib = src.join("lib.rs");
241
242 write_file(
243 &lib,
244 "mod outer {\n macro_rules! generated {\n () => {\n mod fake {\n pub fn hidden() {}\n }\n };\n }\n}\n",
245 );
246
247 assert_eq!(
248 find_module_path_in_file(&lib.to_string_lossy(), 5, &src).as_deref(),
249 Some("outer")
250 );
251
252 let _ = fs::remove_dir_all(crate_dir);
253 }
254
255 #[test]
256 fn find_module_path_in_file_ignores_modules_inside_macro_invocation_bodies() {
257 let crate_dir = unique_temp_dir("macro_invocation_body");
258 let src = crate_dir.join("src");
259 let lib = src.join("lib.rs");
260
261 write_file(
262 &lib,
263 "mod outer {\n generated! {\n mod fake {\n pub fn hidden() {}\n }\n }\n\n mod inner {\n pub fn marker() {}\n }\n}\n",
264 );
265
266 assert_eq!(
267 find_module_path_in_file(&lib.to_string_lossy(), 4, &src).as_deref(),
268 Some("outer")
269 );
270 assert_eq!(
271 find_module_path_in_file(&lib.to_string_lossy(), 8, &src).as_deref(),
272 Some("outer::inner")
273 );
274
275 let _ = fs::remove_dir_all(crate_dir);
276 }
277
278 #[test]
279 fn find_module_path_in_file_ignores_modules_inside_macro_invocations_for_all_delimiters() {
280 let crate_dir = unique_temp_dir("macro_invocation_delimiters");
281 let src = crate_dir.join("src");
282 let lib = src.join("lib.rs");
283
284 for (label, open, close) in [
285 ("brace", "{", "}"),
286 ("paren", "(", ")"),
287 ("bracket", "[", "]"),
288 ] {
289 write_file(
290 &lib,
291 &format!(
292 "mod outer {{\n generated!{open}\n mod fake {{\n pub fn hidden() {{}}\n }}\n {close};\n\n mod inner {{\n pub fn marker() {{}}\n }}\n}}\n"
293 ),
294 );
295
296 assert_eq!(
297 find_module_path_in_file(&lib.to_string_lossy(), 4, &src).as_deref(),
298 Some("outer"),
299 "fake module should stay opaque for {label} delimiter"
300 );
301 assert_eq!(
302 find_module_path_in_file(&lib.to_string_lossy(), 9, &src).as_deref(),
303 Some("outer::inner"),
304 "real nested module should resolve for {label} delimiter"
305 );
306 }
307
308 let _ = fs::remove_dir_all(crate_dir);
309 }
310
311 #[test]
312 fn find_module_path_invalidates_stale_line_cache_when_file_changes() {
313 let crate_dir = unique_temp_dir("invalidate_cache");
314 let src = crate_dir.join("src");
315 let lib = src.join("lib.rs");
316
317 write_file(
318 &lib,
319 "mod outer {\n mod inner {\n pub fn marker() {}\n }\n}\n",
320 );
321
322 let lib_path = lib.to_string_lossy().to_string();
323 let first = find_module_path(&lib_path, 3);
324 assert_eq!(first.as_deref(), Some("outer::inner"));
325
326 thread::sleep(Duration::from_millis(2));
328 write_file(
329 &lib,
330 "mod changed {\n mod deeper {\n pub fn marker() {}\n }\n}\n",
331 );
332
333 let second = find_module_path(&lib_path, 3);
334 assert_eq!(second.as_deref(), Some("changed::deeper"));
335
336 let _ = fs::remove_dir_all(crate_dir);
337 }
338
339 #[test]
340 fn stale_line_entries_are_replaced_after_file_change() {
341 let crate_dir = unique_temp_dir("stale_line_entries");
342 let src = crate_dir.join("src");
343 let lib = src.join("lib.rs");
344
345 write_file(
346 &lib,
347 "mod outer {\n mod inner {\n pub fn marker() {}\n }\n}\n",
348 );
349
350 let lib_path = lib.to_string_lossy().to_string();
351 let _ = find_module_path(&lib_path, 2);
352 let _ = find_module_path(&lib_path, 3);
353 assert_eq!(cache::line_cache_entries_for(&lib_path), 2);
354
355 thread::sleep(Duration::from_millis(2));
357 write_file(
358 &lib,
359 "mod changed {\n mod deeper {\n pub fn marker() {}\n }\n}\n",
360 );
361
362 let refreshed = find_module_path(&lib_path, 3);
363 assert_eq!(refreshed.as_deref(), Some("changed::deeper"));
364 assert_eq!(cache::line_cache_entries_for(&lib_path), 1);
365
366 let second_line = find_module_path(&lib_path, 2);
367 assert_eq!(second_line.as_deref(), Some("changed::deeper"));
368 assert_eq!(cache::line_cache_entries_for(&lib_path), 2);
369
370 let _ = fs::remove_dir_all(crate_dir);
371 }
372
373 #[test]
374 fn find_module_path_handles_non_src_fixture_files_with_nested_modules() {
375 let crate_dir = unique_temp_dir("non_src_fixture");
376 let tests_ui = crate_dir.join("tests").join("ui");
377 let fixture = tests_ui.join("fixture.rs");
378
379 write_file(
380 &fixture,
381 "pub mod public_flow {\n #[allow(dead_code)]\n pub struct Machine;\n}\n",
382 );
383
384 assert_eq!(
385 find_module_path(&fixture.to_string_lossy(), 3).as_deref(),
386 Some("fixture::public_flow")
387 );
388
389 let _ = fs::remove_dir_all(crate_dir);
390 }
391
392 #[test]
393 fn find_module_path_handles_nested_trybuild_style_fixture() {
394 let fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
395 .join("../statum-macros/tests/ui/valid_helper_trait_visibility.rs");
396
397 assert_eq!(
398 find_module_path(&fixture.to_string_lossy(), 30).as_deref(),
399 Some("valid_helper_trait_visibility::public_flow")
400 );
401 assert_eq!(
402 find_module_path(&fixture.to_string_lossy(), 39).as_deref(),
403 Some("valid_helper_trait_visibility::public_flow")
404 );
405 assert_eq!(
406 find_module_path(&fixture.to_string_lossy(), 103).as_deref(),
407 Some("valid_helper_trait_visibility::crate_flow")
408 );
409 }
410
411 #[test]
412 fn find_module_path_handles_sibling_trybuild_modules() {
413 let fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
414 .join("../statum-macros/tests/ui/valid_matrix.rs");
415
416 assert_eq!(
417 find_module_path(&fixture.to_string_lossy(), 18).as_deref(),
418 Some("valid_matrix::simple")
419 );
420 assert_eq!(
421 find_module_path(&fixture.to_string_lossy(), 47).as_deref(),
422 Some("valid_matrix::data_state")
423 );
424 assert_eq!(
425 find_module_path(&fixture.to_string_lossy(), 69).as_deref(),
426 Some("valid_matrix::wrappers_option")
427 );
428 assert_eq!(
429 find_module_path(&fixture.to_string_lossy(), 113).as_deref(),
430 Some("valid_matrix::validators_sync")
431 );
432 }
433}