syncable_cli/analyzer/
dependency_parser.rs

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