1use anyhow::Result;
4use serde_json::{Value, json};
5use std::collections::HashMap;
6use std::fs;
7use std::path::Path;
8
9use crate::cache::CacheManager;
10
11pub fn detect_project_type(_cache: &CacheManager, root: &Path) -> Result<String> {
13 let indicators = detect_project_type_indicators(root);
14
15 if indicators.is_empty() {
16 return Ok("Unknown project type".to_string());
17 }
18
19 let mut output = Vec::new();
20 output.push(format!("{}\n", indicators[0].category));
21
22 if indicators.len() > 1 || !indicators[0].details.is_empty() {
23 output.push("Indicators:".to_string());
24 for indicator in &indicators {
25 for detail in &indicator.details {
26 output.push(format!("- {}", detail));
27 }
28 }
29 }
30
31 Ok(output.join("\n"))
32}
33
34pub fn detect_project_type_json(_cache: &CacheManager, root: &Path) -> Result<Value> {
36 let indicators = detect_project_type_indicators(root);
37
38 if indicators.is_empty() {
39 return Ok(json!({
40 "category": "unknown",
41 "indicators": []
42 }));
43 }
44
45 let primary = &indicators[0];
46 let all_details: Vec<String> = indicators.iter().flat_map(|i| i.details.clone()).collect();
47
48 Ok(json!({
49 "category": primary.category,
50 "indicators": all_details,
51 }))
52}
53
54struct ProjectIndicator {
55 category: String,
56 details: Vec<String>,
57}
58
59fn detect_project_type_indicators(root: &Path) -> Vec<ProjectIndicator> {
60 let mut indicators = Vec::new();
61
62 if root.join("Cargo.toml").exists() {
64 let has_main = root.join("src/main.rs").exists();
65 let has_lib = root.join("src/lib.rs").exists();
66
67 let (category, details) = if has_main && has_lib {
68 (
69 "Rust CLI Tool with Library API".to_string(),
70 vec![
71 "Binary entry point: src/main.rs".to_string(),
72 "Library API: src/lib.rs".to_string(),
73 ],
74 )
75 } else if has_main {
76 (
77 "Rust CLI Tool".to_string(),
78 vec!["Binary entry point: src/main.rs".to_string()],
79 )
80 } else if has_lib {
81 (
82 "Rust Library".to_string(),
83 vec!["Library API: src/lib.rs".to_string()],
84 )
85 } else {
86 ("Rust Project".to_string(), vec![])
87 };
88
89 indicators.push(ProjectIndicator { category, details });
90 }
91
92 if root.join("package.json").exists() {
94 let mut details = Vec::new();
95 let category;
96
97 if let Ok(content) = fs::read_to_string(root.join("package.json")) {
99 if content.contains("\"next\"") {
100 category = "Next.js Application".to_string();
101 details.push("Framework: Next.js".to_string());
102 } else if content.contains("\"react\"") {
103 category = "React Application".to_string();
104 details.push("Framework: React".to_string());
105 } else if content.contains("\"vue\"") {
106 category = "Vue Application".to_string();
107 details.push("Framework: Vue".to_string());
108 } else if content.contains("\"express\"") {
109 category = "Express.js API".to_string();
110 details.push("Framework: Express".to_string());
111 } else if root.join("src").exists() || root.join("index.ts").exists() {
112 category = "TypeScript/JavaScript Project".to_string();
113 } else {
114 category = "Node.js Project".to_string();
115 }
116
117 indicators.push(ProjectIndicator { category, details });
118 }
119 }
120
121 if root.join("pyproject.toml").exists()
123 || root.join("setup.py").exists()
124 || root.join("requirements.txt").exists()
125 {
126 let mut details = Vec::new();
127 let category;
128
129 if root.join("manage.py").exists() {
130 category = "Django Application".to_string();
131 details.push("Framework: Django".to_string());
132 details.push("Entry point: manage.py".to_string());
133 } else if root.join("app.py").exists() {
134 category = "Flask Application".to_string();
135 details.push("Entry point: app.py".to_string());
136 } else if root.join("__main__.py").exists() || root.join("main.py").exists() {
137 category = "Python CLI Tool".to_string();
138 } else {
139 category = "Python Project".to_string();
140 }
141
142 indicators.push(ProjectIndicator { category, details });
143 }
144
145 if root.join("go.mod").exists() {
147 let has_cmd = root.join("cmd").exists();
148 let has_main_go = root.join("main.go").exists();
149
150 let (category, details) = if has_cmd {
151 (
152 "Go CLI Tool".to_string(),
153 vec!["Entry points in cmd/".to_string()],
154 )
155 } else if has_main_go {
156 (
157 "Go Application".to_string(),
158 vec!["Entry point: main.go".to_string()],
159 )
160 } else {
161 ("Go Library".to_string(), vec![])
162 };
163
164 indicators.push(ProjectIndicator { category, details });
165 }
166
167 if is_monorepo(root) {
169 let project_count = count_subprojects(root);
170 indicators.push(ProjectIndicator {
171 category: format!("Monorepo ({} projects)", project_count),
172 details: vec!["Multiple package files detected".to_string()],
173 });
174 }
175
176 indicators
177}
178
179fn is_monorepo(root: &Path) -> bool {
181 count_subprojects(root) >= 2
182}
183
184fn count_subprojects(root: &Path) -> usize {
186 let package_files = ["package.json", "Cargo.toml", "go.mod", "pyproject.toml"];
187 let mut count = 0;
188
189 if let Ok(entries) = fs::read_dir(root) {
190 for entry in entries.filter_map(|e| e.ok()) {
191 let path = entry.path();
192 if path.is_dir() {
193 for pkg_file in &package_files {
194 if path.join(pkg_file).exists() {
195 count += 1;
196 break;
197 }
198 }
199 }
200 }
201 }
202
203 count
204}
205
206pub fn find_entry_points(root: &Path) -> Result<Vec<String>> {
208 let mut entry_points = Vec::new();
209
210 let entry_files = [
212 ("src/main.rs", "Rust binary"),
213 ("src/lib.rs", "Rust library"),
214 ("main.rs", "Rust binary"),
215 ("index.ts", "TypeScript"),
216 ("index.js", "JavaScript"),
217 ("main.ts", "TypeScript"),
218 ("server.ts", "TypeScript server"),
219 ("app.ts", "TypeScript app"),
220 ("src/index.ts", "TypeScript"),
221 ("main.py", "Python"),
222 ("__main__.py", "Python module"),
223 ("app.py", "Python app"),
224 ("manage.py", "Django"),
225 ("main.go", "Go"),
226 ];
227
228 for (file, description) in &entry_files {
229 let path = root.join(file);
230 if path.exists()
231 && let Ok(_metadata) = fs::metadata(&path)
232 {
233 let lines = count_lines_in_file(&path).unwrap_or(0);
234 entry_points.push(format!("- {} ({}, {} lines)", file, description, lines));
235 }
236 }
237
238 let bin_dir = root.join("src/bin");
240 if bin_dir.exists()
241 && let Ok(entries) = fs::read_dir(&bin_dir)
242 {
243 for entry in entries.filter_map(|e| e.ok()) {
244 let name = entry.file_name();
245 entry_points.push(format!(
246 "- src/bin/{} (Rust binary)",
247 name.to_string_lossy()
248 ));
249 }
250 }
251
252 let cmd_dir = root.join("cmd");
254 if cmd_dir.exists()
255 && let Ok(entries) = fs::read_dir(&cmd_dir)
256 {
257 for entry in entries.filter_map(|e| e.ok()) {
258 if entry.path().is_dir() {
259 let name = entry.file_name();
260 entry_points.push(format!("- cmd/{} (Go binary)", name.to_string_lossy()));
261 }
262 }
263 }
264
265 Ok(entry_points)
266}
267
268pub fn find_entry_points_json(root: &Path) -> Result<Value> {
270 let entry_points = find_entry_points(root)?;
271
272 let parsed: Vec<Value> = entry_points
273 .iter()
274 .filter_map(|ep| {
275 let parts: Vec<&str> = ep.split(" (").collect();
277 if parts.len() >= 2 {
278 let path = parts[0].trim_start_matches("- ");
279 let desc_lines: Vec<&str> = parts[1].trim_end_matches(')').split(", ").collect();
280 let description = desc_lines[0];
281 let lines = desc_lines
282 .get(1)
283 .and_then(|s| s.trim_end_matches(" lines").parse::<usize>().ok());
284
285 Some(json!({
286 "path": path,
287 "type": description,
288 "lines": lines,
289 }))
290 } else {
291 None
292 }
293 })
294 .collect();
295
296 Ok(json!(parsed))
297}
298
299pub fn get_file_distribution(cache: &CacheManager) -> Result<String> {
301 use crate::semantic::context::CodebaseContext;
302
303 let context = CodebaseContext::extract(cache)?;
304
305 let mut output = Vec::new();
306
307 for lang in &context.languages {
309 let label = if lang.percentage > 60.0 {
310 format!(
311 "{} files ({:.1}%) - Primary language",
312 lang.file_count, lang.percentage
313 )
314 } else {
315 format!("{} files ({:.1}%)", lang.file_count, lang.percentage)
316 };
317
318 output.push(format!("- {}: {}", lang.name, label));
319 }
320
321 let total_lines: usize = context
323 .languages
324 .iter()
325 .map(|l| l.file_count * 50) .sum();
327 output.push(format!(
328 "\nTotal: {} files, ~{} lines",
329 context.total_files, total_lines
330 ));
331
332 Ok(output.join("\n"))
333}
334
335pub fn get_file_distribution_json(cache: &CacheManager) -> Result<Value> {
337 use crate::semantic::context::CodebaseContext;
338
339 let context = CodebaseContext::extract(cache)?;
340
341 let languages: Vec<Value> = context
342 .languages
343 .iter()
344 .map(|lang| {
345 json!({
346 "language": lang.name,
347 "count": lang.file_count,
348 "percentage": lang.percentage,
349 })
350 })
351 .collect();
352
353 Ok(json!(languages))
354}
355
356pub fn detect_test_layout(root: &Path) -> Result<String> {
358 let mut output = Vec::new();
359
360 let test_dirs = ["tests", "test", "__tests__", "spec", "benches"];
362 let mut found_test_dirs = Vec::new();
363
364 for dir in &test_dirs {
365 let test_path = root.join(dir);
366 if test_path.exists() && test_path.is_dir() {
367 let count = count_files_recursive(&test_path)?;
368 found_test_dirs.push(format!("{}/ ({} files)", dir, count));
369 }
370 }
371
372 let has_inline_tests = has_inline_tests(root)?;
374 let has_separate_tests = !found_test_dirs.is_empty();
375
376 let pattern = match (has_separate_tests, has_inline_tests) {
377 (true, true) => "Separate test directory + inline test modules",
378 (true, false) => "Separate test directory",
379 (false, true) => "Inline test modules only",
380 (false, false) => "No tests detected",
381 };
382
383 output.push(format!("Pattern: {}", pattern));
384
385 if !found_test_dirs.is_empty() {
386 output.push(format!("Test directories: {}", found_test_dirs.join(", ")));
387 }
388
389 let test_file_count: usize = found_test_dirs.len();
391 let src_file_count = count_files_recursive(&root.join("src")).unwrap_or(100);
392
393 if test_file_count > 0 && src_file_count > 0 {
394 let ratio = test_file_count as f64 / src_file_count as f64;
395 output.push(format!("Test-to-source ratio: {:.2}", ratio));
396 }
397
398 Ok(output.join("\n"))
399}
400
401pub fn detect_test_layout_json(root: &Path) -> Result<Value> {
403 let has_inline = has_inline_tests(root)?;
404 let test_dirs = ["tests", "test", "__tests__", "spec"];
405
406 let mut found_dirs = Vec::new();
407 let mut total_test_files = 0;
408
409 for dir in &test_dirs {
410 let path = root.join(dir);
411 if path.exists() {
412 let count = count_files_recursive(&path)?;
413 total_test_files += count;
414 found_dirs.push(format!("{}/", dir));
415 }
416 }
417
418 let pattern = match (!found_dirs.is_empty(), has_inline) {
419 (true, true) => "separate_directory_plus_inline",
420 (true, false) => "separate_directory",
421 (false, true) => "inline_only",
422 (false, false) => "none",
423 };
424
425 let src_files = count_files_recursive(&root.join("src")).unwrap_or(100);
426 let ratio = if src_files > 0 {
427 total_test_files as f64 / src_files as f64
428 } else {
429 0.0
430 };
431
432 Ok(json!({
433 "pattern": pattern,
434 "test_files": total_test_files,
435 "test_directories": found_dirs,
436 "test_to_source_ratio": ratio,
437 }))
438}
439
440fn has_inline_tests(root: &Path) -> Result<bool> {
442 let src_dir = root.join("src");
444 if !src_dir.exists() {
445 return Ok(false);
446 }
447
448 if let Ok(entries) = fs::read_dir(&src_dir) {
449 for entry in entries.filter_map(|e| e.ok()) {
450 let path = entry.path();
451 if path.extension().and_then(|e| e.to_str()) == Some("rs")
452 && let Ok(content) = fs::read_to_string(&path)
453 && (content.contains("#[cfg(test)]") || content.contains("#[test]"))
454 {
455 return Ok(true);
456 }
457 }
458 }
459
460 Ok(false)
461}
462
463pub fn detect_frameworks(root: &Path) -> Result<String> {
465 let frameworks = detect_frameworks_list(root)?;
466
467 if frameworks.is_empty() {
468 return Ok("No frameworks detected".to_string());
469 }
470
471 let output: Vec<String> = frameworks
472 .iter()
473 .map(|(name, category)| format!("- {}: {}", category, name))
474 .collect();
475
476 Ok(output.join("\n"))
477}
478
479pub fn detect_frameworks_json(root: &Path) -> Result<Value> {
481 let frameworks = detect_frameworks_list(root)?;
482
483 let json_frameworks: Vec<Value> = frameworks
484 .iter()
485 .map(|(name, category)| {
486 json!({
487 "name": name,
488 "category": category,
489 })
490 })
491 .collect();
492
493 Ok(json!(json_frameworks))
494}
495
496fn detect_frameworks_list(root: &Path) -> Result<Vec<(String, String)>> {
497 let mut frameworks = Vec::new();
498
499 detect_rust_frameworks(root, &mut frameworks);
500 detect_js_ts_frameworks(root, &mut frameworks);
501 detect_python_frameworks(root, &mut frameworks);
502 detect_php_frameworks(root, &mut frameworks);
503 detect_go_frameworks(root, &mut frameworks);
504 detect_java_frameworks(root, &mut frameworks);
505 detect_csharp_frameworks(root, &mut frameworks);
506 detect_ruby_frameworks(root, &mut frameworks);
507 detect_kotlin_frameworks(root, &mut frameworks);
508 detect_c_cpp_frameworks(root, &mut frameworks);
509 detect_zig_frameworks(root, &mut frameworks);
510
511 Ok(frameworks)
512}
513
514fn detect_rust_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
516 let cargo_toml = root.join("Cargo.toml");
517 if !cargo_toml.exists() {
518 return;
519 }
520
521 if let Ok(content) = fs::read_to_string(&cargo_toml) {
522 if content.contains("tokio") {
524 frameworks.push(("tokio".to_string(), "Async Runtime".to_string()));
525 }
526 if content.contains("async-std") {
527 frameworks.push(("async-std".to_string(), "Async Runtime".to_string()));
528 }
529
530 if content.contains("axum") {
532 frameworks.push(("axum".to_string(), "Web Framework".to_string()));
533 }
534 if content.contains("actix-web") {
535 frameworks.push(("actix-web".to_string(), "Web Framework".to_string()));
536 }
537 if content.contains("rocket") {
538 frameworks.push(("Rocket".to_string(), "Web Framework".to_string()));
539 }
540 if content.contains("warp") {
541 frameworks.push(("Warp".to_string(), "Web Framework".to_string()));
542 }
543
544 if content.contains("clap") {
546 frameworks.push(("clap".to_string(), "CLI Framework".to_string()));
547 }
548
549 if content.contains("diesel") {
551 frameworks.push(("Diesel".to_string(), "ORM".to_string()));
552 }
553 if content.contains("sqlx") {
554 frameworks.push(("SQLx".to_string(), "ORM".to_string()));
555 }
556 if content.contains("sea-orm") {
557 frameworks.push(("SeaORM".to_string(), "ORM".to_string()));
558 }
559
560 if content.contains("criterion") {
562 frameworks.push(("Criterion".to_string(), "Benchmarking".to_string()));
563 }
564 }
565}
566
567fn detect_js_ts_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
569 let package_json = root.join("package.json");
570 if !package_json.exists() {
571 return;
572 }
573
574 if let Ok(content) = fs::read_to_string(&package_json) {
575 if content.contains("\"next\"") {
577 frameworks.push(("Next.js".to_string(), "Web Framework".to_string()));
578 }
579 if content.contains("\"nuxt\"") {
580 frameworks.push(("Nuxt".to_string(), "Vue Framework".to_string()));
581 }
582 if content.contains("\"@sveltejs/kit\"") {
583 frameworks.push(("SvelteKit".to_string(), "Svelte Framework".to_string()));
584 }
585 if content.contains("\"@remix-run/react\"") {
586 frameworks.push(("Remix".to_string(), "Web Framework".to_string()));
587 }
588 if content.contains("\"astro\"") {
589 frameworks.push(("Astro".to_string(), "Web Framework".to_string()));
590 }
591
592 if content.contains("\"react\"") {
594 frameworks.push(("React".to_string(), "UI Library".to_string()));
595 }
596 if content.contains("\"vue\"") {
597 frameworks.push(("Vue".to_string(), "UI Framework".to_string()));
598 }
599 if content.contains("\"svelte\"") {
600 frameworks.push(("Svelte".to_string(), "UI Framework".to_string()));
601 }
602 if content.contains("\"@angular/core\"") {
603 frameworks.push(("Angular".to_string(), "Web Framework".to_string()));
604 }
605
606 if content.contains("\"express\"") {
608 frameworks.push(("Express".to_string(), "Web Framework".to_string()));
609 }
610 if content.contains("\"@nestjs/core\"") {
611 frameworks.push(("NestJS".to_string(), "Web Framework".to_string()));
612 }
613 if content.contains("\"koa\"") {
614 frameworks.push(("Koa".to_string(), "Web Framework".to_string()));
615 }
616 if content.contains("\"fastify\"") {
617 frameworks.push(("Fastify".to_string(), "Web Framework".to_string()));
618 }
619
620 if content.contains("\"jest\"") {
622 frameworks.push(("Jest".to_string(), "Testing Framework".to_string()));
623 }
624 if content.contains("\"vitest\"") {
625 frameworks.push(("Vitest".to_string(), "Testing Framework".to_string()));
626 }
627 if content.contains("\"@playwright/test\"") {
628 frameworks.push(("Playwright".to_string(), "E2E Testing".to_string()));
629 }
630 if content.contains("\"cypress\"") {
631 frameworks.push(("Cypress".to_string(), "E2E Testing".to_string()));
632 }
633
634 if content.contains("\"vite\"") {
636 frameworks.push(("Vite".to_string(), "Build Tool".to_string()));
637 }
638 }
639}
640
641fn detect_python_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
643 let reqs_files = ["requirements.txt", "pyproject.toml"];
644
645 for file in &reqs_files {
646 let path = root.join(file);
647 if !path.exists() {
648 continue;
649 }
650
651 if let Ok(content) = fs::read_to_string(&path) {
652 if content.contains("django") {
654 frameworks.push(("Django".to_string(), "Web Framework".to_string()));
655 }
656 if content.contains("flask") {
657 frameworks.push(("Flask".to_string(), "Web Framework".to_string()));
658 }
659 if content.contains("fastapi") {
660 frameworks.push(("FastAPI".to_string(), "Web Framework".to_string()));
661 }
662 if content.contains("tornado") {
663 frameworks.push(("Tornado".to_string(), "Web Framework".to_string()));
664 }
665
666 if content.contains("pytest") {
668 frameworks.push(("pytest".to_string(), "Testing Framework".to_string()));
669 }
670
671 if content.contains("sqlalchemy") {
673 frameworks.push(("SQLAlchemy".to_string(), "ORM".to_string()));
674 }
675
676 if content.contains("click") {
678 frameworks.push(("Click".to_string(), "CLI Framework".to_string()));
679 }
680 if content.contains("typer") {
681 frameworks.push(("Typer".to_string(), "CLI Framework".to_string()));
682 }
683 }
684 }
685}
686
687fn detect_php_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
689 let composer_json = root.join("composer.json");
690 if !composer_json.exists() {
691 return;
692 }
693
694 if let Ok(content) = fs::read_to_string(&composer_json) {
695 if content.contains("\"laravel/framework\"") {
697 frameworks.push(("Laravel".to_string(), "Web Framework".to_string()));
698 }
699 if content.contains("\"symfony/symfony\"") {
700 frameworks.push(("Symfony".to_string(), "Web Framework".to_string()));
701 }
702 if content.contains("\"slim/slim\"") {
703 frameworks.push(("Slim".to_string(), "Web Framework".to_string()));
704 }
705 if content.contains("\"cakephp/cakephp\"") {
706 frameworks.push(("CakePHP".to_string(), "Web Framework".to_string()));
707 }
708
709 if content.contains("\"phpunit/phpunit\"") {
711 frameworks.push(("PHPUnit".to_string(), "Testing Framework".to_string()));
712 }
713 if content.contains("\"pestphp/pest\"") {
714 frameworks.push(("Pest".to_string(), "Testing Framework".to_string()));
715 }
716
717 if content.contains("\"doctrine/orm\"") {
719 frameworks.push(("Doctrine ORM".to_string(), "ORM".to_string()));
720 }
721 }
722}
723
724fn detect_go_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
726 let go_mod = root.join("go.mod");
727 if !go_mod.exists() {
728 return;
729 }
730
731 if let Ok(content) = fs::read_to_string(&go_mod) {
732 if content.contains("gin-gonic/gin") {
734 frameworks.push(("Gin".to_string(), "Web Framework".to_string()));
735 }
736 if content.contains("labstack/echo") {
737 frameworks.push(("Echo".to_string(), "Web Framework".to_string()));
738 }
739 if content.contains("gofiber/fiber") {
740 frameworks.push(("Fiber".to_string(), "Web Framework".to_string()));
741 }
742 if content.contains("go-chi/chi") {
743 frameworks.push(("Chi".to_string(), "Web Framework".to_string()));
744 }
745 if content.contains("gorilla/mux") {
746 frameworks.push(("Gorilla Mux".to_string(), "Web Framework".to_string()));
747 }
748
749 if content.contains("spf13/cobra") {
751 frameworks.push(("Cobra".to_string(), "CLI Framework".to_string()));
752 }
753 if content.contains("urfave/cli") {
754 frameworks.push(("urfave/cli".to_string(), "CLI Framework".to_string()));
755 }
756
757 if content.contains("go-gorm/gorm") || content.contains("gorm.io/gorm") {
759 frameworks.push(("GORM".to_string(), "ORM".to_string()));
760 }
761
762 if content.contains("stretchr/testify") {
764 frameworks.push(("Testify".to_string(), "Testing Framework".to_string()));
765 }
766 }
767}
768
769fn detect_java_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
771 let pom_xml = root.join("pom.xml");
773 if pom_xml.exists()
774 && let Ok(content) = fs::read_to_string(&pom_xml)
775 {
776 detect_java_frameworks_from_content(&content, frameworks);
777 }
778
779 let build_gradle = root.join("build.gradle");
781 if build_gradle.exists()
782 && let Ok(content) = fs::read_to_string(&build_gradle)
783 {
784 detect_java_frameworks_from_content(&content, frameworks);
785 }
786
787 let build_gradle_kts = root.join("build.gradle.kts");
789 if build_gradle_kts.exists()
790 && let Ok(content) = fs::read_to_string(&build_gradle_kts)
791 {
792 detect_java_frameworks_from_content(&content, frameworks);
793 }
794}
795
796fn detect_java_frameworks_from_content(content: &str, frameworks: &mut Vec<(String, String)>) {
797 if content.contains("spring-boot") {
799 frameworks.push(("Spring Boot".to_string(), "Web Framework".to_string()));
800 }
801 if content.contains("quarkus") {
802 frameworks.push(("Quarkus".to_string(), "Web Framework".to_string()));
803 }
804 if content.contains("micronaut") {
805 frameworks.push(("Micronaut".to_string(), "Web Framework".to_string()));
806 }
807
808 if content.contains("junit-jupiter") {
810 frameworks.push(("JUnit 5".to_string(), "Testing Framework".to_string()));
811 } else if content.contains("junit") {
812 frameworks.push(("JUnit".to_string(), "Testing Framework".to_string()));
813 }
814 if content.contains("mockito") {
815 frameworks.push(("Mockito".to_string(), "Testing Framework".to_string()));
816 }
817
818 if content.contains("hibernate") {
820 frameworks.push(("Hibernate".to_string(), "ORM".to_string()));
821 }
822}
823
824fn detect_csharp_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
826 if let Ok(entries) = fs::read_dir(root) {
827 for entry in entries.filter_map(|e| e.ok()) {
828 let path = entry.path();
829 if path.extension().and_then(|e| e.to_str()) == Some("csproj") {
830 if let Ok(content) = fs::read_to_string(&path) {
831 if content.contains("Microsoft.AspNetCore") {
833 frameworks.push(("ASP.NET Core".to_string(), "Web Framework".to_string()));
834 }
835
836 if content.contains("xUnit") || content.contains("xunit") {
838 frameworks.push(("xUnit".to_string(), "Testing Framework".to_string()));
839 }
840 if content.contains("NUnit") {
841 frameworks.push(("NUnit".to_string(), "Testing Framework".to_string()));
842 }
843 if content.contains("MSTest") {
844 frameworks.push(("MSTest".to_string(), "Testing Framework".to_string()));
845 }
846
847 if content.contains("EntityFrameworkCore") {
849 frameworks.push(("Entity Framework Core".to_string(), "ORM".to_string()));
850 }
851 }
852 break; }
854 }
855 }
856}
857
858fn detect_ruby_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
860 let gemfile = root.join("Gemfile");
861 if !gemfile.exists() {
862 return;
863 }
864
865 if let Ok(content) = fs::read_to_string(&gemfile) {
866 if content.contains("gem 'rails'") || content.contains("gem \"rails\"") {
868 frameworks.push(("Rails".to_string(), "Web Framework".to_string()));
869 }
870 if content.contains("gem 'sinatra'") || content.contains("gem \"sinatra\"") {
871 frameworks.push(("Sinatra".to_string(), "Web Framework".to_string()));
872 }
873 if content.contains("gem 'hanami'") || content.contains("gem \"hanami\"") {
874 frameworks.push(("Hanami".to_string(), "Web Framework".to_string()));
875 }
876
877 if content.contains("gem 'rspec'") || content.contains("gem \"rspec\"") {
879 frameworks.push(("RSpec".to_string(), "Testing Framework".to_string()));
880 }
881 if content.contains("gem 'minitest'") || content.contains("gem \"minitest\"") {
882 frameworks.push(("Minitest".to_string(), "Testing Framework".to_string()));
883 }
884
885 if content.contains("gem 'sidekiq'") || content.contains("gem \"sidekiq\"") {
887 frameworks.push(("Sidekiq".to_string(), "Background Jobs".to_string()));
888 }
889 }
890}
891
892fn detect_kotlin_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
894 let build_gradle_kts = root.join("build.gradle.kts");
895 if !build_gradle_kts.exists() {
896 return;
897 }
898
899 if let Ok(content) = fs::read_to_string(&build_gradle_kts) {
900 if content.contains("ktor") {
902 frameworks.push(("Ktor".to_string(), "Web Framework".to_string()));
903 }
904
905 if content.contains("kotest") {
907 frameworks.push(("Kotest".to_string(), "Testing Framework".to_string()));
908 }
909 if content.contains("mockk") {
910 frameworks.push(("MockK".to_string(), "Testing Framework".to_string()));
911 }
912
913 if content.contains("kotlinx-coroutines") {
915 frameworks.push(("Kotlin Coroutines".to_string(), "Async Runtime".to_string()));
916 }
917 }
918}
919
920fn detect_c_cpp_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
922 let cmake_lists = root.join("CMakeLists.txt");
924 if cmake_lists.exists()
925 && let Ok(content) = fs::read_to_string(&cmake_lists)
926 {
927 if content.contains("GTest") || content.contains("gtest") {
929 frameworks.push(("Google Test".to_string(), "Testing Framework".to_string()));
930 }
931 if content.contains("Catch2") {
932 frameworks.push(("Catch2".to_string(), "Testing Framework".to_string()));
933 }
934
935 if content.contains("Boost") {
937 frameworks.push(("Boost".to_string(), "C++ Libraries".to_string()));
938 }
939
940 if content.contains("Qt") || content.contains("qt") {
942 frameworks.push(("Qt".to_string(), "GUI Framework".to_string()));
943 }
944 if content.contains("wxWidgets") {
945 frameworks.push(("wxWidgets".to_string(), "GUI Framework".to_string()));
946 }
947 }
948
949 let vcpkg_json = root.join("vcpkg.json");
951 if vcpkg_json.exists()
952 && let Ok(content) = fs::read_to_string(&vcpkg_json)
953 {
954 if content.contains("\"gtest\"") {
955 frameworks.push(("Google Test".to_string(), "Testing Framework".to_string()));
956 }
957 if content.contains("\"catch2\"") {
958 frameworks.push(("Catch2".to_string(), "Testing Framework".to_string()));
959 }
960 if content.contains("\"boost\"") {
961 frameworks.push(("Boost".to_string(), "C++ Libraries".to_string()));
962 }
963 }
964}
965
966fn detect_zig_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
968 let build_zig = root.join("build.zig");
969 if !build_zig.exists() {
970 return;
971 }
972
973 if let Ok(content) = fs::read_to_string(&build_zig) {
974 if content.contains("zap") {
976 frameworks.push(("Zap".to_string(), "Web Framework".to_string()));
977 }
978 if content.contains("zhp") {
979 frameworks.push(("ZHP".to_string(), "Web Framework".to_string()));
980 }
981 }
982}
983
984pub fn find_config_files(root: &Path) -> Result<String> {
986 let configs = find_config_files_list(root)?;
987
988 if configs.is_empty() {
989 return Ok("No configuration files found".to_string());
990 }
991
992 let mut grouped: HashMap<String, Vec<String>> = HashMap::new();
994 for (path, category) in configs {
995 grouped.entry(category).or_default().push(path);
996 }
997
998 let mut output = Vec::new();
999 for (category, files) in grouped {
1000 output.push(format!("{}:", category));
1001 for file in files {
1002 output.push(format!("- {}", file));
1003 }
1004 output.push(String::new()); }
1006
1007 Ok(output.join("\n"))
1008}
1009
1010pub fn find_config_files_json(root: &Path) -> Result<Value> {
1012 let configs = find_config_files_list(root)?;
1013
1014 let json_configs: Vec<Value> = configs
1015 .iter()
1016 .map(|(path, category)| {
1017 json!({
1018 "path": path,
1019 "category": category,
1020 })
1021 })
1022 .collect();
1023
1024 Ok(json!(json_configs))
1025}
1026
1027fn find_config_files_list(root: &Path) -> Result<Vec<(String, String)>> {
1028 let mut configs = Vec::new();
1029
1030 let manifests = [
1032 ("Cargo.toml", "Project Manifest"),
1033 ("package.json", "Project Manifest"),
1034 ("pyproject.toml", "Project Manifest"),
1035 ("go.mod", "Project Manifest"),
1036 ("pom.xml", "Project Manifest"),
1037 ("build.gradle", "Project Manifest"),
1038 ];
1039
1040 for (file, category) in &manifests {
1041 if root.join(file).exists() {
1042 configs.push((file.to_string(), category.to_string()));
1043 }
1044 }
1045
1046 let tool_configs = [
1048 (".gitignore", "Version Control"),
1049 (".gitattributes", "Version Control"),
1050 ("rustfmt.toml", "Code Formatting"),
1051 (".prettierrc", "Code Formatting"),
1052 (".eslintrc", "Code Linting"),
1053 ("tsconfig.json", "TypeScript Config"),
1054 (".reflex/config.toml", "Tool Config"),
1055 ];
1056
1057 for (file, category) in &tool_configs {
1058 if root.join(file).exists() {
1059 configs.push((file.to_string(), category.to_string()));
1060 }
1061 }
1062
1063 let docs = [
1065 ("README.md", "Documentation"),
1066 ("CLAUDE.md", "Documentation"),
1067 ("CONTRIBUTING.md", "Documentation"),
1068 ("LICENSE", "Documentation"),
1069 ];
1070
1071 for (file, category) in &docs {
1072 if root.join(file).exists() {
1073 configs.push((file.to_string(), category.to_string()));
1074 }
1075 }
1076
1077 Ok(configs)
1078}
1079
1080fn count_lines_in_file(path: &Path) -> Result<usize> {
1082 let content = fs::read_to_string(path)?;
1083 Ok(content.lines().count())
1084}
1085
1086fn count_files_recursive(dir: &Path) -> Result<usize> {
1088 let mut count = 0;
1089
1090 if let Ok(entries) = fs::read_dir(dir) {
1091 for entry in entries.filter_map(|e| e.ok()) {
1092 let path = entry.path();
1093 if path.is_dir() {
1094 count += count_files_recursive(&path)?;
1095 } else {
1096 count += 1;
1097 }
1098 }
1099 }
1100
1101 Ok(count)
1102}