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    if crate::workspace::load_root_deno_workspace_patterns(dir)
89        .ok()
90        .flatten()
91        .is_some_and(|(_path, patterns)| !patterns.is_empty())
92    {
93        return true;
94    }
95    let pkg_path = dir.join("package.json");
96    if !pkg_path.exists() {
97        return false;
98    }
99    let Ok(content) = std::fs::read_to_string(&pkg_path) else {
100        return false;
101    };
102    let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) else {
103        return false;
104    };
105    value
106        .get("workspaces")
107        .is_some_and(|v| v.is_array() || v.is_object())
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn config_fixable_true_when_config_exists() {
116        let dir = tempfile::tempdir().unwrap();
117        std::fs::write(dir.path().join(".fallowrc.json"), "{}").unwrap();
118        assert!(is_config_fixable(dir.path(), None));
119    }
120
121    #[test]
122    fn config_fixable_true_when_can_create_at_root() {
123        let dir = tempfile::tempdir().unwrap();
124        assert!(is_config_fixable(dir.path(), None));
125    }
126
127    #[test]
128    fn config_fixable_false_when_monorepo_subpackage() {
129        let dir = tempfile::tempdir().unwrap();
130        std::fs::write(
131            dir.path().join("pnpm-workspace.yaml"),
132            "packages:\n  - packages/*\n",
133        )
134        .unwrap();
135        let sub = dir.path().join("packages/app");
136        std::fs::create_dir_all(&sub).unwrap();
137        assert!(!is_config_fixable(&sub, None));
138    }
139}