Skip to main content

berry/
parse.rs

1use nom::IResult;
2use nom::{
3  Parser,
4  branch::alt,
5  bytes::complete::{is_not, tag, take_until, take_while, take_while1},
6  character::complete::{char, newline, space0, space1},
7  combinator::{map, opt, recognize},
8  multi::fold_many0,
9  sequence::{delimited, preceded, terminated},
10};
11use smallvec::SmallVec;
12
13use crate::ident::{Descriptor, Ident};
14use crate::locator::Locator;
15use crate::lockfile::{
16  Entry, Lockfile, parse_constraints, parse_metadata, parse_resolutions, parse_yarn_header,
17};
18use crate::metadata::{DependencyMeta, PeerDependencyMeta};
19use crate::package::{LinkType, Package};
20
21/// Parse a package entry into a full lockfile Entry
22fn parse_entry(input: &str) -> IResult<&str, Entry<'_>> {
23  map(parse_package_entry, |(descriptors, package)| {
24    Entry::new(descriptors, package)
25  })
26  .parse(input)
27}
28
29/// Entrypoint for parsing a yarn lockfile
30pub fn parse_lockfile(file_contents: &str) -> IResult<&str, Lockfile<'_>> {
31  let (rest, (_, _)) = parse_yarn_header(file_contents)?;
32  let (rest, metadata) = parse_metadata(rest)?;
33
34  // Consume any blank lines after metadata
35  let (rest, _) = opt(newline).parse(rest)?;
36
37  // Optionally parse resolutions and constraints if present
38  let (rest, resolutions) = match parse_resolutions(rest) {
39    Ok((rest2, r)) => (rest2, Some(r)),
40    Err(_) => (rest, None),
41  };
42
43  let (rest, constraints) = match parse_constraints(rest) {
44    Ok((rest2, c)) => (rest2, Some(c)),
45    Err(_) => (rest, None),
46  };
47
48  // Parse all package entries as full Entries
49  // Use fold_many0 to avoid intermediate Vec allocation from many0
50  let (rest, entries) = fold_many0(parse_entry, Vec::new, |mut entries, entry| {
51    entries.push(entry);
52    entries
53  })
54  .parse(rest)?;
55
56  // Consume any trailing content (backticks, semicolons, whitespace, etc.)
57  // perf: use take_while instead of many0 - zero allocation
58  let (rest, _) =
59    take_while(|c: char| c == '`' || c == ';' || c == '\n' || c == ' ' || c == '\t' || c == '\r')
60      .parse(rest)?;
61
62  Ok((
63    rest,
64    Lockfile {
65      metadata,
66      entries,
67      resolutions,
68      constraints,
69    },
70  ))
71}
72
73/// Parse a single package entry from the lockfile
74///
75/// Example input:
76///
77/// ```text
78/// "debug@npm:1.0.0":
79///   version: 1.0.0
80///   resolution: "debug@npm:1.0.0"
81///   dependencies:
82///     ms: 0.6.2
83///   checksum: edfec8784737afbeea43cc78c3f56c33b88d3e751cc7220ae7a1c5370ff099e7352703275bdb56ea9967f92961231ce0625f8234d82259047303849671153f03
84///   languageName: node
85///   linkType: hard
86/// ```
87pub fn parse_package_entry(
88  input: &str,
89) -> IResult<&str, (SmallVec<[Descriptor<'_>; 4]>, Package<'_>)> {
90  let (rest, descriptors) = parse_descriptor_line(input)?;
91  let (rest, _) = newline.parse(rest)?; // consume newline after descriptor
92  let (rest, package) = parse_package_properties(rest)?;
93
94  Ok((rest, (descriptors, package)))
95}
96
97/// Parse a package descriptor line like: "debug@npm:1.0.0":, eslint-config-turbo@latest:, or ? "conditional@npm:1.0.0":
98/// Uses `SmallVec<[Descriptor; 4]>` since most descriptor lines have 1-3 descriptors
99pub fn parse_descriptor_line(input: &str) -> IResult<&str, SmallVec<[Descriptor<'_>; 4]>> {
100  // Check for optional '? ' prefix for wrapped-line descriptors
101  let (rest, has_line_wrap_marker) = opt(tag("? ")).parse(input)?;
102  let is_wrapped_line = has_line_wrap_marker.is_some();
103
104  // Handle both quoted and unquoted descriptors
105  // Optimisation: when descriptors are prefixed with ? they are often "wrapped", so we check for that first
106  let (rest, descriptor_string) = if is_wrapped_line {
107    // For wrapped-line descriptors, try newline-wrapped format first
108    alt((
109      // Handle very long descriptor lines that wrap: "very long descriptor..."\n:
110      delimited(
111        char('"'),
112        take_until("\""),
113        terminated(char('"'), preceded(newline, char(':'))),
114      ),
115      delimited(char('"'), take_until("\":"), tag("\":")), // Quoted: "package@npm:version":
116      terminated(take_until(":"), char(':')),              // Unquoted: package@latest:
117    ))
118    .parse(rest)?
119  } else {
120    // For normal descriptors, skip the newline-wrapped check entirely (performance optimisation)
121    alt((
122      delimited(char('"'), take_until("\":"), tag("\":")), // Quoted: "package@npm:version":
123      terminated(take_until(":"), char(':')),              // Unquoted: package@latest:
124    ))
125    .parse(rest)?
126  };
127
128  // Parse first descriptor (borrowed data)
129  let (remaining, first_data) = parse_single_descriptor(descriptor_string)?;
130
131  // Parse subsequent descriptors into SmallVec of borrowed data
132  // Most descriptor lines have 1-3 descriptors, so capacity of 3 for additional is plenty
133  let (remaining, additional_data) = fold_many0(
134    preceded((space0, char(','), space0), parse_single_descriptor),
135    SmallVec::<[_; 3]>::new,
136    |mut acc, data| {
137      acc.push(data);
138      acc
139    },
140  )
141  .parse(remaining)?;
142
143  if !remaining.is_empty() {
144    // Return an error if descriptor parsing didn't consume the entire string
145    // This ensures we catch parsing bugs early rather than silently ignoring content
146    return Err(nom::Err::Error(nom::error::Error::new(
147      remaining,
148      nom::error::ErrorKind::Complete,
149    )));
150  }
151
152  // Convert all borrowed data to Descriptors in a single pass
153  // Using SmallVec<[Descriptor; 4]> avoids heap allocation for common case (1-4 descriptors)
154  let mut descriptors: SmallVec<[Descriptor<'_>; 4]> =
155    SmallVec::with_capacity(1 + additional_data.len());
156  descriptors.push(convert_to_descriptor(first_data));
157  for data in additional_data {
158    descriptors.push(convert_to_descriptor(data));
159  }
160
161  Ok((rest, descriptors))
162}
163
164/// Convert parsed descriptor data (borrowed strings) to Descriptor
165/// All strings remain borrowed from the input - zero allocation!
166#[inline]
167fn convert_to_descriptor<'a>((name_part, full_range): (&'a str, &'a str)) -> Descriptor<'a> {
168  let ident = parse_name_to_ident(name_part);
169  Descriptor::new(ident, full_range)
170}
171
172/// Parse a single descriptor string like "debug@npm:1.0.0", "c@*", or "is-odd@patch:is-odd@npm%3A3.0.1#~/.yarn/patches/is-odd-npm-3.0.1-93c3c3f41b.patch"
173/// Returns borrowed strings to avoid allocations during parsing
174/// Returns (`name_part`, `full_range`) where `full_range` includes protocol if present (e.g., "npm:^1.0.0")
175fn parse_single_descriptor(input: &str) -> IResult<&str, (&str, &str)> {
176  // Parse package name first
177  let (after_name, name_part) = parse_package_name(input)?;
178
179  // Parse @ separator
180  let (after_at, _) = char('@').parse(after_name)?;
181
182  // Everything after @ until comma or quote is the full range (including protocol if present)
183  let (remaining, full_range) = take_while1(|c: char| c != ',' && c != '"').parse(after_at)?;
184
185  // Trim trailing whitespace from range to match JS parser behavior (e.g., "npm:^1.0.4 " -> "npm:^1.0.4")
186  let full_range = full_range.trim_end();
187
188  Ok((remaining, (name_part, full_range)))
189}
190
191/// Helper function to parse name part into Ident (zero-copy)
192#[inline]
193fn parse_name_to_ident(name_part: &str) -> Ident<'_> {
194  name_part.strip_prefix('@').map_or_else(
195    || Ident::new(None, name_part),
196    |stripped| {
197      // Scoped package: @babel/code-frame -> scope="@babel", name="code-frame"
198      stripped.split_once('/').map_or_else(
199        // Malformed scoped package (no slash), treat as simple name
200        || Ident::new(None, name_part),
201        |(scope_without_at, name)| {
202          // We need to include @ in scope, but we only have scope_without_at
203          // The original name_part has it: "@scope/name"
204          // scope starts at index 0, ends at index 1+scope_without_at.len()
205          let scope_end = 1 + scope_without_at.len();
206          let scope = &name_part[..scope_end];
207          Ident::new(Some(scope), name)
208        },
209      )
210    },
211  )
212}
213
214/// Parse a package name, which can be scoped (@babel/code-frame) or simple (debug)
215fn parse_package_name(input: &str) -> IResult<&str, &str> {
216  alt((
217    // Scoped package: @scope/name (both scope and name can contain dots)
218    recognize((
219      char('@'),
220      take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_' || c == '.'),
221      char('/'),
222      take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_' || c == '.'),
223    )),
224    // non-scoped package name: debug (can contain dots like fs.realpath)
225    take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_' || c == '.'),
226  ))
227  .parse(input)
228}
229
230/// Parse indented key-value properties for a package
231/// perf: use `fold_many0` to build the Package directly without intermediate Vec allocation
232pub fn parse_package_properties(input: &str) -> IResult<&str, Package<'_>> {
233  // Build package directly using fold_many0 - no intermediate Vec allocation
234  let (rest, package) = fold_many0(
235    parse_property_line,
236    || Package::new("unknown", LinkType::Hard),
237    |mut package, property_value| {
238      match property_value {
239        PropertyValue::Simple(key, value) => match key {
240          "version" => {
241            package.version = Some(value.trim_matches('"'));
242          }
243          "resolution" => {
244            let raw = value.trim_matches('"');
245            // Best-effort parse of the resolution into a Locator: split on '@' first occurrence
246            // For scoped packages, find the second @ (after the scope)
247            // Examples: "debug@npm:1.0.0", "@babel/core@npm:1.0.0"
248            let at_index = if raw.starts_with('@') {
249              // Scoped package - find @ after the /
250              raw
251                .find('/')
252                .and_then(|slash| raw[slash..].find('@').map(|i| slash + i))
253            } else {
254              raw.find('@')
255            };
256            if let Some(at_index) = at_index {
257              let (name_part, reference_with_at) = raw.split_at(at_index);
258              let ident = parse_name_to_ident(name_part);
259              // split_at keeps the '@' on the right; remove it
260              let reference = reference_with_at.trim_start_matches('@');
261              package.resolution_locator = Some(Locator::new(ident, reference));
262            }
263            package.resolution = Some(raw);
264          }
265          "languageName" => {
266            package.language_name = value;
267          }
268          "linkType" => {
269            package.link_type =
270              LinkType::try_from(value).unwrap_or_else(|()| panic!("Invalid link type: {value}"));
271          }
272          "checksum" => {
273            package.checksum = Some(value);
274          }
275          "conditions" => {
276            package.conditions = Some(value);
277          }
278          _ => {
279            panic!("Unknown property encountered in package entry: {key}");
280          }
281        },
282        PropertyValue::Dependencies(dependencies) => {
283          // Store the parsed dependencies in the package
284          for (dep_name, dep_range) in dependencies {
285            let ident = parse_name_to_ident(dep_name);
286            let descriptor = Descriptor::new(ident, dep_range);
287            package.dependencies.insert(ident, descriptor);
288          }
289        }
290        PropertyValue::PeerDependencies(peer_dependencies) => {
291          // Store the parsed peer dependencies in the package
292          for (dep_name, dep_range) in peer_dependencies {
293            let ident = parse_name_to_ident(dep_name);
294            let descriptor = Descriptor::new(ident, dep_range);
295            package.peer_dependencies.insert(ident, descriptor);
296          }
297        }
298        PropertyValue::Bin(binaries) => {
299          // Store the parsed binary executables in the package
300          for (bin_name, bin_path) in binaries {
301            package.bin.insert(bin_name, bin_path);
302          }
303        }
304        PropertyValue::DependenciesMeta(meta) => {
305          // Store the parsed dependency metadata in the package
306          for (dep_name, dep_meta) in meta {
307            let ident = parse_name_to_ident(dep_name);
308            package.dependencies_meta.insert(ident, Some(dep_meta));
309          }
310        }
311        PropertyValue::PeerDependenciesMeta(meta) => {
312          // Store the parsed peer dependency metadata in the package
313          for (dep_name, dep_meta) in meta {
314            let ident = parse_name_to_ident(dep_name);
315            package.peer_dependencies_meta.insert(ident, dep_meta);
316          }
317        }
318      }
319      package
320    },
321  )
322  .parse(input)?;
323
324  // Consume any trailing whitespace and blank lines
325  let (rest, ()) = fold_many0(
326    alt((tag("\n"), tag(" "), tag("\t"), tag("\r"))),
327    || (),
328    |(), _| (),
329  )
330  .parse(rest)?;
331
332  Ok((rest, package))
333}
334
335/// Parse a single property line with 2-space indentation
336fn parse_property_line(input: &str) -> IResult<&str, PropertyValue<'_>> {
337  // Try simple property first
338  if let Ok((rest, (key, value))) = parse_simple_property(input) {
339    return Ok((rest, PropertyValue::Simple(key, value)));
340  }
341
342  // Try dependencies block
343  if let Ok((rest, dependencies)) = parse_dependencies_block(input) {
344    return Ok((rest, PropertyValue::Dependencies(dependencies)));
345  }
346
347  // Try peer dependencies block
348  if let Ok((rest, peer_dependencies)) = parse_peer_dependencies_block(input) {
349    return Ok((rest, PropertyValue::PeerDependencies(peer_dependencies)));
350  }
351
352  // Try bin block
353  if let Ok((rest, binaries)) = parse_bin_block(input) {
354    return Ok((rest, PropertyValue::Bin(binaries)));
355  }
356
357  // Try dependenciesMeta block
358  if let Ok((rest, meta)) = parse_dependencies_meta_block(input) {
359    return Ok((rest, PropertyValue::DependenciesMeta(meta)));
360  }
361
362  // Try peerDependenciesMeta block
363  if let Ok((rest, meta)) = parse_peer_dependencies_meta_block(input) {
364    return Ok((rest, PropertyValue::PeerDependenciesMeta(meta)));
365  }
366
367  // If nothing matches, return an error
368  Err(nom::Err::Error(nom::error::Error::new(
369    input,
370    nom::error::ErrorKind::Alt,
371  )))
372}
373
374/// Enum to represent different types of property values
375#[derive(Debug)]
376enum PropertyValue<'a> {
377  Simple(&'a str, &'a str),
378  Dependencies(Vec<(&'a str, &'a str)>),
379  PeerDependencies(Vec<(&'a str, &'a str)>),
380  Bin(Vec<(&'a str, &'a str)>),
381  DependenciesMeta(Vec<(&'a str, DependencyMeta)>),
382  PeerDependenciesMeta(Vec<(&'a str, PeerDependencyMeta)>),
383}
384
385/// Parse a simple key-value property line
386pub fn parse_simple_property(input: &str) -> IResult<&str, (&str, &str)> {
387  let (rest, (_, key, _, _, value, _)) = (
388    tag("  "), // 2-space indentation
389    take_while1(|c: char| c.is_alphanumeric() || c == '_'),
390    char(':'),
391    space1,
392    is_not("\r\n"),
393    opt(newline),
394  )
395    .parse(input)?;
396
397  Ok((rest, (key, value)))
398}
399
400/// Parse a dependencies block
401fn parse_dependencies_block(input: &str) -> IResult<&str, Vec<(&str, &str)>> {
402  let (rest, (_, _, dependencies)) = (
403    tag("  dependencies:"),
404    newline,
405    fold_many0(parse_dependency_line, Vec::new, |mut acc, item| {
406      acc.push(item);
407      acc
408    }),
409  )
410    .parse(input)?;
411
412  Ok((rest, dependencies))
413}
414
415/// Parse a peerDependencies block
416fn parse_peer_dependencies_block(input: &str) -> IResult<&str, Vec<(&str, &str)>> {
417  let (rest, (_, _, peer_dependencies)) = (
418    tag("  peerDependencies:"),
419    newline,
420    fold_many0(parse_dependency_line, Vec::new, |mut acc, item| {
421      acc.push(item);
422      acc
423    }),
424  )
425    .parse(input)?;
426
427  Ok((rest, peer_dependencies))
428}
429
430/// Parse a bin block
431fn parse_bin_block(input: &str) -> IResult<&str, Vec<(&str, &str)>> {
432  let (rest, (_, _, binaries)) = (
433    tag("  bin:"),
434    newline,
435    fold_many0(parse_bin_line, Vec::new, |mut acc, item| {
436      acc.push(item);
437      acc
438    }),
439  )
440    .parse(input)?;
441
442  Ok((rest, binaries))
443}
444
445/// Parse a dependenciesMeta block
446fn parse_dependencies_meta_block(input: &str) -> IResult<&str, Vec<(&str, DependencyMeta)>> {
447  let (rest, (_, _, meta)) = (
448    tag("  dependenciesMeta:"),
449    newline,
450    fold_many0(
451      alt((
452        parse_dependency_meta_entry_inline,
453        parse_dependency_meta_entry_nested,
454      )),
455      Vec::new,
456      |mut acc, item| {
457        acc.push(item);
458        acc
459      },
460    ),
461  )
462    .parse(input)?;
463
464  Ok((rest, meta))
465}
466
467/// Parse a peerDependenciesMeta block
468fn parse_peer_dependencies_meta_block(
469  input: &str,
470) -> IResult<&str, Vec<(&str, PeerDependencyMeta)>> {
471  let (rest, (_, _, meta)) = (
472    tag("  peerDependenciesMeta:"),
473    newline,
474    fold_many0(
475      alt((
476        parse_peer_dependency_meta_entry_inline,
477        parse_peer_dependency_meta_entry_nested,
478      )),
479      Vec::new,
480      |mut acc, item| {
481        acc.push(item);
482        acc
483      },
484    ),
485  )
486    .parse(input)?;
487
488  Ok((rest, meta))
489}
490
491/// Parse a single dependency line with 4-space indentation
492fn parse_dependency_line(input: &str) -> IResult<&str, (&str, &str)> {
493  let (rest, (_, dep_name, _, _, dep_range, _)) = (
494    tag("    "),
495    alt((
496      delimited(
497        char('"'),
498        take_while1(|c: char| {
499          c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
500        }),
501        char('"'),
502      ),
503      take_while1(|c: char| {
504        c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
505      }),
506    )),
507    char(':'),
508    space1,
509    take_until("\n"),
510    opt(newline),
511  )
512    .parse(input)?;
513
514  let clean_range = dep_range.trim().trim_matches('"');
515  Ok((rest, (dep_name, clean_range)))
516}
517
518/// Parse a single bin line with 4-space indentation
519fn parse_bin_line(input: &str) -> IResult<&str, (&str, &str)> {
520  let (rest, (_, bin_name, _, _, bin_path, _)) = (
521    tag("    "),
522    take_while1(|c: char| {
523      c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
524    }),
525    char(':'),
526    space1,
527    is_not("\r\n"),
528    newline,
529  )
530    .parse(input)?;
531
532  let clean_path = bin_path.trim().trim_matches('"');
533  Ok((rest, (bin_name, clean_path)))
534}
535
536/// Parse a single dependency meta line with inline format
537fn parse_dependency_meta_entry_inline(input: &str) -> IResult<&str, (&str, DependencyMeta)> {
538  let (rest, (_, dep_name, _, _, meta_content, _)) = (
539    tag("    "),
540    alt((
541      delimited(
542        char('"'),
543        take_while1(|c: char| {
544          c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
545        }),
546        char('"'),
547      ),
548      take_while1(|c: char| {
549        c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
550      }),
551    )),
552    char(':'),
553    space1,
554    parse_meta_object,
555    opt(newline),
556  )
557    .parse(input)?;
558
559  Ok((rest, (dep_name, meta_content)))
560}
561
562/// Parse a single dependency meta entry with nested format
563fn parse_dependency_meta_entry_nested(input: &str) -> IResult<&str, (&str, DependencyMeta)> {
564  let (rest, (_, dep_name, _, _, meta_content, _)) = (
565    tag("    "),
566    alt((
567      delimited(
568        char('"'),
569        take_while1(|c: char| {
570          c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
571        }),
572        char('"'),
573      ),
574      take_while1(|c: char| {
575        c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
576      }),
577    )),
578    char(':'),
579    newline,
580    parse_dependency_meta_object_indented,
581    opt(newline),
582  )
583    .parse(input)?;
584
585  Ok((rest, (dep_name, meta_content)))
586}
587
588/// Parse a dependency meta object with 6-space indentation
589fn parse_dependency_meta_object_indented(input: &str) -> IResult<&str, DependencyMeta> {
590  fn parse_indented_bool_line<'a>(
591    prop: &'static str,
592  ) -> impl Fn(&'a str) -> IResult<&'a str, bool> {
593    move |input: &str| {
594      let (rest, (_, _, _, _, value, _)) = (
595        tag("      "),
596        tag(prop),
597        char(':'),
598        space1,
599        alt((tag("true"), tag("false"))),
600        newline,
601      )
602        .parse(input)?;
603      Ok((rest, value == "true"))
604    }
605  }
606
607  let init = || (None, None, None);
608  let (rest, (built, optional, unplugged)) = fold_many0(
609    alt((
610      map(parse_indented_bool_line("built"), |v| (Some(v), None, None)),
611      map(parse_indented_bool_line("optional"), |v| {
612        (None, Some(v), None)
613      }),
614      map(parse_indented_bool_line("unplugged"), |v| {
615        (None, None, Some(v))
616      }),
617    )),
618    init,
619    |(b_acc, o_acc, u_acc), (b, o, u)| (b.or(b_acc), o.or(o_acc), u.or(u_acc)),
620  )
621  .parse(input)?;
622
623  Ok((
624    rest,
625    DependencyMeta {
626      built,
627      optional,
628      unplugged,
629    },
630  ))
631}
632
633/// Parse a single peer dependency meta entry with inline format
634fn parse_peer_dependency_meta_entry_inline(
635  input: &str,
636) -> IResult<&str, (&str, PeerDependencyMeta)> {
637  let (rest, (_, dep_name, _, _, meta_content, _)) = (
638    tag("    "),
639    alt((
640      delimited(
641        char('"'),
642        take_while1(|c: char| {
643          c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
644        }),
645        char('"'),
646      ),
647      take_while1(|c: char| {
648        c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
649      }),
650    )),
651    char(':'),
652    space1,
653    parse_peer_meta_object,
654    opt(newline),
655  )
656    .parse(input)?;
657
658  Ok((rest, (dep_name, meta_content)))
659}
660
661/// Parse a single peer dependency meta entry with nested format
662fn parse_peer_dependency_meta_entry_nested(
663  input: &str,
664) -> IResult<&str, (&str, PeerDependencyMeta)> {
665  let (rest, (_, dep_name, _, _, meta_content, _)) = (
666    tag("    "),
667    alt((
668      delimited(
669        char('"'),
670        take_while1(|c: char| {
671          c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
672        }),
673        char('"'),
674      ),
675      take_while1(|c: char| {
676        c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
677      }),
678    )),
679    char(':'),
680    newline,
681    parse_peer_meta_object_indented,
682    opt(newline),
683  )
684    .parse(input)?;
685
686  Ok((rest, (dep_name, meta_content)))
687}
688
689/// Parse a peer dependency meta object with inline format
690fn parse_peer_meta_object(input: &str) -> IResult<&str, PeerDependencyMeta> {
691  let (rest, _) = char('{')(input)?;
692  let (rest, _) = space0(rest)?;
693  let (rest, optional) = parse_bool_property_inline("optional")(rest)?;
694  let (rest, _) = space0(rest)?;
695  let (rest, _) = char('}')(rest)?;
696
697  Ok((rest, PeerDependencyMeta { optional }))
698}
699
700/// Parse a boolean property for inline format
701fn parse_bool_property_inline(prop_name: &str) -> impl Fn(&str) -> IResult<&str, bool> + '_ {
702  move |input| {
703    let (rest, (_, _, _, value)) = (
704      tag(prop_name),
705      char(':'),
706      space1,
707      alt((tag("true"), tag("false"))),
708    )
709      .parse(input)?;
710
711    Ok((rest, value == "true"))
712  }
713}
714
715/// Parse a peer dependency meta object with 6-space indentation
716fn parse_peer_meta_object_indented(input: &str) -> IResult<&str, PeerDependencyMeta> {
717  let (rest, (_, _, _, optional, _)) = (
718    tag("      "),
719    tag("optional:"),
720    space1,
721    alt((tag("true"), tag("false"))),
722    newline,
723  )
724    .parse(input)?;
725
726  Ok((
727    rest,
728    PeerDependencyMeta {
729      optional: optional == "true",
730    },
731  ))
732}
733
734/// Parse a dependency meta object like "{ built: true, optional: false }"
735fn parse_meta_object(input: &str) -> IResult<&str, DependencyMeta> {
736  let (rest, _) = char('{')(input)?;
737  let (rest, _) = space0(rest)?;
738
739  let (rest, built) = opt(parse_bool_property_inline("built")).parse(rest)?;
740  let (rest, _) = opt((space0, char(','), space0)).parse(rest)?;
741
742  let (rest, optional) = opt(parse_bool_property_inline("optional")).parse(rest)?;
743  let (rest, _) = opt((space0, char(','), space0)).parse(rest)?;
744
745  let (rest, unplugged) = opt(parse_bool_property_inline("unplugged")).parse(rest)?;
746  let (rest, _) = space0(rest)?;
747
748  let (rest, _) = char('}')(rest)?;
749
750  Ok((
751    rest,
752    DependencyMeta {
753      built,
754      optional,
755      unplugged,
756    },
757  ))
758}
759
760#[cfg(test)]
761mod tests {
762  use super::*;
763
764  #[test]
765  fn test_parse_dependency_line_simple() {
766    let input = "    ms: 0.6.2\n";
767    let result = parse_dependency_line(input);
768    assert!(result.is_ok());
769    let (remaining, (dep_name, dep_range)) = result.unwrap();
770    assert_eq!(remaining, "");
771    assert_eq!(dep_name, "ms");
772    assert_eq!(dep_range, "0.6.2");
773  }
774
775  #[test]
776  fn test_parse_dependency_line_scoped_package() {
777    let input = "    @babel/code-frame: ^7.12.11\n";
778    let result = parse_dependency_line(input);
779    assert!(result.is_ok());
780    let (remaining, (dep_name, dep_range)) = result.unwrap();
781    assert_eq!(remaining, "");
782    assert_eq!(dep_name, "@babel/code-frame");
783    assert_eq!(dep_range, "^7.12.11");
784  }
785
786  #[test]
787  fn test_parse_descriptor_line_simple() {
788    let input = r#""debug@npm:1.0.0":"#;
789    let result = parse_descriptor_line(input);
790    assert!(result.is_ok());
791    let (remaining, descriptors) = result.unwrap();
792    assert_eq!(remaining, "");
793    assert_eq!(descriptors.len(), 1);
794
795    let descriptor = &descriptors[0];
796    assert_eq!(descriptor.ident().name(), "debug");
797    assert_eq!(descriptor.ident().scope(), None);
798  }
799
800  #[test]
801  fn test_parse_descriptor_line_scoped_package() {
802    let input = r#""@babel/code-frame@npm:7.12.11":"#;
803    let result = parse_descriptor_line(input);
804    assert!(result.is_ok());
805    let (remaining, descriptors) = result.unwrap();
806    assert_eq!(remaining, "");
807    assert_eq!(descriptors.len(), 1);
808
809    let descriptor = &descriptors[0];
810    assert_eq!(descriptor.ident().name(), "code-frame");
811    assert_eq!(descriptor.ident().scope(), Some("@babel"));
812  }
813
814  #[test]
815  fn test_parse_package_properties_minimal() {
816    let input = r#"  version: 1.0.0
817  resolution: "debug@npm:1.0.0"
818  languageName: node
819  linkType: hard
820"#;
821    let result = parse_package_properties(input);
822    assert!(result.is_ok());
823    let (remaining, package) = result.unwrap();
824    assert_eq!(remaining, "");
825    assert_eq!(package.version, Some("1.0.0"));
826    assert_eq!(package.resolution, Some("debug@npm:1.0.0"));
827    assert_eq!(package.language_name, "node");
828    assert_eq!(package.link_type, LinkType::Hard);
829  }
830
831  #[test]
832  fn test_parse_multiple_packages() {
833    let input = r#"# This file is generated by running "yarn install" inside your project.
834# Manual changes might be lost - proceed with caution!
835
836__metadata:
837  version: 8
838  cacheKey: 10
839
840"@actions/http-client@npm:^2.2.0":
841  version: 2.2.3
842  resolution: "@actions/http-client@npm:2.2.3"
843  languageName: node
844  linkType: hard
845
846"@actions/io@npm:^1.0.1, @actions/io@npm:^1.1.3":
847  version: 1.1.3
848  resolution: "@actions/io@npm:1.1.3"
849  languageName: node
850  linkType: hard
851"#;
852
853    let result = parse_lockfile(input);
854    assert!(result.is_ok());
855    let (remaining, lockfile) = result.unwrap();
856    assert_eq!(lockfile.entries.len(), 2);
857    assert!(remaining.is_empty());
858  }
859
860  #[test]
861  fn test_parse_descriptor_line_multi_descriptor() {
862    let input = r#""c@*, c@workspace:packages/c":"#;
863    let result = parse_descriptor_line(input);
864    assert!(result.is_ok());
865    let (remaining, descriptors) = result.unwrap();
866    assert_eq!(remaining, "");
867    assert_eq!(descriptors.len(), 2);
868
869    assert_eq!(descriptors[0].ident().name(), "c");
870    assert_eq!(descriptors[1].ident().name(), "c");
871  }
872
873  #[test]
874  fn test_parse_peer_dependencies_meta_real_format() {
875    let input = r"  peerDependenciesMeta:
876    graphql-ws:
877      optional: true
878    react:
879      optional: true
880";
881
882    let result = parse_peer_dependencies_meta_block(input);
883    assert!(result.is_ok());
884    let (remaining, meta) = result.unwrap();
885    assert_eq!(meta.len(), 2);
886    assert!(remaining.is_empty());
887  }
888
889  #[test]
890  fn test_descriptor_range_includes_npm_protocol() {
891    let input = r#""debug@npm:^4.3.0":"#;
892    let result = parse_descriptor_line(input);
893    assert!(result.is_ok());
894    let (_, descriptors) = result.unwrap();
895    assert_eq!(descriptors.len(), 1);
896
897    let descriptor = &descriptors[0];
898    assert_eq!(descriptor.ident().name(), "debug");
899    assert_eq!(descriptor.range(), "npm:^4.3.0");
900  }
901
902  #[test]
903  fn test_descriptor_range_includes_npm_protocol_scoped() {
904    let input = r#""@babel/core@npm:^7.0.0":"#;
905    let result = parse_descriptor_line(input);
906    assert!(result.is_ok());
907    let (_, descriptors) = result.unwrap();
908    assert_eq!(descriptors.len(), 1);
909
910    let descriptor = &descriptors[0];
911    assert_eq!(descriptor.ident().name(), "core");
912    assert_eq!(descriptor.ident().scope(), Some("@babel"));
913    assert_eq!(descriptor.range(), "npm:^7.0.0");
914  }
915
916  #[test]
917  fn test_descriptor_range_no_protocol() {
918    let input = r#""c@*":"#;
919    let result = parse_descriptor_line(input);
920    assert!(result.is_ok());
921    let (_, descriptors) = result.unwrap();
922    assert_eq!(descriptors.len(), 1);
923
924    let descriptor = &descriptors[0];
925    assert_eq!(descriptor.ident().name(), "c");
926    assert_eq!(descriptor.range(), "*");
927  }
928
929  #[test]
930  fn test_descriptor_range_workspace_protocol() {
931    let input = r#""my-package@workspace:packages/my-package":"#;
932    let result = parse_descriptor_line(input);
933    assert!(result.is_ok());
934    let (_, descriptors) = result.unwrap();
935    assert_eq!(descriptors.len(), 1);
936
937    let descriptor = &descriptors[0];
938    assert_eq!(descriptor.ident().name(), "my-package");
939    assert_eq!(descriptor.range(), "workspace:packages/my-package");
940  }
941
942  #[test]
943  fn test_descriptor_range_multiple_with_npm_protocol() {
944    let input = r#""prompts@npm:^2.0.1, prompts@npm:^2.4.2":"#;
945    let result = parse_descriptor_line(input);
946    assert!(result.is_ok());
947    let (_, descriptors) = result.unwrap();
948    assert_eq!(descriptors.len(), 2);
949
950    assert_eq!(descriptors[0].ident().name(), "prompts");
951    assert_eq!(descriptors[0].range(), "npm:^2.0.1");
952
953    assert_eq!(descriptors[1].ident().name(), "prompts");
954    assert_eq!(descriptors[1].range(), "npm:^2.4.2");
955  }
956
957  #[test]
958  fn test_descriptor_range_mixed_protocols() {
959    let input = r#""c@*, c@workspace:packages/c":"#;
960    let result = parse_descriptor_line(input);
961    assert!(result.is_ok());
962    let (_, descriptors) = result.unwrap();
963    assert_eq!(descriptors.len(), 2);
964
965    assert_eq!(descriptors[0].ident().name(), "c");
966    assert_eq!(descriptors[0].range(), "*");
967
968    assert_eq!(descriptors[1].ident().name(), "c");
969    assert_eq!(descriptors[1].range(), "workspace:packages/c");
970  }
971
972  #[test]
973  fn test_descriptor_range_patch_protocol() {
974    let input = r#""is-odd@patch:is-odd@npm%3A3.0.1#~/.yarn/patches/is-odd.patch":"#;
975    let result = parse_descriptor_line(input);
976    assert!(result.is_ok());
977    let (_, descriptors) = result.unwrap();
978    assert_eq!(descriptors.len(), 1);
979
980    let descriptor = &descriptors[0];
981    assert_eq!(descriptor.ident().name(), "is-odd");
982    assert_eq!(
983      descriptor.range(),
984      "patch:is-odd@npm%3A3.0.1#~/.yarn/patches/is-odd.patch"
985    );
986  }
987
988  #[test]
989  fn test_descriptor_range_trims_trailing_whitespace() {
990    // Some lockfiles have trailing spaces in descriptor ranges (e.g., "@oclif/screen@npm^1.0.4 ")
991    let input = r#""@oclif/screen@npm:^1.0.4 ":"#;
992    let result = parse_descriptor_line(input);
993    assert!(result.is_ok());
994    let (_, descriptors) = result.unwrap();
995    assert_eq!(descriptors.len(), 1);
996
997    let descriptor = &descriptors[0];
998    assert_eq!(descriptor.ident().name(), "screen");
999    assert_eq!(descriptor.ident().scope(), Some("@oclif"));
1000    // Trailing whitespace should be trimmed to match JS parser behavior
1001    assert_eq!(descriptor.range(), "npm:^1.0.4");
1002  }
1003}