1extern crate proc_macro;
2
3mod cache;
4mod parser;
5mod pathing;
6
7use std::path::Path;
8
9use cache::{
10 clear_line_cache_for_file, file_fingerprint, get_or_parse_file_modules, store_line_result,
11 CacheLookup,
12};
13use parser::{parse_file_modules, resolve_module_path_from_lines};
14use pathing::normalize_file_path;
15pub use pathing::{
16 module_path_from_file, module_path_from_file_with_root, module_path_to_file,
17 module_root_from_file,
18};
19
20pub fn get_source_info() -> Option<(String, usize)> {
22 let span = std::panic::catch_unwind(proc_macro::Span::call_site).ok()?;
25 let line_number = span.start().line();
26
27 if let Some(local_file) = span.local_file() {
28 return Some((local_file.to_string_lossy().into_owned(), line_number));
29 }
30
31 let file_path = span.file();
32 if file_path.is_empty() {
33 None
34 } else {
35 Some((file_path, line_number))
36 }
37}
38
39pub fn find_module_path(file_path: &str, line_number: usize) -> Option<String> {
41 let normalized_file_path = normalize_file_path(file_path);
42 let fingerprint = file_fingerprint(&normalized_file_path)?;
43
44 match cache::cached_line_result(&normalized_file_path, line_number, fingerprint) {
45 CacheLookup::Fresh(module_path) => return module_path,
46 CacheLookup::Stale => clear_line_cache_for_file(&normalized_file_path),
49 CacheLookup::Missing => {}
50 }
51
52 let parsed_file = get_or_parse_file_modules(&normalized_file_path, fingerprint)?;
53 let resolved = resolve_module_path_from_lines(
54 &parsed_file.base_module,
55 &parsed_file.line_modules,
56 line_number,
57 );
58
59 store_line_result(
60 &normalized_file_path,
61 line_number,
62 fingerprint,
63 resolved.clone(),
64 );
65
66 resolved
67}
68
69pub fn find_module_path_in_file(
71 file_path: &str,
72 line_number: usize,
73 module_root: &Path,
74) -> Option<String> {
75 let normalized_file_path = normalize_file_path(file_path);
76 let (base_module, line_modules) = parse_file_modules(&normalized_file_path, module_root)?;
77 resolve_module_path_from_lines(&base_module, &line_modules, line_number)
78}
79
80pub fn get_pseudo_module_path() -> String {
82 get_source_info()
83 .and_then(|(file, line)| find_module_path(&file, line))
84 .unwrap_or_else(|| "unknown".to_string())
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use std::fs;
91 use std::path::{Path, PathBuf};
92 use std::thread;
93 use std::time::Duration;
94 use std::time::{SystemTime, UNIX_EPOCH};
95
96 fn unique_temp_dir(label: &str) -> PathBuf {
97 let nanos = SystemTime::now()
98 .duration_since(UNIX_EPOCH)
99 .expect("clock")
100 .as_nanos();
101 let dir = std::env::temp_dir().join(format!("statum_module_path_{label}_{nanos}"));
102 fs::create_dir_all(&dir).expect("create temp dir");
103 dir
104 }
105
106 fn write_file(path: &Path, contents: &str) {
107 if let Some(parent) = path.parent() {
108 fs::create_dir_all(parent).expect("create parent");
109 }
110 fs::write(path, contents).expect("write file");
111 }
112
113 #[test]
114 fn module_path_from_file_handles_lib_mod_and_nested_paths() {
115 assert_eq!(module_path_from_file("/tmp/project/src/lib.rs"), "crate");
116 assert_eq!(module_path_from_file("/tmp/project/src/main.rs"), "crate");
117 assert_eq!(
118 module_path_from_file("/tmp/project/src/foo/bar.rs"),
119 "foo::bar"
120 );
121 assert_eq!(module_path_from_file("/tmp/project/src/foo/mod.rs"), "foo");
122 }
123
124 #[test]
125 fn module_path_to_file_resolves_crate_rs_and_mod_rs() {
126 let crate_dir = unique_temp_dir("to_file");
127 let src = crate_dir.join("src");
128 let lib = src.join("lib.rs");
129 let workflow = src.join("workflow.rs");
130 let worker_mod = src.join("worker").join("mod.rs");
131
132 write_file(&lib, "pub mod workflow; pub mod worker;");
133 write_file(&workflow, "pub fn run() {}");
134 write_file(&worker_mod, "pub fn spawn() {}");
135
136 let current = workflow.to_string_lossy().into_owned();
137 let module_root = src;
138
139 assert_eq!(
140 module_path_to_file("crate", ¤t, &module_root),
141 Some(lib.clone())
142 );
143 assert_eq!(
144 module_path_to_file("crate::workflow", ¤t, &module_root),
145 Some(workflow.clone())
146 );
147 assert_eq!(
148 module_path_to_file("crate::worker", ¤t, &module_root),
149 Some(worker_mod.clone())
150 );
151
152 let _ = fs::remove_dir_all(crate_dir);
153 }
154
155 #[test]
156 fn find_module_path_in_file_resolves_nested_inline_modules() {
157 let crate_dir = unique_temp_dir("nested_mods");
158 let src = crate_dir.join("src");
159 let lib = src.join("lib.rs");
160
161 write_file(
162 &lib,
163 "mod outer {\n mod inner {\n pub fn marker() {}\n }\n}\n",
164 );
165
166 let found = find_module_path_in_file(&lib.to_string_lossy(), 3, &src);
167 assert_eq!(found.as_deref(), Some("outer::inner"));
168
169 let _ = fs::remove_dir_all(crate_dir);
170 }
171
172 #[test]
173 fn find_module_path_in_file_handles_raw_identifier_modules() {
174 let crate_dir = unique_temp_dir("raw_ident_mods");
175 let src = crate_dir.join("src");
176 let lib = src.join("lib.rs");
177
178 write_file(
179 &lib,
180 "#[cfg(any())]\npub(crate) mod r#async {\n pub mod r#type {\n pub fn marker() {}\n }\n}\n",
181 );
182
183 let found = find_module_path_in_file(&lib.to_string_lossy(), 4, &src);
184 assert_eq!(found.as_deref(), Some("r#async::r#type"));
185
186 let _ = fs::remove_dir_all(crate_dir);
187 }
188
189 #[test]
190 fn find_module_path_in_file_ignores_mod_tokens_in_comments_and_raw_strings() {
191 let crate_dir = unique_temp_dir("comments_and_raw_strings");
192 let src = crate_dir.join("src");
193 let lib = src.join("lib.rs");
194
195 write_file(
196 &lib,
197 "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",
198 );
199
200 let found = find_module_path_in_file(&lib.to_string_lossy(), 14, &src);
201 assert_eq!(found.as_deref(), Some("outer::inner"));
202
203 let _ = fs::remove_dir_all(crate_dir);
204 }
205
206 #[test]
207 fn find_module_path_invalidates_stale_line_cache_when_file_changes() {
208 let crate_dir = unique_temp_dir("invalidate_cache");
209 let src = crate_dir.join("src");
210 let lib = src.join("lib.rs");
211
212 write_file(
213 &lib,
214 "mod outer {\n mod inner {\n pub fn marker() {}\n }\n}\n",
215 );
216
217 let lib_path = lib.to_string_lossy().to_string();
218 let first = find_module_path(&lib_path, 3);
219 assert_eq!(first.as_deref(), Some("outer::inner"));
220
221 thread::sleep(Duration::from_millis(2));
223 write_file(
224 &lib,
225 "mod changed {\n mod deeper {\n pub fn marker() {}\n }\n}\n",
226 );
227
228 let second = find_module_path(&lib_path, 3);
229 assert_eq!(second.as_deref(), Some("changed::deeper"));
230
231 let _ = fs::remove_dir_all(crate_dir);
232 }
233
234 #[test]
235 fn stale_line_entries_are_replaced_after_file_change() {
236 let crate_dir = unique_temp_dir("stale_line_entries");
237 let src = crate_dir.join("src");
238 let lib = src.join("lib.rs");
239
240 write_file(
241 &lib,
242 "mod outer {\n mod inner {\n pub fn marker() {}\n }\n}\n",
243 );
244
245 let lib_path = lib.to_string_lossy().to_string();
246 let _ = find_module_path(&lib_path, 2);
247 let _ = find_module_path(&lib_path, 3);
248 assert_eq!(cache::line_cache_entries_for(&lib_path), 2);
249
250 thread::sleep(Duration::from_millis(2));
252 write_file(
253 &lib,
254 "mod changed {\n mod deeper {\n pub fn marker() {}\n }\n}\n",
255 );
256
257 let refreshed = find_module_path(&lib_path, 3);
258 assert_eq!(refreshed.as_deref(), Some("changed::deeper"));
259 assert_eq!(cache::line_cache_entries_for(&lib_path), 1);
260
261 let second_line = find_module_path(&lib_path, 2);
262 assert_eq!(second_line.as_deref(), Some("changed::deeper"));
263 assert_eq!(cache::line_cache_entries_for(&lib_path), 2);
264
265 let _ = fs::remove_dir_all(crate_dir);
266 }
267}