syncable_cli/analyzer/
dependency_parser.rs

1use crate::analyzer::{AnalysisConfig, DetectedLanguage, DependencyMap};
2use crate::analyzer::vulnerability_checker::{VulnerabilityChecker, VulnerabilityInfo};
3use crate::error::{Result, AnalysisError};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::Path;
7use std::fs;
8use log::{debug, info, warn};
9
10/// Detailed dependency information
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct DependencyInfo {
13    pub name: String,
14    pub version: String,
15    pub dep_type: DependencyType,
16    pub license: String,
17    pub source: Option<String>,
18    pub language: Language,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
22pub enum DependencyType {
23    Production,
24    Dev,
25    Optional,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
29pub enum Language {
30    Rust,
31    JavaScript,
32    TypeScript,
33    Python,
34    Go,
35    Java,
36    Kotlin,
37    Unknown,
38}
39
40impl Language {
41    pub fn as_str(&self) -> &str {
42        match self {
43            Language::Rust => "Rust",
44            Language::JavaScript => "JavaScript",
45            Language::TypeScript => "TypeScript",
46            Language::Python => "Python",
47            Language::Go => "Go",
48            Language::Java => "Java",
49            Language::Kotlin => "Kotlin",
50            Language::Unknown => "Unknown",
51        }
52    }
53}
54
55/// Vulnerability information
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57pub struct Vulnerability {
58    pub id: String,
59    pub severity: VulnerabilitySeverity,
60    pub description: String,
61    pub fixed_in: Option<String>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
65pub enum VulnerabilitySeverity {
66    Critical,
67    High,
68    Medium,
69    Low,
70    Info,
71}
72
73/// Legacy dependency info for existing code
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
75pub struct LegacyDependencyInfo {
76    pub version: String,
77    pub is_dev: bool,
78    pub license: Option<String>,
79    pub vulnerabilities: Vec<Vulnerability>,
80    pub source: String, // npm, crates.io, pypi, etc.
81}
82
83/// Enhanced dependency map with detailed information
84pub type DetailedDependencyMap = HashMap<String, LegacyDependencyInfo>;
85
86/// Result of dependency analysis
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
88pub struct DependencyAnalysis {
89    pub dependencies: DetailedDependencyMap,
90    pub total_count: usize,
91    pub production_count: usize,
92    pub dev_count: usize,
93    pub vulnerable_count: usize,
94    pub license_summary: HashMap<String, usize>,
95}
96
97/// New dependency parser for vulnerability checking
98pub struct DependencyParser;
99
100impl DependencyParser {
101    pub fn new() -> Self {
102        Self
103    }
104    
105    /// Check vulnerabilities for dependencies using the vulnerability checker
106    async fn check_vulnerabilities_for_dependencies(
107        &self,
108        dependencies: &HashMap<Language, Vec<DependencyInfo>>,
109        project_path: &Path,
110    ) -> HashMap<String, Vec<VulnerabilityInfo>> {
111        let mut vulnerability_map = HashMap::new();
112        
113        let checker = VulnerabilityChecker::new();
114        
115        match checker.check_all_dependencies(dependencies, project_path).await {
116            Ok(report) => {
117                info!("Found {} total vulnerabilities across all dependencies", report.total_vulnerabilities);
118                
119                // Map vulnerabilities by dependency name
120                for vuln_dep in report.vulnerable_dependencies {
121                    vulnerability_map.insert(vuln_dep.name, vuln_dep.vulnerabilities);
122                }
123            }
124            Err(e) => {
125                warn!("Failed to check vulnerabilities: {}", e);
126            }
127        }
128        
129        vulnerability_map
130    }
131    
132    /// Convert VulnerabilityInfo to legacy Vulnerability format
133    fn convert_vulnerability_info(vuln_info: &VulnerabilityInfo) -> Vulnerability {
134        Vulnerability {
135            id: vuln_info.id.clone(),
136            severity: match vuln_info.severity {
137                crate::analyzer::vulnerability_checker::VulnerabilitySeverity::Critical => VulnerabilitySeverity::Critical,
138                crate::analyzer::vulnerability_checker::VulnerabilitySeverity::High => VulnerabilitySeverity::High,
139                crate::analyzer::vulnerability_checker::VulnerabilitySeverity::Medium => VulnerabilitySeverity::Medium,
140                crate::analyzer::vulnerability_checker::VulnerabilitySeverity::Low => VulnerabilitySeverity::Low,
141                crate::analyzer::vulnerability_checker::VulnerabilitySeverity::Info => VulnerabilitySeverity::Info,
142            },
143            description: vuln_info.description.clone(),
144            fixed_in: vuln_info.patched_versions.clone(),
145        }
146    }
147    
148    pub fn parse_all_dependencies(&self, project_root: &Path) -> Result<HashMap<Language, Vec<DependencyInfo>>> {
149        let mut dependencies = HashMap::new();
150        
151        // Check for Rust
152        if project_root.join("Cargo.toml").exists() {
153            let rust_deps = self.parse_rust_deps(project_root)?;
154            if !rust_deps.is_empty() {
155                dependencies.insert(Language::Rust, rust_deps);
156            }
157        }
158        
159        // Check for JavaScript/TypeScript
160        if project_root.join("package.json").exists() {
161            let js_deps = self.parse_js_deps(project_root)?;
162            if !js_deps.is_empty() {
163                dependencies.insert(Language::JavaScript, js_deps);
164            }
165        }
166        
167        // Check for Python
168        if project_root.join("requirements.txt").exists() || 
169           project_root.join("pyproject.toml").exists() ||
170           project_root.join("Pipfile").exists() {
171            let py_deps = self.parse_python_deps(project_root)?;
172            if !py_deps.is_empty() {
173                dependencies.insert(Language::Python, py_deps);
174            }
175        }
176        
177        // Check for Go
178        if project_root.join("go.mod").exists() {
179            let go_deps = self.parse_go_deps(project_root)?;
180            if !go_deps.is_empty() {
181                dependencies.insert(Language::Go, go_deps);
182            }
183        }
184        
185        // Check for Java/Kotlin
186        if project_root.join("pom.xml").exists() || project_root.join("build.gradle").exists() {
187            let java_deps = self.parse_java_deps(project_root)?;
188            if !java_deps.is_empty() {
189                dependencies.insert(Language::Java, java_deps);
190            }
191        }
192        
193        Ok(dependencies)
194    }
195    
196    fn parse_rust_deps(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
197        let cargo_lock = project_root.join("Cargo.lock");
198        let cargo_toml = project_root.join("Cargo.toml");
199        
200        let mut deps = Vec::new();
201        
202        // First try to parse from Cargo.lock (complete dependency tree)
203        if cargo_lock.exists() {
204            let content = fs::read_to_string(&cargo_lock)?;
205            let parsed: toml::Value = toml::from_str(&content)
206                .map_err(|e| AnalysisError::DependencyParsing {
207                    file: "Cargo.lock".to_string(),
208                    reason: e.to_string(),
209                })?;
210            
211            // Parse package list from Cargo.lock
212            if let Some(packages) = parsed.get("package").and_then(|p| p.as_array()) {
213                for package in packages {
214                    if let Some(package_table) = package.as_table() {
215                        if let (Some(name), Some(version)) = (
216                            package_table.get("name").and_then(|n| n.as_str()),
217                            package_table.get("version").and_then(|v| v.as_str())
218                        ) {
219                            // Determine if it's a direct dependency by checking Cargo.toml
220                            let dep_type = self.get_rust_dependency_type(name, &cargo_toml);
221                            
222                            deps.push(DependencyInfo {
223                                name: name.to_string(),
224                                version: version.to_string(),
225                                dep_type,
226                                license: detect_rust_license(name).unwrap_or_else(|| "Unknown".to_string()),
227                                source: Some("crates.io".to_string()),
228                                language: Language::Rust,
229                            });
230                        }
231                    }
232                }
233            }
234        } else if cargo_toml.exists() {
235            // Fallback to Cargo.toml if Cargo.lock doesn't exist
236            let content = fs::read_to_string(&cargo_toml)?;
237            let parsed: toml::Value = toml::from_str(&content)
238                .map_err(|e| AnalysisError::DependencyParsing {
239                    file: "Cargo.toml".to_string(),
240                    reason: e.to_string(),
241                })?;
242            
243            // Parse regular dependencies
244            if let Some(dependencies) = parsed.get("dependencies").and_then(|d| d.as_table()) {
245                for (name, value) in dependencies {
246                    let version = extract_version_from_toml_value(value);
247                    deps.push(DependencyInfo {
248                        name: name.clone(),
249                        version,
250                        dep_type: DependencyType::Production,
251                        license: detect_rust_license(name).unwrap_or_else(|| "Unknown".to_string()),
252                        source: Some("crates.io".to_string()),
253                        language: Language::Rust,
254                    });
255                }
256            }
257            
258            // Parse dev dependencies
259            if let Some(dev_deps) = parsed.get("dev-dependencies").and_then(|d| d.as_table()) {
260                for (name, value) in dev_deps {
261                    let version = extract_version_from_toml_value(value);
262                    deps.push(DependencyInfo {
263                        name: name.clone(),
264                        version,
265                        dep_type: DependencyType::Dev,
266                        license: detect_rust_license(name).unwrap_or_else(|| "Unknown".to_string()),
267                        source: Some("crates.io".to_string()),
268                        language: Language::Rust,
269                    });
270                }
271            }
272        }
273        
274        Ok(deps)
275    }
276    
277    fn get_rust_dependency_type(&self, dep_name: &str, cargo_toml_path: &Path) -> DependencyType {
278        if !cargo_toml_path.exists() {
279            return DependencyType::Production;
280        }
281        
282        if let Ok(content) = fs::read_to_string(cargo_toml_path) {
283            if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
284                // Check if it's in dev-dependencies
285                if let Some(dev_deps) = parsed.get("dev-dependencies").and_then(|d| d.as_table()) {
286                    if dev_deps.contains_key(dep_name) {
287                        return DependencyType::Dev;
288                    }
289                }
290                
291                // Check if it's in regular dependencies
292                if let Some(deps) = parsed.get("dependencies").and_then(|d| d.as_table()) {
293                    if deps.contains_key(dep_name) {
294                        return DependencyType::Production;
295                    }
296                }
297            }
298        }
299        
300        // Default to production for transitive dependencies
301        DependencyType::Production
302    }
303    
304    fn parse_js_deps(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
305        let package_json = project_root.join("package.json");
306        let content = fs::read_to_string(&package_json)?;
307        let parsed: serde_json::Value = serde_json::from_str(&content)
308            .map_err(|e| AnalysisError::DependencyParsing {
309                file: "package.json".to_string(),
310                reason: e.to_string(),
311            })?;
312        
313        let mut deps = Vec::new();
314        
315        // Parse regular dependencies
316        if let Some(dependencies) = parsed.get("dependencies").and_then(|d| d.as_object()) {
317            for (name, version) in dependencies {
318                if let Some(ver_str) = version.as_str() {
319                    deps.push(DependencyInfo {
320                        name: name.clone(),
321                        version: ver_str.to_string(),
322                        dep_type: DependencyType::Production,
323                        license: detect_npm_license(name).unwrap_or_else(|| "Unknown".to_string()),
324                        source: Some("npm".to_string()),
325                        language: Language::JavaScript,
326                    });
327                }
328            }
329        }
330        
331        // Parse dev dependencies
332        if let Some(dev_deps) = parsed.get("devDependencies").and_then(|d| d.as_object()) {
333            for (name, version) in dev_deps {
334                if let Some(ver_str) = version.as_str() {
335                    deps.push(DependencyInfo {
336                        name: name.clone(),
337                        version: ver_str.to_string(),
338                        dep_type: DependencyType::Dev,
339                        license: detect_npm_license(name).unwrap_or_else(|| "Unknown".to_string()),
340                        source: Some("npm".to_string()),
341                        language: Language::JavaScript,
342                    });
343                }
344            }
345        }
346        
347        Ok(deps)
348    }
349    
350    fn parse_python_deps(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
351        let mut deps = Vec::new();
352        
353        // Try pyproject.toml first (modern Python packaging)
354        let pyproject = project_root.join("pyproject.toml");
355        if pyproject.exists() {
356            debug!("Found pyproject.toml, parsing Python dependencies");
357            let content = fs::read_to_string(&pyproject)?;
358            if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
359                // Poetry dependencies
360                if let Some(poetry_deps) = parsed
361                    .get("tool")
362                    .and_then(|t| t.get("poetry"))
363                    .and_then(|p| p.get("dependencies"))
364                    .and_then(|d| d.as_table())
365                {
366                    debug!("Found Poetry dependencies in pyproject.toml");
367                    for (name, value) in poetry_deps {
368                        if name != "python" {
369                            let version = extract_version_from_toml_value(value);
370                            deps.push(DependencyInfo {
371                                name: name.clone(),
372                                version,
373                                dep_type: DependencyType::Production,
374                                license: detect_pypi_license(name).unwrap_or_else(|| "Unknown".to_string()),
375                                source: Some("pypi".to_string()),
376                                language: Language::Python,
377                            });
378                        }
379                    }
380                }
381                
382                // Poetry dev dependencies
383                if let Some(poetry_dev_deps) = parsed
384                    .get("tool")
385                    .and_then(|t| t.get("poetry"))
386                    .and_then(|p| p.get("group"))
387                    .and_then(|g| g.get("dev"))
388                    .and_then(|d| d.get("dependencies"))
389                    .and_then(|d| d.as_table())
390                    .or_else(|| {
391                        // Fallback to older Poetry format
392                        parsed
393                            .get("tool")
394                            .and_then(|t| t.get("poetry"))
395                            .and_then(|p| p.get("dev-dependencies"))
396                            .and_then(|d| d.as_table())
397                    })
398                {
399                    debug!("Found Poetry dev dependencies in pyproject.toml");
400                    for (name, value) in poetry_dev_deps {
401                        let version = extract_version_from_toml_value(value);
402                        deps.push(DependencyInfo {
403                            name: name.clone(),
404                            version,
405                            dep_type: DependencyType::Dev,
406                            license: detect_pypi_license(name).unwrap_or_else(|| "Unknown".to_string()),
407                            source: Some("pypi".to_string()),
408                            language: Language::Python,
409                        });
410                    }
411                }
412                
413                // PEP 621 dependencies (setuptools, flit, hatch, pdm)
414                if let Some(project_deps) = parsed
415                    .get("project")
416                    .and_then(|p| p.get("dependencies"))
417                    .and_then(|d| d.as_array())
418                {
419                    debug!("Found PEP 621 dependencies in pyproject.toml");
420                    for dep in project_deps {
421                        if let Some(dep_str) = dep.as_str() {
422                            let (name, version) = self.parse_python_requirement_spec(dep_str);
423                            deps.push(DependencyInfo {
424                                name: name.clone(),
425                                version,
426                                dep_type: DependencyType::Production,
427                                license: detect_pypi_license(&name).unwrap_or_else(|| "Unknown".to_string()),
428                                source: Some("pypi".to_string()),
429                                language: Language::Python,
430                            });
431                        }
432                    }
433                }
434                
435                // PEP 621 optional dependencies (test, dev, etc.)
436                if let Some(optional_deps) = parsed
437                    .get("project")
438                    .and_then(|p| p.get("optional-dependencies"))
439                    .and_then(|d| d.as_table())
440                {
441                    debug!("Found PEP 621 optional dependencies in pyproject.toml");
442                    for (group_name, group_deps) in optional_deps {
443                        if let Some(deps_array) = group_deps.as_array() {
444                            let is_dev = group_name.contains("dev") || group_name.contains("test");
445                            for dep in deps_array {
446                                if let Some(dep_str) = dep.as_str() {
447                                    let (name, version) = self.parse_python_requirement_spec(dep_str);
448                                    deps.push(DependencyInfo {
449                                        name: name.clone(),
450                                        version,
451                                        dep_type: if is_dev { DependencyType::Dev } else { DependencyType::Optional },
452                                        license: detect_pypi_license(&name).unwrap_or_else(|| "Unknown".to_string()),
453                                        source: Some("pypi".to_string()),
454                                        language: Language::Python,
455                                    });
456                                }
457                            }
458                        }
459                    }
460                }
461                
462                // PDM dependencies
463                if let Some(pdm_deps) = parsed
464                    .get("tool")
465                    .and_then(|t| t.get("pdm"))
466                    .and_then(|p| p.get("dev-dependencies"))
467                    .and_then(|d| d.as_table())
468                {
469                    debug!("Found PDM dev dependencies in pyproject.toml");
470                    for (group_name, group_deps) in pdm_deps {
471                        if let Some(deps_array) = group_deps.as_array() {
472                            for dep in deps_array {
473                                if let Some(dep_str) = dep.as_str() {
474                                    let (name, version) = self.parse_python_requirement_spec(dep_str);
475                                    deps.push(DependencyInfo {
476                                        name: name.clone(),
477                                        version,
478                                        dep_type: DependencyType::Dev,
479                                        license: detect_pypi_license(&name).unwrap_or_else(|| "Unknown".to_string()),
480                                        source: Some("pypi".to_string()),
481                                        language: Language::Python,
482                                    });
483                                }
484                            }
485                        }
486                    }
487                }
488                
489                // Setuptools dependencies (legacy)
490                if let Some(setuptools_deps) = parsed
491                    .get("tool")
492                    .and_then(|t| t.get("setuptools"))
493                    .and_then(|s| s.get("dynamic"))
494                    .and_then(|d| d.get("dependencies"))
495                    .and_then(|d| d.as_array())
496                {
497                    debug!("Found setuptools dependencies in pyproject.toml");
498                    for dep in setuptools_deps {
499                        if let Some(dep_str) = dep.as_str() {
500                            let (name, version) = self.parse_python_requirement_spec(dep_str);
501                            deps.push(DependencyInfo {
502                                name: name.clone(),
503                                version,
504                                dep_type: DependencyType::Production,
505                                license: detect_pypi_license(&name).unwrap_or_else(|| "Unknown".to_string()),
506                                source: Some("pypi".to_string()),
507                                language: Language::Python,
508                            });
509                        }
510                    }
511                }
512            }
513        }
514        
515        // Try Pipfile (pipenv)
516        let pipfile = project_root.join("Pipfile");
517        if pipfile.exists() && deps.is_empty() {
518            debug!("Found Pipfile, parsing pipenv dependencies");
519            let content = fs::read_to_string(&pipfile)?;
520            if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
521                // Production dependencies
522                if let Some(packages) = parsed.get("packages").and_then(|p| p.as_table()) {
523                    for (name, value) in packages {
524                        let version = extract_version_from_toml_value(value);
525                        deps.push(DependencyInfo {
526                            name: name.clone(),
527                            version,
528                            dep_type: DependencyType::Production,
529                            license: detect_pypi_license(name).unwrap_or_else(|| "Unknown".to_string()),
530                            source: Some("pypi".to_string()),
531                            language: Language::Python,
532                        });
533                    }
534                }
535                
536                // Dev dependencies
537                if let Some(dev_packages) = parsed.get("dev-packages").and_then(|p| p.as_table()) {
538                    for (name, value) in dev_packages {
539                        let version = extract_version_from_toml_value(value);
540                        deps.push(DependencyInfo {
541                            name: name.clone(),
542                            version,
543                            dep_type: DependencyType::Dev,
544                            license: detect_pypi_license(name).unwrap_or_else(|| "Unknown".to_string()),
545                            source: Some("pypi".to_string()),
546                            language: Language::Python,
547                        });
548                    }
549                }
550            }
551        }
552        
553        // Try requirements.txt (legacy, but still widely used)
554        let requirements_txt = project_root.join("requirements.txt");
555        if requirements_txt.exists() && deps.is_empty() {
556            debug!("Found requirements.txt, parsing legacy Python dependencies");
557            let content = fs::read_to_string(&requirements_txt)?;
558            for line in content.lines() {
559                let line = line.trim();
560                if !line.is_empty() && !line.starts_with('#') && !line.starts_with('-') {
561                    let (name, version) = self.parse_python_requirement_spec(line);
562                    deps.push(DependencyInfo {
563                        name: name.clone(),
564                        version,
565                        dep_type: DependencyType::Production,
566                        license: detect_pypi_license(&name).unwrap_or_else(|| "Unknown".to_string()),
567                        source: Some("pypi".to_string()),
568                        language: Language::Python,
569                    });
570                }
571            }
572        }
573        
574        // Try requirements-dev.txt
575        let requirements_dev = project_root.join("requirements-dev.txt");
576        if requirements_dev.exists() {
577            debug!("Found requirements-dev.txt, parsing dev dependencies");
578            let content = fs::read_to_string(&requirements_dev)?;
579            for line in content.lines() {
580                let line = line.trim();
581                if !line.is_empty() && !line.starts_with('#') && !line.starts_with('-') {
582                    let (name, version) = self.parse_python_requirement_spec(line);
583                    deps.push(DependencyInfo {
584                        name: name.clone(),
585                        version,
586                        dep_type: DependencyType::Dev,
587                        license: detect_pypi_license(&name).unwrap_or_else(|| "Unknown".to_string()),
588                        source: Some("pypi".to_string()),
589                        language: Language::Python,
590                    });
591                }
592            }
593        }
594        
595        debug!("Parsed {} Python dependencies", deps.len());
596        if !deps.is_empty() {
597            debug!("Sample Python dependencies:");
598            for dep in deps.iter().take(5) {
599                debug!("  - {} v{} ({:?})", dep.name, dep.version, dep.dep_type);
600            }
601        }
602        
603        Ok(deps)
604    }
605    
606    fn parse_go_deps(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
607        let go_mod = project_root.join("go.mod");
608        let content = fs::read_to_string(&go_mod)?;
609        let mut deps = Vec::new();
610        let mut in_require_block = false;
611        
612        for line in content.lines() {
613            let trimmed = line.trim();
614            
615            if trimmed.starts_with("require (") {
616                in_require_block = true;
617                continue;
618            }
619            
620            if in_require_block && trimmed == ")" {
621                in_require_block = false;
622                continue;
623            }
624            
625            if in_require_block || trimmed.starts_with("require ") {
626                let parts: Vec<&str> = trimmed
627                    .trim_start_matches("require ")
628                    .split_whitespace()
629                    .collect();
630                
631                if parts.len() >= 2 {
632                    let name = parts[0];
633                    let version = parts[1];
634                    
635                    deps.push(DependencyInfo {
636                        name: name.to_string(),
637                        version: version.to_string(),
638                        dep_type: DependencyType::Production,
639                        license: detect_go_license(name).unwrap_or("Unknown".to_string()),
640                        source: Some("go modules".to_string()),
641                        language: Language::Go,
642                    });
643                }
644            }
645        }
646        
647        Ok(deps)
648    }
649    
650    /// Parse a Python requirement specification string (e.g., "package>=1.0.0")
651    fn parse_python_requirement_spec(&self, spec: &str) -> (String, String) {
652        // Handle requirement specification formats like:
653        // - package==1.0.0
654        // - package>=1.0.0,<2.0.0
655        // - package~=1.0.0
656        // - package[extra]>=1.0.0
657        // - package
658        
659        let spec = spec.trim();
660        
661        // Remove any index URLs or other options
662        let spec = if let Some(index) = spec.find("--") {
663            &spec[..index]
664        } else {
665            spec
666        }.trim();
667        
668        // Find the package name (before any version operators)
669        let version_operators = ['=', '>', '<', '~', '!'];
670        let version_start = spec.find(&version_operators[..]);
671        
672        if let Some(pos) = version_start {
673            // Extract package name (including any extras)
674            let package_part = spec[..pos].trim();
675            let version_part = spec[pos..].trim();
676            
677            // Handle extras like package[extra] - keep them as part of the name
678            let package_name = if package_part.contains('[') && package_part.contains(']') {
679                // For packages with extras, extract just the base name
680                if let Some(bracket_start) = package_part.find('[') {
681                    package_part[..bracket_start].trim().to_string()
682                } else {
683                    package_part.to_string()
684                }
685            } else {
686                package_part.to_string()
687            };
688            
689            (package_name, version_part.to_string())
690        } else {
691            // No version specified - handle potential extras
692            let package_name = if spec.contains('[') && spec.contains(']') {
693                if let Some(bracket_start) = spec.find('[') {
694                    spec[..bracket_start].trim().to_string()
695                } else {
696                    spec.to_string()
697                }
698            } else {
699                spec.to_string()
700            };
701            
702            (package_name, "*".to_string())
703        }
704    }
705    
706    fn parse_java_deps(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
707        let mut deps = Vec::new();
708        
709        debug!("Parsing Java dependencies in: {}", project_root.display());
710        
711        // Check for Maven pom.xml
712        let pom_xml = project_root.join("pom.xml");
713        if pom_xml.exists() {
714            debug!("Found pom.xml, parsing Maven dependencies");
715            let content = fs::read_to_string(&pom_xml)?;
716            
717            // Try to use the dependency:list Maven command first for accurate results
718            if let Ok(maven_deps) = self.parse_maven_dependencies_with_command(project_root) {
719                if !maven_deps.is_empty() {
720                    debug!("Successfully parsed {} Maven dependencies using mvn command", maven_deps.len());
721                    deps.extend(maven_deps);
722                }
723            }
724            
725            // If no deps from command, fall back to XML parsing
726            if deps.is_empty() {
727                debug!("Falling back to XML parsing for Maven dependencies");
728                let xml_deps = self.parse_pom_xml(&content)?;
729                debug!("Parsed {} dependencies from pom.xml", xml_deps.len());
730                deps.extend(xml_deps);
731            }
732        }
733        
734        // Check for Gradle build.gradle or build.gradle.kts
735        let build_gradle = project_root.join("build.gradle");
736        let build_gradle_kts = project_root.join("build.gradle.kts");
737        
738        if (build_gradle.exists() || build_gradle_kts.exists()) && deps.is_empty() {
739            debug!("Found Gradle build file, parsing Gradle dependencies");
740            
741                         // Try to use the dependencies Gradle command first
742             if let Ok(gradle_deps) = self.parse_gradle_dependencies_with_command(project_root) {
743                if !gradle_deps.is_empty() {
744                    debug!("Successfully parsed {} Gradle dependencies using gradle command", gradle_deps.len());
745                    deps.extend(gradle_deps);
746                }
747            }
748            
749            // If no deps from command, fall back to build file parsing
750            if deps.is_empty() {
751                if build_gradle.exists() {
752                    debug!("Falling back to build.gradle parsing");
753                    let content = fs::read_to_string(&build_gradle)?;
754                    let gradle_deps = self.parse_gradle_build(&content)?;
755                    debug!("Parsed {} dependencies from build.gradle", gradle_deps.len());
756                    deps.extend(gradle_deps);
757                }
758                
759                if build_gradle_kts.exists() && deps.is_empty() {
760                    debug!("Falling back to build.gradle.kts parsing");
761                    let content = fs::read_to_string(&build_gradle_kts)?;
762                    let gradle_deps = self.parse_gradle_build(&content)?; // Same logic works for .kts
763                    debug!("Parsed {} dependencies from build.gradle.kts", gradle_deps.len());
764                    deps.extend(gradle_deps);
765                }
766            }
767        }
768        
769        debug!("Total Java dependencies found: {}", deps.len());
770        if !deps.is_empty() {
771            debug!("Sample dependencies:");
772            for dep in deps.iter().take(5) {
773                debug!("  - {} v{}", dep.name, dep.version);
774            }
775        }
776        
777        Ok(deps)
778    }
779    
780    /// Parse Maven dependencies using mvn dependency:list command
781    fn parse_maven_dependencies_with_command(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
782        use std::process::Command;
783        
784        let output = Command::new("mvn")
785            .args(&["dependency:list", "-DoutputFile=deps.txt", "-DappendOutput=false", "-DincludeScope=compile"])
786            .current_dir(project_root)
787            .output();
788            
789        match output {
790            Ok(result) if result.status.success() => {
791                // Read the generated deps.txt file
792                let deps_file = project_root.join("deps.txt");
793                if deps_file.exists() {
794                    let content = fs::read_to_string(&deps_file)?;
795                    let deps = self.parse_maven_dependency_list(&content)?;
796                    
797                    // Clean up
798                    let _ = fs::remove_file(&deps_file);
799                    
800                    return Ok(deps);
801                }
802            }
803            _ => {
804                debug!("Maven command failed or not available, falling back to XML parsing");
805            }
806        }
807        
808        Ok(vec![])
809    }
810    
811    /// Parse Gradle dependencies using gradle dependencies command
812    fn parse_gradle_dependencies_with_command(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
813        use std::process::Command;
814        
815        // Try gradle first, then gradlew
816        let gradle_cmds = vec!["gradle", "./gradlew"];
817        
818        for gradle_cmd in gradle_cmds {
819            let output = Command::new(gradle_cmd)
820                .args(&["dependencies", "--configuration=runtimeClasspath", "--console=plain"])
821                .current_dir(project_root)
822                .output();
823                
824            match output {
825                Ok(result) if result.status.success() => {
826                    let output_str = String::from_utf8_lossy(&result.stdout);
827                    let deps = self.parse_gradle_dependency_tree(&output_str)?;
828                    if !deps.is_empty() {
829                        return Ok(deps);
830                    }
831                }
832                _ => {
833                    debug!("Gradle command '{}' failed, trying next", gradle_cmd);
834                    continue;
835                }
836            }
837        }
838        
839        debug!("All Gradle commands failed, falling back to build file parsing");
840        Ok(vec![])
841    }
842    
843    /// Parse Maven dependency list output
844    fn parse_maven_dependency_list(&self, content: &str) -> Result<Vec<DependencyInfo>> {
845        let mut deps = Vec::new();
846        
847        for line in content.lines() {
848            let trimmed = line.trim();
849            if trimmed.is_empty() || trimmed.starts_with("The following") || trimmed.starts_with("---") {
850                continue;
851            }
852            
853            // Format: groupId:artifactId:type:version:scope
854            let parts: Vec<&str> = trimmed.split(':').collect();
855            if parts.len() >= 4 {
856                let group_id = parts[0];
857                let artifact_id = parts[1];
858                let version = parts[3];
859                let scope = if parts.len() > 4 { parts[4] } else { "compile" };
860                
861                let name = format!("{}:{}", group_id, artifact_id);
862                let dep_type = match scope {
863                    "test" | "provided" => DependencyType::Dev,
864                    _ => DependencyType::Production,
865                };
866                
867                deps.push(DependencyInfo {
868                    name,
869                    version: version.to_string(),
870                    dep_type,
871                    license: "Unknown".to_string(),
872                    source: Some("maven".to_string()),
873                    language: Language::Java,
874                });
875            }
876        }
877        
878        Ok(deps)
879    }
880    
881    /// Parse Gradle dependency tree output
882    fn parse_gradle_dependency_tree(&self, content: &str) -> Result<Vec<DependencyInfo>> {
883        let mut deps = Vec::new();
884        
885        for line in content.lines() {
886            let trimmed = line.trim();
887            
888            // Look for dependency lines that match pattern: +--- group:artifact:version
889            if (trimmed.starts_with("+---") || trimmed.starts_with("\\---") || trimmed.starts_with("|")) 
890                && trimmed.contains(':') {
891                
892                // Extract the dependency part
893                let dep_part = if let Some(pos) = trimmed.find(' ') {
894                    &trimmed[pos + 1..]
895                } else {
896                    trimmed
897                };
898                
899                // Remove additional markers and get clean dependency string
900                let clean_dep = dep_part
901                    .replace(" (*)", "")
902                    .replace(" (c)", "")
903                    .replace(" (n)", "")
904                    .replace("(*)", "")
905                    .trim()
906                    .to_string();
907                
908                let parts: Vec<&str> = clean_dep.split(':').collect();
909                if parts.len() >= 3 {
910                    let group_id = parts[0];
911                    let artifact_id = parts[1];
912                    let version = parts[2];
913                    
914                    let name = format!("{}:{}", group_id, artifact_id);
915                    
916                    deps.push(DependencyInfo {
917                        name,
918                        version: version.to_string(),
919                        dep_type: DependencyType::Production,
920                        license: "Unknown".to_string(),
921                        source: Some("gradle".to_string()),
922                        language: Language::Java,
923                    });
924                }
925            }
926        }
927        
928        Ok(deps)
929    }
930    
931    /// Parse pom.xml file directly (fallback method)
932    fn parse_pom_xml(&self, content: &str) -> Result<Vec<DependencyInfo>> {
933        let mut deps = Vec::new();
934        let mut in_dependencies = false;
935        let mut in_dependency = false;
936        let mut current_group_id = String::new();
937        let mut current_artifact_id = String::new();
938        let mut current_version = String::new();
939        let mut current_scope = String::new();
940        
941        for line in content.lines() {
942            let trimmed = line.trim();
943            
944            if trimmed.contains("<dependencies>") {
945                in_dependencies = true;
946                continue;
947            }
948            
949            if trimmed.contains("</dependencies>") {
950                in_dependencies = false;
951                continue;
952            }
953            
954            if in_dependencies {
955                if trimmed.contains("<dependency>") {
956                    in_dependency = true;
957                    current_group_id.clear();
958                    current_artifact_id.clear();
959                    current_version.clear();
960                    current_scope.clear();
961                    continue;
962                }
963                
964                if trimmed.contains("</dependency>") && in_dependency {
965                    in_dependency = false;
966                    
967                    if !current_group_id.is_empty() && !current_artifact_id.is_empty() {
968                        let name = format!("{}:{}", current_group_id, current_artifact_id);
969                        let version = if current_version.is_empty() { 
970                            "unknown".to_string() 
971                        } else { 
972                            current_version.clone() 
973                        };
974                        
975                        let dep_type = match current_scope.as_str() {
976                            "test" | "provided" => DependencyType::Dev,
977                            _ => DependencyType::Production,
978                        };
979                        
980                        deps.push(DependencyInfo {
981                            name,
982                            version,
983                            dep_type,
984                            license: "Unknown".to_string(),
985                            source: Some("maven".to_string()),
986                            language: Language::Java,
987                        });
988                    }
989                    continue;
990                }
991                
992                if in_dependency {
993                    if trimmed.contains("<groupId>") {
994                        current_group_id = extract_xml_value(trimmed, "groupId").to_string();
995                    } else if trimmed.contains("<artifactId>") {
996                        current_artifact_id = extract_xml_value(trimmed, "artifactId").to_string();
997                    } else if trimmed.contains("<version>") {
998                        current_version = extract_xml_value(trimmed, "version").to_string();
999                    } else if trimmed.contains("<scope>") {
1000                        current_scope = extract_xml_value(trimmed, "scope").to_string();
1001                    }
1002                }
1003            }
1004        }
1005        
1006        Ok(deps)
1007    }
1008    
1009    /// Parse Gradle build file directly (fallback method)
1010    fn parse_gradle_build(&self, content: &str) -> Result<Vec<DependencyInfo>> {
1011        let mut deps = Vec::new();
1012        
1013        for line in content.lines() {
1014            let trimmed = line.trim();
1015            
1016            // Look for dependency declarations
1017            if (trimmed.starts_with("implementation ") || 
1018                trimmed.starts_with("compile ") ||
1019                trimmed.starts_with("api ") ||
1020                trimmed.starts_with("runtimeOnly ") ||
1021                trimmed.starts_with("testImplementation ") ||
1022                trimmed.starts_with("testCompile ")) {
1023                
1024                if let Some(dep_str) = extract_gradle_dependency(trimmed) {
1025                    let parts: Vec<&str> = dep_str.split(':').collect();
1026                    if parts.len() >= 3 {
1027                        let group_id = parts[0];
1028                        let artifact_id = parts[1];
1029                        let version = parts[2].trim_matches('"').trim_matches('\'');
1030                        
1031                        let name = format!("{}:{}", group_id, artifact_id);
1032                        let dep_type = if trimmed.starts_with("test") {
1033                            DependencyType::Dev
1034                        } else {
1035                            DependencyType::Production
1036                        };
1037                        
1038                        deps.push(DependencyInfo {
1039                            name,
1040                            version: version.to_string(),
1041                            dep_type,
1042                            license: "Unknown".to_string(),
1043                            source: Some("gradle".to_string()),
1044                            language: Language::Java,
1045                        });
1046                    }
1047                }
1048            }
1049        }
1050        
1051        Ok(deps)
1052    }
1053}
1054
1055/// Parses project dependencies from various manifest files
1056pub fn parse_dependencies(
1057    project_root: &Path,
1058    languages: &[DetectedLanguage],
1059    _config: &AnalysisConfig,
1060) -> Result<DependencyMap> {
1061    let mut all_dependencies = DependencyMap::new();
1062    
1063    for language in languages {
1064        let deps = match language.name.as_str() {
1065            "Rust" => parse_rust_dependencies(project_root)?,
1066            "JavaScript" | "TypeScript" | "JavaScript/TypeScript" => parse_js_dependencies(project_root)?,
1067            "Python" => parse_python_dependencies(project_root)?,
1068            "Go" => parse_go_dependencies(project_root)?,
1069            "Java" | "Kotlin" | "Java/Kotlin" => parse_jvm_dependencies(project_root)?,
1070            _ => DependencyMap::new(),
1071        };
1072        all_dependencies.extend(deps);
1073    }
1074    
1075    Ok(all_dependencies)
1076}
1077
1078/// Parse detailed dependencies with vulnerability and license information
1079pub async fn parse_detailed_dependencies(
1080    project_root: &Path,
1081    languages: &[DetectedLanguage],
1082    _config: &AnalysisConfig,
1083) -> Result<DependencyAnalysis> {
1084    let mut detailed_deps = DetailedDependencyMap::new();
1085    let mut license_summary = HashMap::new();
1086    
1087    // First, get all dependencies without vulnerabilities
1088    for language in languages {
1089        let deps = match language.name.as_str() {
1090            "Rust" => parse_rust_dependencies_detailed(project_root)?,
1091            "JavaScript" | "TypeScript" | "JavaScript/TypeScript" => parse_js_dependencies_detailed(project_root)?,
1092            "Python" => parse_python_dependencies_detailed(project_root)?,
1093            "Go" => parse_go_dependencies_detailed(project_root)?,
1094            "Java" | "Kotlin" | "Java/Kotlin" => parse_jvm_dependencies_detailed(project_root)?,
1095            _ => DetailedDependencyMap::new(),
1096        };
1097        
1098        // Update license summary
1099        for (_, dep_info) in &deps {
1100            if let Some(license) = &dep_info.license {
1101                *license_summary.entry(license.clone()).or_insert(0) += 1;
1102            }
1103        }
1104        
1105        detailed_deps.extend(deps);
1106    }
1107    
1108    // Check vulnerabilities for all dependencies
1109    let parser = DependencyParser::new();
1110    let all_deps = parser.parse_all_dependencies(project_root)?;
1111    let vulnerability_map = parser.check_vulnerabilities_for_dependencies(&all_deps, project_root).await;
1112    
1113    // Update dependencies with vulnerability information
1114    for (dep_name, dep_info) in detailed_deps.iter_mut() {
1115        if let Some(vulns) = vulnerability_map.get(dep_name) {
1116            dep_info.vulnerabilities = vulns.iter()
1117                .map(|v| DependencyParser::convert_vulnerability_info(v))
1118                .collect();
1119        }
1120    }
1121    
1122    let total_count = detailed_deps.len();
1123    let production_count = detailed_deps.values().filter(|d| !d.is_dev).count();
1124    let dev_count = detailed_deps.values().filter(|d| d.is_dev).count();
1125    let vulnerable_count = detailed_deps.values().filter(|d| !d.vulnerabilities.is_empty()).count();
1126    
1127    Ok(DependencyAnalysis {
1128        dependencies: detailed_deps,
1129        total_count,
1130        production_count,
1131        dev_count,
1132        vulnerable_count,
1133        license_summary,
1134    })
1135}
1136
1137/// Parse Rust dependencies from Cargo.toml
1138fn parse_rust_dependencies(project_root: &Path) -> Result<DependencyMap> {
1139    let cargo_toml = project_root.join("Cargo.toml");
1140    if !cargo_toml.exists() {
1141        return Ok(DependencyMap::new());
1142    }
1143    
1144    let content = fs::read_to_string(&cargo_toml)?;
1145    let parsed: toml::Value = toml::from_str(&content)
1146        .map_err(|e| AnalysisError::DependencyParsing {
1147            file: "Cargo.toml".to_string(),
1148            reason: e.to_string(),
1149        })?;
1150    
1151    let mut deps = DependencyMap::new();
1152    
1153    // Parse regular dependencies
1154    if let Some(dependencies) = parsed.get("dependencies").and_then(|d| d.as_table()) {
1155        for (name, value) in dependencies {
1156            let version = extract_version_from_toml_value(value);
1157            deps.insert(name.clone(), version);
1158        }
1159    }
1160    
1161    // Parse dev dependencies
1162    if let Some(dev_deps) = parsed.get("dev-dependencies").and_then(|d| d.as_table()) {
1163        for (name, value) in dev_deps {
1164            let version = extract_version_from_toml_value(value);
1165            deps.insert(format!("{} (dev)", name), version);
1166        }
1167    }
1168    
1169    Ok(deps)
1170}
1171
1172/// Parse detailed Rust dependencies
1173fn parse_rust_dependencies_detailed(project_root: &Path) -> Result<DetailedDependencyMap> {
1174    let cargo_toml = project_root.join("Cargo.toml");
1175    if !cargo_toml.exists() {
1176        return Ok(DetailedDependencyMap::new());
1177    }
1178    
1179    let content = fs::read_to_string(&cargo_toml)?;
1180    let parsed: toml::Value = toml::from_str(&content)
1181        .map_err(|e| AnalysisError::DependencyParsing {
1182            file: "Cargo.toml".to_string(),
1183            reason: e.to_string(),
1184        })?;
1185    
1186    let mut deps = DetailedDependencyMap::new();
1187    
1188    // Parse regular dependencies
1189    if let Some(dependencies) = parsed.get("dependencies").and_then(|d| d.as_table()) {
1190        for (name, value) in dependencies {
1191            let version = extract_version_from_toml_value(value);
1192            deps.insert(name.clone(), LegacyDependencyInfo {
1193                version,
1194                is_dev: false,
1195                license: detect_rust_license(name),
1196                vulnerabilities: vec![], // Populated by vulnerability checker in parse_detailed_dependencies
1197                source: "crates.io".to_string(),
1198            });
1199        }
1200    }
1201    
1202    // Parse dev dependencies
1203    if let Some(dev_deps) = parsed.get("dev-dependencies").and_then(|d| d.as_table()) {
1204        for (name, value) in dev_deps {
1205            let version = extract_version_from_toml_value(value);
1206            deps.insert(name.clone(), LegacyDependencyInfo {
1207                version,
1208                is_dev: true,
1209                license: detect_rust_license(name),
1210                vulnerabilities: vec![], // Populated by vulnerability checker in parse_detailed_dependencies
1211                source: "crates.io".to_string(),
1212            });
1213        }
1214    }
1215    
1216    Ok(deps)
1217}
1218
1219/// Parse JavaScript/Node.js dependencies from package.json
1220fn parse_js_dependencies(project_root: &Path) -> Result<DependencyMap> {
1221    let package_json = project_root.join("package.json");
1222    if !package_json.exists() {
1223        return Ok(DependencyMap::new());
1224    }
1225    
1226    let content = fs::read_to_string(&package_json)?;
1227    let parsed: serde_json::Value = serde_json::from_str(&content)
1228        .map_err(|e| AnalysisError::DependencyParsing {
1229            file: "package.json".to_string(),
1230            reason: e.to_string(),
1231        })?;
1232    
1233    let mut deps = DependencyMap::new();
1234    
1235    // Parse regular dependencies
1236    if let Some(dependencies) = parsed.get("dependencies").and_then(|d| d.as_object()) {
1237        for (name, version) in dependencies {
1238            if let Some(ver_str) = version.as_str() {
1239                deps.insert(name.clone(), ver_str.to_string());
1240            }
1241        }
1242    }
1243    
1244    // Parse dev dependencies
1245    if let Some(dev_deps) = parsed.get("devDependencies").and_then(|d| d.as_object()) {
1246        for (name, version) in dev_deps {
1247            if let Some(ver_str) = version.as_str() {
1248                deps.insert(format!("{} (dev)", name), ver_str.to_string());
1249            }
1250        }
1251    }
1252    
1253    Ok(deps)
1254}
1255
1256/// Parse detailed JavaScript dependencies
1257fn parse_js_dependencies_detailed(project_root: &Path) -> Result<DetailedDependencyMap> {
1258    let package_json = project_root.join("package.json");
1259    if !package_json.exists() {
1260        return Ok(DetailedDependencyMap::new());
1261    }
1262    
1263    let content = fs::read_to_string(&package_json)?;
1264    let parsed: serde_json::Value = serde_json::from_str(&content)
1265        .map_err(|e| AnalysisError::DependencyParsing {
1266            file: "package.json".to_string(),
1267            reason: e.to_string(),
1268        })?;
1269    
1270    let mut deps = DetailedDependencyMap::new();
1271    
1272    // Parse regular dependencies
1273    if let Some(dependencies) = parsed.get("dependencies").and_then(|d| d.as_object()) {
1274        for (name, version) in dependencies {
1275            if let Some(ver_str) = version.as_str() {
1276                deps.insert(name.clone(), LegacyDependencyInfo {
1277                    version: ver_str.to_string(),
1278                    is_dev: false,
1279                    license: detect_npm_license(name),
1280                    vulnerabilities: vec![], // Populated by vulnerability checker in parse_detailed_dependencies
1281                    source: "npm".to_string(),
1282                });
1283            }
1284        }
1285    }
1286    
1287    // Parse dev dependencies
1288    if let Some(dev_deps) = parsed.get("devDependencies").and_then(|d| d.as_object()) {
1289        for (name, version) in dev_deps {
1290            if let Some(ver_str) = version.as_str() {
1291                deps.insert(name.clone(), LegacyDependencyInfo {
1292                    version: ver_str.to_string(),
1293                    is_dev: true,
1294                    license: detect_npm_license(name),
1295                    vulnerabilities: vec![], // Populated by vulnerability checker in parse_detailed_dependencies
1296                    source: "npm".to_string(),
1297                });
1298            }
1299        }
1300    }
1301    
1302    Ok(deps)
1303}
1304
1305/// Parse Python dependencies from requirements.txt, Pipfile, or pyproject.toml
1306fn parse_python_dependencies(project_root: &Path) -> Result<DependencyMap> {
1307    let mut deps = DependencyMap::new();
1308    
1309    // Try requirements.txt first
1310    let requirements_txt = project_root.join("requirements.txt");
1311    if requirements_txt.exists() {
1312        let content = fs::read_to_string(&requirements_txt)?;
1313        for line in content.lines() {
1314            if !line.trim().is_empty() && !line.starts_with('#') {
1315                let parts: Vec<&str> = line.split(&['=', '>', '<', '~', '!'][..]).collect();
1316                if !parts.is_empty() {
1317                    let name = parts[0].trim();
1318                    let version = if parts.len() > 1 {
1319                        line[name.len()..].trim().to_string()
1320                    } else {
1321                        "*".to_string()
1322                    };
1323                    deps.insert(name.to_string(), version);
1324                }
1325            }
1326        }
1327    }
1328    
1329    // Try pyproject.toml
1330    let pyproject = project_root.join("pyproject.toml");
1331    if pyproject.exists() {
1332        let content = fs::read_to_string(&pyproject)?;
1333        if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
1334            // Poetry dependencies
1335            if let Some(poetry_deps) = parsed
1336                .get("tool")
1337                .and_then(|t| t.get("poetry"))
1338                .and_then(|p| p.get("dependencies"))
1339                .and_then(|d| d.as_table())
1340            {
1341                for (name, value) in poetry_deps {
1342                    if name != "python" {
1343                        let version = extract_version_from_toml_value(value);
1344                        deps.insert(name.clone(), version);
1345                    }
1346                }
1347            }
1348            
1349            // Poetry dev dependencies
1350            if let Some(poetry_dev_deps) = parsed
1351                .get("tool")
1352                .and_then(|t| t.get("poetry"))
1353                .and_then(|p| p.get("dev-dependencies"))
1354                .and_then(|d| d.as_table())
1355            {
1356                for (name, value) in poetry_dev_deps {
1357                    let version = extract_version_from_toml_value(value);
1358                    deps.insert(format!("{} (dev)", name), version);
1359                }
1360            }
1361            
1362            // PEP 621 dependencies
1363            if let Some(project_deps) = parsed
1364                .get("project")
1365                .and_then(|p| p.get("dependencies"))
1366                .and_then(|d| d.as_array())
1367            {
1368                for dep in project_deps {
1369                    if let Some(dep_str) = dep.as_str() {
1370                        let parts: Vec<&str> = dep_str.split(&['=', '>', '<', '~', '!'][..]).collect();
1371                        if !parts.is_empty() {
1372                            let name = parts[0].trim();
1373                            let version = if parts.len() > 1 {
1374                                dep_str[name.len()..].trim().to_string()
1375                            } else {
1376                                "*".to_string()
1377                            };
1378                            deps.insert(name.to_string(), version);
1379                        }
1380                    }
1381                }
1382            }
1383        }
1384    }
1385    
1386    Ok(deps)
1387}
1388
1389/// Parse detailed Python dependencies
1390fn parse_python_dependencies_detailed(project_root: &Path) -> Result<DetailedDependencyMap> {
1391    let mut deps = DetailedDependencyMap::new();
1392    
1393    // Try requirements.txt first
1394    let requirements_txt = project_root.join("requirements.txt");
1395    if requirements_txt.exists() {
1396        let content = fs::read_to_string(&requirements_txt)?;
1397        for line in content.lines() {
1398            if !line.trim().is_empty() && !line.starts_with('#') {
1399                let parts: Vec<&str> = line.split(&['=', '>', '<', '~', '!'][..]).collect();
1400                if !parts.is_empty() {
1401                    let name = parts[0].trim();
1402                    let version = if parts.len() > 1 {
1403                        line[name.len()..].trim().to_string()
1404                    } else {
1405                        "*".to_string()
1406                    };
1407                    deps.insert(name.to_string(), LegacyDependencyInfo {
1408                        version,
1409                        is_dev: false,
1410                        license: detect_pypi_license(name),
1411                        vulnerabilities: vec![], // Populated by vulnerability checker in parse_detailed_dependencies
1412                        source: "pypi".to_string(),
1413                    });
1414                }
1415            }
1416        }
1417    }
1418    
1419    // Try pyproject.toml for more detailed info
1420    let pyproject = project_root.join("pyproject.toml");
1421    if pyproject.exists() {
1422        let content = fs::read_to_string(&pyproject)?;
1423        if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
1424            // Poetry dependencies
1425            if let Some(poetry_deps) = parsed
1426                .get("tool")
1427                .and_then(|t| t.get("poetry"))
1428                .and_then(|p| p.get("dependencies"))
1429                .and_then(|d| d.as_table())
1430            {
1431                for (name, value) in poetry_deps {
1432                    if name != "python" {
1433                        let version = extract_version_from_toml_value(value);
1434                        deps.insert(name.clone(), LegacyDependencyInfo {
1435                            version,
1436                            is_dev: false,
1437                            license: detect_pypi_license(name),
1438                            vulnerabilities: vec![],
1439                            source: "pypi".to_string(),
1440                        });
1441                    }
1442                }
1443            }
1444            
1445            // Poetry dev dependencies
1446            if let Some(poetry_dev_deps) = parsed
1447                .get("tool")
1448                .and_then(|t| t.get("poetry"))
1449                .and_then(|p| p.get("dev-dependencies"))
1450                .and_then(|d| d.as_table())
1451            {
1452                for (name, value) in poetry_dev_deps {
1453                    let version = extract_version_from_toml_value(value);
1454                    deps.insert(name.clone(), LegacyDependencyInfo {
1455                        version,
1456                        is_dev: true,
1457                        license: detect_pypi_license(name),
1458                        vulnerabilities: vec![],
1459                        source: "pypi".to_string(),
1460                    });
1461                }
1462            }
1463        }
1464    }
1465    
1466    Ok(deps)
1467}
1468
1469/// Parse Go dependencies from go.mod
1470fn parse_go_dependencies(project_root: &Path) -> Result<DependencyMap> {
1471    let go_mod = project_root.join("go.mod");
1472    if !go_mod.exists() {
1473        return Ok(DependencyMap::new());
1474    }
1475    
1476    let content = fs::read_to_string(&go_mod)?;
1477    let mut deps = DependencyMap::new();
1478    let mut in_require_block = false;
1479    
1480    for line in content.lines() {
1481        let trimmed = line.trim();
1482        
1483        if trimmed.starts_with("require (") {
1484            in_require_block = true;
1485            continue;
1486        }
1487        
1488        if in_require_block && trimmed == ")" {
1489            in_require_block = false;
1490            continue;
1491        }
1492        
1493        if in_require_block || trimmed.starts_with("require ") {
1494            let parts: Vec<&str> = trimmed
1495                .trim_start_matches("require ")
1496                .split_whitespace()
1497                .collect();
1498            
1499            if parts.len() >= 2 {
1500                let name = parts[0];
1501                let version = parts[1];
1502                deps.insert(name.to_string(), version.to_string());
1503            }
1504        }
1505    }
1506    
1507    Ok(deps)
1508}
1509
1510/// Parse detailed Go dependencies
1511fn parse_go_dependencies_detailed(project_root: &Path) -> Result<DetailedDependencyMap> {
1512    let go_mod = project_root.join("go.mod");
1513    if !go_mod.exists() {
1514        return Ok(DetailedDependencyMap::new());
1515    }
1516    
1517    let content = fs::read_to_string(&go_mod)?;
1518    let mut deps = DetailedDependencyMap::new();
1519    let mut in_require_block = false;
1520    
1521    for line in content.lines() {
1522        let trimmed = line.trim();
1523        
1524        if trimmed.starts_with("require (") {
1525            in_require_block = true;
1526            continue;
1527        }
1528        
1529        if in_require_block && trimmed == ")" {
1530            in_require_block = false;
1531            continue;
1532        }
1533        
1534        if in_require_block || trimmed.starts_with("require ") {
1535            let parts: Vec<&str> = trimmed
1536                .trim_start_matches("require ")
1537                .split_whitespace()
1538                .collect();
1539            
1540            if parts.len() >= 2 {
1541                let name = parts[0];
1542                let version = parts[1];
1543                let is_indirect = parts.len() > 2 && parts.contains(&"//") && parts.contains(&"indirect");
1544                
1545                deps.insert(name.to_string(), LegacyDependencyInfo {
1546                    version: version.to_string(),
1547                    is_dev: is_indirect,
1548                    license: detect_go_license(name),
1549                    vulnerabilities: vec![], // Populated by vulnerability checker in parse_detailed_dependencies
1550                    source: "go modules".to_string(),
1551                });
1552            }
1553        }
1554    }
1555    
1556    Ok(deps)
1557}
1558
1559/// Parse JVM dependencies from pom.xml or build.gradle
1560fn parse_jvm_dependencies(project_root: &Path) -> Result<DependencyMap> {
1561    let mut deps = DependencyMap::new();
1562    
1563    // Try pom.xml (Maven)
1564    let pom_xml = project_root.join("pom.xml");
1565    if pom_xml.exists() {
1566        // Simple XML parsing for demonstration
1567        // In production, use a proper XML parser
1568        let content = fs::read_to_string(&pom_xml)?;
1569        let lines: Vec<&str> = content.lines().collect();
1570        
1571        for i in 0..lines.len() {
1572            if lines[i].contains("<dependency>") {
1573                let mut group_id = "";
1574                let mut artifact_id = "";
1575                let mut version = "";
1576                
1577                for j in i..lines.len() {
1578                    if lines[j].contains("</dependency>") {
1579                        break;
1580                    }
1581                    if lines[j].contains("<groupId>") {
1582                        group_id = extract_xml_value(lines[j], "groupId");
1583                    }
1584                    if lines[j].contains("<artifactId>") {
1585                        artifact_id = extract_xml_value(lines[j], "artifactId");
1586                    }
1587                    if lines[j].contains("<version>") {
1588                        version = extract_xml_value(lines[j], "version");
1589                    }
1590                }
1591                
1592                if !group_id.is_empty() && !artifact_id.is_empty() {
1593                    let name = format!("{}:{}", group_id, artifact_id);
1594                    deps.insert(name, version.to_string());
1595                }
1596            }
1597        }
1598    }
1599    
1600    // Try build.gradle (Gradle)
1601    let build_gradle = project_root.join("build.gradle");
1602    if build_gradle.exists() {
1603        let content = fs::read_to_string(&build_gradle)?;
1604        
1605        // Simple pattern matching for Gradle dependencies
1606        for line in content.lines() {
1607            let trimmed = line.trim();
1608            if trimmed.starts_with("implementation") || 
1609               trimmed.starts_with("compile") ||
1610               trimmed.starts_with("testImplementation") ||
1611               trimmed.starts_with("testCompile") {
1612                
1613                if let Some(dep_str) = extract_gradle_dependency(trimmed) {
1614                    let parts: Vec<&str> = dep_str.split(':').collect();
1615                    if parts.len() >= 3 {
1616                        let name = format!("{}:{}", parts[0], parts[1]);
1617                        let version = parts[2];
1618                        let is_test = trimmed.starts_with("test");
1619                        let key = if is_test { format!("{} (test)", name) } else { name };
1620                        deps.insert(key, version.to_string());
1621                    }
1622                }
1623            }
1624        }
1625    }
1626    
1627    Ok(deps)
1628}
1629
1630/// Parse detailed JVM dependencies
1631fn parse_jvm_dependencies_detailed(project_root: &Path) -> Result<DetailedDependencyMap> {
1632    let mut deps = DetailedDependencyMap::new();
1633    
1634    // Try pom.xml (Maven)
1635    let pom_xml = project_root.join("pom.xml");
1636    if pom_xml.exists() {
1637        let content = fs::read_to_string(&pom_xml)?;
1638        let lines: Vec<&str> = content.lines().collect();
1639        
1640        for i in 0..lines.len() {
1641            if lines[i].contains("<dependency>") {
1642                let mut group_id = "";
1643                let mut artifact_id = "";
1644                let mut version = "";
1645                let mut scope = "compile";
1646                
1647                for j in i..lines.len() {
1648                    if lines[j].contains("</dependency>") {
1649                        break;
1650                    }
1651                    if lines[j].contains("<groupId>") {
1652                        group_id = extract_xml_value(lines[j], "groupId");
1653                    }
1654                    if lines[j].contains("<artifactId>") {
1655                        artifact_id = extract_xml_value(lines[j], "artifactId");
1656                    }
1657                    if lines[j].contains("<version>") {
1658                        version = extract_xml_value(lines[j], "version");
1659                    }
1660                    if lines[j].contains("<scope>") {
1661                        scope = extract_xml_value(lines[j], "scope");
1662                    }
1663                }
1664                
1665                if !group_id.is_empty() && !artifact_id.is_empty() {
1666                    let name = format!("{}:{}", group_id, artifact_id);
1667                    deps.insert(name.clone(), LegacyDependencyInfo {
1668                        version: version.to_string(),
1669                        is_dev: scope == "test" || scope == "provided",
1670                        license: detect_maven_license(&name),
1671                        vulnerabilities: vec![], // Populated by vulnerability checker in parse_detailed_dependencies
1672                        source: "maven".to_string(),
1673                    });
1674                }
1675            }
1676        }
1677    }
1678    
1679    // Try build.gradle (Gradle)
1680    let build_gradle = project_root.join("build.gradle");
1681    if build_gradle.exists() {
1682        let content = fs::read_to_string(&build_gradle)?;
1683        
1684        for line in content.lines() {
1685            let trimmed = line.trim();
1686            if trimmed.starts_with("implementation") || 
1687               trimmed.starts_with("compile") ||
1688               trimmed.starts_with("testImplementation") ||
1689               trimmed.starts_with("testCompile") {
1690                
1691                if let Some(dep_str) = extract_gradle_dependency(trimmed) {
1692                    let parts: Vec<&str> = dep_str.split(':').collect();
1693                    if parts.len() >= 3 {
1694                        let name = format!("{}:{}", parts[0], parts[1]);
1695                        let version = parts[2];
1696                        let is_test = trimmed.starts_with("test");
1697                        
1698                        deps.insert(name.clone(), LegacyDependencyInfo {
1699                            version: version.to_string(),
1700                            is_dev: is_test,
1701                            license: detect_maven_license(&name),
1702                            vulnerabilities: vec![],
1703                            source: "gradle".to_string(),
1704                        });
1705                    }
1706                }
1707            }
1708        }
1709    }
1710    
1711    Ok(deps)
1712}
1713
1714// Helper functions
1715
1716fn extract_version_from_toml_value(value: &toml::Value) -> String {
1717    match value {
1718        toml::Value::String(s) => s.clone(),
1719        toml::Value::Table(t) => {
1720            t.get("version")
1721                .and_then(|v| v.as_str())
1722                .unwrap_or("*")
1723                .to_string()
1724        }
1725        _ => "*".to_string(),
1726    }
1727}
1728
1729fn extract_xml_value<'a>(line: &'a str, tag: &str) -> &'a str {
1730    let start_tag = format!("<{}>", tag);
1731    let end_tag = format!("</{}>", tag);
1732    
1733    if let Some(start) = line.find(&start_tag) {
1734        if let Some(end) = line.find(&end_tag) {
1735            return &line[start + start_tag.len()..end];
1736        }
1737    }
1738    ""
1739}
1740
1741fn extract_gradle_dependency(line: &str) -> Option<&str> {
1742    // Handle various Gradle dependency formats
1743    if let Some(start) = line.find('\'') {
1744        if let Some(end) = line.rfind('\'') {
1745            if start < end {
1746                return Some(&line[start + 1..end]);
1747            }
1748        }
1749    }
1750    if let Some(start) = line.find('"') {
1751        if let Some(end) = line.rfind('"') {
1752            if start < end {
1753                return Some(&line[start + 1..end]);
1754            }
1755        }
1756    }
1757    None
1758}
1759
1760// License detection helpers (simplified - in production, use a proper license database)
1761
1762fn detect_rust_license(crate_name: &str) -> Option<String> {
1763    // Common Rust crates and their licenses
1764    match crate_name {
1765        "serde" | "serde_json" | "tokio" | "clap" => Some("MIT OR Apache-2.0".to_string()),
1766        "actix-web" => Some("MIT OR Apache-2.0".to_string()),
1767        _ => Some("Unknown".to_string()),
1768    }
1769}
1770
1771fn detect_npm_license(package_name: &str) -> Option<String> {
1772    // Common npm packages and their licenses
1773    match package_name {
1774        "react" | "vue" | "angular" => Some("MIT".to_string()),
1775        "express" => Some("MIT".to_string()),
1776        "webpack" => Some("MIT".to_string()),
1777        _ => Some("Unknown".to_string()),
1778    }
1779}
1780
1781fn detect_pypi_license(package_name: &str) -> Option<String> {
1782    // Common Python packages and their licenses
1783    match package_name {
1784        "django" => Some("BSD-3-Clause".to_string()),
1785        "flask" => Some("BSD-3-Clause".to_string()),
1786        "requests" => Some("Apache-2.0".to_string()),
1787        "numpy" | "pandas" => Some("BSD-3-Clause".to_string()),
1788        _ => Some("Unknown".to_string()),
1789    }
1790}
1791
1792fn detect_go_license(module_name: &str) -> Option<String> {
1793    // Common Go modules and their licenses
1794    if module_name.starts_with("github.com/gin-gonic/") {
1795        Some("MIT".to_string())
1796    } else if module_name.starts_with("github.com/gorilla/") {
1797        Some("BSD-3-Clause".to_string())
1798    } else {
1799        Some("Unknown".to_string())
1800    }
1801}
1802
1803fn detect_maven_license(artifact: &str) -> Option<String> {
1804    // Common Maven artifacts and their licenses
1805    if artifact.starts_with("org.springframework") {
1806        Some("Apache-2.0".to_string())
1807    } else if artifact.starts_with("junit:junit") {
1808        Some("EPL-1.0".to_string())
1809    } else {
1810        Some("Unknown".to_string())
1811    }
1812}
1813
1814#[cfg(test)]
1815mod tests {
1816    use super::*;
1817    use tempfile::TempDir;
1818    use std::fs;
1819    
1820    #[test]
1821    fn test_parse_rust_dependencies() {
1822        let temp_dir = TempDir::new().unwrap();
1823        let cargo_toml = r#"
1824[package]
1825name = "test"
1826version = "0.1.0"
1827
1828[dependencies]
1829serde = "1.0"
1830tokio = { version = "1.0", features = ["full"] }
1831
1832[dev-dependencies]
1833assert_cmd = "2.0"
1834"#;
1835        
1836        fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml).unwrap();
1837        
1838        let deps = parse_rust_dependencies(temp_dir.path()).unwrap();
1839        assert_eq!(deps.get("serde"), Some(&"1.0".to_string()));
1840        assert_eq!(deps.get("tokio"), Some(&"1.0".to_string()));
1841        assert_eq!(deps.get("assert_cmd (dev)"), Some(&"2.0".to_string()));
1842    }
1843    
1844    #[test]
1845    fn test_parse_js_dependencies() {
1846        let temp_dir = TempDir::new().unwrap();
1847        let package_json = r#"{
1848  "name": "test",
1849  "version": "1.0.0",
1850  "dependencies": {
1851    "express": "^4.18.0",
1852    "react": "^18.0.0"
1853  },
1854  "devDependencies": {
1855    "jest": "^29.0.0"
1856  }
1857}"#;
1858        
1859        fs::write(temp_dir.path().join("package.json"), package_json).unwrap();
1860        
1861        let deps = parse_js_dependencies(temp_dir.path()).unwrap();
1862        assert_eq!(deps.get("express"), Some(&"^4.18.0".to_string()));
1863        assert_eq!(deps.get("react"), Some(&"^18.0.0".to_string()));
1864        assert_eq!(deps.get("jest (dev)"), Some(&"^29.0.0".to_string()));
1865    }
1866    
1867    #[test]
1868    fn test_vulnerability_severity() {
1869        let vuln = Vulnerability {
1870            id: "CVE-2023-1234".to_string(),
1871            severity: VulnerabilitySeverity::High,
1872            description: "Test vulnerability".to_string(),
1873            fixed_in: Some("1.0.1".to_string()),
1874        };
1875        
1876        assert!(matches!(vuln.severity, VulnerabilitySeverity::High));
1877    }
1878    
1879    #[test]
1880    fn test_parse_python_requirement_spec() {
1881        let parser = DependencyParser::new();
1882        
1883        // Test basic package name
1884        let (name, version) = parser.parse_python_requirement_spec("requests");
1885        assert_eq!(name, "requests");
1886        assert_eq!(version, "*");
1887        
1888        // Test package with exact version
1889        let (name, version) = parser.parse_python_requirement_spec("requests==2.28.0");
1890        assert_eq!(name, "requests");
1891        assert_eq!(version, "==2.28.0");
1892        
1893        // Test package with version constraint
1894        let (name, version) = parser.parse_python_requirement_spec("requests>=2.25.0,<3.0.0");
1895        assert_eq!(name, "requests");
1896        assert_eq!(version, ">=2.25.0,<3.0.0");
1897        
1898        // Test package with extras
1899        let (name, version) = parser.parse_python_requirement_spec("fastapi[all]>=0.95.0");
1900        assert_eq!(name, "fastapi");
1901        assert_eq!(version, ">=0.95.0");
1902        
1903        // Test package with tilde operator
1904        let (name, version) = parser.parse_python_requirement_spec("django~=4.1.0");
1905        assert_eq!(name, "django");
1906        assert_eq!(version, "~=4.1.0");
1907    }
1908
1909    #[test]
1910    fn test_parse_pyproject_toml_poetry() {
1911        use std::fs;
1912        use tempfile::tempdir;
1913        
1914        let dir = tempdir().unwrap();
1915        let pyproject_path = dir.path().join("pyproject.toml");
1916        
1917        let pyproject_content = r#"
1918[tool.poetry]
1919name = "test-project"
1920version = "0.1.0"
1921
1922[tool.poetry.dependencies]
1923python = "^3.9"
1924fastapi = "^0.95.0"
1925uvicorn = {extras = ["standard"], version = "^0.21.0"}
1926
1927[tool.poetry.group.dev.dependencies]
1928pytest = "^7.0.0"
1929black = "^23.0.0"
1930"#;
1931        
1932        fs::write(&pyproject_path, pyproject_content).unwrap();
1933        
1934        let parser = DependencyParser::new();
1935        let deps = parser.parse_python_deps(dir.path()).unwrap();
1936        
1937        assert!(!deps.is_empty());
1938        
1939        // Check that we found FastAPI and Uvicorn as production dependencies
1940        let fastapi = deps.iter().find(|d| d.name == "fastapi");
1941        assert!(fastapi.is_some());
1942        assert!(matches!(fastapi.unwrap().dep_type, DependencyType::Production));
1943        
1944        let uvicorn = deps.iter().find(|d| d.name == "uvicorn");
1945        assert!(uvicorn.is_some());
1946        assert!(matches!(uvicorn.unwrap().dep_type, DependencyType::Production));
1947        
1948        // Check that we found pytest and black as dev dependencies
1949        let pytest = deps.iter().find(|d| d.name == "pytest");
1950        assert!(pytest.is_some());
1951        assert!(matches!(pytest.unwrap().dep_type, DependencyType::Dev));
1952        
1953        let black = deps.iter().find(|d| d.name == "black");
1954        assert!(black.is_some());
1955        assert!(matches!(black.unwrap().dep_type, DependencyType::Dev));
1956        
1957        // Make sure we didn't include python as a dependency
1958        assert!(deps.iter().find(|d| d.name == "python").is_none());
1959    }
1960
1961    #[test]
1962    fn test_parse_pyproject_toml_pep621() {
1963        use std::fs;
1964        use tempfile::tempdir;
1965        
1966        let dir = tempdir().unwrap();
1967        let pyproject_path = dir.path().join("pyproject.toml");
1968        
1969        let pyproject_content = r#"
1970[project]
1971name = "test-project"
1972version = "0.1.0"
1973dependencies = [
1974    "fastapi>=0.95.0",
1975    "uvicorn[standard]>=0.21.0",
1976    "pydantic>=1.10.0"
1977]
1978
1979[project.optional-dependencies]
1980test = [
1981    "pytest>=7.0.0",
1982    "pytest-cov>=4.0.0"
1983]
1984dev = [
1985    "black>=23.0.0",
1986    "mypy>=1.0.0"
1987]
1988"#;
1989        
1990        fs::write(&pyproject_path, pyproject_content).unwrap();
1991        
1992        let parser = DependencyParser::new();
1993        let deps = parser.parse_python_deps(dir.path()).unwrap();
1994        
1995        assert!(!deps.is_empty());
1996        
1997        // Check production dependencies
1998        let prod_deps: Vec<_> = deps.iter().filter(|d| matches!(d.dep_type, DependencyType::Production)).collect();
1999        assert_eq!(prod_deps.len(), 3);
2000        assert!(prod_deps.iter().any(|d| d.name == "fastapi"));
2001        assert!(prod_deps.iter().any(|d| d.name == "uvicorn"));
2002        assert!(prod_deps.iter().any(|d| d.name == "pydantic"));
2003        
2004        // Check dev/test dependencies
2005        let dev_deps: Vec<_> = deps.iter().filter(|d| matches!(d.dep_type, DependencyType::Dev)).collect();
2006        assert!(dev_deps.iter().any(|d| d.name == "pytest"));
2007        assert!(dev_deps.iter().any(|d| d.name == "black"));
2008        assert!(dev_deps.iter().any(|d| d.name == "mypy"));
2009        
2010        // Check optional dependencies (test group is treated as dev)
2011        let test_deps: Vec<_> = deps.iter().filter(|d| d.name == "pytest-cov").collect();
2012        assert_eq!(test_deps.len(), 1);
2013        assert!(matches!(test_deps[0].dep_type, DependencyType::Dev));
2014    }
2015
2016    #[test]
2017    fn test_parse_pipfile() {
2018        use std::fs;
2019        use tempfile::tempdir;
2020        
2021        let dir = tempdir().unwrap();
2022        let pipfile_path = dir.path().join("Pipfile");
2023        
2024        let pipfile_content = r#"
2025[[source]]
2026url = "https://pypi.org/simple"
2027verify_ssl = true
2028name = "pypi"
2029
2030[packages]
2031django = "~=4.1.0"
2032django-rest-framework = "*"
2033psycopg2 = ">=2.9.0"
2034
2035[dev-packages]
2036pytest = "*"
2037flake8 = "*"
2038black = ">=22.0.0"
2039"#;
2040        
2041        fs::write(&pipfile_path, pipfile_content).unwrap();
2042        
2043        let parser = DependencyParser::new();
2044        let deps = parser.parse_python_deps(dir.path()).unwrap();
2045        
2046        assert!(!deps.is_empty());
2047        
2048        // Check production dependencies
2049        let prod_deps: Vec<_> = deps.iter().filter(|d| matches!(d.dep_type, DependencyType::Production)).collect();
2050        assert_eq!(prod_deps.len(), 3);
2051        assert!(prod_deps.iter().any(|d| d.name == "django"));
2052        assert!(prod_deps.iter().any(|d| d.name == "django-rest-framework"));
2053        assert!(prod_deps.iter().any(|d| d.name == "psycopg2"));
2054        
2055        // Check dev dependencies
2056        let dev_deps: Vec<_> = deps.iter().filter(|d| matches!(d.dep_type, DependencyType::Dev)).collect();
2057        assert_eq!(dev_deps.len(), 3);
2058        assert!(dev_deps.iter().any(|d| d.name == "pytest"));
2059        assert!(dev_deps.iter().any(|d| d.name == "flake8"));
2060        assert!(dev_deps.iter().any(|d| d.name == "black"));
2061    }
2062
2063    #[test]
2064    fn test_dependency_analysis_summary() {
2065        let mut deps = DetailedDependencyMap::new();
2066        deps.insert("prod-dep".to_string(), LegacyDependencyInfo {
2067            version: "1.0.0".to_string(),
2068            is_dev: false,
2069            license: Some("MIT".to_string()),
2070            vulnerabilities: vec![],
2071            source: "npm".to_string(),
2072        });
2073        deps.insert("dev-dep".to_string(), LegacyDependencyInfo {
2074            version: "2.0.0".to_string(),
2075            is_dev: true,
2076            license: Some("MIT".to_string()),
2077            vulnerabilities: vec![],
2078            source: "npm".to_string(),
2079        });
2080        
2081        let analysis = DependencyAnalysis {
2082            dependencies: deps,
2083            total_count: 2,
2084            production_count: 1,
2085            dev_count: 1,
2086            vulnerable_count: 0,
2087            license_summary: {
2088                let mut map = HashMap::new();
2089                map.insert("MIT".to_string(), 2);
2090                map
2091            },
2092        };
2093        
2094        assert_eq!(analysis.total_count, 2);
2095        assert_eq!(analysis.production_count, 1);
2096        assert_eq!(analysis.dev_count, 1);
2097        assert_eq!(analysis.license_summary.get("MIT"), Some(&2));
2098    }
2099}