aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-php-shim/src/runtime.rs13
-rw-r--r--crates/shirabe-semver/src/version_parser.rs148
-rw-r--r--crates/shirabe/src/package/loader/root_package_loader.rs5
-rw-r--r--crates/shirabe/tests/installed_versions_test.rs27
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();