aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe')
-rw-r--r--crates/shirabe/src/plugin/capability/capability.rs14
-rw-r--r--crates/shirabe/src/plugin/capability/command_provider.rs8
-rw-r--r--crates/shirabe/src/plugin/capable.rs7
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs292
-rw-r--r--crates/shirabe/src/plugin/plugin_manager.rs138
-rw-r--r--crates/shirabe/tests/plugin/plugin_installer_test.rs122
6 files changed, 494 insertions, 87 deletions
diff --git a/crates/shirabe/src/plugin/capability/capability.rs b/crates/shirabe/src/plugin/capability/capability.rs
index 1788d9a0..aa70ffa0 100644
--- a/crates/shirabe/src/plugin/capability/capability.rs
+++ b/crates/shirabe/src/plugin/capability/capability.rs
@@ -1,4 +1,14 @@
//! ref: composer/src/Composer/Plugin/Capability/Capability.php
-// TODO(plugin): Marker interface for Plugin capabilities. Every new Capability which is added to the Plugin API must implement this interface.
-pub trait Capability {}
+use crate::plugin::capability::CommandProvider;
+
+/// Marker interface for Plugin capabilities. Every new Capability which is added to the
+/// Plugin API must implement this interface.
+///
+/// The accessor replaces PHP's `instanceof` downcast on a capability instance of unknown
+/// concrete type (the way `PluginInterface::as_capable` does for plugins).
+pub trait Capability {
+ fn as_command_provider(&self) -> Option<&dyn CommandProvider> {
+ None
+ }
+}
diff --git a/crates/shirabe/src/plugin/capability/command_provider.rs b/crates/shirabe/src/plugin/capability/command_provider.rs
index 6d6f6cf1..2453860a 100644
--- a/crates/shirabe/src/plugin/capability/command_provider.rs
+++ b/crates/shirabe/src/plugin/capability/command_provider.rs
@@ -1,9 +1,13 @@
//! ref: composer/src/Composer/Plugin/Capability/CommandProvider.php
-// TODO(plugin): Commands Provider Interface. Plugins implementing this capability provide a list of commands.
use crate::command::BaseCommand;
use crate::plugin::capability::Capability;
+/// Commands Provider Interface. Plugins implementing this capability provide a list of
+/// commands.
+///
+/// The sole implementor is the PHP capability proxy (Composer itself never implements a
+/// capability), so the method is fallible: the answer crosses the RPC boundary.
pub trait CommandProvider: Capability {
- fn get_commands(&self) -> Vec<Box<dyn BaseCommand>>;
+ fn get_commands(&self) -> anyhow::Result<Vec<Box<dyn BaseCommand>>>;
}
diff --git a/crates/shirabe/src/plugin/capable.rs b/crates/shirabe/src/plugin/capable.rs
index 830a7dab..3d3b8e3b 100644
--- a/crates/shirabe/src/plugin/capable.rs
+++ b/crates/shirabe/src/plugin/capable.rs
@@ -1,9 +1,12 @@
//! ref: composer/src/Composer/Plugin/Capable.php
use indexmap::IndexMap;
+use shirabe_php_shim::PhpMixed;
// The sole implementor is the PHP plugin proxy (Composer itself never implements Capable), so
-// the trait is fallible: the answer crosses the RPC boundary.
+// the trait is fallible: the answer crosses the RPC boundary. Values are PhpMixed, not String:
+// PHP's getCapabilities() may return anything, and PluginManager validates only the queried
+// key, so narrowing the type here would reject maps upstream Composer accepts.
pub trait Capable {
- fn get_capabilities(&self) -> anyhow::Result<IndexMap<String, String>>;
+ fn get_capabilities(&self) -> anyhow::Result<IndexMap<String, PhpMixed>>;
}
diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs
index 70061542..9cccb1af 100644
--- a/crates/shirabe/src/plugin/php_plugin_proxy.rs
+++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs
@@ -6,22 +6,30 @@
//! (`$composer`, `$io`) live in the R table here.
use crate::autoload::ClassLoader;
+use crate::command::BaseCommand;
use crate::composer::ComposerHandle;
use crate::event_dispatcher::event_dispatcher::dispatch_event_method;
-use crate::event_dispatcher::{EventInterface, EventSubscriberInterface, SubscribedEventEntry};
+use crate::event_dispatcher::{
+ EventInterface, EventSubscriberInterface, SubscribedEventEntry, unwrap_php_result,
+};
use crate::installer::InstallationManagerInterface;
use crate::io::IOInterface;
use crate::package::handle::AnyPackage;
use crate::package::{DisplayMode, PackageInterfaceHandle};
+use crate::plugin::capability::{Capability, CommandProvider};
+use crate::plugin::capable::Capable;
use crate::plugin::plugin_interface::PluginInterface;
use crate::repository::{
InstalledArrayRepository, InstalledFilesystemRepository, RepositoryInterfaceHandle,
RepositoryManagerInterface,
};
use indexmap::IndexMap;
+use shirabe_external_packages::symfony::console::command::command::Command;
+use shirabe_external_packages::symfony::console::input::InputInterface;
+use shirabe_external_packages::symfony::console::output::OutputInterface;
use shirabe_php_rpc::{
- PhpObjHandle, PhpThrow, PluginValue, RustMethodDispatcher, RustObjHandle, call_php_method,
- release_php_handle,
+ PhpObjHandle, PhpThrow, PluginValue, RustMethodDispatcher, RustObjHandle,
+ call_function_with_dispatcher, call_php_method, release_php_handle,
};
use shirabe_php_shim::PhpMixed;
@@ -576,23 +584,7 @@ impl PhpPluginProxy {
composer: &ComposerHandle,
io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
) -> anyhow::Result<()> {
- let composer_rhandle = register_composer_entity(composer);
- let io_rhandle = register_io_entity(io);
- let io_class = io_stub_class(io)?;
- let args = vec![
- PluginValue::RustHandle(RustObjHandle {
- rhandle: composer_rhandle,
- class: "Composer\\Composer".to_string(),
- epoch: 0,
- snapshot: None,
- }),
- PluginValue::RustHandle(RustObjHandle {
- rhandle: io_rhandle,
- class: io_class.to_string(),
- epoch: 0,
- snapshot: None,
- }),
- ];
+ let args = vec![composer_handle_value(composer), io_handle_value(io)?];
let outcome = call_php_method(
self.phandle,
method,
@@ -675,6 +667,18 @@ impl PluginInterface for PhpPluginProxy {
}
}
+ fn as_capable(&self) -> Option<&dyn Capable> {
+ if self
+ .implements
+ .iter()
+ .any(|interface| interface == "Composer\\Plugin\\Capable")
+ {
+ Some(self)
+ } else {
+ None
+ }
+ }
+
fn __as_php_plugin_proxy(&self) -> Option<&PhpPluginProxy> {
Some(self)
}
@@ -712,6 +716,45 @@ impl EventSubscriberInterface for PhpPluginProxy {
}
}
+impl Capable for PhpPluginProxy {
+ fn get_capabilities(&self) -> anyhow::Result<IndexMap<String, PhpMixed>> {
+ let outcome = call_php_method(
+ self.phandle,
+ "getCapabilities",
+ Vec::new(),
+ Some(&mut PluginRpcDispatcher::default()),
+ )?;
+ let value = match outcome {
+ Ok(value) => value,
+ // TODO(plugin): the original exception class is collapsed to RuntimeException on
+ // this side of the boundary.
+ Err(throw) => {
+ return Err(anyhow::anyhow!(shirabe_php_shim::RuntimeException {
+ message: throw.message,
+ code: throw.code,
+ }));
+ }
+ };
+ // PHP: `(array) $plugin->getCapabilities()` — the interface declares no return type,
+ // so a non-array return is cast. A handle in the map (a plugin putting an object among
+ // its capability values) has no PhpMixed image and fails the conversion explicitly.
+ let entries = match value {
+ PluginValue::Null => IndexMap::new(),
+ PluginValue::Array(map) | PluginValue::Object(map) => map
+ .into_iter()
+ .map(|(k, v)| Ok((String::from_utf8_lossy(&k).into_owned(), v.to_php_mixed()?)))
+ .collect::<anyhow::Result<_>>()?,
+ PluginValue::List(items) => items
+ .into_iter()
+ .enumerate()
+ .map(|(i, v)| Ok((i.to_string(), v.to_php_mixed()?)))
+ .collect::<anyhow::Result<_>>()?,
+ scalar => IndexMap::from([("0".to_string(), scalar.to_php_mixed()?)]),
+ };
+ Ok(entries)
+ }
+}
+
/// Decodes a `getSubscribedEvents()` wire value into the three shapes `addSubscriber`
/// distinguishes: `'method'`, `['method', priority]`, and `[['method', priority], ...]`.
/// A shape outside these is an explicit error, never a silently dropped listener.
@@ -798,3 +841,212 @@ impl Drop for PhpPluginProxy {
let _ = release_php_handle(self.phandle);
}
}
+
+/// Wire value handing the shared Rust-side `$composer` to the child, interned in the R table.
+pub fn composer_handle_value(composer: &ComposerHandle) -> PluginValue {
+ rust_handle_value(register_composer_entity(composer), "Composer\\Composer")
+}
+
+/// Wire value handing the shared Rust-side `$io` to the child, interned in the R table.
+pub fn io_handle_value(
+ io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+) -> anyhow::Result<PluginValue> {
+ let class = io_stub_class(io)?;
+ Ok(rust_handle_value(register_io_entity(io), class))
+}
+
+/// `is_a($obj, $class)` evaluated in the worker: the child's own class table answers, so
+/// parent classes are covered (a `PhpObjHandle`'s `implements` lists interfaces only).
+pub(crate) fn php_is_a(handle: &PhpObjHandle, class: &str) -> anyhow::Result<bool> {
+ let value = unwrap_php_result(call_function_with_dispatcher(
+ "is_a",
+ vec![
+ PluginValue::PhpHandle(handle.clone()),
+ PluginValue::string(class),
+ ],
+ Some(&mut PluginRpcDispatcher::default()),
+ ))?;
+ Ok(matches!(value, PluginValue::Bool(true)))
+}
+
+/// `Capability` adapter for a capability entity living in the PHP child process, for
+/// capability interfaces that add no methods of their own (the plain
+/// `Composer\Plugin\Capability\Capability` marker).
+#[derive(Debug)]
+pub struct PhpCapabilityProxy {
+ pub(crate) handle: PhpObjHandle,
+}
+
+impl PhpCapabilityProxy {
+ pub(crate) fn new(handle: PhpObjHandle) -> Self {
+ Self { handle }
+ }
+}
+
+impl Capability for PhpCapabilityProxy {}
+
+impl Drop for PhpCapabilityProxy {
+ fn drop(&mut self) {
+ let _ = release_php_handle(self.handle.phandle);
+ }
+}
+
+/// `CommandProvider` adapter for a capability entity living in the PHP child process.
+#[derive(Debug)]
+pub struct PhpCommandProviderProxy {
+ handle: PhpObjHandle,
+}
+
+impl PhpCommandProviderProxy {
+ pub(crate) fn new(handle: PhpObjHandle) -> Self {
+ Self { handle }
+ }
+}
+
+impl Capability for PhpCommandProviderProxy {
+ fn as_command_provider(&self) -> Option<&dyn CommandProvider> {
+ Some(self)
+ }
+}
+
+impl CommandProvider for PhpCommandProviderProxy {
+ fn get_commands(&self) -> anyhow::Result<Vec<Box<dyn BaseCommand>>> {
+ let value = unwrap_php_result(call_php_method(
+ self.handle.phandle,
+ "getCommands",
+ Vec::new(),
+ Some(&mut PluginRpcDispatcher::default()),
+ ))?;
+ // PHP's getCommands(): array is unenforceable on the wire, and a
+ // `Vec<Box<dyn BaseCommand>>` asserts every element up front, so the two checks
+ // Application::getPluginCommands performs on the raw value live here, with its
+ // messages.
+ let items = match value {
+ PluginValue::List(items) => items,
+ PluginValue::Array(map) => map.into_values().collect(),
+ _ => {
+ return Err(anyhow::anyhow!(
+ shirabe_php_shim::UnexpectedValueException {
+ message: format!(
+ "Plugin capability {} failed to return an array from getCommands",
+ self.handle.class
+ ),
+ code: 0,
+ }
+ ));
+ }
+ };
+ let mut commands: Vec<Box<dyn BaseCommand>> = Vec::new();
+ for item in items {
+ let command_handle = match item {
+ PluginValue::PhpHandle(handle) => handle,
+ _ => return Err(invalid_command_error(&self.handle)),
+ };
+ if !php_is_a(&command_handle, "Composer\\Command\\BaseCommand")? {
+ return Err(invalid_command_error(&self.handle));
+ }
+ commands.push(Box::new(PhpCommandProxy::new(command_handle)?));
+ }
+ Ok(commands)
+ }
+}
+
+fn invalid_command_error(capability: &PhpObjHandle) -> anyhow::Error {
+ anyhow::anyhow!(shirabe_php_shim::UnexpectedValueException {
+ message: format!(
+ "Plugin capability {} returned an invalid value, we expected an array of Composer\\Command\\BaseCommand objects",
+ capability.class
+ ),
+ code: 0,
+ })
+}
+
+impl Drop for PhpCommandProviderProxy {
+ fn drop(&mut self) {
+ let _ = release_php_handle(self.handle.phandle);
+ }
+}
+
+/// `BaseCommand` adapter for a command entity living in the PHP child process. The Rust-side
+/// command state mirrors the child's (the name is read back over RPC at construction, after
+/// the PHP constructor ran `configure()`); running the command needs the PHP-side Symfony
+/// Application and is an explicit error until that exists.
+#[derive(Debug)]
+pub struct PhpCommandProxy {
+ base_command_data: crate::command::BaseCommandData,
+ handle: PhpObjHandle,
+}
+
+impl PhpCommandProxy {
+ pub(crate) fn new(handle: PhpObjHandle) -> anyhow::Result<Self> {
+ let data = crate::command::BaseCommandData::new(None);
+ let name = unwrap_php_result(call_php_method(
+ handle.phandle,
+ "getName",
+ Vec::new(),
+ Some(&mut PluginRpcDispatcher::default()),
+ ))?;
+ match name {
+ PluginValue::Null => {}
+ PluginValue::String(bytes) => {
+ Command::set_name(&data, &String::from_utf8_lossy(&bytes))?;
+ }
+ other => {
+ return Err(anyhow::anyhow!(shirabe_php_shim::RuntimeException {
+ message: format!(
+ "{}::getName() returned an unsupported shape over RPC: {other:?}",
+ handle.class
+ ),
+ code: 0,
+ }));
+ }
+ }
+ Ok(Self {
+ base_command_data: data,
+ handle,
+ })
+ }
+}
+
+impl Command for PhpCommandProxy {
+ fn execute(
+ &self,
+ _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<i64> {
+ // TODO(plugin): executing a plugin-provided command requires the PHP-side Symfony
+ // Application; until then this is an explicit error, never a silent no-op.
+ Err(anyhow::anyhow!(shirabe_php_shim::RuntimeException {
+ message: format!(
+ "cannot execute plugin-provided command {} yet: running PHP commands is not supported",
+ self.handle.class
+ ),
+ code: 0,
+ }))
+ }
+
+ shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
+}
+
+impl BaseCommand for PhpCommandProxy {
+ fn base_command_data(&self) -> &crate::command::BaseCommandData {
+ &self.base_command_data
+ }
+
+ crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
+}
+
+impl shirabe_php_shim::PhpClass for PhpCommandProxy {
+ fn php_class_name(&self) -> &'static str {
+ // TODO(plugin): PhpClass reports only &'static str, but this command's PHP class name
+ // is runtime data (`self.handle.class`); callers needing get_class() must read the
+ // handle instead.
+ panic!("PhpCommandProxy has no static PHP class name; read the handle's class instead")
+ }
+}
+
+impl Drop for PhpCommandProxy {
+ fn drop(&mut self) {
+ let _ = release_php_handle(self.handle.phandle);
+ }
+}
diff --git a/crates/shirabe/src/plugin/plugin_manager.rs b/crates/shirabe/src/plugin/plugin_manager.rs
index d720bcfe..9e52c01f 100644
--- a/crates/shirabe/src/plugin/plugin_manager.rs
+++ b/crates/shirabe/src/plugin/plugin_manager.rs
@@ -14,7 +14,9 @@ use crate::package::base_package::{self};
use crate::package::version::VersionParser;
use crate::plugin::PluginBlockedException;
use crate::plugin::capability::Capability;
-use crate::plugin::php_plugin_proxy::{PhpPluginProxy, PluginRpcDispatcher};
+use crate::plugin::php_plugin_proxy::{
+ PhpCapabilityProxy, PhpCommandProviderProxy, PhpPluginProxy, PluginRpcDispatcher, php_is_a,
+};
use crate::plugin::plugin_interface::{self, PluginInterface};
use crate::repository::InstalledRepository;
use crate::repository::RepositoryInterfaceHandle;
@@ -25,9 +27,9 @@ use indexmap::IndexMap;
use shirabe_external_packages::composer::pcre::Preg;
use shirabe_php_rpc::{PluginValue, call_function_with_dispatcher};
use shirabe_php_shim::{
- E_USER_DEPRECATED, PhpMixed, RuntimeException, UnexpectedValueException, array_key_exists,
- dirname, file_get_contents, implode, ksort, php_regex, preg_quote, strrpos, strtr_array,
- substr, trigger_error, trim, var_export_str, version_compare,
+ E_USER_DEPRECATED, PhpMixed, RuntimeException, UnexpectedValueException, dirname, empty,
+ file_get_contents, implode, ksort, php_regex, preg_quote, strrpos, strtr_array, substr,
+ trigger_error, trim, var_export, var_export_str, version_compare,
};
use shirabe_semver::constraint::SimpleConstraint;
@@ -966,36 +968,26 @@ impl PluginManager {
let capabilities = capable.get_capabilities()?;
// PHP: !empty($capabilities[$capability]) && is_string($capabilities[$capability]) && trim($capabilities[$capability])
- if let Some(s) = capabilities.get(capability) {
+ if let Some(value) = capabilities.get(capability)
+ && !empty(value)
+ && let PhpMixed::String(s) = value
+ {
let trimmed = trim(s, Some(" \t\n\r\0\u{0B}"));
- if !s.is_empty() && s != "0" && !trimmed.is_empty() {
+ // PHP evaluates trim(...) in boolean context: "" and "0" are falsy.
+ if !trimmed.is_empty() && trimmed != "0" {
return Ok(Some(trimmed));
}
}
- // PHP: empty($capabilities[$capability]) — true for null, false, 0, "", "0", [], or missing key.
- // In Rust the values are typed as String, so we only need to consider "", "0".
- let cap_is_empty = match capabilities.get(capability) {
- None => true,
- Some(s) if s.is_empty() || s == "0" => true,
- _ => false,
- };
- if array_key_exists(capability, &capabilities)
- && (cap_is_empty
- || trim(
- capabilities
- .get(capability)
- .map(|s| s.as_str())
- .unwrap_or(""),
- Some(" \t\n\r\0\u{0B}"),
- )
- .is_empty())
- {
+ // PHP: array_key_exists($capability, $capabilities) && (empty(...) || !is_string(...)
+ // || !trim(...)). Once the first branch has declined, a present key always fails one
+ // of the three disjuncts, so a present key unconditionally throws here.
+ if let Some(value) = capabilities.get(capability) {
return Err(UnexpectedValueException {
message: format!(
"Plugin {} provided invalid capability class name(s), got {}",
plugin.get_class_name(),
- var_export_str(capabilities.get(capability).unwrap(), true)
+ var_export(value, true)
),
code: 0,
}
@@ -1009,36 +1001,110 @@ impl PluginManager {
&self,
plugin: &dyn PluginInterface,
capability_class_name: &str,
- _ctor_args: IndexMap<String, PhpMixed>,
+ ctor_args: IndexMap<String, PluginValue>,
) -> anyhow::Result<Option<Box<dyn Capability>>> {
- // TODO(plugin): instantiate plugin capability via runtime class lookup
- let _capability_class =
+ let capability_class =
match self.get_capability_implementation_class_name(plugin, capability_class_name)? {
Some(c) => c,
None => return Ok(None),
};
- // PHP: requires class_exists / new $capabilityClass($ctorArgs); cannot be performed in Rust without a runtime registry.
- Ok(None)
+
+ // PHP: if (!class_exists($capabilityClass))
+ let exists = unwrap_php_result(call_function_with_dispatcher(
+ "class_exists",
+ vec![PluginValue::string(capability_class.clone())],
+ Some(&mut PluginRpcDispatcher::default()),
+ ))?;
+ if !matches!(exists, PluginValue::Bool(true)) {
+ return Err(RuntimeException {
+ message: format!(
+ "Cannot instantiate Capability, as class {} from plugin {} does not exist.",
+ capability_class,
+ plugin.get_class_name()
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ // PHP: $ctorArgs['plugin'] = $plugin; the capability constructor receives the plugin
+ // instance itself, so a plugin with no PHP-side entity cannot be represented.
+ let plugin_value = match plugin.__as_php_plugin_proxy() {
+ Some(proxy) => PluginValue::PhpHandle(shirabe_php_rpc::PhpObjHandle {
+ phandle: proxy.phandle,
+ class: proxy.class.clone(),
+ implements: proxy.implements.clone(),
+ }),
+ None => anyhow::bail!(
+ "cannot instantiate capability {capability_class}: plugin {} has no PHP-side entity to pass as $ctorArgs['plugin']",
+ plugin.get_class_name()
+ ),
+ };
+ let mut ctor_args = ctor_args;
+ ctor_args.insert("plugin".to_string(), plugin_value);
+ let args_value = PluginValue::Array(
+ ctor_args
+ .into_iter()
+ .map(|(k, v)| (k.into_bytes(), v))
+ .collect(),
+ );
+
+ // PHP: $capabilityObj = new $capabilityClass($ctorArgs);
+ let capability_obj = unwrap_php_result(shirabe_php_rpc::new_object(
+ &capability_class,
+ vec![args_value],
+ Some(&mut PluginRpcDispatcher::default()),
+ ))?;
+ let handle = match capability_obj {
+ PluginValue::PhpHandle(handle) => handle,
+ other => anyhow::bail!(
+ "worker returned a non-object for new {capability_class}(...): {other:?}"
+ ),
+ };
+
+ // PHP: if (!$capabilityObj instanceof Capability || !$capabilityObj instanceof $capabilityClassName)
+ if !php_is_a(&handle, "Composer\\Plugin\\Capability\\Capability")?
+ || !php_is_a(&handle, capability_class_name)?
+ {
+ return Err(RuntimeException {
+ message: format!(
+ "Class {capability_class} must implement both Composer\\Plugin\\Capability\\Capability and {capability_class_name}."
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ match capability_class_name {
+ "Composer\\Plugin\\Capability\\CommandProvider" => {
+ Ok(Some(Box::new(PhpCommandProviderProxy::new(handle))))
+ }
+ "Composer\\Plugin\\Capability\\Capability" => {
+ Ok(Some(Box::new(PhpCapabilityProxy::new(handle))))
+ }
+ // A capability interface outside composer-plugin-api that the instanceof checks
+ // accepted (the plugin ships its own): no Rust adapter exists for its methods.
+ other => anyhow::bail!("capability {other} is recognized but not yet adapted"),
+ }
}
pub fn get_plugin_capabilities(
&self,
capability_class_name: &str,
- ctor_args: IndexMap<String, PhpMixed>,
- ) -> Vec<Box<dyn Capability>> {
- // TODO(plugin): aggregate capabilities across all loaded plugins
+ ctor_args: IndexMap<String, PluginValue>,
+ ) -> anyhow::Result<Vec<Box<dyn Capability>>> {
let mut capabilities: Vec<Box<dyn Capability>> = vec![];
for plugin in self.get_plugins() {
- if let Ok(Some(capability)) = self.get_plugin_capability(
+ if let Some(capability) = self.get_plugin_capability(
&*plugin.borrow(),
capability_class_name,
ctor_args.clone(),
- ) {
+ )? {
capabilities.push(capability);
}
}
- capabilities
+ Ok(capabilities)
}
fn parse_allowed_plugins(
diff --git a/crates/shirabe/tests/plugin/plugin_installer_test.rs b/crates/shirabe/tests/plugin/plugin_installer_test.rs
index 70548ba6..ec5f4fe3 100644
--- a/crates/shirabe/tests/plugin/plugin_installer_test.rs
+++ b/crates/shirabe/tests/plugin/plugin_installer_test.rs
@@ -18,7 +18,7 @@ use shirabe::json::JsonFile;
use shirabe::package::loader::{ArrayLoader, JsonLoader, JsonLoaderInput};
use shirabe::package::{Locker, LockerInterface, PackageInterfaceHandle, RootPackageHandle};
use shirabe::plugin::plugin_interface::PluginInterface;
-use shirabe::plugin::{Capable, PluginManager};
+use shirabe::plugin::{Capable, PluginManager, composer_handle_value, io_handle_value};
use shirabe::repository::{
InstalledArrayRepository, InstalledRepositoryInterfaceHandle, RepositoryInterfaceHandle,
RepositoryManagerInterface,
@@ -633,13 +633,44 @@ fn test_plugin_range_constraints_work_only_with_certain_api_version() {
todo!()
}
-#[ignore = "get_plugin_capability never instantiates a capability class (TODO(plugin) in plugin/plugin_manager.rs); Capability::CommandProvider/BaseCommand runtime instantiation is unported"]
#[test]
fn test_command_provider_capability() {
- // TODO(phase-d): get_plugin_capability never instantiates a capability class (TODO(plugin)
- // in plugin/plugin_manager.rs); Capability::CommandProvider/BaseCommand runtime
- // instantiation is also unported.
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ // PHP mocks the repository so getPackages() returns [$this->packages[7]] (plugin-v8);
+ // the real InstalledArrayRepository reaches the same state by adding the package.
+ set_up
+ .repository
+ .borrow_mut()
+ .add_package(set_up.packages[7].clone())
+ .unwrap();
+ let _installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ let mut ctor_args = IndexMap::new();
+ ctor_args.insert(
+ "composer".to_string(),
+ composer_handle_value(&set_up.composer),
+ );
+ ctor_args.insert("io".to_string(), io_handle_value(&set_up.io_dyn).unwrap());
+ let caps = set_up
+ .pm
+ .borrow()
+ .get_plugin_capabilities("Composer\\Plugin\\Capability\\CommandProvider", ctor_args)
+ .unwrap();
+ assert_eq!(1, caps.len());
+ // PHP: assertInstanceOf('Composer\Plugin\Capability\CommandProvider', $caps[0]).
+ let provider = caps[0]
+ .as_command_provider()
+ .expect("the capability implements CommandProvider");
+
+ let commands = provider.get_commands().unwrap();
+ assert_eq!(1, commands.len());
+ // PHP: assertInstanceOf('Composer\Command\BaseCommand', $commands[0]) is witnessed by the
+ // element type of Vec<Box<dyn BaseCommand>>.
}
// A hand-written stub is used in place of PHPUnit's
@@ -692,27 +723,63 @@ fn test_incapable_plugin_is_correctly_detected() {
assert!(result.is_none());
}
-#[ignore = "Requires runtime instantiation of Mock\\Capability via get_plugin_capability; not implemented (TODO(plugin))"]
+#[ignore = "the mocked Capable plugin is Rust-native and cannot cross the RPC boundary: getPluginCapability passes the plugin itself as $ctorArgs['plugin'] to Mock\\Capability, and the test reads $capability->args back; PluginInterface has no rust-proxy stub (TODO(plugin))"]
#[test]
fn test_capability_implements_composer_plugin_api_class_and_is_constructed_with_args() {
- // TODO(phase-d): requires runtime instantiation of Mock\Capability via
- // get_plugin_capability; not implemented (TODO(plugin)).
+ // TODO(phase-d): the mocked Capable plugin is Rust-native and cannot cross the RPC
+ // boundary — getPluginCapability must pass the plugin itself as $ctorArgs['plugin'] to
+ // Mock\Capability's constructor, and the test reads $capability->args back over RPC;
+ // PluginInterface has no rust-proxy stub (TODO(plugin)).
todo!()
}
-// PluginManager::get_capability_implementation_class_name (via Capable::get_capabilities)
-// resolves capability class names through an IndexMap<String, String>, so most of PHP's
-// invalidImplementationClassNames data provider (null, 0, 1000, [1], [], stdClass) cannot be
-// represented at all in the ported type — only the string entries ("", " ") could be
-// constructed. Per the phase-d rule against porting a subset of a data provider, this whole
-// test must stay unported rather than dropping the non-string cases.
-#[ignore = "Capable::get_capabilities is typed IndexMap<String, String>; most of the invalidImplementationClassNames data provider (null, 0, 1000, [1], [], stdClass) is not representable, and porting only the string cases would drop data-provider entries (TODO(phase-d))"]
+/// PHP data provider `invalidImplementationClassNames`, one PhpMixed per entry.
+fn invalid_implementation_class_names() -> Vec<PhpMixed> {
+ vec![
+ PhpMixed::Null,
+ PhpMixed::String(String::new()),
+ PhpMixed::Int(0),
+ PhpMixed::Int(1000),
+ PhpMixed::String(" ".to_string()),
+ PhpMixed::List(vec![PhpMixed::Int(1)]),
+ PhpMixed::List(vec![]),
+ // PHP: new \stdClass()
+ PhpMixed::Object(IndexMap::new()),
+ ]
+}
+
#[test]
fn test_querying_with_invalid_capability_class_name_throws() {
- // TODO(phase-d): Capable::get_capabilities is typed IndexMap<String, String>; most of the
- // invalidImplementationClassNames data provider (null, 0, 1000, [1], [], stdClass) is not
- // representable, and porting only the string cases would drop data-provider entries.
- todo!()
+ let capability_api = "Composer\\Plugin\\Capability\\Capability";
+ for invalid_implementation_class_name in invalid_implementation_class_names() {
+ let set_up = set_up();
+ let plugin = CapablePlugin {
+ capabilities: IndexMap::from([(
+ capability_api.to_string(),
+ invalid_implementation_class_name.clone(),
+ )]),
+ get_capabilities_calls: std::cell::RefCell::new(0),
+ };
+
+ let err =
+ match set_up
+ .pm
+ .borrow()
+ .get_plugin_capability(&plugin, capability_api, IndexMap::new())
+ {
+ Err(err) => err,
+ Ok(_) => panic!(
+ "expected UnexpectedValueException for {invalid_implementation_class_name:?}"
+ ),
+ };
+ assert!(
+ err.downcast_ref::<shirabe_php_shim::UnexpectedValueException>()
+ .is_some(),
+ "expected UnexpectedValueException for {invalid_implementation_class_name:?}, got: {err}"
+ );
+ // PHP: ->expects($this->once())->method('getCapabilities').
+ assert_eq!(1, *plugin.get_capabilities_calls.borrow());
+ }
}
// A hand-written stub plays the role of PHPUnit's
@@ -721,6 +788,7 @@ fn test_querying_with_invalid_capability_class_name_throws() {
// is reproduced with a call counter asserted after the call.
#[derive(Debug)]
struct CapablePlugin {
+ capabilities: IndexMap<String, PhpMixed>,
get_capabilities_calls: std::cell::RefCell<i64>,
}
@@ -759,9 +827,9 @@ impl PluginInterface for CapablePlugin {
}
impl Capable for CapablePlugin {
- fn get_capabilities(&self) -> anyhow::Result<IndexMap<String, String>> {
+ fn get_capabilities(&self) -> anyhow::Result<IndexMap<String, PhpMixed>> {
*self.get_capabilities_calls.borrow_mut() += 1;
- Ok(IndexMap::new())
+ Ok(self.capabilities.clone())
}
}
@@ -770,6 +838,7 @@ fn test_querying_non_provided_capability_returns_null_safely() {
let set_up = set_up();
let plugin = CapablePlugin {
+ capabilities: IndexMap::new(),
get_capabilities_calls: std::cell::RefCell::new(0),
};
@@ -786,10 +855,13 @@ fn test_querying_non_provided_capability_returns_null_safely() {
assert_eq!(1, *plugin.get_capabilities_calls.borrow());
}
-#[ignore = "Requires runtime get_plugin_capability with PHP-class-name capability lookup (class_exists/instanceof checks are unported TODO(plugin)); not implemented"]
+#[ignore = "the '\\stdClass' data-provider case reaches new \\stdClass($ctorArgs), which needs the Rust-native mocked plugin passed as $ctorArgs['plugin'] over RPC (no PluginInterface rust-proxy stub exists, TODO(plugin)); porting only the NonExistentClassLikeMiddleClass case would drop a data-provider entry (TODO(phase-d))"]
#[test]
fn test_querying_with_non_existing_or_wrong_capability_class_types_throws() {
- // TODO(phase-d): requires runtime get_plugin_capability with PHP-class-name capability lookup
- // (class_exists/instanceof checks are unported TODO(plugin)); not implemented.
+ // TODO(phase-d): the '\stdClass' data-provider case reaches new \stdClass($ctorArgs),
+ // which needs the Rust-native mocked plugin passed as $ctorArgs['plugin'] over RPC — no
+ // PluginInterface rust-proxy stub exists (TODO(plugin)). Porting only the
+ // NonExistentClassLikeMiddleClass case (which fails at class_exists, before the plugin is
+ // passed) would drop a data-provider entry.
todo!()
}