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