1use std::{
21 fs::{read_dir, remove_dir as remove_directory, rename},
22 path::{Path, PathBuf}
23};
24
25use masterror::AppResult;
26
27use crate::error::IoError;
28
29#[derive(Debug, Clone)]
33pub struct ModRsIssue {
34 pub path: PathBuf,
36 pub suggested: PathBuf,
38 pub message: String,
40 pub line: usize,
42 pub column: usize
44}
45
46#[derive(Debug, Default)]
50pub struct ModRsResult {
51 pub issues: Vec<ModRsIssue>
53}
54
55impl ModRsResult {
56 #[inline]
58 pub fn new() -> Self {
59 Self {
60 issues: Vec::new()
61 }
62 }
63
64 #[inline]
66 pub fn len(&self) -> usize {
67 self.issues.len()
68 }
69
70 #[inline]
72 pub fn is_empty(&self) -> bool {
73 self.issues.is_empty()
74 }
75}
76
77pub fn find_mod_rs_issues(path: &str) -> AppResult<ModRsResult> {
99 let root = Path::new(path);
100 let mut result = ModRsResult::new();
101
102 if root.is_file() {
103 if is_mod_rs(root)
104 && let Some(issue) = create_issue(root)
105 {
106 result.issues.push(issue);
107 }
108 return Ok(result);
109 }
110
111 collect_mod_rs_recursive(root, &mut result)?;
112 Ok(result)
113}
114
115fn collect_mod_rs_recursive(dir: &Path, result: &mut ModRsResult) -> AppResult<()> {
122 let entries = read_dir(dir).map_err(IoError::from)?;
123
124 for entry in entries {
125 let entry = entry.map_err(IoError::from)?;
126 let path = entry.path();
127
128 if path.is_dir() {
129 collect_mod_rs_recursive(&path, result)?;
130 } else if is_mod_rs(&path)
131 && let Some(issue) = create_issue(&path)
132 {
133 result.issues.push(issue);
134 }
135 }
136
137 Ok(())
138}
139
140#[inline]
150fn is_mod_rs(path: &Path) -> bool {
151 path.file_name()
152 .and_then(|n| n.to_str())
153 .map(|n| n == "mod.rs")
154 .unwrap_or(false)
155}
156
157fn create_issue(path: &Path) -> Option<ModRsIssue> {
167 let parent = path.parent()?;
168 let module_name = parent.file_name()?.to_str()?;
169 let grandparent = parent.parent()?;
170
171 let suggested = grandparent.join(format!("{}.rs", module_name));
172
173 Some(ModRsIssue {
174 path: path.to_path_buf(),
175 suggested,
176 message: format!(
177 "Use `{}.rs` instead of `{}/mod.rs` (modern module style)",
178 module_name, module_name
179 ),
180 line: 1,
181 column: 1
182 })
183}
184
185pub fn fix_mod_rs(issue: &ModRsIssue) -> AppResult<()> {
212 rename(&issue.path, &issue.suggested).map_err(IoError::from)?;
213 if let Some(parent) = issue.path.parent()
214 && is_directory_empty(parent)?
215 {
216 remove_directory(parent).map_err(IoError::from)?;
217 }
218 Ok(())
219}
220
221pub fn fix_all_mod_rs(path: &str) -> AppResult<usize> {
240 let result = find_mod_rs_issues(path)?;
241 let count = result.len();
242
243 for issue in result.issues {
244 fix_mod_rs(&issue)?;
245 }
246
247 Ok(count)
248}
249
250fn is_directory_empty(dir: &Path) -> AppResult<bool> {
260 let mut entries = read_dir(dir).map_err(IoError::from)?;
261 Ok(entries.next().is_none())
262}
263
264#[cfg(test)]
265mod tests {
266 use std::fs::{create_dir, read_to_string, write};
267
268 use tempfile::TempDir;
269
270 use super::*;
271
272 #[test]
273 fn test_find_no_mod_rs() {
274 let temp = TempDir::new().unwrap();
275 let file = temp.path().join("lib.rs");
276 write(&file, "fn main() {}").unwrap();
277
278 let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
279 assert!(result.is_empty());
280 }
281
282 #[test]
283 fn test_find_mod_rs() {
284 let temp = TempDir::new().unwrap();
285 let subdir = temp.path().join("analyzers");
286 create_dir(&subdir).unwrap();
287 let mod_rs = subdir.join("mod.rs");
288 write(&mod_rs, "pub mod test;").unwrap();
289
290 let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
291 assert_eq!(result.len(), 1);
292 assert!(result.issues[0].message.contains("analyzers"));
293 }
294
295 #[test]
296 fn test_find_multiple_mod_rs() {
297 let temp = TempDir::new().unwrap();
298
299 let dir1 = temp.path().join("foo");
300 create_dir(&dir1).unwrap();
301 write(dir1.join("mod.rs"), "// foo").unwrap();
302
303 let dir2 = temp.path().join("bar");
304 create_dir(&dir2).unwrap();
305 write(dir2.join("mod.rs"), "// bar").unwrap();
306
307 let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
308 assert_eq!(result.len(), 2);
309 }
310
311 #[test]
312 fn test_fix_mod_rs() {
313 let temp = TempDir::new().unwrap();
314 let subdir = temp.path().join("utils");
315 create_dir(&subdir).unwrap();
316 let mod_rs = subdir.join("mod.rs");
317 write(&mod_rs, "pub fn helper() {}").unwrap();
318
319 let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
320 assert_eq!(result.len(), 1);
321
322 fix_mod_rs(&result.issues[0]).unwrap();
323
324 assert!(!mod_rs.exists());
325 let new_file = temp.path().join("utils.rs");
326 assert!(new_file.exists());
327 assert_eq!(read_to_string(&new_file).unwrap(), "pub fn helper() {}");
328 assert!(!subdir.exists());
329 }
330
331 #[test]
332 fn test_fix_mod_rs_keeps_dir_with_other_files() {
333 let temp = TempDir::new().unwrap();
334 let subdir = temp.path().join("services");
335 create_dir(&subdir).unwrap();
336 write(subdir.join("mod.rs"), "pub mod api;").unwrap();
337 write(subdir.join("api.rs"), "fn api() {}").unwrap();
338
339 let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
340 fix_mod_rs(&result.issues[0]).unwrap();
341
342 assert!(subdir.exists());
343 assert!(subdir.join("api.rs").exists());
344 assert!(temp.path().join("services.rs").exists());
345 }
346
347 #[test]
348 fn test_fix_all_mod_rs() {
349 let temp = TempDir::new().unwrap();
350
351 let dir1 = temp.path().join("module1");
352 create_dir(&dir1).unwrap();
353 write(dir1.join("mod.rs"), "// 1").unwrap();
354
355 let dir2 = temp.path().join("module2");
356 create_dir(&dir2).unwrap();
357 write(dir2.join("mod.rs"), "// 2").unwrap();
358
359 let fixed = fix_all_mod_rs(temp.path().to_str().unwrap()).unwrap();
360 assert_eq!(fixed, 2);
361
362 assert!(temp.path().join("module1.rs").exists());
363 assert!(temp.path().join("module2.rs").exists());
364 }
365
366 #[test]
367 fn test_issue_message() {
368 let temp = TempDir::new().unwrap();
369 let subdir = temp.path().join("handlers");
370 create_dir(&subdir).unwrap();
371 write(subdir.join("mod.rs"), "").unwrap();
372
373 let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
374 assert!(result.issues[0].message.contains("handlers.rs"));
375 assert!(result.issues[0].message.contains("handlers/mod.rs"));
376 }
377
378 #[test]
379 fn test_suggested_path() {
380 let temp = TempDir::new().unwrap();
381 let subdir = temp.path().join("core");
382 create_dir(&subdir).unwrap();
383 write(subdir.join("mod.rs"), "").unwrap();
384
385 let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
386 assert_eq!(result.issues[0].suggested, temp.path().join("core.rs"));
387 }
388
389 #[test]
390 fn test_nested_mod_rs() {
391 let temp = TempDir::new().unwrap();
392 let level1 = temp.path().join("level1");
393 let level2 = level1.join("level2");
394 create_dir(&level1).unwrap();
395 create_dir(&level2).unwrap();
396 write(level2.join("mod.rs"), "// nested").unwrap();
397
398 let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
399 assert_eq!(result.len(), 1);
400 assert!(result.issues[0].message.contains("level2"));
401 assert_eq!(result.issues[0].suggested, level1.join("level2.rs"));
402 }
403
404 #[test]
405 fn test_single_file_check() {
406 let temp = TempDir::new().unwrap();
407 let subdir = temp.path().join("single");
408 create_dir(&subdir).unwrap();
409 let mod_rs = subdir.join("mod.rs");
410 write(&mod_rs, "").unwrap();
411
412 let result = find_mod_rs_issues(mod_rs.to_str().unwrap()).unwrap();
413 assert_eq!(result.len(), 1);
414 }
415
416 #[test]
417 fn test_non_mod_rs_file() {
418 let temp = TempDir::new().unwrap();
419 let file = temp.path().join("lib.rs");
420 write(&file, "fn main() {}").unwrap();
421
422 let result = find_mod_rs_issues(file.to_str().unwrap()).unwrap();
423 assert!(result.is_empty());
424 }
425
426 #[test]
427 fn test_line_column() {
428 let temp = TempDir::new().unwrap();
429 let subdir = temp.path().join("pos");
430 create_dir(&subdir).unwrap();
431 write(subdir.join("mod.rs"), "").unwrap();
432
433 let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
434 assert_eq!(result.issues[0].line, 1);
435 assert_eq!(result.issues[0].column, 1);
436 }
437
438 #[test]
439 fn test_empty_directory() {
440 let temp = TempDir::new().unwrap();
441 let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
442 assert!(result.is_empty());
443 }
444
445 #[test]
446 fn test_result_default() {
447 let result = ModRsResult::default();
448 assert!(result.is_empty());
449 assert_eq!(result.len(), 0);
450 }
451}