syncable_cli/analyzer/
dependency_parser.rs

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