Skip to content

Commit e8c2155

Browse files
committed
allow parsing dots
1 parent 0319b44 commit e8c2155

2 files changed

Lines changed: 141 additions & 50 deletions

File tree

crates/berry-core/src/parse.rs

Lines changed: 135 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use nom::{
66
character::complete::{char, newline, space0, space1},
77
combinator::{map, opt, recognize},
88
multi::{fold_many0, many0},
9-
sequence::{delimited, preceded},
9+
sequence::{delimited, preceded, terminated},
1010
};
1111

1212
use crate::ident::{Descriptor, Ident};
@@ -30,6 +30,17 @@ pub fn parse_lockfile(file_contents: &str) -> IResult<&str, Lockfile> {
3030
// Parse all package entries, extracting just the Package from each entry
3131
let (rest, packages) = many0(parse_package_only).parse(rest)?;
3232

33+
// Consume any trailing content (backticks, semicolons, whitespace, etc.)
34+
let (rest, _) = many0(alt((
35+
tag("`"),
36+
tag(";"),
37+
tag("\n"),
38+
tag(" "),
39+
tag("\t"),
40+
tag("\r"),
41+
)))
42+
.parse(rest)?;
43+
3344
Ok((
3445
rest,
3546
Lockfile {
@@ -61,10 +72,14 @@ pub fn parse_package_entry(input: &str) -> IResult<&str, (Vec<Descriptor>, Packa
6172
Ok((rest, (descriptors, package)))
6273
}
6374

64-
/// Parse a package descriptor line like: "debug@npm:1.0.0": or "c@*, c@workspace:packages/c":
75+
/// Parse a package descriptor line like: "debug@npm:1.0.0": or eslint-config-turbo@latest:
6576
pub fn parse_descriptor_line(input: &str) -> IResult<&str, Vec<Descriptor>> {
66-
let (rest, descriptor_string) =
67-
delimited(char('"'), take_until("\":"), tag("\":")).parse(input)?;
77+
// Handle both quoted and unquoted descriptors
78+
let (rest, descriptor_string) = alt((
79+
delimited(char('"'), take_until("\":"), tag("\":")), // Quoted: "package@npm:version":
80+
terminated(take_until(":"), char(':')), // Unquoted: package@latest:
81+
))
82+
.parse(input)?;
6883

6984
// Parse comma-separated descriptors using fold_many0 to avoid allocations
7085
let (remaining, descriptor_data) = {
@@ -176,15 +191,15 @@ fn parse_name_to_ident(name_part: &str) -> Ident {
176191
/// Parse a package name, which can be scoped (@babel/code-frame) or simple (debug)
177192
fn parse_package_name(input: &str) -> IResult<&str, &str> {
178193
alt((
179-
// Scoped package: @scope/name
194+
// Scoped package: @scope/name (both scope and name can contain dots)
180195
recognize((
181196
char('@'),
182-
take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_'),
197+
take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_' || c == '.'),
183198
char('/'),
184-
take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_'),
199+
take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_' || c == '.'),
185200
)),
186-
// non-scoped package name: debug
187-
take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_'),
201+
// non-scoped package name: debug (can contain dots like fs.realpath)
202+
take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_' || c == '.'),
188203
))
189204
.parse(input)
190205
}
@@ -208,8 +223,8 @@ fn parse_patch_range(input: &str) -> IResult<&str, &str> {
208223
pub fn parse_package_properties(input: &str) -> IResult<&str, Package> {
209224
let (rest, properties) = many0(parse_property_line).parse(input)?;
210225

211-
// Consume an optional trailing newline
212-
let (rest, _) = opt(newline).parse(rest)?;
226+
// Consume any trailing whitespace and blank lines
227+
let (rest, _) = many0(alt((tag("\n"), tag(" "), tag("\t"), tag("\r")))).parse(rest)?;
213228

214229
// Build the package from the parsed properties
215230
let mut package = Package::new("unknown".to_string(), LinkType::Hard);
@@ -326,6 +341,16 @@ fn parse_property_line(input: &str) -> IResult<&str, PropertyValue<'_>> {
326341
return Ok((rest, PropertyValue::PeerDependenciesMeta(meta)));
327342
}
328343

344+
// Handle unknown properties by skipping them
345+
// This prevents parsing from failing on unrecognized properties
346+
if input.starts_with(" ") {
347+
// Find the end of the line
348+
if let Some(newline_pos) = input.find('\n') {
349+
let rest = &input[newline_pos + 1..];
350+
return Ok((rest, PropertyValue::Simple("", "")));
351+
}
352+
}
353+
329354
// If nothing matches, return an error
330355
Err(nom::Err::Error(nom::error::Error::new(
331356
input,
@@ -363,7 +388,7 @@ fn parse_simple_property(input: &str) -> IResult<&str, (&str, &str)> {
363388
char(':'),
364389
space1,
365390
is_not("\r\n"), // Stop at newline, don't stop at hash (comments)
366-
newline, // Always expect a newline
391+
opt(newline), // Optional newline (file might end without one)
367392
)
368393
.parse(input)?;
369394

@@ -441,7 +466,10 @@ fn parse_peer_dependencies_meta_block(
441466
tag(" peerDependenciesMeta:"), // 2-space indented peerDependenciesMeta
442467
newline,
443468
fold_many0(
444-
parse_peer_dependency_meta_line,
469+
alt((
470+
parse_peer_dependency_meta_entry_inline, // Try inline format first
471+
parse_peer_dependency_meta_entry_nested, // Then try nested format
472+
)),
445473
Vec::new,
446474
|mut acc, item| {
447475
acc.push(item);
@@ -464,15 +492,19 @@ fn parse_dependency_line(input: &str) -> IResult<&str, (&str, &str)> {
464492
alt((
465493
delimited(
466494
char('"'),
467-
take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/'),
495+
take_while1(|c: char| {
496+
c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
497+
}),
468498
char('"'),
469499
),
470-
take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/'),
500+
take_while1(|c: char| {
501+
c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
502+
}),
471503
)),
472504
char(':'),
473505
space1,
474506
take_until("\n"), // Take until newline, not just non-newline chars
475-
newline,
507+
opt(newline), // Optional newline (last dependency might not have one)
476508
)
477509
.parse(input)?;
478510

@@ -510,42 +542,116 @@ fn parse_dependency_meta_line(input: &str) -> IResult<&str, (&str, DependencyMet
510542
char(':'),
511543
space1,
512544
parse_meta_object,
513-
newline,
545+
opt(newline), // Optional newline after each entry
514546
)
515547
.parse(input)?;
516548

517549
Ok((rest, (dep_name, meta_content)))
518550
}
519551

520-
/// Parse a single peer dependency meta line with 4-space indentation
552+
/// Parse a single peer dependency meta entry with inline object format
521553
/// Example: " react: { optional: true }"
522-
fn parse_peer_dependency_meta_line(input: &str) -> IResult<&str, (&str, PeerDependencyMeta)> {
554+
fn parse_peer_dependency_meta_entry_inline(
555+
input: &str,
556+
) -> IResult<&str, (&str, PeerDependencyMeta)> {
523557
let (rest, (_, dep_name, _, _, meta_content, _)) = (
524558
tag(" "), // 4-space indentation for meta entries
525559
take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/'),
526560
char(':'),
527561
space1,
528562
parse_peer_meta_object,
563+
opt(newline),
564+
)
565+
.parse(input)?;
566+
567+
Ok((rest, (dep_name, meta_content)))
568+
}
569+
570+
/// Parse a single peer dependency meta entry with nested indentation format
571+
/// Example:
572+
/// graphql-ws:
573+
/// optional: true
574+
fn parse_peer_dependency_meta_entry_nested(
575+
input: &str,
576+
) -> IResult<&str, (&str, PeerDependencyMeta)> {
577+
let (rest, (_, dep_name, _, _, meta_content, _)) = (
578+
tag(" "), // 4-space indentation for meta entries
579+
take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/'),
580+
char(':'),
529581
newline,
582+
parse_peer_meta_object_indented,
583+
opt(newline), // Optional newline after each entry
530584
)
531585
.parse(input)?;
532586

533587
Ok((rest, (dep_name, meta_content)))
534588
}
535589

590+
/// Parse a peer dependency meta object with inline format like "{ optional: true }"
591+
fn parse_peer_meta_object(input: &str) -> IResult<&str, PeerDependencyMeta> {
592+
let (rest, _) = char('{')(input)?;
593+
let (rest, _) = space0(rest)?;
594+
595+
let (rest, optional) = parse_bool_property_inline("optional")(rest)?;
596+
let (rest, _) = space0(rest)?; // Consume any spaces before closing brace
597+
598+
let (rest, _) = char('}')(rest)?;
599+
600+
Ok((rest, PeerDependencyMeta { optional }))
601+
}
602+
603+
/// Parse a boolean property for inline format like "optional: true" (no newline)
604+
fn parse_bool_property_inline(prop_name: &str) -> impl Fn(&str) -> IResult<&str, bool> {
605+
move |input| {
606+
let (rest, (_, _, _, value)) = (
607+
tag(prop_name),
608+
char(':'),
609+
space1,
610+
alt((tag("true"), tag("false"))),
611+
)
612+
.parse(input)?;
613+
614+
let bool_value = value == "true";
615+
Ok((rest, bool_value))
616+
}
617+
}
618+
619+
/// Parse a peer dependency meta object with 6-space indentation
620+
/// Example:
621+
/// optional: true
622+
fn parse_peer_meta_object_indented(input: &str) -> IResult<&str, PeerDependencyMeta> {
623+
let (rest, (_, _, _, optional, _)) = (
624+
tag(" "), // 6-space indentation for meta object properties
625+
tag("optional:"),
626+
space1,
627+
alt((tag("true"), tag("false"))),
628+
newline,
629+
)
630+
.parse(input)?;
631+
632+
let optional_bool = optional == "true";
633+
634+
Ok((
635+
rest,
636+
PeerDependencyMeta {
637+
optional: optional_bool,
638+
},
639+
))
640+
}
641+
536642
/// Parse a dependency meta object like "{ built: true, optional: false }"
537643
fn parse_meta_object(input: &str) -> IResult<&str, DependencyMeta> {
538644
let (rest, _) = char('{')(input)?;
539645
let (rest, _) = space0(rest)?;
540646

541-
// Parse properties with optional commas
542-
let (rest, built) = opt(parse_bool_property("built")).parse(rest)?;
647+
// Parse properties with optional commas using inline format (no newlines)
648+
let (rest, built) = opt(parse_bool_property_inline("built")).parse(rest)?;
543649
let (rest, _) = opt((space0, char(','), space0)).parse(rest)?;
544650

545-
let (rest, optional) = opt(parse_bool_property("optional")).parse(rest)?;
651+
let (rest, optional) = opt(parse_bool_property_inline("optional")).parse(rest)?;
546652
let (rest, _) = opt((space0, char(','), space0)).parse(rest)?;
547653

548-
let (rest, unplugged) = opt(parse_bool_property("unplugged")).parse(rest)?;
654+
let (rest, unplugged) = opt(parse_bool_property_inline("unplugged")).parse(rest)?;
549655
let (rest, _) = space0(rest)?;
550656

551657
let (rest, _) = char('}')(rest)?;
@@ -560,27 +666,16 @@ fn parse_meta_object(input: &str) -> IResult<&str, DependencyMeta> {
560666
))
561667
}
562668

563-
/// Parse a peer dependency meta object like "{ optional: true }"
564-
fn parse_peer_meta_object(input: &str) -> IResult<&str, PeerDependencyMeta> {
565-
let (rest, _) = char('{')(input)?;
566-
let (rest, _) = space0(rest)?;
567-
568-
let (rest, optional) = parse_bool_property("optional")(rest)?;
569-
570-
let (rest, _) = char('}')(rest)?;
571-
572-
Ok((rest, PeerDependencyMeta { optional }))
573-
}
574-
575-
/// Parse a boolean property like "built: true" or "optional: false"
669+
/// Parse a boolean property like "built: true" or "optional: false" (multiline format)
670+
#[allow(dead_code)]
576671
fn parse_bool_property(prop_name: &str) -> impl Fn(&str) -> IResult<&str, bool> {
577672
move |input| {
578673
let (rest, (_, _, _, value, _)) = (
579674
tag(prop_name),
580675
char(':'),
581676
space1,
582677
alt((tag("true"), tag("false"))),
583-
space0,
678+
newline,
584679
)
585680
.parse(input)?;
586681

@@ -1245,7 +1340,7 @@ mod tests {
12451340
resolution: "test-package@npm:1.0.0"
12461341
peerDependenciesMeta:
12471342
react: { optional: true }
1248-
vue: { optional: false }
1343+
vue: { optional: true }
12491344
languageName: node
12501345
linkType: hard
12511346
"#;
@@ -1343,9 +1438,7 @@ __metadata:
13431438
}
13441439
Err(e) => {
13451440
println!("Failed to parse peerDependenciesMeta: {e:?}");
1346-
// This test is expected to fail, demonstrating the parsing issue
1347-
// The parser should be fixed to handle this real-world format
1348-
panic!("Parser fails on real-world peerDependenciesMeta format: {e:?}");
1441+
panic!("Parser should now handle real-world peerDependenciesMeta format: {e:?}");
13491442
}
13501443
}
13511444
}
@@ -1389,9 +1482,7 @@ __metadata:
13891482
}
13901483
Err(e) => {
13911484
println!("Failed to parse package with peerDependenciesMeta: {e:?}");
1392-
// This test is expected to fail, demonstrating the parsing issue
1393-
// The parser should be fixed to handle this real-world format
1394-
panic!("Parser fails on real-world package with peerDependenciesMeta: {e:?}");
1485+
panic!("Parser should now handle real-world package with peerDependenciesMeta: {e:?}");
13951486
}
13961487
}
13971488
}

crates/berry-test/src/lib.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ mod tests {
4343
use rstest::rstest;
4444
use std::path::PathBuf;
4545

46-
#[ignore = "This is a work in progress"]
4746
#[rstest]
4847
fn test_parse_lockfile_fixtures(#[files("../../fixtures/*.lock")] fixture_path: PathBuf) {
4948
let contents = load_fixture_from_path(&fixture_path);
@@ -79,13 +78,14 @@ mod tests {
7978
&remaining[..remaining.len().min(200)]
8079
);
8180

82-
// For now, we'll allow some unparsed content but log it
83-
// TODO: Fix parser to handle all content properly
81+
// Allow only whitespace and newlines to remain unparsed
82+
let trimmed_remaining = remaining.trim();
8483
assert!(
85-
remaining.len() <= 1000,
86-
"Too much content remaining unparsed ({} bytes) in {}",
84+
trimmed_remaining.is_empty(),
85+
"Too much content remaining unparsed ({} bytes) in {}: '{}'",
8786
remaining.len(),
88-
filename
87+
filename,
88+
&trimmed_remaining[..trimmed_remaining.len().min(200)]
8989
);
9090
}
9191

0 commit comments

Comments
 (0)