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 if let Ok(_metadata) = fs::metadata(&path) {
232 let lines = count_lines_in_file(&path).unwrap_or(0);
233 entry_points.push(format!("- {} ({}, {} lines)", file, description, lines));
234 }
235 }
236 }
237
238 let bin_dir = root.join("src/bin");
240 if bin_dir.exists() {
241 if let Ok(entries) = fs::read_dir(&bin_dir) {
242 for entry in entries.filter_map(|e| e.ok()) {
243 let name = entry.file_name();
244 entry_points.push(format!(
245 "- src/bin/{} (Rust binary)",
246 name.to_string_lossy()
247 ));
248 }
249 }
250 }
251
252 let cmd_dir = root.join("cmd");
254 if cmd_dir.exists() {
255 if let Ok(entries) = fs::read_dir(&cmd_dir) {
256 for entry in entries.filter_map(|e| e.ok()) {
257 if entry.path().is_dir() {
258 let name = entry.file_name();
259 entry_points.push(format!("- cmd/{} (Go binary)", name.to_string_lossy()));
260 }
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 if lang.percentage > 20.0 {
315 format!("{} files ({:.1}%)", lang.file_count, lang.percentage)
316 } else {
317 format!("{} files ({:.1}%)", lang.file_count, lang.percentage)
318 };
319
320 output.push(format!("- {}: {}", lang.name, label));
321 }
322
323 let total_lines: usize = context
325 .languages
326 .iter()
327 .map(|l| l.file_count * 50) .sum();
329 output.push(format!(
330 "\nTotal: {} files, ~{} lines",
331 context.total_files, total_lines
332 ));
333
334 Ok(output.join("\n"))
335}
336
337pub fn get_file_distribution_json(cache: &CacheManager) -> Result<Value> {
339 use crate::semantic::context::CodebaseContext;
340
341 let context = CodebaseContext::extract(cache)?;
342
343 let languages: Vec<Value> = context
344 .languages
345 .iter()
346 .map(|lang| {
347 json!({
348 "language": lang.name,
349 "count": lang.file_count,
350 "percentage": lang.percentage,
351 })
352 })
353 .collect();
354
355 Ok(json!(languages))
356}
357
358pub fn detect_test_layout(root: &Path) -> Result<String> {
360 let mut output = Vec::new();
361
362 let test_dirs = ["tests", "test", "__tests__", "spec", "benches"];
364 let mut found_test_dirs = Vec::new();
365
366 for dir in &test_dirs {
367 let test_path = root.join(dir);
368 if test_path.exists() && test_path.is_dir() {
369 let count = count_files_recursive(&test_path)?;
370 found_test_dirs.push(format!("{}/ ({} files)", dir, count));
371 }
372 }
373
374 let has_inline_tests = has_inline_tests(root)?;
376 let has_separate_tests = !found_test_dirs.is_empty();
377
378 let pattern = match (has_separate_tests, has_inline_tests) {
379 (true, true) => "Separate test directory + inline test modules",
380 (true, false) => "Separate test directory",
381 (false, true) => "Inline test modules only",
382 (false, false) => "No tests detected",
383 };
384
385 output.push(format!("Pattern: {}", pattern));
386
387 if !found_test_dirs.is_empty() {
388 output.push(format!("Test directories: {}", found_test_dirs.join(", ")));
389 }
390
391 let test_file_count: usize = found_test_dirs.len();
393 let src_file_count = count_files_recursive(&root.join("src")).unwrap_or(100);
394
395 if test_file_count > 0 && src_file_count > 0 {
396 let ratio = test_file_count as f64 / src_file_count as f64;
397 output.push(format!("Test-to-source ratio: {:.2}", ratio));
398 }
399
400 Ok(output.join("\n"))
401}
402
403pub fn detect_test_layout_json(root: &Path) -> Result<Value> {
405 let has_inline = has_inline_tests(root)?;
406 let test_dirs = ["tests", "test", "__tests__", "spec"];
407
408 let mut found_dirs = Vec::new();
409 let mut total_test_files = 0;
410
411 for dir in &test_dirs {
412 let path = root.join(dir);
413 if path.exists() {
414 let count = count_files_recursive(&path)?;
415 total_test_files += count;
416 found_dirs.push(format!("{}/", dir));
417 }
418 }
419
420 let pattern = match (!found_dirs.is_empty(), has_inline) {
421 (true, true) => "separate_directory_plus_inline",
422 (true, false) => "separate_directory",
423 (false, true) => "inline_only",
424 (false, false) => "none",
425 };
426
427 let src_files = count_files_recursive(&root.join("src")).unwrap_or(100);
428 let ratio = if src_files > 0 {
429 total_test_files as f64 / src_files as f64
430 } else {
431 0.0
432 };
433
434 Ok(json!({
435 "pattern": pattern,
436 "test_files": total_test_files,
437 "test_directories": found_dirs,
438 "test_to_source_ratio": ratio,
439 }))
440}
441
442fn has_inline_tests(root: &Path) -> Result<bool> {
444 let src_dir = root.join("src");
446 if !src_dir.exists() {
447 return Ok(false);
448 }
449
450 if let Ok(entries) = fs::read_dir(&src_dir) {
451 for entry in entries.filter_map(|e| e.ok()) {
452 let path = entry.path();
453 if path.extension().and_then(|e| e.to_str()) == Some("rs") {
454 if let Ok(content) = fs::read_to_string(&path) {
455 if content.contains("#[cfg(test)]") || content.contains("#[test]") {
456 return Ok(true);
457 }
458 }
459 }
460 }
461 }
462
463 Ok(false)
464}
465
466pub fn detect_frameworks(root: &Path) -> Result<String> {
468 let frameworks = detect_frameworks_list(root)?;
469
470 if frameworks.is_empty() {
471 return Ok("No frameworks detected".to_string());
472 }
473
474 let output: Vec<String> = frameworks
475 .iter()
476 .map(|(name, category)| format!("- {}: {}", category, name))
477 .collect();
478
479 Ok(output.join("\n"))
480}
481
482pub fn detect_frameworks_json(root: &Path) -> Result<Value> {
484 let frameworks = detect_frameworks_list(root)?;
485
486 let json_frameworks: Vec<Value> = frameworks
487 .iter()
488 .map(|(name, category)| {
489 json!({
490 "name": name,
491 "category": category,
492 })
493 })
494 .collect();
495
496 Ok(json!(json_frameworks))
497}
498
499fn detect_frameworks_list(root: &Path) -> Result<Vec<(String, String)>> {
500 let mut frameworks = Vec::new();
501
502 detect_rust_frameworks(root, &mut frameworks);
503 detect_js_ts_frameworks(root, &mut frameworks);
504 detect_python_frameworks(root, &mut frameworks);
505 detect_php_frameworks(root, &mut frameworks);
506 detect_go_frameworks(root, &mut frameworks);
507 detect_java_frameworks(root, &mut frameworks);
508 detect_csharp_frameworks(root, &mut frameworks);
509 detect_ruby_frameworks(root, &mut frameworks);
510 detect_kotlin_frameworks(root, &mut frameworks);
511 detect_c_cpp_frameworks(root, &mut frameworks);
512 detect_zig_frameworks(root, &mut frameworks);
513
514 Ok(frameworks)
515}
516
517fn detect_rust_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
519 let cargo_toml = root.join("Cargo.toml");
520 if !cargo_toml.exists() {
521 return;
522 }
523
524 if let Ok(content) = fs::read_to_string(&cargo_toml) {
525 if content.contains("tokio") {
527 frameworks.push(("tokio".to_string(), "Async Runtime".to_string()));
528 }
529 if content.contains("async-std") {
530 frameworks.push(("async-std".to_string(), "Async Runtime".to_string()));
531 }
532
533 if content.contains("axum") {
535 frameworks.push(("axum".to_string(), "Web Framework".to_string()));
536 }
537 if content.contains("actix-web") {
538 frameworks.push(("actix-web".to_string(), "Web Framework".to_string()));
539 }
540 if content.contains("rocket") {
541 frameworks.push(("Rocket".to_string(), "Web Framework".to_string()));
542 }
543 if content.contains("warp") {
544 frameworks.push(("Warp".to_string(), "Web Framework".to_string()));
545 }
546
547 if content.contains("clap") {
549 frameworks.push(("clap".to_string(), "CLI Framework".to_string()));
550 }
551
552 if content.contains("diesel") {
554 frameworks.push(("Diesel".to_string(), "ORM".to_string()));
555 }
556 if content.contains("sqlx") {
557 frameworks.push(("SQLx".to_string(), "ORM".to_string()));
558 }
559 if content.contains("sea-orm") {
560 frameworks.push(("SeaORM".to_string(), "ORM".to_string()));
561 }
562
563 if content.contains("criterion") {
565 frameworks.push(("Criterion".to_string(), "Benchmarking".to_string()));
566 }
567 }
568}
569
570fn detect_js_ts_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
572 let package_json = root.join("package.json");
573 if !package_json.exists() {
574 return;
575 }
576
577 if let Ok(content) = fs::read_to_string(&package_json) {
578 if content.contains("\"next\"") {
580 frameworks.push(("Next.js".to_string(), "Web Framework".to_string()));
581 }
582 if content.contains("\"nuxt\"") {
583 frameworks.push(("Nuxt".to_string(), "Vue Framework".to_string()));
584 }
585 if content.contains("\"@sveltejs/kit\"") {
586 frameworks.push(("SvelteKit".to_string(), "Svelte Framework".to_string()));
587 }
588 if content.contains("\"@remix-run/react\"") {
589 frameworks.push(("Remix".to_string(), "Web Framework".to_string()));
590 }
591 if content.contains("\"astro\"") {
592 frameworks.push(("Astro".to_string(), "Web Framework".to_string()));
593 }
594
595 if content.contains("\"react\"") {
597 frameworks.push(("React".to_string(), "UI Library".to_string()));
598 }
599 if content.contains("\"vue\"") {
600 frameworks.push(("Vue".to_string(), "UI Framework".to_string()));
601 }
602 if content.contains("\"svelte\"") {
603 frameworks.push(("Svelte".to_string(), "UI Framework".to_string()));
604 }
605 if content.contains("\"@angular/core\"") {
606 frameworks.push(("Angular".to_string(), "Web Framework".to_string()));
607 }
608
609 if content.contains("\"express\"") {
611 frameworks.push(("Express".to_string(), "Web Framework".to_string()));
612 }
613 if content.contains("\"@nestjs/core\"") {
614 frameworks.push(("NestJS".to_string(), "Web Framework".to_string()));
615 }
616 if content.contains("\"koa\"") {
617 frameworks.push(("Koa".to_string(), "Web Framework".to_string()));
618 }
619 if content.contains("\"fastify\"") {
620 frameworks.push(("Fastify".to_string(), "Web Framework".to_string()));
621 }
622
623 if content.contains("\"jest\"") {
625 frameworks.push(("Jest".to_string(), "Testing Framework".to_string()));
626 }
627 if content.contains("\"vitest\"") {
628 frameworks.push(("Vitest".to_string(), "Testing Framework".to_string()));
629 }
630 if content.contains("\"@playwright/test\"") {
631 frameworks.push(("Playwright".to_string(), "E2E Testing".to_string()));
632 }
633 if content.contains("\"cypress\"") {
634 frameworks.push(("Cypress".to_string(), "E2E Testing".to_string()));
635 }
636
637 if content.contains("\"vite\"") {
639 frameworks.push(("Vite".to_string(), "Build Tool".to_string()));
640 }
641 }
642}
643
644fn detect_python_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
646 let reqs_files = ["requirements.txt", "pyproject.toml"];
647
648 for file in &reqs_files {
649 let path = root.join(file);
650 if !path.exists() {
651 continue;
652 }
653
654 if let Ok(content) = fs::read_to_string(&path) {
655 if content.contains("django") {
657 frameworks.push(("Django".to_string(), "Web Framework".to_string()));
658 }
659 if content.contains("flask") {
660 frameworks.push(("Flask".to_string(), "Web Framework".to_string()));
661 }
662 if content.contains("fastapi") {
663 frameworks.push(("FastAPI".to_string(), "Web Framework".to_string()));
664 }
665 if content.contains("tornado") {
666 frameworks.push(("Tornado".to_string(), "Web Framework".to_string()));
667 }
668
669 if content.contains("pytest") {
671 frameworks.push(("pytest".to_string(), "Testing Framework".to_string()));
672 }
673
674 if content.contains("sqlalchemy") {
676 frameworks.push(("SQLAlchemy".to_string(), "ORM".to_string()));
677 }
678
679 if content.contains("click") {
681 frameworks.push(("Click".to_string(), "CLI Framework".to_string()));
682 }
683 if content.contains("typer") {
684 frameworks.push(("Typer".to_string(), "CLI Framework".to_string()));
685 }
686 }
687 }
688}
689
690fn detect_php_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
692 let composer_json = root.join("composer.json");
693 if !composer_json.exists() {
694 return;
695 }
696
697 if let Ok(content) = fs::read_to_string(&composer_json) {
698 if content.contains("\"laravel/framework\"") {
700 frameworks.push(("Laravel".to_string(), "Web Framework".to_string()));
701 }
702 if content.contains("\"symfony/symfony\"") {
703 frameworks.push(("Symfony".to_string(), "Web Framework".to_string()));
704 }
705 if content.contains("\"slim/slim\"") {
706 frameworks.push(("Slim".to_string(), "Web Framework".to_string()));
707 }
708 if content.contains("\"cakephp/cakephp\"") {
709 frameworks.push(("CakePHP".to_string(), "Web Framework".to_string()));
710 }
711
712 if content.contains("\"phpunit/phpunit\"") {
714 frameworks.push(("PHPUnit".to_string(), "Testing Framework".to_string()));
715 }
716 if content.contains("\"pestphp/pest\"") {
717 frameworks.push(("Pest".to_string(), "Testing Framework".to_string()));
718 }
719
720 if content.contains("\"doctrine/orm\"") {
722 frameworks.push(("Doctrine ORM".to_string(), "ORM".to_string()));
723 }
724 }
725}
726
727fn detect_go_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
729 let go_mod = root.join("go.mod");
730 if !go_mod.exists() {
731 return;
732 }
733
734 if let Ok(content) = fs::read_to_string(&go_mod) {
735 if content.contains("gin-gonic/gin") {
737 frameworks.push(("Gin".to_string(), "Web Framework".to_string()));
738 }
739 if content.contains("labstack/echo") {
740 frameworks.push(("Echo".to_string(), "Web Framework".to_string()));
741 }
742 if content.contains("gofiber/fiber") {
743 frameworks.push(("Fiber".to_string(), "Web Framework".to_string()));
744 }
745 if content.contains("go-chi/chi") {
746 frameworks.push(("Chi".to_string(), "Web Framework".to_string()));
747 }
748 if content.contains("gorilla/mux") {
749 frameworks.push(("Gorilla Mux".to_string(), "Web Framework".to_string()));
750 }
751
752 if content.contains("spf13/cobra") {
754 frameworks.push(("Cobra".to_string(), "CLI Framework".to_string()));
755 }
756 if content.contains("urfave/cli") {
757 frameworks.push(("urfave/cli".to_string(), "CLI Framework".to_string()));
758 }
759
760 if content.contains("go-gorm/gorm") || content.contains("gorm.io/gorm") {
762 frameworks.push(("GORM".to_string(), "ORM".to_string()));
763 }
764
765 if content.contains("stretchr/testify") {
767 frameworks.push(("Testify".to_string(), "Testing Framework".to_string()));
768 }
769 }
770}
771
772fn detect_java_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
774 let pom_xml = root.join("pom.xml");
776 if pom_xml.exists() {
777 if let Ok(content) = fs::read_to_string(&pom_xml) {
778 detect_java_frameworks_from_content(&content, frameworks);
779 }
780 }
781
782 let build_gradle = root.join("build.gradle");
784 if build_gradle.exists() {
785 if let Ok(content) = fs::read_to_string(&build_gradle) {
786 detect_java_frameworks_from_content(&content, frameworks);
787 }
788 }
789
790 let build_gradle_kts = root.join("build.gradle.kts");
792 if build_gradle_kts.exists() {
793 if let Ok(content) = fs::read_to_string(&build_gradle_kts) {
794 detect_java_frameworks_from_content(&content, frameworks);
795 }
796 }
797}
798
799fn detect_java_frameworks_from_content(content: &str, frameworks: &mut Vec<(String, String)>) {
800 if content.contains("spring-boot") {
802 frameworks.push(("Spring Boot".to_string(), "Web Framework".to_string()));
803 }
804 if content.contains("quarkus") {
805 frameworks.push(("Quarkus".to_string(), "Web Framework".to_string()));
806 }
807 if content.contains("micronaut") {
808 frameworks.push(("Micronaut".to_string(), "Web Framework".to_string()));
809 }
810
811 if content.contains("junit-jupiter") {
813 frameworks.push(("JUnit 5".to_string(), "Testing Framework".to_string()));
814 } else if content.contains("junit") {
815 frameworks.push(("JUnit".to_string(), "Testing Framework".to_string()));
816 }
817 if content.contains("mockito") {
818 frameworks.push(("Mockito".to_string(), "Testing Framework".to_string()));
819 }
820
821 if content.contains("hibernate") {
823 frameworks.push(("Hibernate".to_string(), "ORM".to_string()));
824 }
825}
826
827fn detect_csharp_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
829 if let Ok(entries) = fs::read_dir(root) {
830 for entry in entries.filter_map(|e| e.ok()) {
831 let path = entry.path();
832 if path.extension().and_then(|e| e.to_str()) == Some("csproj") {
833 if let Ok(content) = fs::read_to_string(&path) {
834 if content.contains("Microsoft.AspNetCore") {
836 frameworks.push(("ASP.NET Core".to_string(), "Web Framework".to_string()));
837 }
838
839 if content.contains("xUnit") || content.contains("xunit") {
841 frameworks.push(("xUnit".to_string(), "Testing Framework".to_string()));
842 }
843 if content.contains("NUnit") {
844 frameworks.push(("NUnit".to_string(), "Testing Framework".to_string()));
845 }
846 if content.contains("MSTest") {
847 frameworks.push(("MSTest".to_string(), "Testing Framework".to_string()));
848 }
849
850 if content.contains("EntityFrameworkCore") {
852 frameworks.push(("Entity Framework Core".to_string(), "ORM".to_string()));
853 }
854 }
855 break; }
857 }
858 }
859}
860
861fn detect_ruby_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
863 let gemfile = root.join("Gemfile");
864 if !gemfile.exists() {
865 return;
866 }
867
868 if let Ok(content) = fs::read_to_string(&gemfile) {
869 if content.contains("gem 'rails'") || content.contains("gem \"rails\"") {
871 frameworks.push(("Rails".to_string(), "Web Framework".to_string()));
872 }
873 if content.contains("gem 'sinatra'") || content.contains("gem \"sinatra\"") {
874 frameworks.push(("Sinatra".to_string(), "Web Framework".to_string()));
875 }
876 if content.contains("gem 'hanami'") || content.contains("gem \"hanami\"") {
877 frameworks.push(("Hanami".to_string(), "Web Framework".to_string()));
878 }
879
880 if content.contains("gem 'rspec'") || content.contains("gem \"rspec\"") {
882 frameworks.push(("RSpec".to_string(), "Testing Framework".to_string()));
883 }
884 if content.contains("gem 'minitest'") || content.contains("gem \"minitest\"") {
885 frameworks.push(("Minitest".to_string(), "Testing Framework".to_string()));
886 }
887
888 if content.contains("gem 'sidekiq'") || content.contains("gem \"sidekiq\"") {
890 frameworks.push(("Sidekiq".to_string(), "Background Jobs".to_string()));
891 }
892 }
893}
894
895fn detect_kotlin_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
897 let build_gradle_kts = root.join("build.gradle.kts");
898 if !build_gradle_kts.exists() {
899 return;
900 }
901
902 if let Ok(content) = fs::read_to_string(&build_gradle_kts) {
903 if content.contains("ktor") {
905 frameworks.push(("Ktor".to_string(), "Web Framework".to_string()));
906 }
907
908 if content.contains("kotest") {
910 frameworks.push(("Kotest".to_string(), "Testing Framework".to_string()));
911 }
912 if content.contains("mockk") {
913 frameworks.push(("MockK".to_string(), "Testing Framework".to_string()));
914 }
915
916 if content.contains("kotlinx-coroutines") {
918 frameworks.push(("Kotlin Coroutines".to_string(), "Async Runtime".to_string()));
919 }
920 }
921}
922
923fn detect_c_cpp_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
925 let cmake_lists = root.join("CMakeLists.txt");
927 if cmake_lists.exists() {
928 if let Ok(content) = fs::read_to_string(&cmake_lists) {
929 if content.contains("GTest") || content.contains("gtest") {
931 frameworks.push(("Google Test".to_string(), "Testing Framework".to_string()));
932 }
933 if content.contains("Catch2") {
934 frameworks.push(("Catch2".to_string(), "Testing Framework".to_string()));
935 }
936
937 if content.contains("Boost") {
939 frameworks.push(("Boost".to_string(), "C++ Libraries".to_string()));
940 }
941
942 if content.contains("Qt") || content.contains("qt") {
944 frameworks.push(("Qt".to_string(), "GUI Framework".to_string()));
945 }
946 if content.contains("wxWidgets") {
947 frameworks.push(("wxWidgets".to_string(), "GUI Framework".to_string()));
948 }
949 }
950 }
951
952 let vcpkg_json = root.join("vcpkg.json");
954 if vcpkg_json.exists() {
955 if let Ok(content) = fs::read_to_string(&vcpkg_json) {
956 if content.contains("\"gtest\"") {
957 frameworks.push(("Google Test".to_string(), "Testing Framework".to_string()));
958 }
959 if content.contains("\"catch2\"") {
960 frameworks.push(("Catch2".to_string(), "Testing Framework".to_string()));
961 }
962 if content.contains("\"boost\"") {
963 frameworks.push(("Boost".to_string(), "C++ Libraries".to_string()));
964 }
965 }
966 }
967}
968
969fn detect_zig_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
971 let build_zig = root.join("build.zig");
972 if !build_zig.exists() {
973 return;
974 }
975
976 if let Ok(content) = fs::read_to_string(&build_zig) {
977 if content.contains("zap") {
979 frameworks.push(("Zap".to_string(), "Web Framework".to_string()));
980 }
981 if content.contains("zhp") {
982 frameworks.push(("ZHP".to_string(), "Web Framework".to_string()));
983 }
984 }
985}
986
987pub fn find_config_files(root: &Path) -> Result<String> {
989 let configs = find_config_files_list(root)?;
990
991 if configs.is_empty() {
992 return Ok("No configuration files found".to_string());
993 }
994
995 let mut grouped: HashMap<String, Vec<String>> = HashMap::new();
997 for (path, category) in configs {
998 grouped.entry(category).or_default().push(path);
999 }
1000
1001 let mut output = Vec::new();
1002 for (category, files) in grouped {
1003 output.push(format!("{}:", category));
1004 for file in files {
1005 output.push(format!("- {}", file));
1006 }
1007 output.push(String::new()); }
1009
1010 Ok(output.join("\n"))
1011}
1012
1013pub fn find_config_files_json(root: &Path) -> Result<Value> {
1015 let configs = find_config_files_list(root)?;
1016
1017 let json_configs: Vec<Value> = configs
1018 .iter()
1019 .map(|(path, category)| {
1020 json!({
1021 "path": path,
1022 "category": category,
1023 })
1024 })
1025 .collect();
1026
1027 Ok(json!(json_configs))
1028}
1029
1030fn find_config_files_list(root: &Path) -> Result<Vec<(String, String)>> {
1031 let mut configs = Vec::new();
1032
1033 let manifests = [
1035 ("Cargo.toml", "Project Manifest"),
1036 ("package.json", "Project Manifest"),
1037 ("pyproject.toml", "Project Manifest"),
1038 ("go.mod", "Project Manifest"),
1039 ("pom.xml", "Project Manifest"),
1040 ("build.gradle", "Project Manifest"),
1041 ];
1042
1043 for (file, category) in &manifests {
1044 if root.join(file).exists() {
1045 configs.push((file.to_string(), category.to_string()));
1046 }
1047 }
1048
1049 let tool_configs = [
1051 (".gitignore", "Version Control"),
1052 (".gitattributes", "Version Control"),
1053 ("rustfmt.toml", "Code Formatting"),
1054 (".prettierrc", "Code Formatting"),
1055 (".eslintrc", "Code Linting"),
1056 ("tsconfig.json", "TypeScript Config"),
1057 (".reflex/config.toml", "Tool Config"),
1058 ];
1059
1060 for (file, category) in &tool_configs {
1061 if root.join(file).exists() {
1062 configs.push((file.to_string(), category.to_string()));
1063 }
1064 }
1065
1066 let docs = [
1068 ("README.md", "Documentation"),
1069 ("CLAUDE.md", "Documentation"),
1070 ("CONTRIBUTING.md", "Documentation"),
1071 ("LICENSE", "Documentation"),
1072 ];
1073
1074 for (file, category) in &docs {
1075 if root.join(file).exists() {
1076 configs.push((file.to_string(), category.to_string()));
1077 }
1078 }
1079
1080 Ok(configs)
1081}
1082
1083fn count_lines_in_file(path: &Path) -> Result<usize> {
1085 let content = fs::read_to_string(path)?;
1086 Ok(content.lines().count())
1087}
1088
1089fn count_files_recursive(dir: &Path) -> Result<usize> {
1091 let mut count = 0;
1092
1093 if let Ok(entries) = fs::read_dir(dir) {
1094 for entry in entries.filter_map(|e| e.ok()) {
1095 let path = entry.path();
1096 if path.is_dir() {
1097 count += count_files_recursive(&path)?;
1098 } else {
1099 count += 1;
1100 }
1101 }
1102 }
1103
1104 Ok(count)
1105}