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