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