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_python_fastapi_detection() {
125        let language = DetectedLanguage {
126            name: "Python".to_string(),
127            version: Some("3.11.0".to_string()),
128            confidence: 0.95,
129            files: vec![PathBuf::from("main.py")],
130            main_dependencies: vec![
131                "fastapi".to_string(),
132                "uvicorn".to_string(),
133                "pydantic".to_string(),
134            ],
135            dev_dependencies: vec!["pytest".to_string()],
136            package_manager: Some("pip".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        // Should detect FastAPI and Uvicorn
145        let fastapi = technologies.iter().find(|t| t.name == "FastAPI");
146        let uvicorn = technologies.iter().find(|t| t.name == "Uvicorn");
147        
148        if let Some(fastapi_tech) = fastapi {
149            assert!(matches!(fastapi_tech.category, TechnologyCategory::BackendFramework));
150            assert!(fastapi_tech.is_primary);
151        }
152        
153        if let Some(uvicorn_tech) = uvicorn {
154            assert!(matches!(uvicorn_tech.category, TechnologyCategory::Runtime));
155            assert!(!uvicorn_tech.is_primary);
156        }
157    }
158    
159    #[test]
160    fn test_go_gin_detection() {
161        let language = DetectedLanguage {
162            name: "Go".to_string(),
163            version: Some("1.21.0".to_string()),
164            confidence: 0.95,
165            files: vec![PathBuf::from("main.go")],
166            main_dependencies: vec![
167                "github.com/gin-gonic/gin".to_string(),
168                "gorm.io/gorm".to_string(),
169            ],
170            dev_dependencies: vec!["github.com/stretchr/testify".to_string()],
171            package_manager: Some("go mod".to_string()),
172        };
173        
174        let config = AnalysisConfig::default();
175        let project_root = Path::new(".");
176        
177        let technologies = detect_frameworks(project_root, &[language], &config).unwrap();
178        
179        // Should detect Gin and GORM
180        let gin = technologies.iter().find(|t| t.name == "Gin");
181        let gorm = technologies.iter().find(|t| t.name == "GORM");
182        
183        if let Some(gin_tech) = gin {
184            assert!(matches!(gin_tech.category, TechnologyCategory::BackendFramework));
185            assert!(gin_tech.is_primary);
186        }
187        
188        if let Some(gorm_tech) = gorm {
189            assert!(matches!(gorm_tech.category, TechnologyCategory::Database));
190            assert!(!gorm_tech.is_primary);
191        }
192    }
193    
194    #[test]
195    fn test_java_spring_boot_detection() {
196        let language = DetectedLanguage {
197            name: "Java".to_string(),
198            version: Some("17.0.0".to_string()),
199            confidence: 0.95,
200            files: vec![PathBuf::from("src/main/java/Application.java")],
201            main_dependencies: vec![
202                "spring-boot".to_string(),
203                "spring-web".to_string(),
204            ],
205            dev_dependencies: vec!["junit".to_string()],
206            package_manager: Some("maven".to_string()),
207        };
208        
209        let config = AnalysisConfig::default();
210        let project_root = Path::new(".");
211        
212        let technologies = detect_frameworks(project_root, &[language], &config).unwrap();
213        
214        // Should detect Spring Boot
215        let spring_boot = technologies.iter().find(|t| t.name == "Spring Boot");
216        
217        if let Some(spring) = spring_boot {
218            assert!(matches!(spring.category, TechnologyCategory::BackendFramework));
219            assert!(spring.is_primary);
220        }
221    }
222
223    #[test]
224    fn test_technology_conflicts_resolution() {
225        let language = DetectedLanguage {
226            name: "Rust".to_string(),
227            version: Some("1.70.0".to_string()),
228            confidence: 0.95,
229            files: vec![PathBuf::from("src/main.rs")],
230            main_dependencies: vec![
231                "tokio".to_string(),
232                "async-std".to_string(), // These should conflict
233            ],
234            dev_dependencies: vec![],
235            package_manager: Some("cargo".to_string()),
236        };
237        
238        let config = AnalysisConfig::default();
239        let project_root = Path::new(".");
240        
241        let technologies = detect_frameworks(project_root, &[language], &config).unwrap();
242        
243        // Should only have one async runtime (higher confidence wins)
244        let async_runtimes: Vec<_> = technologies.iter()
245            .filter(|t| matches!(t.category, TechnologyCategory::Runtime))
246            .collect();
247        
248        assert!(async_runtimes.len() <= 1, "Should resolve conflicting async runtimes: found {:?}", 
249               async_runtimes.iter().map(|t| &t.name).collect::<Vec<_>>());
250    }
251}