Skip to main content

reflex/context/
detection.rs

1//! Project type and framework detection
2
3use anyhow::Result;
4use serde_json::{Value, json};
5use std::collections::HashMap;
6use std::fs;
7use std::path::Path;
8
9use crate::cache::CacheManager;
10
11/// Detect project type and return formatted string
12pub 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
34/// Detect project type and return JSON
35pub 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    // Check for Rust project
63    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    // Check for JavaScript/TypeScript project
93    if root.join("package.json").exists() {
94        let mut details = Vec::new();
95        let category;
96
97        // Read package.json to detect framework
98        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    // Check for Python project
122    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    // Check for Go project
146    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    // Check for monorepo
168    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
179/// Check if this is a monorepo
180fn is_monorepo(root: &Path) -> bool {
181    count_subprojects(root) >= 2
182}
183
184/// Count number of subprojects (by counting package files in subdirectories)
185fn 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
206/// Find entry point files
207pub fn find_entry_points(root: &Path) -> Result<Vec<String>> {
208    let mut entry_points = Vec::new();
209
210    // Common entry points by language
211    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    // Check for bin/ directories (Rust)
239    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    // Check for cmd/ directories (Go)
253    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
268/// Find entry points (JSON format)
269pub 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            // Parse "- path (description, N lines)" format
276            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
299/// Get file type distribution
300pub 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    // Add language breakdown
308    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    // Add total
324    let total_lines: usize = context
325        .languages
326        .iter()
327        .map(|l| l.file_count * 50) // Rough estimate
328        .sum();
329    output.push(format!(
330        "\nTotal: {} files, ~{} lines",
331        context.total_files, total_lines
332    ));
333
334    Ok(output.join("\n"))
335}
336
337/// Get file distribution (JSON format)
338pub 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
358/// Detect test layout
359pub fn detect_test_layout(root: &Path) -> Result<String> {
360    let mut output = Vec::new();
361
362    // Check for test directories
363    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    // Detect test patterns
375    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    // Count test files vs source files
392    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
403/// Detect test layout (JSON format)
404pub 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
442/// Check if project has inline tests (e.g., #[cfg(test)] in Rust)
443fn has_inline_tests(root: &Path) -> Result<bool> {
444    // Simple heuristic: check if any .rs files contain #[cfg(test)]
445    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
466/// Detect frameworks
467pub 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
482/// Detect frameworks (JSON format)
483pub 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
517/// Detect Rust frameworks from Cargo.toml
518fn 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        // Async runtimes
526        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        // Web frameworks
534        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        // CLI frameworks
548        if content.contains("clap") {
549            frameworks.push(("clap".to_string(), "CLI Framework".to_string()));
550        }
551
552        // ORMs
553        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        // Testing
564        if content.contains("criterion") {
565            frameworks.push(("Criterion".to_string(), "Benchmarking".to_string()));
566        }
567    }
568}
569
570/// Detect JavaScript/TypeScript frameworks from package.json
571fn 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        // Meta-frameworks (check these first as they may include base frameworks)
579        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        // UI libraries/frameworks
596        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        // Backend frameworks
610        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        // Testing frameworks
624        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        // Build tools
638        if content.contains("\"vite\"") {
639            frameworks.push(("Vite".to_string(), "Build Tool".to_string()));
640        }
641    }
642}
643
644/// Detect Python frameworks from requirements.txt and pyproject.toml
645fn 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            // Web frameworks
656            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            // Testing
670            if content.contains("pytest") {
671                frameworks.push(("pytest".to_string(), "Testing Framework".to_string()));
672            }
673
674            // ORMs
675            if content.contains("sqlalchemy") {
676                frameworks.push(("SQLAlchemy".to_string(), "ORM".to_string()));
677            }
678
679            // CLI
680            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
690/// Detect PHP frameworks from composer.json
691fn 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        // Web frameworks
699        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        // Testing
713        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        // ORM
721        if content.contains("\"doctrine/orm\"") {
722            frameworks.push(("Doctrine ORM".to_string(), "ORM".to_string()));
723        }
724    }
725}
726
727/// Detect Go frameworks from go.mod
728fn 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        // Web frameworks
736        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        // CLI frameworks
753        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        // ORM
761        if content.contains("go-gorm/gorm") || content.contains("gorm.io/gorm") {
762            frameworks.push(("GORM".to_string(), "ORM".to_string()));
763        }
764
765        // Testing
766        if content.contains("stretchr/testify") {
767            frameworks.push(("Testify".to_string(), "Testing Framework".to_string()));
768        }
769    }
770}
771
772/// Detect Java frameworks from pom.xml and build.gradle
773fn detect_java_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
774    // Check pom.xml
775    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    // Check build.gradle
783    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    // Check build.gradle.kts
791    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    // Web frameworks
801    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    // Testing
812    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    // ORM
822    if content.contains("hibernate") {
823        frameworks.push(("Hibernate".to_string(), "ORM".to_string()));
824    }
825}
826
827/// Detect C# frameworks from .csproj files
828fn 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                    // Web frameworks
835                    if content.contains("Microsoft.AspNetCore") {
836                        frameworks.push(("ASP.NET Core".to_string(), "Web Framework".to_string()));
837                    }
838
839                    // Testing
840                    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                    // ORM
851                    if content.contains("EntityFrameworkCore") {
852                        frameworks.push(("Entity Framework Core".to_string(), "ORM".to_string()));
853                    }
854                }
855                break; // Only check first .csproj
856            }
857        }
858    }
859}
860
861/// Detect Ruby frameworks from Gemfile
862fn 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        // Web frameworks
870        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        // Testing
881        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        // Background jobs
889        if content.contains("gem 'sidekiq'") || content.contains("gem \"sidekiq\"") {
890            frameworks.push(("Sidekiq".to_string(), "Background Jobs".to_string()));
891        }
892    }
893}
894
895/// Detect Kotlin frameworks from build.gradle.kts
896fn 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        // Web frameworks
904        if content.contains("ktor") {
905            frameworks.push(("Ktor".to_string(), "Web Framework".to_string()));
906        }
907
908        // Testing
909        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        // Coroutines
917        if content.contains("kotlinx-coroutines") {
918            frameworks.push(("Kotlin Coroutines".to_string(), "Async Runtime".to_string()));
919        }
920    }
921}
922
923/// Detect C/C++ frameworks from CMakeLists.txt and vcpkg.json
924fn detect_c_cpp_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
925    // Check CMakeLists.txt
926    let cmake_lists = root.join("CMakeLists.txt");
927    if cmake_lists.exists() {
928        if let Ok(content) = fs::read_to_string(&cmake_lists) {
929            // Testing
930            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            // Libraries
938            if content.contains("Boost") {
939                frameworks.push(("Boost".to_string(), "C++ Libraries".to_string()));
940            }
941
942            // GUI
943            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    // Check vcpkg.json
953    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
969/// Detect Zig frameworks from build.zig
970fn 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        // Web frameworks (limited ecosystem)
978        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
987/// Find configuration files
988pub 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    // Group by category
996    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()); // Blank line
1008    }
1009
1010    Ok(output.join("\n"))
1011}
1012
1013/// Find configuration files (JSON format)
1014pub 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    // Project manifests
1034    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    // Tool configuration
1050    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    // Documentation
1067    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
1083/// Count lines in a file
1084fn count_lines_in_file(path: &Path) -> Result<usize> {
1085    let content = fs::read_to_string(path)?;
1086    Ok(content.lines().count())
1087}
1088
1089/// Count files recursively in a directory
1090fn 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}