syncable_cli/analyzer/
framework_detector.rs

1use crate::analyzer::{AnalysisConfig, DetectedTechnology, DetectedLanguage};
2use crate::analyzer::frameworks::*;
3use crate::error::Result;
4use std::path::Path;
5
6/// Detects technologies (frameworks, libraries, tools) with proper classification
7pub fn detect_frameworks(
8    _project_root: &Path,
9    languages: &[DetectedLanguage],
10    _config: &AnalysisConfig,
11) -> Result<Vec<DetectedTechnology>> {
12    let mut all_technologies = Vec::new();
13    
14    // Initialize language-specific detectors
15    let rust_detector = rust::RustFrameworkDetector;
16    let js_detector = javascript::JavaScriptFrameworkDetector;
17    let python_detector = python::PythonFrameworkDetector;
18    let go_detector = go::GoFrameworkDetector;
19    let java_detector = java::JavaFrameworkDetector;
20    
21    for language in languages {
22        let lang_technologies = match language.name.as_str() {
23            "Rust" => rust_detector.detect_frameworks(language)?,
24            "JavaScript" | "TypeScript" | "JavaScript/TypeScript" => js_detector.detect_frameworks(language)?,
25            "Python" => python_detector.detect_frameworks(language)?,
26            "Go" => go_detector.detect_frameworks(language)?,
27            "Java" | "Kotlin" | "Java/Kotlin" => java_detector.detect_frameworks(language)?,
28            _ => Vec::new(),
29        };
30        all_technologies.extend(lang_technologies);
31    }
32    
33    // Apply exclusivity rules and resolve conflicts
34    let resolved_technologies = FrameworkDetectionUtils::resolve_technology_conflicts(all_technologies);
35    
36    // Mark primary technologies
37    let final_technologies = FrameworkDetectionUtils::mark_primary_technologies(resolved_technologies);
38    
39    // Sort by confidence and remove exact duplicates
40    let mut result = final_technologies;
41    result.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
42    result.dedup_by(|a, b| a.name == b.name);
43    
44    Ok(result)
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::analyzer::{TechnologyCategory, LibraryType};
51    use std::path::PathBuf;
52    
53    #[test]
54    fn test_rust_actix_web_detection() {
55        let language = DetectedLanguage {
56            name: "Rust".to_string(),
57            version: Some("1.70.0".to_string()),
58            confidence: 0.9,
59            files: vec![PathBuf::from("src/main.rs")],
60            main_dependencies: vec!["actix-web".to_string(), "tokio".to_string()],
61            dev_dependencies: vec!["assert_cmd".to_string()],
62            package_manager: Some("cargo".to_string()),
63        };
64        
65        let config = AnalysisConfig::default();
66        let project_root = Path::new(".");
67        
68        let technologies = detect_frameworks(project_root, &[language], &config).unwrap();
69        
70        // Should detect Actix Web and Tokio
71        let actix_web = technologies.iter().find(|t| t.name == "Actix Web");
72        let tokio = technologies.iter().find(|t| t.name == "Tokio");
73        
74        if let Some(actix) = actix_web {
75            assert!(matches!(actix.category, TechnologyCategory::BackendFramework));
76            assert!(actix.is_primary);
77            assert!(actix.confidence > 0.8);
78        }
79        
80        if let Some(tokio_tech) = tokio {
81            assert!(matches!(tokio_tech.category, TechnologyCategory::Runtime));
82            assert!(!tokio_tech.is_primary);
83        }
84    }
85    
86    #[test]
87    fn test_javascript_next_js_detection() {
88        let language = DetectedLanguage {
89            name: "JavaScript".to_string(),
90            version: Some("18.0.0".to_string()),
91            confidence: 0.9,
92            files: vec![PathBuf::from("pages/index.js")],
93            main_dependencies: vec![
94                "next".to_string(),
95                "react".to_string(),
96                "react-dom".to_string(),
97            ],
98            dev_dependencies: vec!["eslint".to_string()],
99            package_manager: Some("npm".to_string()),
100        };
101        
102        let config = AnalysisConfig::default();
103        let project_root = Path::new(".");
104        
105        let technologies = detect_frameworks(project_root, &[language], &config).unwrap();
106        
107        // Should detect Next.js and React
108        let nextjs = technologies.iter().find(|t| t.name == "Next.js");
109        let react = technologies.iter().find(|t| t.name == "React");
110        
111        if let Some(next) = nextjs {
112            assert!(matches!(next.category, TechnologyCategory::MetaFramework));
113            assert!(next.is_primary);
114            assert!(next.requires.contains(&"React".to_string()));
115        }
116        
117        if let Some(react_tech) = react {
118            assert!(matches!(react_tech.category, TechnologyCategory::Library(LibraryType::UI)));
119            assert!(!react_tech.is_primary); // Should be false since Next.js is the meta-framework
120        }
121    }
122
123    #[test]
124    fn test_vite_react_is_not_misclassified_as_next() {
125        let language = DetectedLanguage {
126            name: "TypeScript".to_string(),
127            version: Some("18.0.0".to_string()),
128            confidence: 0.9,
129            files: vec![PathBuf::from("src/App.tsx")],
130            main_dependencies: vec![
131                "react".to_string(),
132                "react-dom".to_string(),
133                "vite".to_string(),
134            ],
135            dev_dependencies: vec!["vite".to_string()],
136            package_manager: Some("npm".to_string()),
137        };
138
139        let config = AnalysisConfig::default();
140        let project_root = Path::new(".");
141
142        let technologies = detect_frameworks(project_root, &[language], &config).unwrap();
143
144        assert!(technologies.iter().any(|t| t.name == "Vite"));
145        assert!(technologies.iter().any(|t| t.name == "React"));
146        assert!(technologies.iter().all(|t| t.name != "Next.js"));
147    }
148
149    #[test]
150    fn test_tanstack_start_detection_over_structure_only() {
151        let language = DetectedLanguage {
152            name: "TypeScript".to_string(),
153            version: Some("18.0.0".to_string()),
154            confidence: 0.9,
155            files: vec![PathBuf::from("app/routes/index.tsx")],
156            main_dependencies: vec![
157                "@tanstack/react-start".to_string(),
158                "@tanstack/react-router".to_string(),
159                "react".to_string(),
160                "react-dom".to_string(),
161            ],
162            dev_dependencies: vec![],
163            package_manager: Some("npm".to_string()),
164        };
165
166        let config = AnalysisConfig::default();
167        let project_root = Path::new(".");
168
169        let technologies = detect_frameworks(project_root, &[language], &config).unwrap();
170
171        assert!(technologies.iter().any(|t| t.name == "Tanstack Start"));
172        assert!(technologies.iter().all(|t| t.name != "Next.js"));
173    }
174    
175    #[test]
176    fn test_python_fastapi_detection() {
177        let language = DetectedLanguage {
178            name: "Python".to_string(),
179            version: Some("3.11.0".to_string()),
180            confidence: 0.95,
181            files: vec![PathBuf::from("main.py")],
182            main_dependencies: vec![
183                "fastapi".to_string(),
184                "uvicorn".to_string(),
185                "pydantic".to_string(),
186            ],
187            dev_dependencies: vec!["pytest".to_string()],
188            package_manager: Some("pip".to_string()),
189        };
190        
191        let config = AnalysisConfig::default();
192        let project_root = Path::new(".");
193        
194        let technologies = detect_frameworks(project_root, &[language], &config).unwrap();
195        
196        // Should detect FastAPI and Uvicorn
197        let fastapi = technologies.iter().find(|t| t.name == "FastAPI");
198        let uvicorn = technologies.iter().find(|t| t.name == "Uvicorn");
199        
200        if let Some(fastapi_tech) = fastapi {
201            assert!(matches!(fastapi_tech.category, TechnologyCategory::BackendFramework));
202            assert!(fastapi_tech.is_primary);
203        }
204        
205        if let Some(uvicorn_tech) = uvicorn {
206            assert!(matches!(uvicorn_tech.category, TechnologyCategory::Runtime));
207            assert!(!uvicorn_tech.is_primary);
208        }
209    }
210    
211    #[test]
212    fn test_go_gin_detection() {
213        let language = DetectedLanguage {
214            name: "Go".to_string(),
215            version: Some("1.21.0".to_string()),
216            confidence: 0.95,
217            files: vec![PathBuf::from("main.go")],
218            main_dependencies: vec![
219                "github.com/gin-gonic/gin".to_string(),
220                "gorm.io/gorm".to_string(),
221            ],
222            dev_dependencies: vec!["github.com/stretchr/testify".to_string()],
223            package_manager: Some("go mod".to_string()),
224        };
225        
226        let config = AnalysisConfig::default();
227        let project_root = Path::new(".");
228        
229        let technologies = detect_frameworks(project_root, &[language], &config).unwrap();
230        
231        // Should detect Gin and GORM
232        let gin = technologies.iter().find(|t| t.name == "Gin");
233        let gorm = technologies.iter().find(|t| t.name == "GORM");
234        
235        if let Some(gin_tech) = gin {
236            assert!(matches!(gin_tech.category, TechnologyCategory::BackendFramework));
237            assert!(gin_tech.is_primary);
238        }
239        
240        if let Some(gorm_tech) = gorm {
241            assert!(matches!(gorm_tech.category, TechnologyCategory::Database));
242            assert!(!gorm_tech.is_primary);
243        }
244    }
245    
246    #[test]
247    fn test_java_spring_boot_detection() {
248        let language = DetectedLanguage {
249            name: "Java".to_string(),
250            version: Some("17.0.0".to_string()),
251            confidence: 0.95,
252            files: vec![PathBuf::from("src/main/java/Application.java")],
253            main_dependencies: vec![
254                "spring-boot".to_string(),
255                "spring-web".to_string(),
256            ],
257            dev_dependencies: vec!["junit".to_string()],
258            package_manager: Some("maven".to_string()),
259        };
260        
261        let config = AnalysisConfig::default();
262        let project_root = Path::new(".");
263        
264        let technologies = detect_frameworks(project_root, &[language], &config).unwrap();
265        
266        // Should detect Spring Boot
267        let spring_boot = technologies.iter().find(|t| t.name == "Spring Boot");
268        
269        if let Some(spring) = spring_boot {
270            assert!(matches!(spring.category, TechnologyCategory::BackendFramework));
271            assert!(spring.is_primary);
272        }
273    }
274
275    #[test]
276    fn test_technology_conflicts_resolution() {
277        let language = DetectedLanguage {
278            name: "Rust".to_string(),
279            version: Some("1.70.0".to_string()),
280            confidence: 0.95,
281            files: vec![PathBuf::from("src/main.rs")],
282            main_dependencies: vec![
283                "tokio".to_string(),
284                "async-std".to_string(), // These should conflict
285            ],
286            dev_dependencies: vec![],
287            package_manager: Some("cargo".to_string()),
288        };
289        
290        let config = AnalysisConfig::default();
291        let project_root = Path::new(".");
292        
293        let technologies = detect_frameworks(project_root, &[language], &config).unwrap();
294        
295        // Should only have one async runtime (higher confidence wins)
296        let async_runtimes: Vec<_> = technologies.iter()
297            .filter(|t| matches!(t.category, TechnologyCategory::Runtime))
298            .collect();
299        
300        assert!(async_runtimes.len() <= 1, "Should resolve conflicting async runtimes: found {:?}", 
301               async_runtimes.iter().map(|t| &t.name).collect::<Vec<_>>());
302    }
303}