diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-25 13:43:49 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-25 23:47:47 +0900 |
| commit | 4573629d9fc7321171cd8b134e68869950b02045 (patch) | |
| tree | 1c1786537f71c6c61bca5fb0bba90fe1e722eac0 /crates | |
| parent | 8d6b521e0034d1bcba594921ebc4f74e87e5ef46 (diff) | |
| download | php-shirabe-4573629d9fc7321171cd8b134e68869950b02045.tar.gz php-shirabe-4573629d9fc7321171cd8b134e68869950b02045.tar.zst php-shirabe-4573629d9fc7321171cd8b134e68869950b02045.zip | |
feat(semver): hand-port constraint AND-split to drop look-around regex
The PCRE delimiter `(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)` used to
split AND-constraints relies on look-around, which the regex crate cannot
compile (parse_constraints panicked). Reproduce its semantics in a hand-written
`split_and_constraints` scanner shared by VersionParser and RootPackageLoader.
Also model `method_exists` for the class-name form (shirabe runs no dumped
Composer ClassLoader) and un-ignore the InstalledVersions tests, serialized via
`#[serial]` since they share global static state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/shirabe-php-shim/src/runtime.rs | 13 | ||||
| -rw-r--r-- | crates/shirabe-semver/src/version_parser.rs | 148 | ||||
| -rw-r--r-- | crates/shirabe/src/package/loader/root_package_loader.rs | 5 | ||||
| -rw-r--r-- | crates/shirabe/tests/installed_versions_test.rs | 27 |
4 files changed, 169 insertions, 24 deletions
diff --git a/crates/shirabe-php-shim/src/runtime.rs b/crates/shirabe-php-shim/src/runtime.rs index bf4d355..c1c5f61 100644 --- a/crates/shirabe-php-shim/src/runtime.rs +++ b/crates/shirabe-php-shim/src/runtime.rs @@ -71,9 +71,16 @@ pub fn defined(name: &str) -> bool { ) } -pub fn method_exists(_object: &PhpMixed, _method_name: &str) -> bool { - // TODO(phase-d): requires runtime class/method reflection, which PhpMixed::Object does not carry. - todo!() +// Models methods available on Composer's own runtime classes when given a class name string. +// Shirabe is a native binary that does not run under a Composer-dumped autoloader, so the dumped +// `Composer\Autoload\ClassLoader` (and its `getRegisteredLoaders`) is absent from the running +// process; the class-name form therefore reports no such method. The object form needs runtime +// reflection that PhpMixed::Object does not carry. +pub fn method_exists(object_or_class: &PhpMixed, _method_name: &str) -> bool { + match object_or_class { + PhpMixed::String(_) => false, + _ => todo!(), + } } // Models the classes available in a standard PHP CLI environment running Composer: diff --git a/crates/shirabe-semver/src/version_parser.rs b/crates/shirabe-semver/src/version_parser.rs index ddde07e..3e4a748 100644 --- a/crates/shirabe-semver/src/version_parser.rs +++ b/crates/shirabe-semver/src/version_parser.rs @@ -297,10 +297,7 @@ impl VersionParser { let mut or_groups: Vec<AnyConstraint> = Vec::new(); for or_constraint in &or_constraints { - let and_constraints = php::preg_split( - "{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}", - or_constraint, - ); + let and_constraints = split_and_constraints(or_constraint); let constraint_objects: Vec<AnyConstraint> = if and_constraints.len() > 1 { let mut objs: Vec<AnyConstraint> = Vec::new(); @@ -762,3 +759,146 @@ impl VersionParser { } } } + +/// Splits a single OR-group of a constraint string into its AND-separated parts. +/// +/// Regex pattern compatibility: +/// Composer splits on the PCRE delimiter `(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)`, which +/// relies on look-behind/look-ahead the `regex` crate cannot express. This reproduces the PCRE +/// semantics (greedy ` *` with backtracking around a single `,`/space core, guarded by the +/// look-arounds) directly over the byte string. Constraint strings are ASCII, and `$` is treated +/// as end-of-string. +pub fn split_and_constraints(subject: &str) -> Vec<String> { + let b = subject.as_bytes(); + let n = b.len(); + + let mut pieces: Vec<String> = Vec::new(); + let mut last = 0usize; + let mut i = 0usize; + while i <= n { + if let Some(end) = match_and_delimiter(b, i) { + pieces.push(subject[last..i].to_string()); + last = end; + i = end; + } else { + i += 1; + } + } + pieces.push(subject[last..].to_string()); + pieces +} + +/// Tries to match the AND-delimiter regex anchored at `i`, returning the match end on success. +fn match_and_delimiter(b: &[u8], i: usize) -> Option<usize> { + let n = b.len(); + + // (?<!^|as|[=>< ,]): no match at the start, right after "as", or after one of `=><`, space, comma. + if i == 0 { + return None; + } + let prev = b[i - 1]; + if matches!(prev, b'=' | b'>' | b'<' | b' ' | b',') { + return None; + } + if i >= 2 && b[i - 2] == b'a' && b[i - 1] == b's' { + return None; + } + + // ` *` is greedy; find the end of the leading space run, then backtrack `j` toward `i`. + let mut space_end = i; + while space_end < n && b[space_end] == b' ' { + space_end += 1; + } + + let mut j = space_end; + loop { + if j < n && matches!(b[j], b',' | b' ') { + // (?<!-) before the `[, ]` core and (?!-) after it. + let ok_before = b[j - 1] != b'-'; + let ok_after = j + 1 >= n || b[j + 1] != b'-'; + if ok_before && ok_after { + // Trailing ` *` is greedy; find its end, then backtrack `k`. + let mut trailing_end = j + 1; + while trailing_end < n && b[trailing_end] == b' ' { + trailing_end += 1; + } + let mut k = trailing_end; + loop { + // (?!,|as|$): the delimiter cannot abut a comma, an "as", or the string end. + let at_end = k >= n; + let is_comma = k < n && b[k] == b','; + let is_as = k + 1 < n && b[k] == b'a' && b[k + 1] == b's'; + if !(at_end || is_comma || is_as) { + return Some(k); + } + if k == j + 1 { + break; + } + k -= 1; + } + } + } + if j == i { + break; + } + j -= 1; + } + None +} + +#[cfg(test)] +mod split_and_constraints_tests { + use super::split_and_constraints; + + fn split(s: &str) -> Vec<String> { + split_and_constraints(s) + } + + #[test] + fn splits_space_separated() { + assert_eq!(split(">=1.0 <2.0"), vec![">=1.0", "<2.0"]); + } + + #[test] + fn splits_comma_separated() { + assert_eq!(split(">=1.0,<2.0"), vec![">=1.0", "<2.0"]); + } + + #[test] + fn splits_comma_space_separated() { + assert_eq!(split(">=1.0, <2.0"), vec![">=1.0", "<2.0"]); + } + + #[test] + fn collapses_multiple_spaces() { + assert_eq!(split(">=1.0 <2.0"), vec![">=1.0", "<2.0"]); + } + + #[test] + fn does_not_split_after_operator() { + assert_eq!(split(">= 1.0"), vec![">= 1.0"]); + } + + #[test] + fn does_not_split_hyphen_range() { + assert_eq!(split("1.0 - 2.0"), vec!["1.0 - 2.0"]); + } + + #[test] + fn does_not_split_around_as() { + assert_eq!(split("1.0 as 1.1"), vec!["1.0 as 1.1"]); + } + + #[test] + fn single_constraint_is_unchanged() { + assert_eq!(split("^1.0"), vec!["^1.0"]); + } + + #[test] + fn three_and_constraints() { + assert_eq!( + split(">=1.0 <2.0 !=1.5"), + vec![">=1.0", "<2.0", "!=1.5"] + ); + } +} diff --git a/crates/shirabe/src/package/loader/root_package_loader.rs b/crates/shirabe/src/package/loader/root_package_loader.rs index 16b905e..9cd1eac 100644 --- a/crates/shirabe/src/package/loader/root_package_loader.rs +++ b/crates/shirabe/src/package/loader/root_package_loader.rs @@ -322,10 +322,7 @@ impl RootPackageLoader { let or_split = Preg::split(r"\s*\|\|?\s*", req_version.trim()); for or_constraint in &or_split { - let and_split = Preg::split( - r"(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)", - or_constraint, - ); + let and_split = shirabe_semver::split_and_constraints(or_constraint); for and_constraint in and_split { constraints.push(and_constraint); } diff --git a/crates/shirabe/tests/installed_versions_test.rs b/crates/shirabe/tests/installed_versions_test.rs index 20c3a85..4de2730 100644 --- a/crates/shirabe/tests/installed_versions_test.rs +++ b/crates/shirabe/tests/installed_versions_test.rs @@ -7,6 +7,7 @@ // IndexMap<String, PhpMixed> with the tmp root substituted for `$dir`. use indexmap::IndexMap; +use serial_test::serial; use shirabe::installed_versions::InstalledVersions; use shirabe_php_shim::{PhpMixed, realpath}; use shirabe_semver::VersionParser; @@ -163,7 +164,7 @@ fn set_up() -> (TempDir, String) { (root, dir) } -#[ignore] +#[serial] #[test] fn test_get_installed_packages() { let (_root, _dir) = set_up(); @@ -182,7 +183,7 @@ fn test_get_installed_packages() { assert_eq!(names, InstalledVersions::get_installed_packages()); } -#[ignore] +#[serial] #[test] fn test_is_installed() { let (_root, _dir) = set_up(); @@ -206,7 +207,7 @@ fn test_is_installed() { } } -#[ignore] +#[serial] #[test] fn test_satisfies() { let (_root, _dir) = set_up(); @@ -241,7 +242,7 @@ fn test_satisfies() { } } -#[ignore] +#[serial] #[test] fn test_get_version_ranges() { let (_root, _dir) = set_up(); @@ -265,7 +266,7 @@ fn test_get_version_ranges() { } } -#[ignore] +#[serial] #[test] fn test_get_version() { let (_root, _dir) = set_up(); @@ -289,7 +290,7 @@ fn test_get_version() { } } -#[ignore] +#[serial] #[test] fn test_get_pretty_version() { let (_root, _dir) = set_up(); @@ -313,7 +314,7 @@ fn test_get_pretty_version() { } } -#[ignore] +#[serial] #[test] fn test_get_version_out_of_bounds() { let (_root, _dir) = set_up(); @@ -321,7 +322,7 @@ fn test_get_version_out_of_bounds() { assert!(InstalledVersions::get_version("not/installed").is_err()); } -#[ignore] +#[serial] #[test] fn test_get_root_package() { let (_root, dir) = set_up(); @@ -342,7 +343,7 @@ fn test_get_root_package() { assert_eq!(expected, InstalledVersions::get_root_package()); } -#[ignore] +#[serial] #[test] fn test_get_raw_data() { let (_root, dir) = set_up(); @@ -350,7 +351,7 @@ fn test_get_raw_data() { assert_eq!(fixture(&dir), InstalledVersions::get_raw_data()); } -#[ignore] +#[serial] #[test] fn test_get_reference() { let (_root, _dir) = set_up(); @@ -374,7 +375,7 @@ fn test_get_reference() { } } -#[ignore] +#[serial] #[test] fn test_get_installed_packages_by_type() { let (_root, _dir) = set_up(); @@ -393,7 +394,7 @@ fn test_get_installed_packages_by_type() { ); } -#[ignore] +#[serial] #[test] fn test_get_install_path() { let (_root, dir) = set_up(); @@ -416,7 +417,7 @@ fn test_get_install_path() { ); } -#[ignore] +#[serial] #[test] fn test_with_class_loader_loaded() { let (_root, _dir) = set_up(); |
