Skip to main content

affected_core/
detect.rs

1use anyhow::Result;
2use std::path::Path;
3use tracing::debug;
4
5use crate::types::Ecosystem;
6
7/// Detect which ecosystem(s) a project uses by scanning for marker files.
8pub fn detect_ecosystems(root: &Path) -> Result<Vec<Ecosystem>> {
9    let mut detected = Vec::new();
10
11    // Cargo: Cargo.toml with [workspace]
12    let cargo_toml = root.join("Cargo.toml");
13    if cargo_toml.exists() {
14        if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
15            if content.contains("[workspace]") {
16                debug!("Detected Cargo workspace at {}", cargo_toml.display());
17                detected.push(Ecosystem::Cargo);
18            }
19        }
20    }
21
22    // Yarn: .yarnrc.yml exists → Ecosystem::Yarn (takes priority over npm)
23    let yarnrc = root.join(".yarnrc.yml");
24    if yarnrc.exists() {
25        debug!("Detected Yarn Berry project via .yarnrc.yml");
26        detected.push(Ecosystem::Yarn);
27    } else {
28        // npm/pnpm: package.json with "workspaces" or pnpm-workspace.yaml
29        let pkg_json = root.join("package.json");
30        let pnpm_ws = root.join("pnpm-workspace.yaml");
31        if pnpm_ws.exists() {
32            debug!("Detected pnpm workspace via pnpm-workspace.yaml");
33            detected.push(Ecosystem::Npm);
34        } else if pkg_json.exists() {
35            if let Ok(content) = std::fs::read_to_string(&pkg_json) {
36                if content.contains("\"workspaces\"") {
37                    debug!("Detected npm workspaces in package.json");
38                    detected.push(Ecosystem::Npm);
39                }
40            }
41        }
42    }
43
44    // Go: go.work (workspace) or go.mod (single module)
45    if root.join("go.work").exists() || root.join("go.mod").exists() {
46        debug!("Detected Go project");
47        detected.push(Ecosystem::Go);
48    }
49
50    // Python: check for Poetry, uv, or generic pyproject.toml
51    let root_pyproject = root.join("pyproject.toml");
52    if root_pyproject.exists() {
53        if let Ok(content) = std::fs::read_to_string(&root_pyproject) {
54            if content.contains("[tool.poetry]") {
55                debug!("Detected Poetry project via [tool.poetry] in pyproject.toml");
56                detected.push(Ecosystem::Python);
57            } else if content.contains("[tool.uv.workspace]") {
58                debug!("Detected uv workspace via [tool.uv.workspace] in pyproject.toml");
59                detected.push(Ecosystem::Python);
60            } else {
61                debug!("Detected generic Python project via pyproject.toml");
62                detected.push(Ecosystem::Python);
63            }
64        } else {
65            detected.push(Ecosystem::Python);
66        }
67    } else {
68        // Scan one level deep for pyproject.toml files
69        let pattern = root.join("*/pyproject.toml");
70        if let Ok(paths) = glob::glob(pattern.to_str().unwrap_or("")) {
71            let count = paths.filter_map(|p| p.ok()).count();
72            if count >= 2 {
73                debug!(
74                    "Detected Python monorepo ({} pyproject.toml files found)",
75                    count
76                );
77                detected.push(Ecosystem::Python);
78            }
79        }
80    }
81
82    // Maven: pom.xml exists at root and contains <modules>
83    let pom_xml = root.join("pom.xml");
84    if pom_xml.exists() {
85        if let Ok(content) = std::fs::read_to_string(&pom_xml) {
86            if content.contains("<modules>") {
87                debug!("Detected Maven multi-module project via pom.xml");
88                detected.push(Ecosystem::Maven);
89            }
90        }
91    }
92
93    // Gradle: settings.gradle or settings.gradle.kts exists
94    if root.join("settings.gradle").exists() || root.join("settings.gradle.kts").exists() {
95        debug!("Detected Gradle project");
96        detected.push(Ecosystem::Gradle);
97    }
98
99    if detected.is_empty() {
100        anyhow::bail!(
101            "No supported project type found at {}.\n\
102             Looked for: Cargo.toml (workspace), package.json (workspaces), \
103             go.work/go.mod, pyproject.toml, pom.xml (modules), settings.gradle(.kts)",
104            root.display()
105        );
106    }
107
108    debug!("Detected ecosystems: {:?}", detected);
109    Ok(detected)
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_detect_cargo_workspace() {
118        let dir = tempfile::tempdir().unwrap();
119        std::fs::write(
120            dir.path().join("Cargo.toml"),
121            "[workspace]\nmembers = [\"crates/*\"]\n",
122        )
123        .unwrap();
124
125        let ecosystems = detect_ecosystems(dir.path()).unwrap();
126        assert_eq!(ecosystems, vec![Ecosystem::Cargo]);
127    }
128
129    #[test]
130    fn test_detect_cargo_without_workspace_ignored() {
131        let dir = tempfile::tempdir().unwrap();
132        std::fs::write(
133            dir.path().join("Cargo.toml"),
134            "[package]\nname = \"solo\"\n",
135        )
136        .unwrap();
137
138        assert!(detect_ecosystems(dir.path()).is_err());
139    }
140
141    #[test]
142    fn test_detect_npm_workspaces() {
143        let dir = tempfile::tempdir().unwrap();
144        std::fs::write(
145            dir.path().join("package.json"),
146            r#"{"name": "root", "workspaces": ["packages/*"]}"#,
147        )
148        .unwrap();
149
150        let ecosystems = detect_ecosystems(dir.path()).unwrap();
151        assert_eq!(ecosystems, vec![Ecosystem::Npm]);
152    }
153
154    #[test]
155    fn test_detect_pnpm_workspace() {
156        let dir = tempfile::tempdir().unwrap();
157        std::fs::write(
158            dir.path().join("pnpm-workspace.yaml"),
159            "packages:\n  - 'packages/*'\n",
160        )
161        .unwrap();
162
163        let ecosystems = detect_ecosystems(dir.path()).unwrap();
164        assert_eq!(ecosystems, vec![Ecosystem::Npm]);
165    }
166
167    #[test]
168    fn test_detect_yarn_workspace() {
169        let dir = tempfile::tempdir().unwrap();
170        std::fs::write(dir.path().join(".yarnrc.yml"), "nodeLinker: pnp\n").unwrap();
171
172        let ecosystems = detect_ecosystems(dir.path()).unwrap();
173        assert_eq!(ecosystems, vec![Ecosystem::Yarn]);
174    }
175
176    #[test]
177    fn test_detect_go_workspace() {
178        let dir = tempfile::tempdir().unwrap();
179        std::fs::write(dir.path().join("go.work"), "go 1.21\n").unwrap();
180
181        let ecosystems = detect_ecosystems(dir.path()).unwrap();
182        assert_eq!(ecosystems, vec![Ecosystem::Go]);
183    }
184
185    #[test]
186    fn test_detect_go_single_module() {
187        let dir = tempfile::tempdir().unwrap();
188        std::fs::write(dir.path().join("go.mod"), "module example.com/foo\n").unwrap();
189
190        let ecosystems = detect_ecosystems(dir.path()).unwrap();
191        assert_eq!(ecosystems, vec![Ecosystem::Go]);
192    }
193
194    #[test]
195    fn test_detect_python_root_pyproject() {
196        let dir = tempfile::tempdir().unwrap();
197        std::fs::write(
198            dir.path().join("pyproject.toml"),
199            "[project]\nname = \"myapp\"\n",
200        )
201        .unwrap();
202
203        let ecosystems = detect_ecosystems(dir.path()).unwrap();
204        assert_eq!(ecosystems, vec![Ecosystem::Python]);
205    }
206
207    #[test]
208    fn test_detect_multiple_ecosystems() {
209        let dir = tempfile::tempdir().unwrap();
210        std::fs::write(dir.path().join("Cargo.toml"), "[workspace]\nmembers = []\n").unwrap();
211        std::fs::write(dir.path().join("go.mod"), "module example.com/x\n").unwrap();
212
213        let ecosystems = detect_ecosystems(dir.path()).unwrap();
214        assert!(ecosystems.contains(&Ecosystem::Cargo));
215        assert!(ecosystems.contains(&Ecosystem::Go));
216        assert_eq!(ecosystems.len(), 2);
217    }
218
219    #[test]
220    fn test_detect_empty_directory_errors() {
221        let dir = tempfile::tempdir().unwrap();
222        assert!(detect_ecosystems(dir.path()).is_err());
223    }
224
225    #[test]
226    fn test_detect_npm_without_workspaces_ignored() {
227        let dir = tempfile::tempdir().unwrap();
228        std::fs::write(
229            dir.path().join("package.json"),
230            r#"{"name": "solo", "version": "1.0.0"}"#,
231        )
232        .unwrap();
233
234        assert!(detect_ecosystems(dir.path()).is_err());
235    }
236
237    #[test]
238    fn test_detect_maven_multi_module() {
239        let dir = tempfile::tempdir().unwrap();
240        std::fs::write(
241            dir.path().join("pom.xml"),
242            r#"<project><modules><module>core</module></modules></project>"#,
243        )
244        .unwrap();
245
246        let ecosystems = detect_ecosystems(dir.path()).unwrap();
247        assert_eq!(ecosystems, vec![Ecosystem::Maven]);
248    }
249
250    #[test]
251    fn test_detect_gradle_groovy() {
252        let dir = tempfile::tempdir().unwrap();
253        std::fs::write(
254            dir.path().join("settings.gradle"),
255            "include ':core', ':app'\n",
256        )
257        .unwrap();
258
259        let ecosystems = detect_ecosystems(dir.path()).unwrap();
260        assert_eq!(ecosystems, vec![Ecosystem::Gradle]);
261    }
262
263    #[test]
264    fn test_detect_gradle_kotlin() {
265        let dir = tempfile::tempdir().unwrap();
266        std::fs::write(
267            dir.path().join("settings.gradle.kts"),
268            "include(\":core\", \":app\")\n",
269        )
270        .unwrap();
271
272        let ecosystems = detect_ecosystems(dir.path()).unwrap();
273        assert_eq!(ecosystems, vec![Ecosystem::Gradle]);
274    }
275
276    #[test]
277    fn test_detect_poetry_project() {
278        let dir = tempfile::tempdir().unwrap();
279        std::fs::write(
280            dir.path().join("pyproject.toml"),
281            "[tool.poetry]\nname = \"myapp\"\n",
282        )
283        .unwrap();
284
285        let ecosystems = detect_ecosystems(dir.path()).unwrap();
286        assert_eq!(ecosystems, vec![Ecosystem::Python]);
287    }
288
289    #[test]
290    fn test_detect_uv_workspace() {
291        let dir = tempfile::tempdir().unwrap();
292        std::fs::write(
293            dir.path().join("pyproject.toml"),
294            "[tool.uv.workspace]\nmembers = [\"packages/*\"]\n",
295        )
296        .unwrap();
297
298        let ecosystems = detect_ecosystems(dir.path()).unwrap();
299        assert_eq!(ecosystems, vec![Ecosystem::Python]);
300    }
301}