aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe')
-rw-r--r--crates/shirabe/src/command/require_command.rs15
-rw-r--r--crates/shirabe/src/event_dispatcher/event_dispatcher.rs37
-rw-r--r--crates/shirabe/tests/command/require_command_test.rs17
3 files changed, 48 insertions, 21 deletions
diff --git a/crates/shirabe/src/command/require_command.rs b/crates/shirabe/src/command/require_command.rs
index 05f86480..c1730942 100644
--- a/crates/shirabe/src/command/require_command.rs
+++ b/crates/shirabe/src/command/require_command.rs
@@ -53,7 +53,7 @@ pub struct RequireCommand {
lock: std::cell::RefCell<String>,
/// contents before modification if the lock file exists
lock_backup: std::cell::RefCell<Option<String>>,
- dependency_resolution_completed: std::cell::Cell<bool>,
+ dependency_resolution_completed: std::rc::Rc<std::cell::Cell<bool>>,
repos: std::cell::RefCell<Option<crate::repository::RepositoryInterfaceHandle>>,
repository_sets:
std::cell::RefCell<IndexMap<String, std::rc::Rc<std::cell::RefCell<RepositorySet>>>>,
@@ -76,7 +76,7 @@ impl RequireCommand {
composer_backup: std::cell::RefCell::new(String::new()),
lock: std::cell::RefCell::new(String::new()),
lock_backup: std::cell::RefCell::new(None),
- dependency_resolution_completed: std::cell::Cell::new(false),
+ dependency_resolution_completed: std::rc::Rc::new(std::cell::Cell::new(false)),
repos: std::cell::RefCell::new(None),
repository_sets: std::cell::RefCell::new(IndexMap::new()),
};
@@ -759,14 +759,13 @@ impl RequireCommand {
self.dependency_resolution_completed.set(false);
// PHP: $composer->getEventDispatcher()->addListener(InstallerEvents::PRE_OPERATIONS_EXEC,
// function () use (&$dependencyResolutionCompleted) { $dependencyResolutionCompleted = true; }, 10000);
- // TODO(phase-c): the event dispatcher's Callable::Closure is a placeholder variant that
- // stores no actual closure, so the listener that flips dependency_resolution_completed
- // cannot be registered. Resolving needs the closure model (Callable holding an Rc<dyn Fn>)
- // plus dependency_resolution_completed shared (Rc<RefCell<bool>>) into both the listener
- // and this command.
+ let dependency_resolution_completed = self.dependency_resolution_completed.clone();
composer.get_event_dispatcher().borrow_mut().add_listener(
InstallerEvents::PRE_OPERATIONS_EXEC,
- crate::event_dispatcher::Callable::Closure,
+ crate::event_dispatcher::Callable::Closure(std::rc::Rc::new(move |_event| {
+ dependency_resolution_completed.set(true);
+ PhpMixed::Null
+ })),
10000,
);
diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
index 6deb130e..1f1d9619 100644
--- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
+++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
@@ -34,16 +34,34 @@ use shirabe_php_shim::{
/// Represents a callable listener. PHP's `callable` may be a string (command, script, or
/// "Class::method"), a `[object|string, method]` pair, or a `\Closure`.
///
-/// TODO(plugin): Subscriber- and Closure-based listeners come from plugins and are not
+/// TODO(plugin): Subscriber-based (`ArrayCallable`) listeners come from plugins and are not
/// implemented yet — only the string forms used by composer.json `scripts` work here.
-#[derive(Debug, Clone)]
+#[derive(Clone)]
pub enum Callable {
String(String),
/// `[$className_or_object, $methodName]` array callable. The first element is represented
/// here as `PhpMixed` to keep parity with PHP's loose typing.
ArrayCallable(Box<PhpMixed>, String),
- /// PHP `\Closure` placeholder.
- Closure,
+ /// PHP `\Closure`, invoked with the event exactly like `$callable($event)` in
+ /// `EventDispatcher::doDispatch`. Today this is only produced by Composer's own commands
+ /// registering an inline listener on themselves (e.g. `RequireCommand`'s
+ /// `dependencyResolutionCompleted` tracker) — Plugin-supplied closures remain out of scope
+ /// pending Plugin API.
+ Closure(std::rc::Rc<dyn Fn(&dyn EventInterface) -> PhpMixed>),
+}
+
+impl std::fmt::Debug for Callable {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Callable::String(s) => f.debug_tuple("String").field(s).finish(),
+ Callable::ArrayCallable(first, method) => f
+ .debug_tuple("ArrayCallable")
+ .field(first)
+ .field(method)
+ .finish(),
+ Callable::Closure(_) => f.write_str("Closure(..)"),
+ }
+ }
}
/// The Event Dispatcher.
@@ -326,7 +344,16 @@ impl EventDispatcher {
}
);
let is_string_callable = matches!(callable, Callable::String(_));
- if !is_string_callable {
+ if let Callable::Closure(ref closure) = callable {
+ let _ = self.make_autoloader(event, &callable);
+ // Closures are always callable in PHP (is_callable() returns true for any \Closure),
+ // so the is_callable()/RuntimeException branch below never applies here.
+ r#return = if matches!(closure(event), PhpMixed::Bool(false)) {
+ 1
+ } else {
+ 0
+ };
+ } else if !is_string_callable {
// TODO(plugin): non-string callable handling — verify is_callable, invoke,
// and replicate the get_class / write_error / is_callable error path from PHP.
let _ = self.make_autoloader(event, &callable);
diff --git a/crates/shirabe/tests/command/require_command_test.rs b/crates/shirabe/tests/command/require_command_test.rs
index b132eb43..bf742ea1 100644
--- a/crates/shirabe/tests/command/require_command_test.rs
+++ b/crates/shirabe/tests/command/require_command_test.rs
@@ -58,10 +58,10 @@ fn test_require_throws_if_none_matches() {
#[test]
#[serial]
-#[ignore = "the prior RefCell re-entrancy panic is fixed; now fails on the pre-operations-exec \
- listener dispatch with \"Subscriber ?::? for event pre-operations-exec is not \
- callable\" (event_dispatcher.rs TODO(plugin): is_callable/invoke for non-string \
- callables is unimplemented)"]
+#[ignore = "the pre-operations-exec listener bug is fixed; now fails with \"RefCell already \
+ borrowed\" at console_io.rs:366 (ConsoleIO::ask_question, reached via \
+ ask_confirmation from RequireCommand::update_requirements_after_resolution) — a \
+ re-entrant IO RefCell borrow, unrelated to event dispatching"]
fn test_require_warns_if_resolved_to_feature_branch() {
let composer_json = serde_json::json!({
"repositories": {
@@ -266,10 +266,11 @@ Using version 1.1.0 for required/pkg",
#[test]
#[serial]
-#[ignore = "the prior RefCell re-entrancy panic is fixed; now fails on the pre-operations-exec \
- listener dispatch with \"Subscriber ?::? for event pre-operations-exec is not \
- callable\" (event_dispatcher.rs TODO(plugin): is_callable/invoke for non-string \
- callables is unimplemented)"]
+#[ignore = "the pre-operations-exec listener bug is fixed; now fails on the first data-provider \
+ case ('warn once for missing ext but a lower package matches') because the \
+ \"<warning>Cannot use required/pkg's latest version 1.2.0 as it requires ext-foobar \
+ ^1 which is missing from your platform.</warning>\" message is never emitted — a \
+ distinct, unimplemented require_command warning path unrelated to event dispatching"]
fn test_require() {
for (label, composer_json, command, expected) in provide_require() {
let _tear_down = init_temp_composer(Some(&composer_json), None, None, true);