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