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_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 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 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 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(), ],
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 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}