From 145b4c0a7fadf6dfd8c03b22c735695037930b0a Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 2 Aug 2026 11:08:57 +0900 Subject: feat(console-input): port the suggested-values backport onto InputArgument/InputOption Composer backports symfony/console 6.1's $suggestedValues parameter in Composer\Console\Input\{InputArgument,InputOption}; the Rust newtypes had dropped it. PHP closures are bound to the command ($this), but a command cannot capture a handle to itself while configure() runs inside new(), so the closure receives the bound command as an explicit `this` argument at call time instead. - add SuggestedValues (list | this-taking closure) and wire it through InputArgument::new5 / InputOption::new6 and their complete() methods - track Composer-typed definition entries by name in BaseCommandData side maps, standing in for PHP's instanceof checks (set_definition converts entries to the Symfony types for storage) - add base_command_complete, the BaseCommand::complete dispatch shared by every Composer command - introduce BaseCommand::base_command_data and make command_data a default method on top of it Co-Authored-By: Claude Fable 5 --- crates/shirabe/src/command/base_command.rs | 95 +++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 7 deletions(-) (limited to 'crates/shirabe/src/command/base_command.rs') diff --git a/crates/shirabe/src/command/base_command.rs b/crates/shirabe/src/command/base_command.rs index e05478f4..7e8a3f5a 100644 --- a/crates/shirabe/src/command/base_command.rs +++ b/crates/shirabe/src/command/base_command.rs @@ -42,6 +42,15 @@ pub struct BaseCommandData { inner: CommandData, pub(crate) composer: std::cell::RefCell>, pub(crate) io: std::cell::RefCell>>>, + /// The definition entries that were registered through the Composer-typed + /// InputArgument/InputOption (which carry the suggested-values backport). PHP checks + /// `$definition->getArgument($name) instanceof Composer\Console\Input\InputArgument`; in + /// this port `set_definition` converts the entries to the Symfony types for storage, so + /// the Composer-typedness is tracked by name in these side maps instead. + pub(crate) composer_arguments: + std::cell::RefCell>>, + pub(crate) composer_options: + std::cell::RefCell>>, } impl BaseCommandData { @@ -50,6 +59,8 @@ impl BaseCommandData { inner: CommandData::new(name), composer: std::cell::RefCell::new(None), io: std::cell::RefCell::new(None), + composer_arguments: std::cell::RefCell::new(IndexMap::new()), + composer_options: std::cell::RefCell::new(IndexMap::new()), } } @@ -65,17 +76,40 @@ impl BaseCommandData { /// `Command` trait. The Symfony state methods are inherited from the [`Command`] supertrait; /// only the Composer-specific behavior and the Composer-typed definition builders live here. pub trait BaseCommand: Command { - /// Access to the embedded Symfony command state. Each command returns - /// `self.base_command_data.command_data()`; this lets the Composer-typed definition - /// builders below forward to `CommandData`'s Symfony-typed entry points. `CommandData` is - /// interior-mutable, so a shared reference is enough. - fn command_data(&self) -> &CommandData; + /// Access to the embedded base-command state. Each command returns + /// `&self.base_command_data`; the definition builders and the completion dispatch below + /// go through it. The state is interior-mutable, so a shared reference is enough. + fn base_command_data(&self) -> &BaseCommandData; + + /// Access to the embedded Symfony command state, forwarding to `CommandData`'s + /// Symfony-typed entry points. + fn command_data(&self) -> &CommandData { + self.base_command_data().command_data() + } /// Sets the definition from Composer-typed argument/option instances. fn set_definition(&self, definition: &[InputDefinitionItem]) -> &Self where Self: Sized, { + let data = self.base_command_data(); + // PHP replaces the whole definition, so the Composer-typed side maps follow suit. + data.composer_arguments.borrow_mut().clear(); + data.composer_options.borrow_mut().clear(); + for item in definition { + match item { + InputDefinitionItem::Argument(argument) => { + data.composer_arguments + .borrow_mut() + .insert(argument.get_name(), argument.clone()); + } + InputDefinitionItem::Option(option) => { + data.composer_options + .borrow_mut() + .insert(option.get_name(), option.clone()); + } + } + } let items = definition .iter() .map(|item| item.to_definition_item()) @@ -255,8 +289,8 @@ impl Command for BaseCommandData { } impl BaseCommand for BaseCommandData { - fn command_data(&self) -> &CommandData { - &self.inner + fn base_command_data(&self) -> &BaseCommandData { + self } fn require_composer( @@ -736,6 +770,53 @@ fn apply_application_defaults( (disable_plugins, disable_scripts) } +/// \Composer\Command\BaseCommand::complete — the suggested-values backport dispatch +/// (BaseCommand.php:198-215). Shared via a free function because Rust has no inheritance; +/// each command's `Command::complete` forwards here. +pub fn base_command_complete( + cmd: &dyn BaseCommand, + input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, + suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, +) -> anyhow::Result<()> { + use shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput; + + // PHP: (string) $input->getCompletionName() + let name = input.get_completion_name().unwrap_or_default(); + // The definition Ref and the side-map Refs must be dropped before the suggestion closure + // runs: the closure may re-enter the command (require_composer etc.). + if CompletionInput::TYPE_OPTION_VALUE == input.get_completion_type() + && cmd.get_definition().has_option(&name) + { + // PHP: ($option = $definition->getOption($name)) instanceof InputOption — the + // Composer-typedness is tracked in the side map (see BaseCommandData). + let option = cmd + .base_command_data() + .composer_options + .borrow() + .get(&name) + .cloned(); + if let Some(option) = option { + return option.complete(cmd, input, suggestions); + } + } else if CompletionInput::TYPE_ARGUMENT_VALUE == input.get_completion_type() + && cmd + .get_definition() + .has_argument(&PhpMixed::String(name.clone())) + { + let argument = cmd + .base_command_data() + .composer_arguments + .borrow() + .get(&name) + .cloned(); + if let Some(argument) = argument { + return argument.complete(cmd, input, suggestions); + } + } + // PHP: parent::complete($input, $suggestions) — Symfony 5.4's Command::complete is a no-op. + Ok(()) +} + /// \Composer\Command\BaseCommand::initialize — runs for every Composer command after the /// input is bound. Shared via a free function because Rust has no inheritance; each command's /// `Command::initialize` forwards here so the leaf's `is_self_update_command()` override (and -- cgit v1.3.1