diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-07-24 20:23:42 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-07-24 20:23:42 +0900 |
| commit | 90d9b1dc0035a70dab76d520b9dcd14ec57273ad (patch) | |
| tree | 30f6576fddcd4b6fb6fc883c28610b324a15b3cc /crates/shirabe | |
| parent | 4a8f5a83b933a06974916b102da673f2a88aebe8 (diff) | |
| download | php-shirabe-90d9b1dc0035a70dab76d520b9dcd14ec57273ad.tar.gz php-shirabe-90d9b1dc0035a70dab76d520b9dcd14ec57273ad.tar.zst php-shirabe-90d9b1dc0035a70dab76d520b9dcd14ec57273ad.zip | |
fix(event-dispatcher): invoke Closure listeners instead of always failing is_callable
RequireCommand registers an inline listener on InstallerEvents::PRE_OPERATIONS_EXEC
to track dependency_resolution_completed, mirroring PHP's `function () use
(&$dependencyResolutionCompleted) { ... }`. This is Composer's own code, not a
Plugin subscriber, but it went through the shared non-string-callable path, which
checked is_callable() against a hardcoded PhpMixed::Null and always failed,
breaking every `require` that reaches the install step.
Callable::Closure now carries the actual Rc<dyn Fn> instead of being a data-less
placeholder, and is invoked directly (Closures are always callable in PHP). The
ArrayCallable path used by future Plugin subscribers is untouched.
Un-ignoring the two require_command_test cases that cited this bug reveals two
separate, pre-existing issues (a missing ext-requirement warning message, and a
RefCell re-entrancy panic in ConsoleIO::ask_question); their #[ignore] reasons
are updated to describe the real current blocker instead of the now-fixed one.
Diffstat (limited to 'crates/shirabe')
| -rw-r--r-- | crates/shirabe/src/command/require_command.rs | 15 | ||||
| -rw-r--r-- | crates/shirabe/src/event_dispatcher/event_dispatcher.rs | 37 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/require_command_test.rs | 17 |
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); |
