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