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