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