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