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#[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#[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#[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, }
95
96pub type DetailedDependencyMap = HashMap<String, LegacyDependencyInfo>;
98
99#[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#[derive(Default)]
112pub struct DependencyParser;
113
114impl DependencyParser {
115 pub fn new() -> Self {
116 Self
117 }
118
119 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 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 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 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 pub fn discover_project_dirs(&self, root: &Path) -> Vec<PathBuf> {
197 let mut dirs = Vec::new();
198
199 if Self::has_package_manifest(root) {
201 dirs.push(root.to_path_buf());
202 }
203
204 self.walk_for_manifests(root, 0, 4, &mut dirs);
206
207 dirs.sort();
209 dirs.dedup();
210
211 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 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 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 fn parse_deps_in_dir(
292 &self,
293 dir: &Path,
294 dependencies: &mut HashMap<Language, Vec<DependencyInfo>>,
295 ) -> Result<()> {
296 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 fn parse_python_requirement_spec(&self, spec: &str) -> (String, String) {
853 let spec = spec.trim();
861
862 let spec = if let Some(index) = spec.find("--") {
864 &spec[..index]
865 } else {
866 spec
867 }
868 .trim();
869
870 let version_operators = ['=', '>', '<', '~', '!'];
872 let version_start = spec.find(&version_operators[..]);
873
874 if let Some(pos) = version_start {
875 let package_part = spec[..pos].trim();
877 let version_part = spec[pos..].trim();
878
879 let package_name = if package_part.contains('[') && package_part.contains(']') {
881 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 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 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 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 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 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 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 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)?; 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 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 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 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 fn parse_gradle_dependencies_with_command(
1035 &self,
1036 project_root: &Path,
1037 ) -> Result<Vec<DependencyInfo>> {
1038 use std::process::Command;
1039
1040 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 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 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 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 if (trimmed.starts_with("+---")
1122 || trimmed.starts_with("\\---")
1123 || trimmed.starts_with("|"))
1124 && trimmed.contains(':')
1125 {
1126 let dep_part = if let Some(pos) = trimmed.find(' ') {
1128 &trimmed[pos + 1..]
1129 } else {
1130 trimmed
1131 };
1132
1133 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 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 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 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
1288pub 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
1313pub 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 let parser = DependencyParser::new();
1324 let project_dirs = parser.discover_project_dirs(project_root);
1325
1326 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 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 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 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
1402fn 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 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 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
1437fn 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 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![], source: "crates.io".to_string(),
1465 },
1466 );
1467 }
1468 }
1469
1470 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![], source: "crates.io".to_string(),
1482 },
1483 );
1484 }
1485 }
1486
1487 Ok(deps)
1488}
1489
1490fn 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 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 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
1527fn 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 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![], source: "npm".to_string(),
1555 },
1556 );
1557 }
1558 }
1559 }
1560
1561 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![], source: "npm".to_string(),
1573 },
1574 );
1575 }
1576 }
1577 }
1578
1579 Ok(deps)
1580}
1581
1582fn parse_python_dependencies(project_root: &Path) -> Result<DependencyMap> {
1584 let mut deps = DependencyMap::new();
1585
1586 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 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 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 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 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
1667fn parse_python_dependencies_detailed(project_root: &Path) -> Result<DetailedDependencyMap> {
1669 let mut deps = DetailedDependencyMap::new();
1670
1671 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![], source: "pypi".to_string(),
1693 },
1694 );
1695 }
1696 }
1697 }
1698 }
1699
1700 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 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 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
1756fn 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
1797fn 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![], source: "go modules".to_string(),
1841 },
1842 );
1843 }
1844 }
1845 }
1846
1847 Ok(deps)
1848}
1849
1850fn parse_jvm_dependencies(project_root: &Path) -> Result<DependencyMap> {
1852 let mut deps = DependencyMap::new();
1853
1854 let pom_xml = project_root.join("pom.xml");
1856 if pom_xml.exists() {
1857 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 let build_gradle = project_root.join("build.gradle");
1893 if build_gradle.exists() {
1894 let content = fs::read_to_string(&build_gradle)?;
1895
1896 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
1924fn parse_jvm_dependencies_detailed(project_root: &Path) -> Result<DetailedDependencyMap> {
1926 let mut deps = DetailedDependencyMap::new();
1927
1928 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![], source: "maven".to_string(),
1969 },
1970 );
1971 }
1972 }
1973 }
1974 }
1975
1976 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
2013fn 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 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
2056fn detect_rust_license(crate_name: &str) -> Option<String> {
2059 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 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 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 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 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 let (name, version) = parser.parse_python_requirement_spec("requests");
2181 assert_eq!(name, "requests");
2182 assert_eq!(version, "*");
2183
2184 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 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 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 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 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 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 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 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 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 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 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 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}