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