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