Skip to main content

reflex/context/
detection.rs

1//! Project type and framework detection
2
3use anyhow::Result;
4use serde_json::{json, Value};
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()
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    // Check for Rust project
65    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    // Check for JavaScript/TypeScript project
95    if root.join("package.json").exists() {
96        let mut details = Vec::new();
97        let category;
98
99        // Read package.json to detect framework
100        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    // Check for Python project
124    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    // Check for Go project
145    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    // Check for monorepo
167    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
178/// Check if this is a monorepo
179fn is_monorepo(root: &Path) -> bool {
180    count_subprojects(root) >= 2
181}
182
183/// Count number of subprojects (by counting package files in subdirectories)
184fn 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
205/// Find entry point files
206pub fn find_entry_points(root: &Path) -> Result<Vec<String>> {
207    let mut entry_points = Vec::new();
208
209    // Common entry points by language
210    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    // Check for bin/ directories (Rust)
238    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    // Check for cmd/ directories (Go)
249    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
264/// Find entry points (JSON format)
265pub 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            // Parse "- path (description, N lines)" format
271            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
293/// Get file type distribution
294pub 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    // Add language breakdown
302    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    // Add total
315    let total_lines: usize = context.languages.iter()
316        .map(|l| l.file_count * 50) // Rough estimate
317        .sum();
318    output.push(format!("\nTotal: {} files, ~{} lines", context.total_files, total_lines));
319
320    Ok(output.join("\n"))
321}
322
323/// Get file distribution (JSON format)
324pub 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
340/// Detect test layout
341pub fn detect_test_layout(root: &Path) -> Result<String> {
342    let mut output = Vec::new();
343
344    // Check for test directories
345    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    // Detect test patterns
357    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    // Count test files vs source files
374    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
385/// Detect test layout (JSON format)
386pub 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
424/// Check if project has inline tests (e.g., #[cfg(test)] in Rust)
425fn has_inline_tests(root: &Path) -> Result<bool> {
426    // Simple heuristic: check if any .rs files contain #[cfg(test)]
427    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
448/// Detect frameworks
449pub 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
463/// Detect frameworks (JSON format)
464pub 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
495/// Detect Rust frameworks from Cargo.toml
496fn 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        // Async runtimes
504        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        // Web frameworks
512        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        // CLI frameworks
526        if content.contains("clap") {
527            frameworks.push(("clap".to_string(), "CLI Framework".to_string()));
528        }
529
530        // ORMs
531        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        // Testing
542        if content.contains("criterion") {
543            frameworks.push(("Criterion".to_string(), "Benchmarking".to_string()));
544        }
545    }
546}
547
548/// Detect JavaScript/TypeScript frameworks from package.json
549fn 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        // Meta-frameworks (check these first as they may include base frameworks)
557        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        // UI libraries/frameworks
574        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        // Backend frameworks
588        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        // Testing frameworks
602        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        // Build tools
616        if content.contains("\"vite\"") {
617            frameworks.push(("Vite".to_string(), "Build Tool".to_string()));
618        }
619    }
620}
621
622/// Detect Python frameworks from requirements.txt and pyproject.toml
623fn 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            // Web frameworks
634            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            // Testing
648            if content.contains("pytest") {
649                frameworks.push(("pytest".to_string(), "Testing Framework".to_string()));
650            }
651
652            // ORMs
653            if content.contains("sqlalchemy") {
654                frameworks.push(("SQLAlchemy".to_string(), "ORM".to_string()));
655            }
656
657            // CLI
658            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
668/// Detect PHP frameworks from composer.json
669fn 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        // Web frameworks
677        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        // Testing
691        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        // ORM
699        if content.contains("\"doctrine/orm\"") {
700            frameworks.push(("Doctrine ORM".to_string(), "ORM".to_string()));
701        }
702    }
703}
704
705/// Detect Go frameworks from go.mod
706fn 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        // Web frameworks
714        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        // CLI frameworks
731        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        // ORM
739        if content.contains("go-gorm/gorm") || content.contains("gorm.io/gorm") {
740            frameworks.push(("GORM".to_string(), "ORM".to_string()));
741        }
742
743        // Testing
744        if content.contains("stretchr/testify") {
745            frameworks.push(("Testify".to_string(), "Testing Framework".to_string()));
746        }
747    }
748}
749
750/// Detect Java frameworks from pom.xml and build.gradle
751fn detect_java_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
752    // Check pom.xml
753    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    // Check build.gradle
761    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    // Check build.gradle.kts
769    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    // Web frameworks
779    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    // Testing
790    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    // ORM
800    if content.contains("hibernate") {
801        frameworks.push(("Hibernate".to_string(), "ORM".to_string()));
802    }
803}
804
805/// Detect C# frameworks from .csproj files
806fn 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                    // Web frameworks
813                    if content.contains("Microsoft.AspNetCore") {
814                        frameworks.push(("ASP.NET Core".to_string(), "Web Framework".to_string()));
815                    }
816
817                    // Testing
818                    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                    // ORM
829                    if content.contains("EntityFrameworkCore") {
830                        frameworks.push(("Entity Framework Core".to_string(), "ORM".to_string()));
831                    }
832                }
833                break; // Only check first .csproj
834            }
835        }
836    }
837}
838
839/// Detect Ruby frameworks from Gemfile
840fn 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        // Web frameworks
848        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        // Testing
859        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        // Background jobs
867        if content.contains("gem 'sidekiq'") || content.contains("gem \"sidekiq\"") {
868            frameworks.push(("Sidekiq".to_string(), "Background Jobs".to_string()));
869        }
870    }
871}
872
873/// Detect Kotlin frameworks from build.gradle.kts
874fn 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        // Web frameworks
882        if content.contains("ktor") {
883            frameworks.push(("Ktor".to_string(), "Web Framework".to_string()));
884        }
885
886        // Testing
887        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        // Coroutines
895        if content.contains("kotlinx-coroutines") {
896            frameworks.push(("Kotlin Coroutines".to_string(), "Async Runtime".to_string()));
897        }
898    }
899}
900
901/// Detect C/C++ frameworks from CMakeLists.txt and vcpkg.json
902fn detect_c_cpp_frameworks(root: &Path, frameworks: &mut Vec<(String, String)>) {
903    // Check CMakeLists.txt
904    let cmake_lists = root.join("CMakeLists.txt");
905    if cmake_lists.exists() {
906        if let Ok(content) = fs::read_to_string(&cmake_lists) {
907            // Testing
908            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            // Libraries
916            if content.contains("Boost") {
917                frameworks.push(("Boost".to_string(), "C++ Libraries".to_string()));
918            }
919
920            // GUI
921            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    // Check vcpkg.json
931    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
947/// Detect Zig frameworks from build.zig
948fn 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        // Web frameworks (limited ecosystem)
956        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
965/// Find configuration files
966pub 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    // Group by category
974    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()); // Blank line
986    }
987
988    Ok(output.join("\n"))
989}
990
991/// Find configuration files (JSON format)
992pub 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    // Project manifests
1009    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    // Tool configuration
1025    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    // Documentation
1042    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
1058/// Count lines in a file
1059fn count_lines_in_file(path: &Path) -> Result<usize> {
1060    let content = fs::read_to_string(path)?;
1061    Ok(content.lines().count())
1062}
1063
1064/// Count files recursively in a directory
1065fn 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}