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