Skip to main content

fallow_config/
fixability.rs

1use std::path::{Path, PathBuf};
2
3use crate::FallowConfig;
4
5/// Classification of whether fallow can apply config edits at `root`.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ConfigFixPlan {
8    /// A fallow config file exists; append entries in place.
9    Edit { config_path: PathBuf },
10    /// No fallow config exists, but a workspace marker sits above `root`,
11    /// so creating one inside this subpackage would fragment the monorepo.
12    BlockedMonorepo { workspace_root: PathBuf },
13    /// No fallow config exists and config creation was disabled.
14    BlockedNoCreate { target: PathBuf },
15    /// No fallow config exists; the writer can create one at `target`.
16    Create { target: PathBuf },
17}
18
19/// Classify how config-editing fixes should behave for `root`.
20#[must_use]
21pub fn classify_config_fix_plan(
22    root: &Path,
23    explicit: Option<&PathBuf>,
24    no_create_config: bool,
25) -> ConfigFixPlan {
26    if let Some(existing) = resolve_existing_config_path(root, explicit) {
27        return ConfigFixPlan::Edit {
28            config_path: existing,
29        };
30    }
31    let target = root.join(".fallowrc.json");
32    if let Some(workspace_root) = find_workspace_root_above(root) {
33        return ConfigFixPlan::BlockedMonorepo { workspace_root };
34    }
35    if no_create_config {
36        return ConfigFixPlan::BlockedNoCreate { target };
37    }
38    ConfigFixPlan::Create { target }
39}
40
41/// Whether `fallow fix --yes` can apply config edits at `root` with default
42/// config-creation behavior. Drives JSON `auto_fixable` for config actions.
43#[must_use]
44pub fn is_config_fixable(root: &Path, explicit: Option<&PathBuf>) -> bool {
45    matches!(
46        classify_config_fix_plan(root, explicit, false),
47        ConfigFixPlan::Edit { .. } | ConfigFixPlan::Create { .. }
48    )
49}
50
51fn resolve_existing_config_path(root: &Path, explicit: Option<&PathBuf>) -> Option<PathBuf> {
52    if let Some(path) = explicit {
53        let absolute = if path.is_absolute() {
54            path.clone()
55        } else {
56            std::env::current_dir().map_or_else(|_| path.clone(), |cwd| cwd.join(path))
57        };
58        if absolute.exists() {
59            return Some(absolute);
60        }
61        return None;
62    }
63    FallowConfig::find_config_path(root)
64}
65
66fn find_workspace_root_above(start: &Path) -> Option<PathBuf> {
67    let mut current = start.parent()?;
68    loop {
69        if has_workspace_marker(current) {
70            return Some(current.to_path_buf());
71        }
72        current = current.parent()?;
73    }
74}
75
76fn has_workspace_marker(dir: &Path) -> bool {
77    const SENTINELS: &[&str] = &[
78        "pnpm-workspace.yaml",
79        "turbo.json",
80        "lerna.json",
81        "rush.json",
82    ];
83    for name in SENTINELS {
84        if dir.join(name).exists() {
85            return true;
86        }
87    }
88    let pkg_path = dir.join("package.json");
89    if !pkg_path.exists() {
90        return false;
91    }
92    let Ok(content) = std::fs::read_to_string(&pkg_path) else {
93        return false;
94    };
95    let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) else {
96        return false;
97    };
98    value
99        .get("workspaces")
100        .is_some_and(|v| v.is_array() || v.is_object())
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn config_fixable_true_when_config_exists() {
109        let dir = tempfile::tempdir().unwrap();
110        std::fs::write(dir.path().join(".fallowrc.json"), "{}").unwrap();
111        assert!(is_config_fixable(dir.path(), None));
112    }
113
114    #[test]
115    fn config_fixable_true_when_can_create_at_root() {
116        let dir = tempfile::tempdir().unwrap();
117        assert!(is_config_fixable(dir.path(), None));
118    }
119
120    #[test]
121    fn config_fixable_false_when_monorepo_subpackage() {
122        let dir = tempfile::tempdir().unwrap();
123        std::fs::write(
124            dir.path().join("pnpm-workspace.yaml"),
125            "packages:\n  - packages/*\n",
126        )
127        .unwrap();
128        let sub = dir.path().join("packages/app");
129        std::fs::create_dir_all(&sub).unwrap();
130        assert!(!is_config_fixable(&sub, None));
131    }
132}