1use crate::models::{
29 DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage, Sha512Digest,
30};
31use crate::parser_warn as warn;
32use crate::parsers::utils::{capped_iteration_limit, npm_purl, parse_sri, truncate_field};
33use serde_json::Value as JsonValue;
34use std::collections::{HashMap, HashSet};
35use std::path::Path;
36use yaml_serde::Value;
37
38use super::PackageParser;
39
40pub struct YarnLockParser;
44
45#[derive(Clone, Debug, PartialEq, Eq)]
46struct ManifestDependencyInfo {
47 scope: &'static str,
48 is_runtime: bool,
49 is_optional: bool,
50}
51
52impl PackageParser for YarnLockParser {
53 const PACKAGE_TYPE: PackageType = PackageType::Npm;
54
55 fn is_match(path: &Path) -> bool {
56 path.file_name()
57 .and_then(|name| name.to_str())
58 .map(|name| name == "yarn.lock")
59 .unwrap_or(false)
60 }
61
62 fn extract_packages(path: &Path) -> Vec<PackageData> {
63 let content = match crate::parsers::utils::read_file_to_string(path, None) {
64 Ok(content) => content,
65 Err(e) => {
66 warn!("Failed to read yarn.lock at {:?}: {}", path, e);
67 return vec![default_package_data(Some(DatasourceId::YarnLock))];
68 }
69 };
70
71 let is_v2 = detect_yarn_version(&content);
72 let manifest_dependencies = load_manifest_dependency_info(path);
73
74 vec![if is_v2 {
75 parse_yarn_v2(&content, &manifest_dependencies)
76 } else {
77 parse_yarn_v1(&content, &manifest_dependencies)
78 }]
79 }
80
81 fn metadata() -> Vec<super::metadata::ParserMetadata> {
82 vec![super::metadata::ParserMetadata {
83 description: "yarn.lock lockfile (v1 and v2+)",
84 file_patterns: &["**/yarn.lock"],
85 package_type: "npm",
86 primary_language: "JavaScript",
87 documentation_url: Some("https://classic.yarnpkg.com/lang/en/docs/yarn-lock/"),
88 }]
89 }
90}
91
92pub fn detect_yarn_version(content: &str) -> bool {
94 content
95 .lines()
96 .take(10)
97 .any(|line| line.contains("__metadata:"))
98}
99
100fn parse_yarn_v2(
102 content: &str,
103 manifest_dependencies: &HashMap<String, ManifestDependencyInfo>,
104) -> PackageData {
105 let yaml_value: Value = match yaml_serde::from_str(content) {
106 Ok(val) => val,
107 Err(e) => {
108 warn!("Failed to parse yarn.lock v2 YAML: {}", e);
109 return default_package_data(Some(DatasourceId::YarnLockV2));
110 }
111 };
112
113 let yaml_map = match yaml_value.as_mapping() {
114 Some(map) => map,
115 None => return default_package_data(Some(DatasourceId::YarnLockV2)),
116 };
117
118 let mut dependencies = Vec::new();
119 let package_extra_data = extract_yarn_v2_package_extra_data(yaml_map);
120
121 let limit = capped_iteration_limit(yaml_map.len(), "yarn.lock v2 entries");
122 for (spec, details) in yaml_map.iter().take(limit) {
123 if spec.as_str().map(|s| s == "__metadata").unwrap_or(false) {
124 continue;
125 }
126
127 let _spec_str = match spec.as_str() {
128 Some(s) => s,
129 None => continue,
130 };
131
132 let details_map = match details.as_mapping() {
133 Some(map) => map,
134 None => continue,
135 };
136
137 let resolution = extract_yaml_string(details_map, "resolution").unwrap_or_default();
138
139 let (namespace_opt, name, resolved_version) = parse_yarn_v2_resolution(&resolution);
140 let namespace = namespace_opt.map(truncate_field).unwrap_or_default();
141 let name = truncate_field(name);
142 let resolved_version = truncate_field(resolved_version);
143 let full_name = full_package_name(&namespace, &name);
144 let manifest_info = manifest_dependencies.get(&full_name);
145 let purl = create_purl(&namespace, &name, &resolved_version).map(truncate_field);
146 let checksum = extract_yaml_string(details_map, "checksum").map(truncate_field);
147
148 let deps_yaml = details_map.get("dependencies");
149 let peer_deps_yaml = details_map.get("peerDependencies");
150 let resolved_extra_data = extract_yarn_v2_resolved_extra_data(details_map, &resolution);
151
152 let nested_deps = parse_yaml_dependencies(deps_yaml);
153 let peer_deps = parse_yaml_dependencies(peer_deps_yaml);
154
155 let all_deps = if peer_deps.is_empty() {
156 nested_deps
157 } else {
158 let mut combined = nested_deps;
159 for mut dep in peer_deps {
160 dep.scope = Some("peerDependencies".to_string());
161 dep.is_optional = None;
165 dep.is_runtime = None;
166 combined.push(dep);
167 }
168 combined
169 };
170
171 let resolved_package = ResolvedPackage {
172 primary_language: Some("JavaScript".to_string()),
173 download_url: None,
174 sha1: None,
175 sha256: None,
176 sha512: checksum.and_then(|h| Sha512Digest::from_hex(&h).ok()),
177 md5: None,
178 is_virtual: true,
179 extra_data: resolved_extra_data,
180 dependencies: all_deps,
181 repository_homepage_url: None,
182 repository_download_url: None,
183 api_data_url: None,
184 datasource_id: Some(DatasourceId::YarnLockV2),
185 purl: None,
186 ..ResolvedPackage::new(
187 YarnLockParser::PACKAGE_TYPE,
188 namespace.clone(),
189 name.clone(),
190 resolved_version.clone(),
191 )
192 };
193
194 let (scope, is_runtime, is_optional, is_direct) = manifest_info
195 .map(|info| {
196 (
197 Some(info.scope.to_string()),
198 Some(info.is_runtime),
199 Some(info.is_optional),
200 Some(true),
201 )
202 })
203 .unwrap_or((None, None, None, None));
204
205 let dependency = Dependency {
206 purl,
207 extracted_requirement: Some(truncate_field(resolved_version.clone())),
208 scope,
209 is_runtime,
210 is_optional,
211 is_pinned: Some(true),
212 is_direct,
213 resolved_package: Some(Box::new(resolved_package)),
214 extra_data: Some(HashMap::from([(
215 "resolution".to_string(),
216 JsonValue::String(truncate_field(resolution)),
217 )])),
218 };
219
220 dependencies.push(dependency);
221 }
222
223 PackageData {
224 package_type: Some(YarnLockParser::PACKAGE_TYPE),
225 namespace: None,
226 name: None,
227 version: None,
228 qualifiers: None,
229 subpath: None,
230 primary_language: None,
231 description: None,
232 release_date: None,
233 parties: Vec::new(),
234 keywords: Vec::new(),
235 homepage_url: None,
236 download_url: None,
237 size: None,
238 sha1: None,
239 md5: None,
240 sha256: None,
241 sha512: None,
242 bug_tracking_url: None,
243 code_view_url: None,
244 vcs_url: None,
245 copyright: None,
246 holder: None,
247 declared_license_expression: None,
248 declared_license_expression_spdx: None,
249 license_detections: Vec::new(),
250 other_license_expression: None,
251 other_license_expression_spdx: None,
252 other_license_detections: Vec::new(),
253 extracted_license_statement: None,
254 notice_text: None,
255 source_packages: Vec::new(),
256 file_references: Vec::new(),
257 is_private: false,
258 is_virtual: false,
259 extra_data: package_extra_data,
260 dependencies,
261 repository_homepage_url: None,
262 repository_download_url: None,
263 api_data_url: None,
264 datasource_id: Some(DatasourceId::YarnLockV2),
265 purl: None,
266 }
267}
268
269fn parse_yarn_v1(
271 content: &str,
272 manifest_dependencies: &HashMap<String, ManifestDependencyInfo>,
273) -> PackageData {
274 let mut dependencies = Vec::new();
275 let mut seen_purls = HashSet::new();
276
277 let block_count = content.split("\n\n").count();
278 let limit = capped_iteration_limit(block_count, "yarn.lock v1 blocks");
279 for block in content.split("\n\n").take(limit) {
280 if is_empty_or_comment_block(block) {
281 continue;
282 }
283
284 if let Some(dep) = parse_yarn_v1_block(block, manifest_dependencies) {
285 if let Some(ref purl) = dep.purl {
286 if seen_purls.insert(purl.clone()) {
287 dependencies.push(dep);
288 }
289 } else {
290 dependencies.push(dep);
291 }
292 }
293 }
294
295 PackageData {
296 package_type: Some(YarnLockParser::PACKAGE_TYPE),
297 namespace: None,
298 name: None,
299 version: None,
300 qualifiers: None,
301 subpath: None,
302 primary_language: None,
303 description: None,
304 release_date: None,
305 parties: Vec::new(),
306 keywords: Vec::new(),
307 homepage_url: None,
308 download_url: None,
309 size: None,
310 sha1: None,
311 md5: None,
312 sha256: None,
313 sha512: None,
314 bug_tracking_url: None,
315 code_view_url: None,
316 vcs_url: None,
317 copyright: None,
318 holder: None,
319 declared_license_expression: None,
320 declared_license_expression_spdx: None,
321 license_detections: Vec::new(),
322 other_license_expression: None,
323 other_license_expression_spdx: None,
324 other_license_detections: Vec::new(),
325 extracted_license_statement: None,
326 notice_text: None,
327 source_packages: Vec::new(),
328 file_references: Vec::new(),
329 is_private: false,
330 is_virtual: false,
331 extra_data: None,
332 dependencies,
333 repository_homepage_url: None,
334 repository_download_url: None,
335 api_data_url: None,
336 datasource_id: Some(DatasourceId::YarnLockV1),
337 purl: None,
338 }
339}
340
341fn is_empty_or_comment_block(block: &str) -> bool {
342 block
343 .lines()
344 .all(|line| line.trim().is_empty() || line.trim().starts_with('#'))
345}
346
347fn parse_integrity_field(integrity: &str) -> Option<String> {
349 parse_sri(integrity).and_then(|(algo, hex_digest)| {
350 if algo == "sha512" {
351 Some(hex_digest)
352 } else {
353 None
354 }
355 })
356}
357
358pub fn extract_namespace_and_name(package_name: &str) -> (String, String) {
360 if package_name.starts_with('@') {
361 let parts: Vec<&str> = package_name.splitn(2, '/').collect();
362 if parts.len() == 2 {
363 (parts[0].to_string(), parts[1].to_string())
364 } else {
365 (String::new(), package_name.to_string())
366 }
367 } else {
368 (String::new(), package_name.to_string())
369 }
370}
371
372fn create_purl(namespace: &str, name: &str, version: &str) -> Option<String> {
373 let full_name = if namespace.is_empty() {
374 name.to_string()
375 } else {
376 format!("{}/{}", namespace, name)
377 };
378 let version_opt = if version.is_empty() {
379 None
380 } else {
381 Some(version)
382 };
383 npm_purl(&full_name, version_opt)
384}
385
386fn default_package_data(datasource_id: Option<DatasourceId>) -> PackageData {
387 PackageData {
388 package_type: Some(YarnLockParser::PACKAGE_TYPE),
389 datasource_id,
390 ..Default::default()
391 }
392}
393
394fn parse_yarn_v1_block(
396 block: &str,
397 manifest_dependencies: &HashMap<String, ManifestDependencyInfo>,
398) -> Option<Dependency> {
399 let lines: Vec<&str> = block.lines().collect();
400 if lines.is_empty() {
401 return None;
402 }
403
404 let requirement_index = lines.iter().position(|line| {
405 let trimmed = line.trim();
406 !trimmed.is_empty() && !trimmed.starts_with('#')
407 })?;
408
409 let requirement_line = lines[requirement_index]
410 .trim()
411 .strip_suffix(':')
412 .unwrap_or_else(|| lines[requirement_index].trim())
413 .trim_matches('"');
414 if requirement_line.is_empty() {
415 return None;
416 }
417
418 let (namespace, name, constraint) = parse_yarn_v1_requirement(requirement_line);
419 let namespace = truncate_field(namespace);
420 let name = truncate_field(name);
421 let constraint = truncate_field(constraint);
422
423 if name.is_empty() {
424 return None;
425 }
426
427 let mut version = String::new();
428 let mut resolved_url = String::new();
429 let mut integrity = String::new();
430 let mut nested_deps = Vec::new();
431
432 for line in &lines[requirement_index + 1..] {
433 let trimmed = line.trim();
434 if trimmed.is_empty() {
435 continue;
436 }
437
438 if trimmed.starts_with("version") {
439 version = truncate_field(extract_quoted_value(trimmed).unwrap_or_default());
440 } else if trimmed.starts_with("resolved") {
441 resolved_url = truncate_field(extract_quoted_value(trimmed).unwrap_or_default());
442 } else if trimmed.starts_with("integrity") {
443 integrity = truncate_field(
444 trimmed
445 .strip_prefix("integrity")
446 .map(|s| s.trim().to_string())
447 .unwrap_or_default(),
448 );
449 } else if trimmed.starts_with("dependencies") {
450 continue;
452 } else if trimmed.starts_with(" ") && !trimmed.starts_with(" ") {
453 let dep_line = trimmed.trim();
455 if let Some(dep) = parse_yarn_v1_dependency_line(dep_line, &namespace, &name, &version)
456 {
457 nested_deps.push(dep);
458 }
459 }
460 }
461
462 let sha512 = if integrity.is_empty() {
463 None
464 } else {
465 parse_integrity_field(&integrity)
466 };
467
468 let full_name = full_package_name(&namespace, &name);
469 let manifest_info = manifest_dependencies.get(&full_name);
470 let purl = create_purl(&namespace, &name, &version).map(truncate_field);
471 let (scope, is_runtime, is_optional, is_direct) = manifest_info
472 .map(|info| {
473 (
474 Some(info.scope.to_string()),
475 Some(info.is_runtime),
476 Some(info.is_optional),
477 Some(true),
478 )
479 })
480 .unwrap_or((None, None, None, None));
481
482 let resolved_package = ResolvedPackage {
483 primary_language: Some("JavaScript".to_string()),
484 download_url: if resolved_url.is_empty() {
485 None
486 } else {
487 Some(resolved_url)
488 },
489 sha1: None,
490 sha256: None,
491 sha512: sha512.and_then(|h| Sha512Digest::from_hex(&h).ok()),
492 md5: None,
493 is_virtual: true,
494 extra_data: None,
495 dependencies: nested_deps,
496 repository_homepage_url: None,
497 repository_download_url: None,
498 api_data_url: None,
499 datasource_id: Some(DatasourceId::YarnLockV1),
500 purl: None,
501 ..ResolvedPackage::new(
502 YarnLockParser::PACKAGE_TYPE,
503 namespace.clone(),
504 name.clone(),
505 version.clone(),
506 )
507 };
508
509 Some(Dependency {
510 purl,
511 extracted_requirement: Some(constraint),
512 scope,
513 is_runtime,
514 is_optional,
515 is_pinned: Some(true),
516 is_direct,
517 resolved_package: Some(Box::new(resolved_package)),
518 extra_data: None,
519 })
520}
521
522fn full_package_name(namespace: &str, name: &str) -> String {
523 if namespace.is_empty() {
524 name.to_string()
525 } else {
526 format!("{namespace}/{name}")
527 }
528}
529
530fn load_manifest_dependency_info(path: &Path) -> HashMap<String, ManifestDependencyInfo> {
531 let Some(parent) = path.parent() else {
532 return HashMap::new();
533 };
534
535 let manifest_path = parent.join("package.json");
536 let Ok(content) = crate::parsers::utils::read_file_to_string(&manifest_path, None) else {
537 return HashMap::new();
538 };
539
540 let Ok(json) = serde_json::from_str::<JsonValue>(&content) else {
541 return HashMap::new();
542 };
543
544 let peer_optional = json
545 .get("peerDependenciesMeta")
546 .and_then(|value| value.as_object())
547 .map(|meta| {
548 meta.iter()
549 .filter_map(|(name, value)| {
550 value
551 .as_object()
552 .and_then(|entry| entry.get("optional"))
553 .and_then(|value| value.as_bool())
554 .map(|optional| (name.clone(), optional))
555 })
556 .collect::<HashMap<_, _>>()
557 })
558 .unwrap_or_default();
559
560 let mut dependencies = HashMap::new();
561 insert_manifest_dependency_info(
562 &mut dependencies,
563 &json,
564 "dependencies",
565 ManifestDependencyInfo {
566 scope: "dependencies",
567 is_runtime: true,
568 is_optional: false,
569 },
570 );
571 insert_manifest_dependency_info(
572 &mut dependencies,
573 &json,
574 "devDependencies",
575 ManifestDependencyInfo {
576 scope: "devDependencies",
577 is_runtime: false,
578 is_optional: true,
579 },
580 );
581 insert_manifest_dependency_info(
582 &mut dependencies,
583 &json,
584 "optionalDependencies",
585 ManifestDependencyInfo {
586 scope: "optionalDependencies",
587 is_runtime: true,
588 is_optional: true,
589 },
590 );
591
592 if let Some(peer_dependencies) = json
593 .get("peerDependencies")
594 .and_then(|value| value.as_object())
595 {
596 let limit =
597 capped_iteration_limit(peer_dependencies.len(), "yarn manifest peerDependencies");
598 for name in peer_dependencies.keys().take(limit) {
599 dependencies.insert(
600 name.clone(),
601 ManifestDependencyInfo {
602 scope: "peerDependencies",
603 is_runtime: false,
604 is_optional: peer_optional.get(name).copied().unwrap_or(false),
605 },
606 );
607 }
608 }
609
610 dependencies
611}
612
613fn insert_manifest_dependency_info(
614 dependencies: &mut HashMap<String, ManifestDependencyInfo>,
615 json: &JsonValue,
616 field: &str,
617 info: ManifestDependencyInfo,
618) {
619 if let Some(entries) = json.get(field).and_then(|value| value.as_object()) {
620 let context = format!("yarn manifest {field}");
621 let limit = capped_iteration_limit(entries.len(), &context);
622 for name in entries.keys().take(limit) {
623 dependencies.insert(name.clone(), info.clone());
624 }
625 }
626}
627
628pub fn parse_yarn_v1_requirement(line: &str) -> (String, String, String) {
630 if line.contains(", ") {
632 let first_part = line.split(", ").next().unwrap_or(line);
634 return parse_single_yarn_v1_requirement(first_part);
635 }
636 parse_single_yarn_v1_requirement(line)
637}
638
639fn parse_single_yarn_v1_requirement(line: &str) -> (String, String, String) {
641 if let Some(at_pos) = line.rfind('@') {
642 let name_part = &line[..at_pos];
643 let constraint = &line[at_pos + 1..];
644
645 let (namespace, name) = resolve_npm_alias_name(name_part);
650
651 if !name.is_empty() {
652 return (namespace, name, constraint.to_string());
653 }
654 }
655
656 (String::new(), String::new(), String::new())
657}
658
659fn resolve_npm_alias_name(name_part: &str) -> (String, String) {
667 if let Some(npm_idx) = name_part.find("@npm:") {
668 let aliased_target = &name_part[npm_idx + "@npm:".len()..];
669 if !aliased_target.is_empty() {
670 return extract_namespace_and_name(aliased_target);
671 }
672 }
673
674 extract_namespace_and_name(name_part)
675}
676
677fn parse_yarn_v1_dependency_line(
679 line: &str,
680 _parent_namespace: &str,
681 _parent_name: &str,
682 parent_version: &str,
683) -> Option<Dependency> {
684 let trimmed = line.trim_matches('"');
685 if !trimmed.contains('@') {
686 return None;
687 }
688
689 let (namespace, name, constraint) = parse_yarn_v1_requirement(trimmed);
690 let namespace = truncate_field(namespace);
691 let name = truncate_field(name);
692 let constraint = truncate_field(constraint);
693
694 let purl = create_purl(&namespace, &name, parent_version).map(truncate_field);
695
696 Some(Dependency {
700 purl,
701 extracted_requirement: Some(constraint),
702 scope: Some("dependencies".to_string()),
703 is_runtime: None,
704 is_optional: None,
705 is_pinned: Some(false),
706 is_direct: Some(false),
707 resolved_package: None,
708 extra_data: None,
709 })
710}
711
712fn extract_quoted_value(line: &str) -> Option<String> {
714 line.find('"').and_then(|start| {
715 let rest = &line[start + 1..];
716 rest.find('"').map(|end| rest[..end].to_string())
717 })
718}
719
720pub fn parse_yarn_v2_resolution(resolution: &str) -> (Option<String>, String, String) {
722 if resolution.contains("@npm:") {
723 let parts: Vec<&str> = resolution.split("@npm:").collect();
724 if parts.len() == 2 {
725 let package_name = parts[0];
726 let version = parts[1];
727 let (namespace, name) = extract_namespace_and_name(package_name);
728 let namespace_opt = if namespace.is_empty() {
729 None
730 } else {
731 Some(namespace)
732 };
733 return (namespace_opt, name, version.to_string());
734 }
735 }
736
737 if let Some((ident, reference)) = split_yarn_locator(resolution) {
738 let (namespace, name) = extract_namespace_and_name(ident);
739 let namespace_opt = if namespace.is_empty() {
740 None
741 } else {
742 Some(namespace)
743 };
744 return (namespace_opt, name, reference.to_string());
745 }
746
747 let (namespace, name) = extract_namespace_and_name(resolution);
748 let namespace_opt = if namespace.is_empty() {
749 None
750 } else {
751 Some(namespace)
752 };
753 (namespace_opt, name, "*".to_string())
754}
755
756fn split_yarn_locator(resolution: &str) -> Option<(&str, &str)> {
757 if resolution.is_empty() {
758 return None;
759 }
760
761 let separator_index = if resolution.starts_with('@') {
762 let slash_index = resolution.find('/')?;
763 let rest = &resolution[slash_index + 1..];
764 let at_index = rest.find('@')?;
765 slash_index + 1 + at_index
766 } else {
767 resolution.find('@')?
768 };
769
770 let ident = &resolution[..separator_index];
771 let reference = &resolution[separator_index + 1..];
772
773 if ident.is_empty() || reference.is_empty() {
774 None
775 } else {
776 Some((ident, reference))
777 }
778}
779
780fn extract_yarn_v2_package_extra_data(
781 yaml_map: &yaml_serde::Mapping,
782) -> Option<HashMap<String, JsonValue>> {
783 let metadata = yaml_map.get("__metadata")?.as_mapping()?;
784 let mut extra_data = HashMap::new();
785
786 for field in ["version", "cacheKey"] {
787 if let Some(value) = metadata.get(field).and_then(yaml_value_to_json) {
788 extra_data.insert(field.to_string(), value);
789 }
790 }
791
792 (!extra_data.is_empty()).then_some(extra_data)
793}
794
795fn extract_yarn_v2_resolved_extra_data(
796 details_map: &yaml_serde::Mapping,
797 resolution: &str,
798) -> Option<HashMap<String, JsonValue>> {
799 let mut extra_data = HashMap::new();
800 extra_data.insert(
801 "resolution".to_string(),
802 JsonValue::String(truncate_field(resolution.to_string())),
803 );
804
805 for field in ["languageName", "linkType", "bin", "dependenciesMeta"] {
806 if let Some(value) = details_map.get(field).and_then(yaml_value_to_json) {
807 extra_data.insert(field.to_string(), value);
808 }
809 }
810
811 Some(extra_data)
812}
813
814fn yaml_value_to_json(value: &Value) -> Option<JsonValue> {
815 serde_json::to_value(value).ok()
816}
817
818fn extract_yaml_string(map: &yaml_serde::Mapping, key: &str) -> Option<String> {
820 map.get(key).and_then(|v| v.as_str()).map(|s| s.to_string())
821}
822
823fn parse_yaml_dependencies(yaml_value: Option<&Value>) -> Vec<Dependency> {
825 let mut dependencies = Vec::new();
826
827 if let Some(deps_value) = yaml_value
828 && let Some(mapping) = deps_value.as_mapping()
829 {
830 let limit = capped_iteration_limit(mapping.len(), "yarn.lock v2 nested dependencies");
831 for (key, value) in mapping.iter().take(limit) {
832 let name = match key.as_str() {
833 Some(s) => s.to_string(),
834 None => continue,
835 };
836
837 let constraint = match value.as_str() {
838 Some(s) => s.to_string(),
839 None => "*".to_string(),
840 };
841
842 let (namespace, dep_name) = extract_namespace_and_name(&name);
843 let namespace = truncate_field(namespace);
844 let dep_name = truncate_field(dep_name);
845 let constraint = truncate_field(constraint);
846 let purl = create_purl(&namespace, &dep_name, &constraint).map(truncate_field);
847
848 dependencies.push(Dependency {
852 purl,
853 extracted_requirement: Some(constraint),
854 scope: Some("dependencies".to_string()),
855 is_runtime: None,
856 is_optional: None,
857 is_pinned: Some(false),
858 is_direct: Some(false),
859 resolved_package: None,
860 extra_data: None,
861 });
862 }
863 }
864 dependencies
865}
866
867#[cfg(test)]
868mod tests {
869 use super::*;
870 use std::path::PathBuf;
871
872 #[test]
873 fn test_is_match_yarn_lock() {
874 let valid_path = PathBuf::from("/some/path/yarn.lock");
875 assert!(YarnLockParser::is_match(&valid_path));
876 }
877
878 #[test]
879 fn test_is_not_match_package_json() {
880 let invalid_path = PathBuf::from("/some/path/package.json");
881 assert!(!YarnLockParser::is_match(&invalid_path));
882 }
883
884 #[test]
885 fn test_detect_yarn_v2() {
886 let content = r#"# This file is generated by running "yarn install"
887__metadata:
888 version: 6
889"#;
890 assert!(detect_yarn_version(content));
891 }
892
893 #[test]
894 fn test_detect_yarn_v1() {
895 let content = r#"# THIS IS AN AUTOGENERATED FILE
896# yarn lockfile v1
897
898abbrev@1:
899 version "1.0.9"
900"#;
901 assert!(!detect_yarn_version(content));
902 }
903
904 #[test]
905 fn test_parse_yarn_v2_uses_yarn_lock_v2_datasource_ids() {
906 let content = r#"# This file is generated by running \"yarn install\"
907__metadata:
908 version: 6
909
910lodash@npm:^4.17.21:
911 version: 4.17.21
912 resolution: "lodash@npm:4.17.21"
913"#;
914
915 let package_data = parse_yarn_v2(content, &HashMap::new());
916
917 assert_eq!(package_data.datasource_id, Some(DatasourceId::YarnLockV2));
918 assert_eq!(
919 package_data.dependencies[0]
920 .resolved_package
921 .as_ref()
922 .and_then(|pkg| pkg.datasource_id),
923 Some(DatasourceId::YarnLockV2)
924 );
925 }
926
927 #[test]
928 fn test_parse_yarn_v1_uses_yarn_lock_v1_datasource_ids() {
929 let content = r#"# THIS IS AN AUTOGENERATED FILE
930# yarn lockfile v1
931
932left-pad@^1.3.0:
933 version \"1.3.0\"
934"#;
935
936 let package_data = parse_yarn_v1(content, &HashMap::new());
937
938 assert_eq!(package_data.datasource_id, Some(DatasourceId::YarnLockV1));
939 assert_eq!(
940 package_data.dependencies[0]
941 .resolved_package
942 .as_ref()
943 .and_then(|pkg| pkg.datasource_id),
944 Some(DatasourceId::YarnLockV1)
945 );
946 }
947
948 #[test]
949 fn test_extract_namespace_and_name_scoped() {
950 let (namespace, name) = extract_namespace_and_name("@types/node");
951 assert_eq!(namespace, "@types");
952 assert_eq!(name, "node");
953 }
954
955 #[test]
956 fn test_extract_namespace_and_name_regular() {
957 let (namespace, name) = extract_namespace_and_name("express");
958 assert_eq!(namespace, "");
959 assert_eq!(name, "express");
960 }
961
962 #[test]
963 fn test_parse_yarn_v1_requirement() {
964 let (namespace, name, constraint) = parse_yarn_v1_requirement("express@^4.0.0");
965 assert_eq!(namespace, "");
966 assert_eq!(name, "express");
967 assert_eq!(constraint, "^4.0.0");
968 }
969
970 #[test]
971 fn test_parse_yarn_v1_requirement_scoped() {
972 let (namespace, name, constraint) = parse_yarn_v1_requirement("@types/node@^18.0.0");
973 assert_eq!(namespace, "@types");
974 assert_eq!(name, "node");
975 assert_eq!(constraint, "^18.0.0");
976 }
977
978 #[test]
979 fn test_parse_yarn_v2_resolution() {
980 let (namespace, name, version) = parse_yarn_v2_resolution("@actions/core@npm:1.2.6");
981 assert_eq!(namespace, Some("@actions".to_string()));
982 assert_eq!(name, "core");
983 assert_eq!(version, "1.2.6");
984 }
985
986 #[test]
987 fn test_parse_yarn_v1_requirement_npm_alias_scoped_target() {
988 let (namespace, name, constraint) =
991 parse_yarn_v1_requirement("react-loadable@npm:@docusaurus/react-loadable@6.0.0");
992 assert_eq!(namespace, "@docusaurus");
993 assert_eq!(name, "react-loadable");
994 assert_eq!(constraint, "6.0.0");
995 }
996
997 #[test]
998 fn test_parse_yarn_v1_requirement_npm_alias_renames_scope() {
999 let (namespace, name, constraint) =
1000 parse_yarn_v1_requirement("ajv@npm:@redocly/ajv@8.18.0");
1001 assert_eq!(namespace, "@redocly");
1002 assert_eq!(name, "ajv");
1003 assert_eq!(constraint, "8.18.0");
1004 }
1005
1006 #[test]
1007 fn test_parse_yarn_v1_requirement_npm_alias_unscoped_target() {
1008 let (namespace, name, constraint) =
1011 parse_yarn_v1_requirement("string-width@npm:string-width-cjs@4.2.3");
1012 assert_eq!(namespace, "");
1013 assert_eq!(name, "string-width-cjs");
1014 assert_eq!(constraint, "4.2.3");
1015 }
1016
1017 #[test]
1018 fn test_parse_yarn_v1_block_npm_alias_emits_aliased_purl() {
1019 let content = r#"# THIS IS AN AUTOGENERATED FILE
1020# yarn lockfile v1
1021
1022"react-loadable@npm:@docusaurus/react-loadable@6.0.0":
1023 version "6.0.0"
1024 resolved "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz"
1025 integrity sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==
1026"#;
1027
1028 let package_data = parse_yarn_v1(content, &HashMap::new());
1029
1030 assert_eq!(package_data.dependencies.len(), 1);
1031 let dep = &package_data.dependencies[0];
1032 assert_eq!(
1033 dep.purl.as_deref(),
1034 Some("pkg:npm/%40docusaurus/react-loadable@6.0.0")
1035 );
1036 let resolved = dep
1037 .resolved_package
1038 .as_ref()
1039 .expect("aliased dependency should carry a resolved package");
1040 assert_eq!(resolved.namespace, "@docusaurus");
1041 assert_eq!(resolved.name, "react-loadable");
1042 assert_eq!(resolved.version, "6.0.0");
1043 }
1044}