use std::{
fs::{read_dir, remove_dir as remove_directory, rename},
path::{Path, PathBuf}
};
use masterror::AppResult;
use crate::error::IoError;
#[derive(Debug, Clone)]
pub struct ModRsIssue {
pub path: PathBuf,
pub suggested: PathBuf,
pub message: String,
pub line: usize,
pub column: usize
}
#[derive(Debug, Default)]
pub struct ModRsResult {
pub issues: Vec<ModRsIssue>
}
impl ModRsResult {
#[inline]
pub fn new() -> Self {
Self {
issues: Vec::new()
}
}
#[inline]
pub fn len(&self) -> usize {
self.issues.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.issues.is_empty()
}
}
pub fn find_mod_rs_issues(path: &str) -> AppResult<ModRsResult> {
let root = Path::new(path);
let mut result = ModRsResult::new();
if root.is_file() {
if is_mod_rs(root)
&& let Some(issue) = create_issue(root)
{
result.issues.push(issue);
}
return Ok(result);
}
collect_mod_rs_recursive(root, &mut result)?;
Ok(result)
}
fn collect_mod_rs_recursive(dir: &Path, result: &mut ModRsResult) -> AppResult<()> {
let entries = read_dir(dir).map_err(IoError::from)?;
for entry in entries {
let entry = entry.map_err(IoError::from)?;
let path = entry.path();
if path.is_dir() {
collect_mod_rs_recursive(&path, result)?;
} else if is_mod_rs(&path)
&& let Some(issue) = create_issue(&path)
{
result.issues.push(issue);
}
}
Ok(())
}
#[inline]
fn is_mod_rs(path: &Path) -> bool {
path.file_name()
.and_then(|n| n.to_str())
.map(|n| n == "mod.rs")
.unwrap_or(false)
}
fn create_issue(path: &Path) -> Option<ModRsIssue> {
let parent = path.parent()?;
let module_name = parent.file_name()?.to_str()?;
let grandparent = parent.parent()?;
let suggested = grandparent.join(format!("{}.rs", module_name));
Some(ModRsIssue {
path: path.to_path_buf(),
suggested,
message: format!(
"Use `{}.rs` instead of `{}/mod.rs` (modern module style)",
module_name, module_name
),
line: 1,
column: 1
})
}
pub fn fix_mod_rs(issue: &ModRsIssue) -> AppResult<()> {
rename(&issue.path, &issue.suggested).map_err(IoError::from)?;
if let Some(parent) = issue.path.parent()
&& is_directory_empty(parent)?
{
remove_directory(parent).map_err(IoError::from)?;
}
Ok(())
}
pub fn fix_all_mod_rs(path: &str) -> AppResult<usize> {
let result = find_mod_rs_issues(path)?;
let count = result.len();
for issue in result.issues {
fix_mod_rs(&issue)?;
}
Ok(count)
}
fn is_directory_empty(dir: &Path) -> AppResult<bool> {
let mut entries = read_dir(dir).map_err(IoError::from)?;
Ok(entries.next().is_none())
}
#[cfg(test)]
mod tests {
use std::fs::{create_dir, read_to_string, write};
use tempfile::TempDir;
use super::*;
#[test]
fn test_find_no_mod_rs() {
let temp = TempDir::new().unwrap();
let file = temp.path().join("lib.rs");
write(&file, "fn main() {}").unwrap();
let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_find_mod_rs() {
let temp = TempDir::new().unwrap();
let subdir = temp.path().join("analyzers");
create_dir(&subdir).unwrap();
let mod_rs = subdir.join("mod.rs");
write(&mod_rs, "pub mod test;").unwrap();
let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
assert_eq!(result.len(), 1);
assert!(result.issues[0].message.contains("analyzers"));
}
#[test]
fn test_find_multiple_mod_rs() {
let temp = TempDir::new().unwrap();
let dir1 = temp.path().join("foo");
create_dir(&dir1).unwrap();
write(dir1.join("mod.rs"), "// foo").unwrap();
let dir2 = temp.path().join("bar");
create_dir(&dir2).unwrap();
write(dir2.join("mod.rs"), "// bar").unwrap();
let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
assert_eq!(result.len(), 2);
}
#[test]
fn test_fix_mod_rs() {
let temp = TempDir::new().unwrap();
let subdir = temp.path().join("utils");
create_dir(&subdir).unwrap();
let mod_rs = subdir.join("mod.rs");
write(&mod_rs, "pub fn helper() {}").unwrap();
let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
assert_eq!(result.len(), 1);
fix_mod_rs(&result.issues[0]).unwrap();
assert!(!mod_rs.exists());
let new_file = temp.path().join("utils.rs");
assert!(new_file.exists());
assert_eq!(read_to_string(&new_file).unwrap(), "pub fn helper() {}");
assert!(!subdir.exists());
}
#[test]
fn test_fix_mod_rs_keeps_dir_with_other_files() {
let temp = TempDir::new().unwrap();
let subdir = temp.path().join("services");
create_dir(&subdir).unwrap();
write(subdir.join("mod.rs"), "pub mod api;").unwrap();
write(subdir.join("api.rs"), "fn api() {}").unwrap();
let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
fix_mod_rs(&result.issues[0]).unwrap();
assert!(subdir.exists());
assert!(subdir.join("api.rs").exists());
assert!(temp.path().join("services.rs").exists());
}
#[test]
fn test_fix_all_mod_rs() {
let temp = TempDir::new().unwrap();
let dir1 = temp.path().join("module1");
create_dir(&dir1).unwrap();
write(dir1.join("mod.rs"), "// 1").unwrap();
let dir2 = temp.path().join("module2");
create_dir(&dir2).unwrap();
write(dir2.join("mod.rs"), "// 2").unwrap();
let fixed = fix_all_mod_rs(temp.path().to_str().unwrap()).unwrap();
assert_eq!(fixed, 2);
assert!(temp.path().join("module1.rs").exists());
assert!(temp.path().join("module2.rs").exists());
}
#[test]
fn test_issue_message() {
let temp = TempDir::new().unwrap();
let subdir = temp.path().join("handlers");
create_dir(&subdir).unwrap();
write(subdir.join("mod.rs"), "").unwrap();
let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
assert!(result.issues[0].message.contains("handlers.rs"));
assert!(result.issues[0].message.contains("handlers/mod.rs"));
}
#[test]
fn test_suggested_path() {
let temp = TempDir::new().unwrap();
let subdir = temp.path().join("core");
create_dir(&subdir).unwrap();
write(subdir.join("mod.rs"), "").unwrap();
let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
assert_eq!(result.issues[0].suggested, temp.path().join("core.rs"));
}
#[test]
fn test_nested_mod_rs() {
let temp = TempDir::new().unwrap();
let level1 = temp.path().join("level1");
let level2 = level1.join("level2");
create_dir(&level1).unwrap();
create_dir(&level2).unwrap();
write(level2.join("mod.rs"), "// nested").unwrap();
let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
assert_eq!(result.len(), 1);
assert!(result.issues[0].message.contains("level2"));
assert_eq!(result.issues[0].suggested, level1.join("level2.rs"));
}
#[test]
fn test_single_file_check() {
let temp = TempDir::new().unwrap();
let subdir = temp.path().join("single");
create_dir(&subdir).unwrap();
let mod_rs = subdir.join("mod.rs");
write(&mod_rs, "").unwrap();
let result = find_mod_rs_issues(mod_rs.to_str().unwrap()).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_non_mod_rs_file() {
let temp = TempDir::new().unwrap();
let file = temp.path().join("lib.rs");
write(&file, "fn main() {}").unwrap();
let result = find_mod_rs_issues(file.to_str().unwrap()).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_line_column() {
let temp = TempDir::new().unwrap();
let subdir = temp.path().join("pos");
create_dir(&subdir).unwrap();
write(subdir.join("mod.rs"), "").unwrap();
let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
assert_eq!(result.issues[0].line, 1);
assert_eq!(result.issues[0].column, 1);
}
#[test]
fn test_empty_directory() {
let temp = TempDir::new().unwrap();
let result = find_mod_rs_issues(temp.path().to_str().unwrap()).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_result_default() {
let result = ModRsResult::default();
assert!(result.is_empty());
assert_eq!(result.len(), 0);
}
}