Skip to main content

split_modules/
project.rs

1//! Recursive project mode: find `.rs` files that violate the one-item-per-file rule.
2
3use std::path::{Path, PathBuf};
4
5use anyhow::Result;
6use walkdir::WalkDir;
7
8use crate::model::Config;
9use crate::plan::build_plan;
10
11/// Directories never worth descending into.
12fn is_ignored_dir(name: &str) -> bool {
13    matches!(name, "target" | ".git" | "node_modules") || name.starts_with('.')
14}
15
16/// Would splitting `path` produce at least `min_groups` module files?
17fn is_candidate(path: &Path, min_groups: usize) -> bool {
18    let Ok(src) = std::fs::read_to_string(path) else {
19        return false;
20    };
21    match build_plan(path, &src) {
22        Ok(plan) => plan.files.len() >= min_groups,
23        Err(_) => false, // unparseable / non-Rust: leave it alone
24    }
25}
26
27/// Collect all eligible files under `root`. If `root` is a single `.rs` file, it is the
28/// only candidate (subject to the same threshold).
29pub fn find_candidates(root: &Path, config: &Config) -> Result<Vec<PathBuf>> {
30    let mut out = Vec::new();
31    if root.is_file() {
32        if is_candidate(root, config.min_groups) {
33            out.push(root.to_path_buf());
34        }
35        return Ok(out);
36    }
37
38    for entry in WalkDir::new(root)
39        .into_iter()
40        .filter_entry(|e| {
41            // Prune ignored directories.
42            if e.file_type().is_dir() {
43                if let Some(name) = e.file_name().to_str() {
44                    if e.depth() > 0 && is_ignored_dir(name) {
45                        return false;
46                    }
47                }
48            }
49            true
50        })
51        .flatten()
52    {
53        let path = entry.path();
54        if path.extension().and_then(|e| e.to_str()) != Some("rs") {
55            continue;
56        }
57        if is_candidate(path, config.min_groups) {
58            out.push(path.to_path_buf());
59        }
60    }
61
62    out.sort();
63    Ok(out)
64}