Skip to main content

diskforge_core/
detector.rs

1use std::path::Path;
2
3use crate::types::ProjectType;
4
5/// Marker file → project type mapping
6const MARKERS: &[(&str, ProjectType)] = &[
7    ("Cargo.toml", ProjectType::Rust),
8    ("pubspec.yaml", ProjectType::Flutter),
9    ("build.gradle", ProjectType::Gradle),
10    ("build.gradle.kts", ProjectType::Gradle),
11    ("pom.xml", ProjectType::Maven),
12    ("Package.swift", ProjectType::Swift),
13    ("CMakeLists.txt", ProjectType::CMake),
14    ("build.zig", ProjectType::Zig),
15    ("turbo.json", ProjectType::Turborepo),
16];
17
18/// Markers that need disambiguation (package.json can be Node, Next.js, or React Native)
19const PACKAGE_JSON_MARKERS: &[(&str, ProjectType)] = &[
20    ("next.config.js", ProjectType::NextJs),
21    ("next.config.mjs", ProjectType::NextJs),
22    ("next.config.ts", ProjectType::NextJs),
23];
24
25/// Detect project type from a directory by checking for marker files
26pub fn detect_project_type(dir: &Path) -> Option<ProjectType> {
27    let entries: Vec<String> = std::fs::read_dir(dir)
28        .ok()?
29        .filter_map(|e| e.ok())
30        .filter_map(|e| e.file_name().into_string().ok())
31        .collect();
32
33    // Check specific markers first (higher priority)
34    for (marker, project_type) in MARKERS {
35        if entries.iter().any(|e| e == *marker) {
36            return Some(project_type.clone());
37        }
38    }
39
40    // Check for .csproj/.fsproj files (.NET)
41    if entries
42        .iter()
43        .any(|e| e.ends_with(".csproj") || e.ends_with(".fsproj"))
44    {
45        return Some(ProjectType::DotNet);
46    }
47
48    // Check for Python projects
49    if entries.iter().any(|e| {
50        e == "pyproject.toml" || e == "setup.py" || e == "requirements.txt" || e == "Pipfile"
51    }) {
52        return Some(ProjectType::Python);
53    }
54
55    // Check for Go modules
56    if entries.iter().any(|e| e == "go.mod") {
57        return Some(ProjectType::Go);
58    }
59
60    // Disambiguate package.json projects
61    if entries.iter().any(|e| e == "package.json") {
62        // Check for Next.js
63        for (marker, project_type) in PACKAGE_JSON_MARKERS {
64            if entries.iter().any(|e| e == *marker) {
65                return Some(project_type.clone());
66            }
67        }
68
69        // Check for React Native
70        if entries.iter().any(|e| e == "ios" || e == "android") {
71            let has_ios_dir = dir.join("ios").is_dir();
72            let has_android_dir = dir.join("android").is_dir();
73            if has_ios_dir || has_android_dir {
74                return Some(ProjectType::ReactNative);
75            }
76        }
77
78        // Check for Podfile (CocoaPods, often inside ios/)
79        if entries.iter().any(|e| e == "Podfile") {
80            return Some(ProjectType::CocoaPods);
81        }
82
83        return Some(ProjectType::Node);
84    }
85
86    None
87}
88
89/// Get the artifact directories for a project type
90pub fn artifact_dirs(project_type: &ProjectType) -> &'static [&'static str] {
91    match project_type {
92        ProjectType::Rust => &["target"],
93        ProjectType::Node => &["node_modules", ".angular"],
94        ProjectType::Flutter => &[".dart_tool", "build"],
95        ProjectType::Gradle => &["build", ".gradle"],
96        ProjectType::Maven => &["target"],
97        ProjectType::Swift => &[".build"],
98        ProjectType::CMake => &["build"],
99        ProjectType::DotNet => &["bin", "obj"],
100        ProjectType::Python => &[
101            "__pycache__",
102            ".venv",
103            "venv",
104            ".mypy_cache",
105            ".pytest_cache",
106            ".ruff_cache",
107            ".tox",
108        ],
109        ProjectType::Go => &[],
110        ProjectType::Zig => &["zig-cache", "zig-out"],
111        ProjectType::Turborepo => &[".turbo"],
112        ProjectType::NextJs => &["node_modules", ".next"],
113        ProjectType::CocoaPods => &["Pods"],
114        ProjectType::ReactNative => &["node_modules", "ios/Pods", "android/build", ".expo"],
115    }
116}
117
118/// Get the rebuild command hint for a project type
119pub fn rebuild_hint(project_type: &ProjectType) -> &'static str {
120    match project_type {
121        ProjectType::Rust => "cargo build",
122        ProjectType::Node => "npm install",
123        ProjectType::Flutter => "flutter pub get",
124        ProjectType::Gradle => "gradle build",
125        ProjectType::Maven => "mvn install",
126        ProjectType::Swift => "swift build",
127        ProjectType::CMake => "cmake --build .",
128        ProjectType::DotNet => "dotnet restore",
129        ProjectType::Python => "pip install -r requirements.txt",
130        ProjectType::Go => "go mod download",
131        ProjectType::Zig => "zig build",
132        ProjectType::Turborepo => "turbo run build",
133        ProjectType::NextJs => "npm install && npm run build",
134        ProjectType::CocoaPods => "pod install",
135        ProjectType::ReactNative => "npm install && cd ios && pod install",
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use std::fs;
143
144    fn temp_project(name: &str, markers: &[&str]) -> std::path::PathBuf {
145        let dir = std::env::temp_dir().join(format!("diskforge_test_{name}"));
146        let _ = fs::remove_dir_all(&dir);
147        fs::create_dir_all(&dir).unwrap();
148        for marker in markers {
149            fs::write(dir.join(marker), "").unwrap();
150        }
151        dir
152    }
153
154    fn cleanup(dir: &std::path::Path) {
155        let _ = fs::remove_dir_all(dir);
156    }
157
158    #[test]
159    fn detect_rust() {
160        let dir = temp_project("rust", &["Cargo.toml"]);
161        assert_eq!(detect_project_type(&dir), Some(ProjectType::Rust));
162        cleanup(&dir);
163    }
164
165    #[test]
166    fn detect_flutter() {
167        let dir = temp_project("flutter", &["pubspec.yaml"]);
168        assert_eq!(detect_project_type(&dir), Some(ProjectType::Flutter));
169        cleanup(&dir);
170    }
171
172    #[test]
173    fn detect_gradle() {
174        let dir = temp_project("gradle", &["build.gradle"]);
175        assert_eq!(detect_project_type(&dir), Some(ProjectType::Gradle));
176        cleanup(&dir);
177    }
178
179    #[test]
180    fn detect_node() {
181        let dir = temp_project("node", &["package.json"]);
182        assert_eq!(detect_project_type(&dir), Some(ProjectType::Node));
183        cleanup(&dir);
184    }
185
186    #[test]
187    fn detect_nextjs() {
188        let dir = temp_project("nextjs", &["package.json", "next.config.js"]);
189        assert_eq!(detect_project_type(&dir), Some(ProjectType::NextJs));
190        cleanup(&dir);
191    }
192
193    #[test]
194    fn detect_python() {
195        let dir = temp_project("python", &["pyproject.toml"]);
196        assert_eq!(detect_project_type(&dir), Some(ProjectType::Python));
197        cleanup(&dir);
198    }
199
200    #[test]
201    fn detect_go() {
202        let dir = temp_project("go", &["go.mod"]);
203        assert_eq!(detect_project_type(&dir), Some(ProjectType::Go));
204        cleanup(&dir);
205    }
206
207    #[test]
208    fn detect_dotnet() {
209        let dir = temp_project("dotnet", &["MyApp.csproj"]);
210        assert_eq!(detect_project_type(&dir), Some(ProjectType::DotNet));
211        cleanup(&dir);
212    }
213
214    #[test]
215    fn detect_empty_dir() {
216        let dir = temp_project("empty", &[]);
217        assert_eq!(detect_project_type(&dir), None);
218        cleanup(&dir);
219    }
220
221    #[test]
222    fn artifact_dirs_and_hints_for_all_types() {
223        let all = [
224            ProjectType::Rust,
225            ProjectType::Node,
226            ProjectType::Flutter,
227            ProjectType::Gradle,
228            ProjectType::Maven,
229            ProjectType::Swift,
230            ProjectType::CMake,
231            ProjectType::DotNet,
232            ProjectType::Python,
233            ProjectType::Go,
234            ProjectType::Zig,
235            ProjectType::Turborepo,
236            ProjectType::NextJs,
237            ProjectType::CocoaPods,
238            ProjectType::ReactNative,
239        ];
240        for pt in &all {
241            let dirs = artifact_dirs(pt);
242            let hint = rebuild_hint(pt);
243            assert!(!hint.is_empty(), "{pt:?} has empty rebuild hint");
244            let _ = dirs;
245        }
246    }
247}