1use crate::analyzer::{AnalysisConfig, DetectedTechnology, DetectedLanguage};
2use crate::analyzer::frameworks::*;
3use crate::error::Result;
4use std::path::Path;
5
6pub 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 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 let resolved_technologies = FrameworkDetectionUtils::resolve_technology_conflicts(all_technologies);
35
36 let final_technologies = FrameworkDetectionUtils::mark_primary_technologies(resolved_technologies);
38
39 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 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 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); }
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 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 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 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(), ],
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 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}