Skip to main content

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