Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 107 additions & 13 deletions crates/berry-core/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,33 @@ pub fn parse_package_entry(input: &str) -> IResult<&str, (Vec<Descriptor>, Packa

/// Parse a package descriptor line like: "debug@npm:1.0.0":, eslint-config-turbo@latest:, or ? "conditional@npm:1.0.0":
pub fn parse_descriptor_line(input: &str) -> IResult<&str, Vec<Descriptor>> {
// Handle optional '? ' prefix for conditional packages
let (rest, _) = opt(tag("? ")).parse(input)?;
// Check for optional '? ' prefix for wrapped-line descriptors
let (rest, has_line_wrap_marker) = opt(tag("? ")).parse(input)?;
let is_wrapped_line = has_line_wrap_marker.is_some();

// Handle both quoted and unquoted descriptors
let (rest, descriptor_string) = alt((
delimited(char('"'), take_until("\":"), tag("\":")), // Quoted: "package@npm:version":
// Handle very long descriptor lines that wrap: "very long descriptor..."\n:
delimited(
char('"'),
take_until("\""),
terminated(char('"'), preceded(newline, char(':'))),
),
terminated(take_until(":"), char(':')), // Unquoted: package@latest:
))
.parse(rest)?;
// Optimisation: when descriptors are prefixed with ? they are often "wrapped", so we check for that first
let (rest, descriptor_string) = if is_wrapped_line {
// For wrapped-line descriptors, try newline-wrapped format first
alt((
// Handle very long descriptor lines that wrap: "very long descriptor..."\n:
delimited(
char('"'),
take_until("\""),
terminated(char('"'), preceded(newline, char(':'))),
),
delimited(char('"'), take_until("\":"), tag("\":")), // Quoted: "package@npm:version":
terminated(take_until(":"), char(':')), // Unquoted: package@latest:
))
.parse(rest)?
} else {
// For normal descriptors, skip the newline-wrapped check entirely (performance optimisation)
alt((
delimited(char('"'), take_until("\":"), tag("\":")), // Quoted: "package@npm:version":
terminated(take_until(":"), char(':')), // Unquoted: package@latest:
))
.parse(rest)?
};

// Parse comma-separated descriptors using fold_many0 to avoid allocations
let (remaining, descriptor_data) = {
Expand Down Expand Up @@ -1734,4 +1746,86 @@ __metadata:
assert_eq!(package.bin.len(), 1);
assert_eq!(package.bin.get("resolve"), Some(&"bin/resolve".to_string()));
}

#[test]
fn test_parse_descriptor_line_wrapped_long_line() {
let input = r#"? "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.0"
:"#;
let result = parse_descriptor_line(input);

assert!(
result.is_ok(),
"Should successfully parse long, wrapped-line descriptor"
);
let (remaining, descriptors) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(descriptors.len(), 3, "Should parse 3 descriptors");

// Verify first descriptor
assert_eq!(descriptors[0].ident().name(), "runtime");
assert_eq!(descriptors[0].ident().scope(), Some("@babel"));
assert_eq!(descriptors[0].range(), "npm:^7.0.0");

// Verify second descriptor
assert_eq!(descriptors[1].ident().name(), "runtime");
assert_eq!(descriptors[1].range(), "npm:^7.1.2");

// Verify third descriptor
assert_eq!(descriptors[2].ident().name(), "runtime");
assert_eq!(descriptors[2].range(), "npm:^7.10.0");
}

#[test]
fn test_parse_descriptor_line_short_no_wrap() {
// Test that short lines with `?` prefix still work (without newline wrap)
let input = r#"? "resolve@patch:resolve@npm%3A^1.0.0#optional!builtin<compat/resolve>":"#;
let result = parse_descriptor_line(input);

assert!(
result.is_ok(),
"Should successfully parse short conditional package without wrap"
);
let (remaining, descriptors) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(descriptors.len(), 1);
assert_eq!(descriptors[0].ident().name(), "resolve");
}

#[test]
fn test_parse_package_entry_long_wrapped_line() {
// Test a complete package entry with the long wrapped descriptor pattern
let input = r#"? "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.0, @babel/runtime@npm:^7.12.0"
:
version: 7.28.4
resolution: "@babel/runtime@npm:7.28.4"
checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163
languageName: node
linkType: hard

"#;
let result = parse_package_entry(input);

assert!(
result.is_ok(),
"Should successfully parse complete package entry with long wrapped descriptor"
);
let (remaining, (descriptors, package)) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(descriptors.len(), 4, "Should parse 4 descriptors");

// Verify descriptors
assert_eq!(descriptors[0].ident().name(), "runtime");
assert_eq!(descriptors[0].ident().scope(), Some("@babel"));
assert_eq!(descriptors[0].range(), "npm:^7.0.0");
assert_eq!(descriptors[3].range(), "npm:^7.12.0");

// Verify the parsed package
assert_eq!(package.version, Some("7.28.4".to_string()));
assert_eq!(
package.resolution,
Some("@babel/runtime@npm:7.28.4".to_string())
);
assert_eq!(package.language_name.as_ref(), "node");
assert_eq!(package.link_type, LinkType::Hard);
}
}
49 changes: 49 additions & 0 deletions fixtures/long-descriptors.yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!

__metadata:
version: 8
cacheKey: 10c

? "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.1.5, @babel/runtime@npm:^7.10.0, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.10.4, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.8, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.18.9, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.4, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.4, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.7.7, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2"
:
version: 7.28.4
resolution: "@babel/runtime@npm:7.28.4"
checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163
languageName: node
linkType: hard

? "resolve@patch:resolve@npm%3A^1.0.0#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.1.4#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.1.x#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.12.0#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.15.1#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.17.0#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.3.2#optional!builtin<compat/resolve>"
:
version: 1.22.10
resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin<compat/resolve>::version=1.22.10&hash=c3c19d"
dependencies:
is-core-module: "npm:^2.16.0"
path-parse: "npm:^1.0.7"
supports-preserve-symlinks-flag: "npm:^1.0.0"
bin:
resolve: bin/resolve
checksum: 10/dc5c99fb47807d3771be3135ac6bdb892186973d0895ab17838f0b85bb575e03111214aa16cb68b6416df3c1dd658081a066dd7a9af6e668c28b0025080b615c
languageName: node
linkType: hard

"is-core-module@npm:^2.16.0":
version: 2.16.0
resolution: "is-core-module@npm:2.16.0"
checksum: 10/abcdef1234567890
languageName: node
linkType: hard

"path-parse@npm:^1.0.7":
version: 1.0.7
resolution: "path-parse@npm:1.0.7"
checksum: 10/fedcba0987654321
languageName: node
linkType: hard

"supports-preserve-symlinks-flag@npm:^1.0.0":
version: 1.0.0
resolution: "supports-preserve-symlinks-flag@npm:1.0.0"
checksum: 10/0123456789abcdef
languageName: node
linkType: hard
Loading