syncable_cli/analyzer/
language_detector.rs

1use crate::analyzer::{AnalysisConfig, DetectedLanguage};
2use crate::common::file_utils;
3use crate::error::Result;
4use serde_json::Value as JsonValue;
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8/// Language detection results with detailed information
9#[derive(Debug, Clone)]
10pub struct LanguageInfo {
11    pub name: String,
12    pub version: Option<String>,
13    pub edition: Option<String>,
14    pub package_manager: Option<String>,
15    pub main_dependencies: Vec<String>,
16    pub dev_dependencies: Vec<String>,
17    pub confidence: f32,
18    pub source_files: Vec<PathBuf>,
19    pub manifest_files: Vec<PathBuf>,
20}
21
22/// Detects programming languages with advanced manifest parsing
23pub fn detect_languages(
24    files: &[PathBuf],
25    config: &AnalysisConfig,
26) -> Result<Vec<DetectedLanguage>> {
27    let mut language_info = HashMap::new();
28    
29    // First pass: collect files by extension and find manifests
30    let mut source_files_by_lang = HashMap::new();
31    let mut manifest_files = Vec::new();
32    
33    for file in files {
34        if let Some(extension) = file.extension().and_then(|e| e.to_str()) {
35            match extension {
36                // Rust files
37                "rs" => source_files_by_lang
38                    .entry("rust")
39                    .or_insert_with(Vec::new)
40                    .push(file.clone()),
41                    
42                // JavaScript/TypeScript files
43                "js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs" => source_files_by_lang
44                    .entry("javascript")
45                    .or_insert_with(Vec::new)
46                    .push(file.clone()),
47                    
48                // Python files
49                "py" | "pyx" | "pyi" => source_files_by_lang
50                    .entry("python")
51                    .or_insert_with(Vec::new)
52                    .push(file.clone()),
53                    
54                // Go files
55                "go" => source_files_by_lang
56                    .entry("go")
57                    .or_insert_with(Vec::new)
58                    .push(file.clone()),
59                    
60                // Java/Kotlin files
61                "java" | "kt" | "kts" => source_files_by_lang
62                    .entry("jvm")
63                    .or_insert_with(Vec::new)
64                    .push(file.clone()),
65                    
66                _ => {}
67            }
68        }
69        
70        // Check for manifest files
71        if let Some(filename) = file.file_name().and_then(|n| n.to_str()) {
72            if is_manifest_file(filename) {
73                manifest_files.push(file.clone());
74            }
75        }
76    }
77    
78    // Second pass: analyze each detected language with manifest parsing
79    if source_files_by_lang.contains_key("rust") || has_manifest(&manifest_files, &["Cargo.toml"]) {
80        if let Ok(info) = analyze_rust_project(&manifest_files, source_files_by_lang.get("rust"), config) {
81            language_info.insert("rust", info);
82        }
83    }
84    
85    if source_files_by_lang.contains_key("javascript") || has_manifest(&manifest_files, &["package.json"]) {
86        if let Ok(info) = analyze_javascript_project(&manifest_files, source_files_by_lang.get("javascript"), config) {
87            language_info.insert("javascript", info);
88        }
89    }
90    
91    if source_files_by_lang.contains_key("python") || has_manifest(&manifest_files, &["requirements.txt", "Pipfile", "pyproject.toml", "setup.py"]) {
92        if let Ok(info) = analyze_python_project(&manifest_files, source_files_by_lang.get("python"), config) {
93            language_info.insert("python", info);
94        }
95    }
96    
97    if source_files_by_lang.contains_key("go") || has_manifest(&manifest_files, &["go.mod"]) {
98        if let Ok(info) = analyze_go_project(&manifest_files, source_files_by_lang.get("go"), config) {
99            language_info.insert("go", info);
100        }
101    }
102    
103    if source_files_by_lang.contains_key("jvm") || has_manifest(&manifest_files, &["pom.xml", "build.gradle", "build.gradle.kts"]) {
104        if let Ok(info) = analyze_jvm_project(&manifest_files, source_files_by_lang.get("jvm"), config) {
105            language_info.insert("jvm", info);
106        }
107    }
108    
109    // Convert to DetectedLanguage format
110    let mut detected_languages = Vec::new();
111    for (_, info) in language_info {
112        detected_languages.push(DetectedLanguage {
113            name: info.name,
114            version: info.version,
115            confidence: info.confidence,
116            files: info.source_files,
117            main_dependencies: info.main_dependencies,
118            dev_dependencies: info.dev_dependencies,
119            package_manager: info.package_manager,
120        });
121    }
122    
123    // Sort by confidence (highest first)
124    detected_languages.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
125    
126    Ok(detected_languages)
127}
128
129/// Analyze Rust project from Cargo.toml
130fn analyze_rust_project(
131    manifest_files: &[PathBuf],
132    source_files: Option<&Vec<PathBuf>>,
133    config: &AnalysisConfig,
134) -> Result<LanguageInfo> {
135    let mut info = LanguageInfo {
136        name: "Rust".to_string(),
137        version: None,
138        edition: None,
139        package_manager: Some("cargo".to_string()),
140        main_dependencies: Vec::new(),
141        dev_dependencies: Vec::new(),
142        confidence: 0.5,
143        source_files: source_files.map_or(Vec::new(), |f| f.clone()),
144        manifest_files: Vec::new(),
145    };
146    
147    // Find and parse Cargo.toml
148    for manifest in manifest_files {
149        if manifest.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
150            info.manifest_files.push(manifest.clone());
151            
152            if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
153                if let Ok(cargo_toml) = toml::from_str::<toml::Value>(&content) {
154                    // Extract edition
155                    if let Some(package) = cargo_toml.get("package") {
156                        if let Some(edition) = package.get("edition").and_then(|e| e.as_str()) {
157                            info.edition = Some(edition.to_string());
158                        }
159                        
160                        // Estimate Rust version from edition
161                        info.version = match info.edition.as_deref() {
162                            Some("2021") => Some("1.56+".to_string()),
163                            Some("2018") => Some("1.31+".to_string()),
164                            Some("2015") => Some("1.0+".to_string()),
165                            _ => Some("unknown".to_string()),
166                        };
167                    }
168                    
169                    // Extract dependencies
170                    if let Some(deps) = cargo_toml.get("dependencies") {
171                        if let Some(deps_table) = deps.as_table() {
172                            for (name, _) in deps_table {
173                                info.main_dependencies.push(name.clone());
174                            }
175                        }
176                    }
177                    
178                    // Extract dev dependencies if enabled
179                    if config.include_dev_dependencies {
180                        if let Some(dev_deps) = cargo_toml.get("dev-dependencies") {
181                            if let Some(dev_deps_table) = dev_deps.as_table() {
182                                for (name, _) in dev_deps_table {
183                                    info.dev_dependencies.push(name.clone());
184                                }
185                            }
186                        }
187                    }
188                    
189                    info.confidence = 0.95; // High confidence with manifest
190                }
191            }
192            break;
193        }
194    }
195    
196    // Boost confidence if we have source files
197    if !info.source_files.is_empty() {
198        info.confidence = (info.confidence + 0.9) / 2.0;
199    }
200    
201    Ok(info)
202}
203
204/// Analyze JavaScript/TypeScript project from package.json
205fn analyze_javascript_project(
206    manifest_files: &[PathBuf],
207    source_files: Option<&Vec<PathBuf>>,
208    config: &AnalysisConfig,
209) -> Result<LanguageInfo> {
210    let mut info = LanguageInfo {
211        name: "JavaScript/TypeScript".to_string(),
212        version: None,
213        edition: None,
214        package_manager: None,
215        main_dependencies: Vec::new(),
216        dev_dependencies: Vec::new(),
217        confidence: 0.5,
218        source_files: source_files.map_or(Vec::new(), |f| f.clone()),
219        manifest_files: Vec::new(),
220    };
221    
222    // Detect package manager from lock files
223    for manifest in manifest_files {
224        if let Some(filename) = manifest.file_name().and_then(|n| n.to_str()) {
225            match filename {
226                "package-lock.json" => info.package_manager = Some("npm".to_string()),
227                "yarn.lock" => info.package_manager = Some("yarn".to_string()),
228                "pnpm-lock.yaml" => info.package_manager = Some("pnpm".to_string()),
229                _ => {}
230            }
231        }
232    }
233    
234    // Default to npm if no package manager detected
235    if info.package_manager.is_none() {
236        info.package_manager = Some("npm".to_string());
237    }
238    
239    // Find and parse package.json
240    for manifest in manifest_files {
241        if manifest.file_name().and_then(|n| n.to_str()) == Some("package.json") {
242            info.manifest_files.push(manifest.clone());
243            
244            if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
245                if let Ok(package_json) = serde_json::from_str::<JsonValue>(&content) {
246                    // Extract Node.js version from engines
247                    if let Some(engines) = package_json.get("engines") {
248                        if let Some(node_version) = engines.get("node").and_then(|v| v.as_str()) {
249                            info.version = Some(node_version.to_string());
250                        }
251                    }
252                    
253                    // Extract dependencies
254                    if let Some(deps) = package_json.get("dependencies") {
255                        if let Some(deps_obj) = deps.as_object() {
256                            for (name, _) in deps_obj {
257                                info.main_dependencies.push(name.clone());
258                            }
259                        }
260                    }
261                    
262                    // Extract dev dependencies if enabled
263                    if config.include_dev_dependencies {
264                        if let Some(dev_deps) = package_json.get("devDependencies") {
265                            if let Some(dev_deps_obj) = dev_deps.as_object() {
266                                for (name, _) in dev_deps_obj {
267                                    info.dev_dependencies.push(name.clone());
268                                }
269                            }
270                        }
271                    }
272                    
273                    info.confidence = 0.95; // High confidence with manifest
274                }
275            }
276            break;
277        }
278    }
279    
280    // Adjust name based on file types
281    if let Some(files) = source_files {
282        let has_typescript = files.iter().any(|f| {
283            f.extension()
284                .and_then(|e| e.to_str())
285                .map_or(false, |ext| ext == "ts" || ext == "tsx")
286        });
287        
288        if has_typescript {
289            info.name = "TypeScript".to_string();
290        } else {
291            info.name = "JavaScript".to_string();
292        }
293    }
294    
295    // Boost confidence if we have source files
296    if !info.source_files.is_empty() {
297        info.confidence = (info.confidence + 0.9) / 2.0;
298    }
299    
300    Ok(info)
301}
302
303/// Analyze Python project from various manifest files
304fn analyze_python_project(
305    manifest_files: &[PathBuf],
306    source_files: Option<&Vec<PathBuf>>,
307    config: &AnalysisConfig,
308) -> Result<LanguageInfo> {
309    let mut info = LanguageInfo {
310        name: "Python".to_string(),
311        version: None,
312        edition: None,
313        package_manager: None,
314        main_dependencies: Vec::new(),
315        dev_dependencies: Vec::new(),
316        confidence: 0.5,
317        source_files: source_files.map_or(Vec::new(), |f| f.clone()),
318        manifest_files: Vec::new(),
319    };
320    
321    // Detect package manager and parse manifest files
322    for manifest in manifest_files {
323        if let Some(filename) = manifest.file_name().and_then(|n| n.to_str()) {
324            info.manifest_files.push(manifest.clone());
325            
326            match filename {
327                "requirements.txt" => {
328                    info.package_manager = Some("pip".to_string());
329                    if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
330                        parse_requirements_txt(&content, &mut info);
331                        info.confidence = 0.85;
332                    }
333                }
334                "Pipfile" => {
335                    info.package_manager = Some("pipenv".to_string());
336                    if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
337                        parse_pipfile(&content, &mut info, config);
338                        info.confidence = 0.90;
339                    }
340                }
341                "pyproject.toml" => {
342                    info.package_manager = Some("poetry/pip".to_string());
343                    if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
344                        parse_pyproject_toml(&content, &mut info, config);
345                        info.confidence = 0.95;
346                    }
347                }
348                "setup.py" => {
349                    info.package_manager = Some("setuptools".to_string());
350                    if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
351                        parse_setup_py(&content, &mut info);
352                        info.confidence = 0.80;
353                    }
354                }
355                _ => {}
356            }
357        }
358    }
359    
360    // Default to pip if no package manager detected
361    if info.package_manager.is_none() && !info.source_files.is_empty() {
362        info.package_manager = Some("pip".to_string());
363        info.confidence = 0.75;
364    }
365    
366    // Boost confidence if we have source files
367    if !info.source_files.is_empty() {
368        info.confidence = (info.confidence + 0.8) / 2.0;
369    }
370    
371    Ok(info)
372}
373
374/// Parse requirements.txt file
375fn parse_requirements_txt(content: &str, info: &mut LanguageInfo) {
376    for line in content.lines() {
377        let line = line.trim();
378        if line.is_empty() || line.starts_with('#') {
379            continue;
380        }
381        
382        // Extract package name (before ==, >=, etc.)
383        if let Some(package_name) = line.split(&['=', '>', '<', '!', '~', ';'][..]).next() {
384            let clean_name = package_name.trim();
385            if !clean_name.is_empty() && !clean_name.starts_with('-') {
386                info.main_dependencies.push(clean_name.to_string());
387            }
388        }
389    }
390}
391
392/// Parse Pipfile (TOML format)
393fn parse_pipfile(content: &str, info: &mut LanguageInfo, config: &AnalysisConfig) {
394    if let Ok(pipfile) = toml::from_str::<toml::Value>(content) {
395        // Extract Python version requirement
396        if let Some(requires) = pipfile.get("requires") {
397            if let Some(python_version) = requires.get("python_version").and_then(|v| v.as_str()) {
398                info.version = Some(format!("~={}", python_version));
399            } else if let Some(python_full) = requires.get("python_full_version").and_then(|v| v.as_str()) {
400                info.version = Some(format!("=={}", python_full));
401            }
402        }
403        
404        // Extract packages
405        if let Some(packages) = pipfile.get("packages") {
406            if let Some(packages_table) = packages.as_table() {
407                for (name, _) in packages_table {
408                    info.main_dependencies.push(name.clone());
409                }
410            }
411        }
412        
413        // Extract dev packages if enabled
414        if config.include_dev_dependencies {
415            if let Some(dev_packages) = pipfile.get("dev-packages") {
416                if let Some(dev_packages_table) = dev_packages.as_table() {
417                    for (name, _) in dev_packages_table {
418                        info.dev_dependencies.push(name.clone());
419                    }
420                }
421            }
422        }
423    }
424}
425
426/// Parse pyproject.toml file
427fn parse_pyproject_toml(content: &str, info: &mut LanguageInfo, config: &AnalysisConfig) {
428    if let Ok(pyproject) = toml::from_str::<toml::Value>(content) {
429        // Extract Python version from project metadata
430        if let Some(project) = pyproject.get("project") {
431            if let Some(requires_python) = project.get("requires-python").and_then(|v| v.as_str()) {
432                info.version = Some(requires_python.to_string());
433            }
434            
435            // Extract dependencies
436            if let Some(dependencies) = project.get("dependencies") {
437                if let Some(deps_array) = dependencies.as_array() {
438                    for dep in deps_array {
439                        if let Some(dep_str) = dep.as_str() {
440                            if let Some(package_name) = dep_str.split(&['=', '>', '<', '!', '~', ';'][..]).next() {
441                                let clean_name = package_name.trim();
442                                if !clean_name.is_empty() {
443                                    info.main_dependencies.push(clean_name.to_string());
444                                }
445                            }
446                        }
447                    }
448                }
449            }
450            
451            // Extract optional dependencies (dev dependencies)
452            if config.include_dev_dependencies {
453                if let Some(optional_deps) = project.get("optional-dependencies") {
454                    if let Some(optional_table) = optional_deps.as_table() {
455                        for (_, deps) in optional_table {
456                            if let Some(deps_array) = deps.as_array() {
457                                for dep in deps_array {
458                                    if let Some(dep_str) = dep.as_str() {
459                                        if let Some(package_name) = dep_str.split(&['=', '>', '<', '!', '~', ';'][..]).next() {
460                                            let clean_name = package_name.trim();
461                                            if !clean_name.is_empty() {
462                                                info.dev_dependencies.push(clean_name.to_string());
463                                            }
464                                        }
465                                    }
466                                }
467                            }
468                        }
469                    }
470                }
471            }
472        }
473        
474        // Check for Poetry configuration
475        if pyproject.get("tool").and_then(|t| t.get("poetry")).is_some() {
476            info.package_manager = Some("poetry".to_string());
477            
478            // Extract Poetry dependencies
479            if let Some(tool) = pyproject.get("tool") {
480                if let Some(poetry) = tool.get("poetry") {
481                    if let Some(dependencies) = poetry.get("dependencies") {
482                        if let Some(deps_table) = dependencies.as_table() {
483                            for (name, _) in deps_table {
484                                if name != "python" {
485                                    info.main_dependencies.push(name.clone());
486                                }
487                            }
488                        }
489                    }
490                    
491                    if config.include_dev_dependencies {
492                        if let Some(dev_dependencies) = poetry.get("group")
493                            .and_then(|g| g.get("dev"))
494                            .and_then(|d| d.get("dependencies")) 
495                        {
496                            if let Some(dev_deps_table) = dev_dependencies.as_table() {
497                                for (name, _) in dev_deps_table {
498                                    info.dev_dependencies.push(name.clone());
499                                }
500                            }
501                        }
502                    }
503                }
504            }
505        }
506    }
507}
508
509/// Parse setup.py file (basic extraction)
510fn parse_setup_py(content: &str, info: &mut LanguageInfo) {
511    // Basic regex-based parsing for common patterns
512    for line in content.lines() {
513        let line = line.trim();
514        
515        // Look for python_requires
516        if line.contains("python_requires") {
517            if let Some(start) = line.find("\"") {
518                if let Some(end) = line[start + 1..].find("\"") {
519                    let version = &line[start + 1..start + 1 + end];
520                    info.version = Some(version.to_string());
521                }
522            } else if let Some(start) = line.find("'") {
523                if let Some(end) = line[start + 1..].find("'") {
524                    let version = &line[start + 1..start + 1 + end];
525                    info.version = Some(version.to_string());
526                }
527            }
528        }
529        
530        // Look for install_requires (basic pattern)
531        if line.contains("install_requires") && line.contains("[") {
532            // This is a simplified parser - could be enhanced
533            info.main_dependencies.push("setuptools-detected".to_string());
534        }
535    }
536}
537
538/// Analyze Go project from go.mod
539fn analyze_go_project(
540    manifest_files: &[PathBuf],
541    source_files: Option<&Vec<PathBuf>>,
542    config: &AnalysisConfig,
543) -> Result<LanguageInfo> {
544    let mut info = LanguageInfo {
545        name: "Go".to_string(),
546        version: None,
547        edition: None,
548        package_manager: Some("go mod".to_string()),
549        main_dependencies: Vec::new(),
550        dev_dependencies: Vec::new(),
551        confidence: 0.5,
552        source_files: source_files.map_or(Vec::new(), |f| f.clone()),
553        manifest_files: Vec::new(),
554    };
555    
556    // Find and parse go.mod
557    for manifest in manifest_files {
558        if let Some(filename) = manifest.file_name().and_then(|n| n.to_str()) {
559            match filename {
560                "go.mod" => {
561                    info.manifest_files.push(manifest.clone());
562                    if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
563                        parse_go_mod(&content, &mut info);
564                        info.confidence = 0.95;
565                    }
566                }
567                "go.sum" => {
568                    info.manifest_files.push(manifest.clone());
569                    // go.sum contains checksums, indicates a real Go project
570                    info.confidence = (info.confidence + 0.9) / 2.0;
571                }
572                _ => {}
573            }
574        }
575    }
576    
577    // Boost confidence if we have source files
578    if !info.source_files.is_empty() {
579        info.confidence = (info.confidence + 0.85) / 2.0;
580    }
581    
582    Ok(info)
583}
584
585/// Parse go.mod file
586fn parse_go_mod(content: &str, info: &mut LanguageInfo) {
587    for line in content.lines() {
588        let line = line.trim();
589        
590        // Parse go version directive
591        if line.starts_with("go ") {
592            let version = line[3..].trim();
593            info.version = Some(version.to_string());
594        }
595        
596        // Parse require block
597        if line.starts_with("require ") {
598            // Single line require
599            let require_line = &line[8..].trim();
600            if let Some(module_name) = require_line.split_whitespace().next() {
601                info.main_dependencies.push(module_name.to_string());
602            }
603        }
604    }
605    
606    // Parse multi-line require blocks
607    let mut in_require_block = false;
608    for line in content.lines() {
609        let line = line.trim();
610        
611        if line == "require (" {
612            in_require_block = true;
613            continue;
614        }
615        
616        if in_require_block {
617            if line == ")" {
618                in_require_block = false;
619                continue;
620            }
621            
622            // Parse dependency line
623            if !line.is_empty() && !line.starts_with("//") {
624                if let Some(module_name) = line.split_whitespace().next() {
625                    info.main_dependencies.push(module_name.to_string());
626                }
627            }
628        }
629    }
630}
631
632/// Analyze JVM project (Java/Kotlin) from build files
633fn analyze_jvm_project(
634    manifest_files: &[PathBuf],
635    source_files: Option<&Vec<PathBuf>>,
636    config: &AnalysisConfig,
637) -> Result<LanguageInfo> {
638    let mut info = LanguageInfo {
639        name: "Java/Kotlin".to_string(),
640        version: None,
641        edition: None,
642        package_manager: None,
643        main_dependencies: Vec::new(),
644        dev_dependencies: Vec::new(),
645        confidence: 0.5,
646        source_files: source_files.map_or(Vec::new(), |f| f.clone()),
647        manifest_files: Vec::new(),
648    };
649    
650    // Detect build tool and parse manifest files
651    for manifest in manifest_files {
652        if let Some(filename) = manifest.file_name().and_then(|n| n.to_str()) {
653            info.manifest_files.push(manifest.clone());
654            
655            match filename {
656                "pom.xml" => {
657                    info.package_manager = Some("maven".to_string());
658                    if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
659                        parse_maven_pom(&content, &mut info, config);
660                        info.confidence = 0.90;
661                    }
662                }
663                "build.gradle" => {
664                    info.package_manager = Some("gradle".to_string());
665                    if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
666                        parse_gradle_build(&content, &mut info, config);
667                        info.confidence = 0.85;
668                    }
669                }
670                "build.gradle.kts" => {
671                    info.package_manager = Some("gradle".to_string());
672                    if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
673                        parse_gradle_kts_build(&content, &mut info, config);
674                        info.confidence = 0.85;
675                    }
676                }
677                _ => {}
678            }
679        }
680    }
681    
682    // Adjust name based on file types
683    if let Some(files) = source_files {
684        let has_kotlin = files.iter().any(|f| {
685            f.extension()
686                .and_then(|e| e.to_str())
687                .map_or(false, |ext| ext == "kt" || ext == "kts")
688        });
689        
690        if has_kotlin {
691            info.name = "Kotlin".to_string();
692        } else {
693            info.name = "Java".to_string();
694        }
695    }
696    
697    // Boost confidence if we have source files
698    if !info.source_files.is_empty() {
699        info.confidence = (info.confidence + 0.8) / 2.0;
700    }
701    
702    Ok(info)
703}
704
705/// Parse Maven pom.xml file (basic XML parsing)
706fn parse_maven_pom(content: &str, info: &mut LanguageInfo, config: &AnalysisConfig) {
707    // Simple regex-based XML parsing for common Maven patterns
708    
709    // Extract Java version from maven.compiler.source or java.version
710    for line in content.lines() {
711        let line = line.trim();
712        
713        // Look for Java version in properties
714        if line.contains("<maven.compiler.source>") {
715            if let Some(version) = extract_xml_content(line, "maven.compiler.source") {
716                info.version = Some(version);
717            }
718        } else if line.contains("<java.version>") {
719            if let Some(version) = extract_xml_content(line, "java.version") {
720                info.version = Some(version);
721            }
722        } else if line.contains("<maven.compiler.target>") && info.version.is_none() {
723            if let Some(version) = extract_xml_content(line, "maven.compiler.target") {
724                info.version = Some(version);
725            }
726        }
727        
728        // Extract dependencies
729        if line.contains("<groupId>") && line.contains("<artifactId>") {
730            // This is a simplified approach - real XML parsing would be better
731            if let Some(group_id) = extract_xml_content(line, "groupId") {
732                if let Some(artifact_id) = extract_xml_content(line, "artifactId") {
733                    let dependency = format!("{}:{}", group_id, artifact_id);
734                    info.main_dependencies.push(dependency);
735                }
736            }
737        } else if line.contains("<artifactId>") && !line.contains("<groupId>") {
738            if let Some(artifact_id) = extract_xml_content(line, "artifactId") {
739                info.main_dependencies.push(artifact_id);
740            }
741        }
742    }
743    
744    // Look for dependencies in a more structured way
745    let mut in_dependencies = false;
746    let mut in_test_dependencies = false;
747    
748    for line in content.lines() {
749        let line = line.trim();
750        
751        if line.contains("<dependencies>") {
752            in_dependencies = true;
753            continue;
754        }
755        
756        if line.contains("</dependencies>") {
757            in_dependencies = false;
758            in_test_dependencies = false;
759            continue;
760        }
761        
762        if in_dependencies && line.contains("<scope>test</scope>") {
763            in_test_dependencies = true;
764        }
765        
766        if in_dependencies && line.contains("<artifactId>") {
767            if let Some(artifact_id) = extract_xml_content(line, "artifactId") {
768                if in_test_dependencies && config.include_dev_dependencies {
769                    info.dev_dependencies.push(artifact_id);
770                } else if !in_test_dependencies {
771                    info.main_dependencies.push(artifact_id);
772                }
773            }
774        }
775    }
776}
777
778/// Parse Gradle build.gradle file (Groovy syntax)
779fn parse_gradle_build(content: &str, info: &mut LanguageInfo, config: &AnalysisConfig) {
780    for line in content.lines() {
781        let line = line.trim();
782        
783        // Look for Java version
784        if line.contains("sourceCompatibility") || line.contains("targetCompatibility") {
785            if let Some(version) = extract_gradle_version(line) {
786                info.version = Some(version);
787            }
788        } else if line.contains("JavaVersion.VERSION_") {
789            if let Some(pos) = line.find("VERSION_") {
790                let version_part = &line[pos + 8..];
791                if let Some(end) = version_part.find(|c: char| !c.is_numeric() && c != '_') {
792                    let version = &version_part[..end].replace('_', ".");
793                    info.version = Some(version.to_string());
794                }
795            }
796        }
797        
798        // Look for dependencies
799        if line.starts_with("implementation ") || line.starts_with("compile ") {
800            if let Some(dep) = extract_gradle_dependency(line) {
801                info.main_dependencies.push(dep);
802            }
803        } else if (line.starts_with("testImplementation ") || line.starts_with("testCompile ")) && config.include_dev_dependencies {
804            if let Some(dep) = extract_gradle_dependency(line) {
805                info.dev_dependencies.push(dep);
806            }
807        }
808    }
809}
810
811/// Parse Gradle build.gradle.kts file (Kotlin syntax)
812fn parse_gradle_kts_build(content: &str, info: &mut LanguageInfo, config: &AnalysisConfig) {
813    // Kotlin DSL is similar to Groovy but with some syntax differences
814    parse_gradle_build(content, info, config); // Reuse the same logic for now
815}
816
817/// Extract content from XML tags
818fn extract_xml_content(line: &str, tag: &str) -> Option<String> {
819    let open_tag = format!("<{}>", tag);
820    let close_tag = format!("</{}>", tag);
821    
822    if let Some(start) = line.find(&open_tag) {
823        if let Some(end) = line.find(&close_tag) {
824            let content_start = start + open_tag.len();
825            if content_start < end {
826                return Some(line[content_start..end].trim().to_string());
827            }
828        }
829    }
830    None
831}
832
833/// Extract version from Gradle configuration line
834fn extract_gradle_version(line: &str) -> Option<String> {
835    // Look for patterns like sourceCompatibility = '11' or sourceCompatibility = "11"
836    if let Some(equals_pos) = line.find('=') {
837        let value_part = line[equals_pos + 1..].trim();
838        if let Some(start_quote) = value_part.find(['\'', '"']) {
839            let quote_char = value_part.chars().nth(start_quote).unwrap();
840            if let Some(end_quote) = value_part[start_quote + 1..].find(quote_char) {
841                let version = &value_part[start_quote + 1..start_quote + 1 + end_quote];
842                return Some(version.to_string());
843            }
844        }
845    }
846    None
847}
848
849/// Extract dependency from Gradle dependency line
850fn extract_gradle_dependency(line: &str) -> Option<String> {
851    // Look for patterns like implementation 'group:artifact:version' or implementation("group:artifact:version")
852    if let Some(start_quote) = line.find(['\'', '"']) {
853        let quote_char = line.chars().nth(start_quote).unwrap();
854        if let Some(end_quote) = line[start_quote + 1..].find(quote_char) {
855            let dependency = &line[start_quote + 1..start_quote + 1 + end_quote];
856            // Extract just the artifact name for simplicity
857            if let Some(last_colon) = dependency.rfind(':') {
858                if let Some(first_colon) = dependency[..last_colon].rfind(':') {
859                    return Some(dependency[first_colon + 1..last_colon].to_string());
860                }
861            }
862            return Some(dependency.to_string());
863        }
864    }
865    None
866}
867
868/// Check if a filename is a known manifest file
869fn is_manifest_file(filename: &str) -> bool {
870    matches!(
871        filename,
872        "Cargo.toml" | "Cargo.lock" |
873        "package.json" | "package-lock.json" | "yarn.lock" | "pnpm-lock.yaml" |
874        "requirements.txt" | "Pipfile" | "Pipfile.lock" | "pyproject.toml" | "setup.py" |
875        "go.mod" | "go.sum" |
876        "pom.xml" | "build.gradle" | "build.gradle.kts"
877    )
878}
879
880/// Check if any of the specified manifest files exist
881fn has_manifest(manifest_files: &[PathBuf], target_files: &[&str]) -> bool {
882    manifest_files.iter().any(|path| {
883        path.file_name()
884            .and_then(|name| name.to_str())
885            .map_or(false, |name| target_files.contains(&name))
886    })
887}
888
889#[cfg(test)]
890mod tests {
891    use super::*;
892    use tempfile::TempDir;
893    use std::fs;
894    
895    #[test]
896    fn test_rust_project_detection() {
897        let temp_dir = TempDir::new().unwrap();
898        let root = temp_dir.path();
899        
900        // Create Cargo.toml
901        let cargo_toml = r#"
902[package]
903name = "test-project"
904version = "0.1.0"
905edition = "2021"
906
907[dependencies]
908serde = "1.0"
909tokio = "1.0"
910
911[dev-dependencies]
912assert_cmd = "2.0"
913"#;
914        fs::write(root.join("Cargo.toml"), cargo_toml).unwrap();
915        fs::create_dir_all(root.join("src")).unwrap();
916        fs::write(root.join("src/main.rs"), "fn main() {}").unwrap();
917        
918        let config = AnalysisConfig::default();
919        let files = vec![
920            root.join("Cargo.toml"),
921            root.join("src/main.rs"),
922        ];
923        
924        let languages = detect_languages(&files, &config).unwrap();
925        assert_eq!(languages.len(), 1);
926        assert_eq!(languages[0].name, "Rust");
927        assert_eq!(languages[0].version, Some("1.56+".to_string()));
928        assert!(languages[0].confidence > 0.9);
929    }
930    
931    #[test]
932    fn test_javascript_project_detection() {
933        let temp_dir = TempDir::new().unwrap();
934        let root = temp_dir.path();
935        
936        // Create package.json
937        let package_json = r#"
938{
939  "name": "test-project",
940  "version": "1.0.0",
941  "engines": {
942    "node": ">=16.0.0"
943  },
944  "dependencies": {
945    "express": "^4.18.0",
946    "lodash": "^4.17.21"
947  },
948  "devDependencies": {
949    "jest": "^29.0.0"
950  }
951}
952"#;
953        fs::write(root.join("package.json"), package_json).unwrap();
954        fs::write(root.join("index.js"), "console.log('hello');").unwrap();
955        
956        let config = AnalysisConfig::default();
957        let files = vec![
958            root.join("package.json"),
959            root.join("index.js"),
960        ];
961        
962        let languages = detect_languages(&files, &config).unwrap();
963        assert_eq!(languages.len(), 1);
964        assert_eq!(languages[0].name, "JavaScript");
965        assert_eq!(languages[0].version, Some(">=16.0.0".to_string()));
966        assert!(languages[0].confidence > 0.9);
967    }
968    
969    #[test]
970    fn test_python_project_detection() {
971        let temp_dir = TempDir::new().unwrap();
972        let root = temp_dir.path();
973        
974        // Create pyproject.toml
975        let pyproject_toml = r#"
976[project]
977name = "test-project"
978version = "0.1.0"
979requires-python = ">=3.8"
980dependencies = [
981    "flask>=2.0.0",
982    "requests>=2.25.0",
983    "pandas>=1.3.0"
984]
985
986[project.optional-dependencies]
987dev = [
988    "pytest>=6.0.0",
989    "black>=21.0.0"
990]
991"#;
992        fs::write(root.join("pyproject.toml"), pyproject_toml).unwrap();
993        fs::write(root.join("app.py"), "print('Hello, World!')").unwrap();
994        
995        let config = AnalysisConfig::default();
996        let files = vec![
997            root.join("pyproject.toml"),
998            root.join("app.py"),
999        ];
1000        
1001        let languages = detect_languages(&files, &config).unwrap();
1002        assert_eq!(languages.len(), 1);
1003        assert_eq!(languages[0].name, "Python");
1004        assert_eq!(languages[0].version, Some(">=3.8".to_string()));
1005        assert!(languages[0].confidence > 0.8);
1006    }
1007    
1008    #[test]
1009    fn test_go_project_detection() {
1010        let temp_dir = TempDir::new().unwrap();
1011        let root = temp_dir.path();
1012        
1013        // Create go.mod
1014        let go_mod = r#"
1015module example.com/myproject
1016
1017go 1.21
1018
1019require (
1020    github.com/gin-gonic/gin v1.9.1
1021    github.com/stretchr/testify v1.8.4
1022    golang.org/x/time v0.3.0
1023)
1024"#;
1025        fs::write(root.join("go.mod"), go_mod).unwrap();
1026        fs::write(root.join("main.go"), "package main\n\nfunc main() {}").unwrap();
1027        
1028        let config = AnalysisConfig::default();
1029        let files = vec![
1030            root.join("go.mod"),
1031            root.join("main.go"),
1032        ];
1033        
1034        let languages = detect_languages(&files, &config).unwrap();
1035        assert_eq!(languages.len(), 1);
1036        assert_eq!(languages[0].name, "Go");
1037        assert_eq!(languages[0].version, Some("1.21".to_string()));
1038        assert!(languages[0].confidence > 0.8);
1039    }
1040    
1041    #[test]
1042    fn test_java_maven_project_detection() {
1043        let temp_dir = TempDir::new().unwrap();
1044        let root = temp_dir.path();
1045        
1046        // Create pom.xml
1047        let pom_xml = r#"
1048<?xml version="1.0" encoding="UTF-8"?>
1049<project xmlns="http://maven.apache.org/POM/4.0.0">
1050    <modelVersion>4.0.0</modelVersion>
1051    
1052    <groupId>com.example</groupId>
1053    <artifactId>test-project</artifactId>
1054    <version>1.0.0</version>
1055    
1056    <properties>
1057        <maven.compiler.source>17</maven.compiler.source>
1058        <maven.compiler.target>17</maven.compiler.target>
1059    </properties>
1060    
1061    <dependencies>
1062        <dependency>
1063            <groupId>org.springframework</groupId>
1064            <artifactId>spring-core</artifactId>
1065            <version>5.3.21</version>
1066        </dependency>
1067        <dependency>
1068            <groupId>junit</groupId>
1069            <artifactId>junit</artifactId>
1070            <version>4.13.2</version>
1071            <scope>test</scope>
1072        </dependency>
1073    </dependencies>
1074</project>
1075"#;
1076        fs::create_dir_all(root.join("src/main/java")).unwrap();
1077        fs::write(root.join("pom.xml"), pom_xml).unwrap();
1078        fs::write(root.join("src/main/java/App.java"), "public class App {}").unwrap();
1079        
1080        let config = AnalysisConfig::default();
1081        let files = vec![
1082            root.join("pom.xml"),
1083            root.join("src/main/java/App.java"),
1084        ];
1085        
1086        let languages = detect_languages(&files, &config).unwrap();
1087        assert_eq!(languages.len(), 1);
1088        assert_eq!(languages[0].name, "Java");
1089        assert_eq!(languages[0].version, Some("17".to_string()));
1090        assert!(languages[0].confidence > 0.8);
1091    }
1092    
1093    #[test]
1094    fn test_kotlin_gradle_project_detection() {
1095        let temp_dir = TempDir::new().unwrap();
1096        let root = temp_dir.path();
1097        
1098        // Create build.gradle.kts
1099        let build_gradle_kts = r#"
1100plugins {
1101    kotlin("jvm") version "1.9.0"
1102    application
1103}
1104
1105java {
1106    sourceCompatibility = JavaVersion.VERSION_17
1107    targetCompatibility = JavaVersion.VERSION_17
1108}
1109
1110dependencies {
1111    implementation("org.jetbrains.kotlin:kotlin-stdlib")
1112    implementation("io.ktor:ktor-server-core:2.3.2")
1113    testImplementation("org.jetbrains.kotlin:kotlin-test")
1114}
1115"#;
1116        fs::create_dir_all(root.join("src/main/kotlin")).unwrap();
1117        fs::write(root.join("build.gradle.kts"), build_gradle_kts).unwrap();
1118        fs::write(root.join("src/main/kotlin/Main.kt"), "fun main() {}").unwrap();
1119        
1120        let config = AnalysisConfig::default();
1121        let files = vec![
1122            root.join("build.gradle.kts"),
1123            root.join("src/main/kotlin/Main.kt"),
1124        ];
1125        
1126        let languages = detect_languages(&files, &config).unwrap();
1127        assert_eq!(languages.len(), 1);
1128        assert_eq!(languages[0].name, "Kotlin");
1129        assert!(languages[0].confidence > 0.8);
1130    }
1131    
1132    #[test]
1133    fn test_python_requirements_txt_detection() {
1134        let temp_dir = TempDir::new().unwrap();
1135        let root = temp_dir.path();
1136        
1137        // Create requirements.txt
1138        let requirements_txt = r#"
1139Flask==2.3.2
1140requests>=2.28.0
1141pandas==1.5.3
1142pytest==7.4.0
1143black>=23.0.0
1144"#;
1145        fs::write(root.join("requirements.txt"), requirements_txt).unwrap();
1146        fs::write(root.join("app.py"), "import flask").unwrap();
1147        
1148        let config = AnalysisConfig::default();
1149        let files = vec![
1150            root.join("requirements.txt"),
1151            root.join("app.py"),
1152        ];
1153        
1154        let languages = detect_languages(&files, &config).unwrap();
1155        assert_eq!(languages.len(), 1);
1156        assert_eq!(languages[0].name, "Python");
1157        assert!(languages[0].confidence > 0.8);
1158    }
1159}