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