diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-16 21:59:35 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-19 22:17:11 +0900 |
| commit | fd733a87364b877d5e66b4973bd61b5379ec4d61 (patch) | |
| tree | 64bd6c37ca1c33581a851eba22e4261bf5b62b49 /crates/shirabe/src | |
| parent | 7f3171b14a89762b2f0c71969d7243f2297af7c9 (diff) | |
| download | php-shirabe-fd733a87364b877d5e66b4973bd61b5379ec4d61.tar.gz php-shirabe-fd733a87364b877d5e66b4973bd61b5379ec4d61.tar.zst php-shirabe-fd733a87364b877d5e66b4973bd61b5379ec4d61.zip | |
feat(command): implement Symfony Command
Diffstat (limited to 'crates/shirabe/src')
41 files changed, 4187 insertions, 2701 deletions
diff --git a/crates/shirabe/src/command/about_command.rs b/crates/shirabe/src/command/about_command.rs index 9b3fca5..1bfa89f 100644 --- a/crates/shirabe/src/command/about_command.rs +++ b/crates/shirabe/src/command/about_command.rs @@ -1,14 +1,24 @@ //! ref: composer/src/Composer/Command/AboutCommand.php +use anyhow::Result; +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_shim::PhpMixed; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::advisory::AuditConfig; use crate::command::BaseCommand; use crate::command::BaseCommandData; -use crate::command::HasBaseCommandData; +use crate::command::base_command::base_command_initialize; use crate::composer; -use crate::composer::ComposerHandle; +use crate::composer::PartialComposerHandle; +use crate::config::Config; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; -use shirabe_external_packages::symfony::console::input::InputInterface; -use shirabe_external_packages::symfony::console::output::OutputInterface; #[derive(Debug)] pub struct AboutCommand { @@ -16,36 +26,59 @@ pub struct AboutCommand { } impl AboutCommand { - pub fn configure(&mut self) { - self.set_name("about") - .set_description("Shows a short information about Composer") - .set_help("<info>php composer.phar about</info>"); + pub fn new() -> Self { + let mut command = AboutCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("AboutCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for AboutCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("about")?; + self.set_description("Shows a short information about Composer"); + self.set_help("<info>php composer.phar about</info>"); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> i64 { + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let composer_version = composer::get_version(); let _ = (input, output); - self.get_io().write(&format!( + self.get_io().borrow().write(&format!( "<info>Composer - Dependency Manager for PHP - version {composer_version}</info>\n\ <comment>Composer is a dependency manager tracking local dependencies of your projects and libraries.\n\ See https://getcomposer.org/ for more information.</comment>" )); - 0 + Ok(0) } -} -impl HasBaseCommandData for AboutCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for AboutCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/archive_command.rs b/crates/shirabe/src/command/archive_command.rs index ee11ae2..12a93e0 100644 --- a/crates/shirabe/src/command/archive_command.rs +++ b/crates/shirabe/src/command/archive_command.rs @@ -3,16 +3,22 @@ use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; +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_shim::{LogicException, get_debug_type}; +use shirabe_php_shim::{LogicException, PhpMixed, get_debug_type}; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; use crate::composer::PartialComposerHandle; use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; use crate::factory::Factory; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::package::archiver::ArchiveManager; @@ -37,33 +43,45 @@ pub struct ArchiveCommand { impl ArchiveCommand { const FORMATS: &'static [&'static str] = &["tar", "tar.gz", "tar.bz2", "zip"]; - pub fn configure(&mut self) { + pub fn new() -> Self { + let mut command = ArchiveCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("ArchiveCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for ArchiveCommand { + fn configure(&mut self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_available_package(99) for `package` argument - self - .set_name("archive") - .set_description("Creates an archive of this composer package") - .set_definition(&[ - InputArgument::new("package", Some(InputArgument::OPTIONAL), "The package to archive instead of the current project", None).unwrap().into(), - InputArgument::new("version", Some(InputArgument::OPTIONAL), "A version constraint to find the package to archive", None).unwrap().into(), - InputOption::new("format", Some(shirabe_php_shim::PhpMixed::String("f".to_string())), Some(InputOption::VALUE_REQUIRED), "Format of the resulting archive: tar, tar.gz, tar.bz2 or zip (default tar)", None).unwrap().into(), - InputOption::new("dir", None, Some(InputOption::VALUE_REQUIRED), "Write the archive to this directory", None).unwrap().into(), - InputOption::new("file", None, Some(InputOption::VALUE_REQUIRED), "Write the archive with the given file name. Note that the format will be appended.", None).unwrap().into(), - InputOption::new("ignore-filters", None, Some(InputOption::VALUE_NONE), "Ignore filters when saving package", None).unwrap().into(), - ]) - .set_help( - "The <info>archive</info> command creates an archive of the specified format\n\ - containing the files and directories of the Composer project or the specified\n\ - package in the specified version and writes it to the specified directory.\n\n\ - <info>php composer.phar archive [--format=zip] [--dir=/foo] [--file=filename] [package [version]]</info>\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#archive" - ); + self.set_name("archive")?; + self.set_description("Creates an archive of this composer package"); + self.set_definition(&[ + InputArgument::new("package", Some(InputArgument::OPTIONAL), "The package to archive instead of the current project", None).unwrap().into(), + InputArgument::new("version", Some(InputArgument::OPTIONAL), "A version constraint to find the package to archive", None).unwrap().into(), + InputOption::new("format", Some(shirabe_php_shim::PhpMixed::String("f".to_string())), Some(InputOption::VALUE_REQUIRED), "Format of the resulting archive: tar, tar.gz, tar.bz2 or zip (default tar)", None).unwrap().into(), + InputOption::new("dir", None, Some(InputOption::VALUE_REQUIRED), "Write the archive to this directory", None).unwrap().into(), + InputOption::new("file", None, Some(InputOption::VALUE_REQUIRED), "Write the archive with the given file name. Note that the format will be appended.", None).unwrap().into(), + InputOption::new("ignore-filters", None, Some(InputOption::VALUE_NONE), "Ignore filters when saving package", None).unwrap().into(), + ]); + self.set_help( + "The <info>archive</info> command creates an archive of the specified format\n\ + containing the files and directories of the Composer project or the specified\n\ + package in the specified version and writes it to the specified directory.\n\n\ + <info>php composer.phar archive [--format=zip] [--dir=/foo] [--file=filename] [package [version]]</info>\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#archive" + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let composer = self.try_composer(None, None); let config = if let Some(ref composer) = composer { @@ -161,6 +179,28 @@ impl ArchiveCommand { Ok(return_code) } + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for ArchiveCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); +} + +impl ArchiveCommand { pub fn archive( &mut self, io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, @@ -367,13 +407,3 @@ impl ArchiveCommand { Ok(Some(complete)) } } - -impl HasBaseCommandData for ArchiveCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data - } - - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } -} diff --git a/crates/shirabe/src/command/audit_command.rs b/crates/shirabe/src/command/audit_command.rs index cd7a799..4677b40 100644 --- a/crates/shirabe/src/command/audit_command.rs +++ b/crates/shirabe/src/command/audit_command.rs @@ -1,10 +1,26 @@ //! ref: composer/src/Composer/Command/AuditCommand.php +use anyhow::Result; +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_shim::{ + InvalidArgumentException, PhpMixed, UnexpectedValueException, array_fill_keys, array_merge, + implode, in_array, +}; +use std::cell::RefCell; +use std::rc::Rc; + use crate::advisory::AuditConfig; use crate::advisory::Auditor; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::repository::CanonicalPackagesTrait; @@ -12,13 +28,6 @@ use crate::repository::InstalledRepository; use crate::repository::RepositoryInterface; use crate::repository::RepositorySet; use crate::repository::RepositoryUtils; -use anyhow::Result; -use shirabe_external_packages::symfony::console::input::InputInterface; -use shirabe_external_packages::symfony::console::output::OutputInterface; -use shirabe_php_shim::{ - InvalidArgumentException, PhpMixed, UnexpectedValueException, array_fill_keys, array_merge, - implode, in_array, -}; #[derive(Debug)] pub struct AuditCommand { @@ -26,31 +35,91 @@ pub struct AuditCommand { } impl AuditCommand { - pub fn configure(&mut self) { - self - .set_name("audit") - .set_description("Checks for security vulnerability advisories for installed packages") - .set_definition(&[ - InputOption::new("no-dev", None, Some(InputOption::VALUE_NONE), "Disables auditing of require-dev packages.", None).unwrap().into(), - InputOption::new("format", Some(PhpMixed::String("f".to_string())), Some(InputOption::VALUE_REQUIRED), "Output format. Must be \"table\", \"plain\", \"json\", or \"summary\".", Some(PhpMixed::String(Auditor::FORMAT_TABLE.to_string()))).unwrap().into(), - InputOption::new("locked", None, Some(InputOption::VALUE_NONE), "Audit based on the lock file instead of the installed packages.", None).unwrap().into(), - InputOption::new("abandoned", None, Some(InputOption::VALUE_REQUIRED), "Behavior on abandoned packages. Must be \"ignore\", \"report\", or \"fail\".", None).unwrap().into(), - InputOption::new("ignore-severity", None, Some(InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED), "Ignore advisories of a certain severity level.", Some(PhpMixed::Array(indexmap::IndexMap::new()))).unwrap().into(), - InputOption::new("ignore-unreachable", None, Some(InputOption::VALUE_NONE), "Ignore repositories that are unreachable or return a non-200 status code.", None).unwrap().into(), - ]) - .set_help( - "The <info>audit</info> command checks for security vulnerability advisories for installed packages.\n\n\ - If you do not want to include dev dependencies in the audit you can omit them with --no-dev\n\n\ - If you want to ignore repositories that are unreachable or return a non-200 status code, use --ignore-unreachable\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#audit" - ); + pub fn new() -> Self { + let mut command = AuditCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("AuditCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for AuditCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("audit")?; + self.set_description("Checks for security vulnerability advisories for installed packages"); + self.set_definition(&[ + InputOption::new( + "no-dev", + None, + Some(InputOption::VALUE_NONE), + "Disables auditing of require-dev packages.", + None, + ) + .unwrap() + .into(), + InputOption::new( + "format", + Some(PhpMixed::String("f".to_string())), + Some(InputOption::VALUE_REQUIRED), + "Output format. Must be \"table\", \"plain\", \"json\", or \"summary\".", + Some(PhpMixed::String(Auditor::FORMAT_TABLE.to_string())), + ) + .unwrap() + .into(), + InputOption::new( + "locked", + None, + Some(InputOption::VALUE_NONE), + "Audit based on the lock file instead of the installed packages.", + None, + ) + .unwrap() + .into(), + InputOption::new( + "abandoned", + None, + Some(InputOption::VALUE_REQUIRED), + "Behavior on abandoned packages. Must be \"ignore\", \"report\", or \"fail\".", + None, + ) + .unwrap() + .into(), + InputOption::new( + "ignore-severity", + None, + Some(InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED), + "Ignore advisories of a certain severity level.", + Some(PhpMixed::Array(indexmap::IndexMap::new())), + ) + .unwrap() + .into(), + InputOption::new( + "ignore-unreachable", + None, + Some(InputOption::VALUE_NONE), + "Ignore repositories that are unreachable or return a non-200 status code.", + None, + ) + .unwrap() + .into(), + ]); + self.set_help( + "The <info>audit</info> command checks for security vulnerability advisories for installed packages.\n\n\ + If you do not want to include dev dependencies in the audit you can omit them with --no-dev\n\n\ + If you want to ignore repositories that are unreachable or return a non-200 status code, use --ignore-unreachable\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#audit" + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let composer = self.require_composer(None, None)?; let packages = self.get_packages(&composer, input.clone())?; @@ -150,6 +219,28 @@ impl AuditCommand { .min(255)) } + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for AuditCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); +} + +impl AuditCommand { fn get_packages( &self, composer: &PartialComposerHandle, @@ -188,13 +279,3 @@ impl AuditCommand { todo!("audit get_packages non-locked branch needs installed-repo conversion") } } - -impl HasBaseCommandData for AuditCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data - } - - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } -} diff --git a/crates/shirabe/src/command/base_command.rs b/crates/shirabe/src/command/base_command.rs index af06ad9..24c1bbf 100644 --- a/crates/shirabe/src/command/base_command.rs +++ b/crates/shirabe/src/command/base_command.rs @@ -4,21 +4,22 @@ use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::symfony::console::Terminal; +use shirabe_external_packages::symfony::console::command::command::{ + Command, CommandData, SetDefinitionArg, +}; use shirabe_external_packages::symfony::console::helper::Table; use shirabe_external_packages::symfony::console::helper::TableSeparator; -use shirabe_external_packages::symfony::console::input::InputDefinition; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::OutputInterface; use shirabe_php_shim::{ - InvalidArgumentException, LogicException, PhpMixed, RuntimeException, UnexpectedValueException, - count, explode, in_array, is_string, max, + AsAny, InvalidArgumentException, LogicException, PhpMixed, RuntimeException, + UnexpectedValueException, count, explode, in_array, is_string, max, }; use std::cell::RefCell; use std::rc::Rc; use crate::advisory::AuditConfig; use crate::advisory::Auditor; -use crate::command::SelfUpdateCommand; use crate::composer::PartialComposerHandle; use crate::config::Config; use crate::console::Application; @@ -40,123 +41,100 @@ pub const SUCCESS: i64 = 0; pub const FAILURE: i64 = 1; pub const INVALID: i64 = 2; -/// \Composer\Composer\Command\BaseCommand + \Symfony\Component\Console\Command\Command -pub trait BaseCommand { - fn new(_name: Option<&str>) -> Self - where - Self: Sized, - { - todo!() - } - - fn get_name(&self) -> Option<String> { - todo!() - } - - fn set_name(&mut self, _name: &str) -> &mut Self - where - Self: Sized, - { - todo!() - } +/// The base-class state shared by all Composer commands: the embedded Symfony command +/// state plus the lazily-resolved Composer instance and IO. +#[derive(Debug)] +pub struct BaseCommandData { + inner: CommandData, + pub(crate) composer: Option<PartialComposerHandle>, + pub(crate) io: Option<Rc<RefCell<dyn IOInterface>>>, +} - fn get_description(&self) -> String { - todo!() +impl BaseCommandData { + pub fn new(name: Option<String>) -> Self { + BaseCommandData { + inner: CommandData::new(name), + composer: None, + io: None, + } } - fn set_description(&mut self, _description: &str) -> &mut Self - where - Self: Sized, - { - todo!() + /// Mutable access to the embedded Symfony command state, used by the Composer-typed definition + /// builders to forward to `CommandData`'s Symfony-typed entry points. + pub(crate) fn command_data_mut(&mut self) -> &mut CommandData { + &mut self.inner } +} - fn set_help(&mut self, _help: &str) -> &mut Self - where - Self: Sized, - { - todo!() - } +/// \Composer\Composer\Command\BaseCommand — the Composer additions on top of the Symfony +/// `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 { + /// Mutable access to the embedded Symfony command state. Each command returns + /// `self.base_command_data.command_data_mut()`; this lets the Composer-typed definition + /// builders below forward to `CommandData`'s Symfony-typed entry points. + fn command_data_mut(&mut self) -> &mut CommandData; - fn set_definition(&mut self, _definition: &[InputDefinitionItem]) -> &mut Self + /// Sets the definition from Composer-typed argument/option instances. + fn set_definition(&mut self, definition: &[InputDefinitionItem]) -> &mut Self where Self: Sized, { - todo!() - } - - fn get_definition(&self) -> &InputDefinition { - todo!() + let items = definition + .iter() + .map(|item| item.to_definition_item()) + .collect(); + self.command_data_mut() + .set_definition(SetDefinitionArg::Array(items)); + self } fn add_argument( &mut self, - _name: &str, - _mode: Option<i64>, - _description: &str, - _default: PhpMixed, + name: &str, + mode: Option<i64>, + description: &str, + default: PhpMixed, ) -> &mut Self where Self: Sized, { - todo!() + self.command_data_mut() + .add_argument(name, mode, description, default) + .expect("command argument definitions in configure() are statically valid"); + self } fn add_option( &mut self, - _name: &str, - _shortcut: Option<&str>, - _mode: Option<i64>, - _description: &str, - _default: PhpMixed, + name: &str, + shortcut: Option<&str>, + mode: Option<i64>, + description: &str, + default: PhpMixed, ) -> &mut Self where Self: Sized, { - todo!() - } - - fn set_aliases(&mut self, _aliases: &[String]) -> &mut Self - where - Self: Sized, - { - todo!() - } - - fn get_aliases(&self) -> Vec<String> { - todo!() - } - - fn set_hidden(&mut self, _hidden: bool) -> &mut Self - where - Self: Sized, - { - todo!() + let shortcut = shortcut + .map(|s| PhpMixed::from(s.to_string())) + .unwrap_or(PhpMixed::Null); + self.command_data_mut() + .add_option(name, shortcut, mode, description, default) + .expect("command option definitions in configure() are statically valid"); + self } - fn is_hidden(&self) -> bool { - todo!() - } - - fn run( - &mut self, - _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<i64> { - todo!() - } - - fn get_helper(&self, _name: &str) -> PhpMixed { - todo!() + /// Whether this command is the self-update command (disables plugins/scripts). + fn is_self_update_command(&self) -> bool { + false } - fn get_helper_set(&self) -> PhpMixed { - todo!() + /// Whether or not this command is meant to call another command. + fn is_proxy_command(&self) -> bool { + false } - /// Gets the application instance for this command. - fn get_application(&self) -> Result<Application>; - /// Retrieves the default Composer\Composer instance or throws fn require_composer( &mut self, @@ -176,27 +154,15 @@ pub trait BaseCommand { /// Removes the cached composer instance fn reset_composer(&mut self) -> Result<()>; - /// Whether or not this command is meant to call another command. - fn is_proxy_command(&self) -> bool; - fn get_io(&mut self) -> Rc<RefCell<dyn IOInterface>>; fn set_io(&mut self, io: Rc<RefCell<dyn IOInterface>>); - // TODO(cli-completion): fn complete(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions); - - /// @inheritDoc - fn initialize( - &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<()>; - /// Calls {@see Factory::create()} with the given arguments, taking into account flags and default states for disabling scripts and plugins fn create_composer_instance( &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + input: Rc<RefCell<dyn InputInterface>>, + io: Rc<RefCell<dyn IOInterface>>, config: Option<IndexMap<String, PhpMixed>>, disable_plugins: bool, disable_scripts: Option<bool>, @@ -206,14 +172,14 @@ pub trait BaseCommand { fn get_preferred_install_options( &self, config: &Config, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + input: Rc<RefCell<dyn InputInterface>>, keep_vcs_requires_prefer_source: bool, ) -> Result<(bool, bool)>; fn get_platform_requirement_filter( &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - ) -> Result<std::rc::Rc<dyn PlatformRequirementFilterInterface>>; + input: Rc<RefCell<dyn InputInterface>>, + ) -> Result<Rc<dyn PlatformRequirementFilterInterface>>; /// @param array<string> $requirements /// @@ -229,11 +195,7 @@ pub trait BaseCommand { ) -> Result<Vec<IndexMap<String, String>>>; /// @param array<TableSeparator|mixed[]> $table - fn render_table( - &self, - table: Vec<PhpMixed>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ); + fn render_table(&self, table: Vec<PhpMixed>, output: Rc<RefCell<dyn OutputInterface>>); fn get_terminal_width(&self) -> i64; @@ -242,7 +204,7 @@ pub trait BaseCommand { /// @return Auditor::FORMAT_* fn get_audit_format( &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + input: Rc<RefCell<dyn InputInterface>>, opt_name: &str, ) -> Result<String>; @@ -250,270 +212,151 @@ pub trait BaseCommand { fn create_audit_config( &self, config: &mut Config, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + input: Rc<RefCell<dyn InputInterface>>, ) -> Result<AuditConfig>; } -#[derive(Debug)] -pub struct BaseCommandData { - pub(crate) composer: Option<PartialComposerHandle>, - pub(crate) io: Option<Rc<RefCell<dyn IOInterface>>>, +/// Forwards every Composer-specific `BaseCommand` method that no command overrides to an +/// embedded field. Each command invokes this once inside its `impl BaseCommand` block, +/// alongside its hand-written `command_data_mut` (and any overridden behavior hooks). The +/// single argument names the field to forward to (always `base_command_data`). +#[macro_export] +macro_rules! delegate_base_command_trait_impls_to_inner { + ($field:ident) => { + shirabe_external_packages::delegate_to_inner!($field, fn require_composer(&mut self, disable_plugins: Option<bool>, disable_scripts: Option<bool>) -> anyhow::Result<$crate::composer::PartialComposerHandle>); + shirabe_external_packages::delegate_to_inner!($field, fn try_composer(&mut self, disable_plugins: Option<bool>, disable_scripts: Option<bool>) -> Option<$crate::composer::PartialComposerHandle>); + shirabe_external_packages::delegate_to_inner!($field, fn set_composer(&mut self, composer: $crate::composer::PartialComposerHandle)); + shirabe_external_packages::delegate_to_inner!($field, fn reset_composer(&mut self) -> anyhow::Result<()>); + shirabe_external_packages::delegate_to_inner!($field, fn get_io(&mut self) -> std::rc::Rc<std::cell::RefCell<dyn $crate::io::IOInterface>>); + shirabe_external_packages::delegate_to_inner!($field, fn set_io(&mut self, io: std::rc::Rc<std::cell::RefCell<dyn $crate::io::IOInterface>>)); + shirabe_external_packages::delegate_to_inner!($field, fn create_composer_instance(&self, input: std::rc::Rc<std::cell::RefCell<dyn shirabe_external_packages::symfony::console::input::InputInterface>>, io: std::rc::Rc<std::cell::RefCell<dyn $crate::io::IOInterface>>, config: Option<indexmap::IndexMap<String, shirabe_php_shim::PhpMixed>>, disable_plugins: bool, disable_scripts: Option<bool>) -> anyhow::Result<$crate::composer::PartialComposerHandle>); + shirabe_external_packages::delegate_to_inner!($field, fn get_preferred_install_options(&self, config: &$crate::config::Config, input: std::rc::Rc<std::cell::RefCell<dyn shirabe_external_packages::symfony::console::input::InputInterface>>, keep_vcs_requires_prefer_source: bool) -> anyhow::Result<(bool, bool)>); + shirabe_external_packages::delegate_to_inner!($field, fn get_platform_requirement_filter(&self, input: std::rc::Rc<std::cell::RefCell<dyn shirabe_external_packages::symfony::console::input::InputInterface>>) -> anyhow::Result<std::rc::Rc<dyn $crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface>>); + shirabe_external_packages::delegate_to_inner!($field, fn format_requirements(&self, requirements: Vec<String>) -> anyhow::Result<indexmap::IndexMap<String, String>>); + shirabe_external_packages::delegate_to_inner!($field, fn normalize_requirements(&self, requirements: Vec<String>) -> anyhow::Result<Vec<indexmap::IndexMap<String, String>>>); + shirabe_external_packages::delegate_to_inner!($field, fn render_table(&self, table: Vec<shirabe_php_shim::PhpMixed>, output: std::rc::Rc<std::cell::RefCell<dyn shirabe_external_packages::symfony::console::output::OutputInterface>>)); + shirabe_external_packages::delegate_to_inner!($field, fn get_terminal_width(&self) -> i64); + shirabe_external_packages::delegate_to_inner!($field, fn get_audit_format(&self, input: std::rc::Rc<std::cell::RefCell<dyn shirabe_external_packages::symfony::console::input::InputInterface>>, opt_name: &str) -> anyhow::Result<String>); + shirabe_external_packages::delegate_to_inner!($field, fn create_audit_config(&self, config: &mut $crate::config::Config, input: std::rc::Rc<std::cell::RefCell<dyn shirabe_external_packages::symfony::console::input::InputInterface>>) -> anyhow::Result<$crate::advisory::AuditConfig>); + }; } -pub trait HasBaseCommandData { - fn base_command_data(&self) -> &BaseCommandData; - fn base_command_data_mut(&mut self) -> &mut BaseCommandData; - - fn composer(&self) -> Option<PartialComposerHandle> { - self.base_command_data().composer.clone() - } - - fn composer_mut(&mut self) -> &mut Option<PartialComposerHandle> { - &mut self.base_command_data_mut().composer - } - - fn io(&self) -> Option<Rc<RefCell<dyn IOInterface>>> { - self.base_command_data().io.clone() - } - - fn io_mut(&mut self) -> &mut Option<Rc<RefCell<dyn IOInterface>>> { - &mut self.base_command_data_mut().io - } - - fn is_self_update_command(&self) -> bool { - false - } +impl Command for BaseCommandData { + shirabe_external_packages::delegate_command_trait_impls_to_inner!(inner); } -impl<C: HasBaseCommandData> BaseCommand for C { - fn get_application(&self) -> Result<Application> { - // PHP: parent::getApplication() returns Symfony Command::$application, the back-reference - // set by Application::add() -> $command->setApplication($this) when the command is - // registered. - // TODO(phase-c): the command holds no Application back-reference. Establishing it requires - // (1) modelling the Symfony command registry (add/find/run), which is an intentional - // todo!() stub, and (2) making Application shared (Rc<RefCell<Application>>) with a Weak - // back-edge to break the Application<->command cycle. Until command registration runs, - // there is no application to return. - todo!() +impl BaseCommand for BaseCommandData { + fn command_data_mut(&mut self) -> &mut CommandData { + &mut self.inner } fn require_composer( &mut self, - _disable_plugins: Option<bool>, - _disable_scripts: Option<bool>, + disable_plugins: Option<bool>, + disable_scripts: Option<bool>, ) -> Result<PartialComposerHandle> { - // PHP: $this->composer ??= $this->getApplication()->getComposer(true, ...). - // TODO(phase-c): blocked on get_application() (see above) — it currently returns an owned - // `Application` by value, which a command cannot hold; a shared Application handle is the - // prerequisite. Application::getComposer itself is already implemented. - let _ = RuntimeException { - message: String::new(), - code: 0, - }; - todo!("require_composer pending get_application() shared-handle support") + if self.composer.is_none() { + let application = self.get_application(); + let Some(application) = application else { + return Err(RuntimeException { + message: "Could not create a Composer\\Composer instance, you must inject one if this command is not used with a Composer\\Console\\Application instance".to_string(), + code: 0, + } + .into()); + }; + let composer = { + let mut app_ref = application.borrow_mut(); + let app_dyn: &mut dyn shirabe_external_packages::symfony::console::application::Application = &mut *app_ref; + let app = app_dyn + .as_any_mut() + .downcast_mut::<Application>() + .expect("a Composer command's application is a shirabe Application"); + app.get_composer(true, disable_plugins, disable_scripts)? + }; + self.composer = composer; + } + + Ok(self + .composer + .clone() + .expect("requireComposer always yields a Composer or errors")) } fn try_composer( &mut self, - _disable_plugins: Option<bool>, - _disable_scripts: Option<bool>, + disable_plugins: Option<bool>, + disable_scripts: Option<bool>, ) -> Option<PartialComposerHandle> { - // PHP: try { return $this->requireComposer(...); } catch (\RuntimeException $e) { return null; } - // TODO(phase-c): blocked on get_application() / require_composer (see above) — needs a - // shared Application handle the command can reach. - todo!("try_composer pending get_application() shared-handle support") + if self.composer.is_none() { + if let Some(application) = self.get_application() { + let result = { + let mut app_ref = application.borrow_mut(); + let app_dyn: &mut dyn shirabe_external_packages::symfony::console::application::Application = &mut *app_ref; + let app = app_dyn + .as_any_mut() + .downcast_mut::<Application>() + .expect("a Composer command's application is a shirabe Application"); + app.get_composer(false, disable_plugins, disable_scripts) + }; + if let Ok(composer) = result { + self.composer = composer; + } + } + } + + self.composer.clone() } fn set_composer(&mut self, composer: PartialComposerHandle) { - *self.composer_mut() = Some(composer); + self.composer = Some(composer); } fn reset_composer(&mut self) -> Result<()> { - *self.composer_mut() = None; - self.get_application()?.reset_composer(); + self.composer = None; + if let Some(application) = self.get_application() { + let mut app_ref = application.borrow_mut(); + let app_dyn: &mut dyn shirabe_external_packages::symfony::console::application::Application = &mut *app_ref; + let app = app_dyn + .as_any_mut() + .downcast_mut::<Application>() + .expect("a Composer command's application is a shirabe Application"); + app.reset_composer(); + } Ok(()) } - fn is_proxy_command(&self) -> bool { - false - } - fn get_io(&mut self) -> Rc<RefCell<dyn IOInterface>> { - if self.io().is_none() { - // PHP: $this->io = $this->getApplication() instanceof Application - // ? $this->getApplication()->getIO() : new NullIO(); - // TODO(phase-c): the application-IO branch needs get_application() (see above); until - // a shared Application handle exists we take the NullIO fallback (PHP's else branch). - *self.io_mut() = Some(Rc::new(RefCell::new(NullIO::new()))); - } - - self.io().unwrap() - } - - fn set_io(&mut self, io: Rc<RefCell<dyn IOInterface>>) { - *self.io_mut() = Some(io); - } - - // TODO(cli-completion): fn complete(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) - - fn initialize( - &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<()> { - // initialize a plugin-enabled Composer instance, either local or global - // PHP also ORs in $this->getApplication()->getDisablePluginsByDefault() / - // getDisableScriptsByDefault(). - // TODO(phase-c): the application-default OR-terms need get_application() (see above); a - // shared Application handle is the prerequisite, so only the input flags are honoured here. - let mut disable_plugins = input - .borrow() - .has_parameter_option(PhpMixed::from(vec!["--no-plugins"]), false); - let mut disable_scripts = input - .borrow() - .has_parameter_option(PhpMixed::from(vec!["--no-scripts"]), false); - - if self.is_self_update_command() { - disable_plugins = true; - disable_scripts = true; - } - - let composer = self.try_composer(Some(disable_plugins), Some(disable_scripts)); - let io = self.get_io(); - - let disable_plugins_kind = if disable_plugins { - crate::factory::DisablePlugins::All - } else { - crate::factory::DisablePlugins::None - }; - let composer = if composer.is_none() { - Factory::create_global(io.clone(), disable_plugins_kind, disable_scripts) - } else { - composer - }; - if let Some(composer) = composer.as_ref() { - // PHP: $this->getName() — the Symfony Command's configured name. - // TODO(phase-c): the command name lives in Symfony Command state (set via configure() - // -> setName()), which this port does not yet carry on BaseCommandData; it requires - // the Symfony Command base-class model (intentional todo!() stub). - let command_name: String = todo!(); - let mut pre_command_run_event = PreCommandRunEvent::new( - PluginEvents::PRE_COMMAND_RUN.to_string(), - input, - command_name, - ); - let pre_command_run_event_name = pre_command_run_event.get_name().to_string(); - let dispatcher = composer.borrow_partial().get_event_dispatcher(); - dispatcher.borrow_mut().dispatch( - Some(&pre_command_run_event_name), - Some(&mut pre_command_run_event), - )?; - } - - if input - .borrow() - .has_parameter_option(PhpMixed::from(vec!["--no-ansi"]), false) - && input.borrow().has_option("no-progress") - { - input - .borrow_mut() - .set_option("no-progress", PhpMixed::Bool(true)); - } - - let env_options: IndexMap<&str, Vec<&str>> = [ - ("COMPOSER_NO_AUDIT", vec!["no-audit"]), - ("COMPOSER_NO_DEV", vec!["no-dev", "update-no-dev"]), - ("COMPOSER_PREFER_STABLE", vec!["prefer-stable"]), - ("COMPOSER_PREFER_LOWEST", vec!["prefer-lowest"]), - ("COMPOSER_MINIMAL_CHANGES", vec!["minimal-changes"]), - ("COMPOSER_WITH_DEPENDENCIES", vec!["with-dependencies"]), - ( - "COMPOSER_WITH_ALL_DEPENDENCIES", - vec!["with-all-dependencies"], - ), - ( - "COMPOSER_NO_SECURITY_BLOCKING", - vec!["no-security-blocking"], - ), - ] - .into_iter() - .collect(); - for (env_name, option_names) in &env_options { - for option_name in option_names { - if true == input.borrow().has_option(option_name) { - if false - == input - .borrow() - .get_option(option_name)? - .as_bool() - .unwrap_or(false) - && Platform::get_env(env_name).map_or(false, |s| !s.is_empty() && s != "0") - { - input - .borrow_mut() - .set_option(option_name, PhpMixed::Bool(true)); - } + if self.io.is_none() { + match self.get_application() { + Some(application) => { + let io = { + let app_ref = application.borrow(); + let app_dyn: &dyn shirabe_external_packages::symfony::console::application::Application = &*app_ref; + let app = app_dyn + .as_any() + .downcast_ref::<Application>() + .expect("a Composer command's application is a shirabe Application"); + app.get_io() + }; + self.io = Some(io); + } + None => { + self.io = Some(Rc::new(RefCell::new(NullIO::new()))); } } } - if true == input.borrow().has_option("ignore-platform-reqs") { - if !input - .borrow() - .get_option("ignore-platform-reqs")? - .as_bool() - .unwrap_or(false) - && Platform::get_env("COMPOSER_IGNORE_PLATFORM_REQS") - .map_or(false, |s| !s.is_empty() && s != "0") - { - input - .borrow_mut() - .set_option("ignore-platform-reqs", PhpMixed::Bool(true)); - - io.write_error("<warning>COMPOSER_IGNORE_PLATFORM_REQS is set. You may experience unexpected errors.</warning>"); - } - } - - if true == input.borrow().has_option("ignore-platform-req") - && (!input.borrow().has_option("ignore-platform-reqs") - || !input - .borrow() - .get_option("ignore-platform-reqs")? - .as_bool() - .unwrap_or(false)) - { - let ignore_platform_req_env = Platform::get_env("COMPOSER_IGNORE_PLATFORM_REQ"); - let ignore_str = ignore_platform_req_env.clone().unwrap_or_default(); - if 0 == count(&input.borrow().get_option("ignore-platform-req")?) - && ignore_platform_req_env.is_some() - && "" != ignore_str - { - input.borrow_mut().set_option( - "ignore-platform-req", - PhpMixed::List( - explode(",", &ignore_str) - .into_iter() - .map(|s| Box::new(PhpMixed::String(s))) - .collect(), - ), - ); - - io.write_error(&format!( - "<warning>COMPOSER_IGNORE_PLATFORM_REQ is set to ignore {}. You may experience unexpected errors.</warning>", - ignore_str - )); - } - } + self.io.clone().unwrap() + } - // TODO(phase-b): requires inner Symfony Command initialize - Ok(()) + fn set_io(&mut self, io: Rc<RefCell<dyn IOInterface>>) { + self.io = Some(io); } fn create_composer_instance( &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + input: Rc<RefCell<dyn InputInterface>>, + io: Rc<RefCell<dyn IOInterface>>, config: Option<IndexMap<String, PhpMixed>>, disable_plugins: bool, disable_scripts: Option<bool>, @@ -529,8 +372,8 @@ impl<C: HasBaseCommandData> BaseCommand for C { // PHP: if ($app instanceof Application && $app->getDisablePluginsByDefault()) $disablePlugins = true; // (same for getDisableScriptsByDefault()). - // TODO(phase-c): these application-default overrides need get_application() (see above) — - // a shared Application handle is the prerequisite, so only the passed/flag values apply. + // TODO(phase-c): these application-default overrides need a shared Application handle + // (deferred), so only the passed/flag values apply. let disable_plugins_kind = if disable_plugins { crate::factory::DisablePlugins::All } else { @@ -543,7 +386,7 @@ impl<C: HasBaseCommandData> BaseCommand for C { fn get_preferred_install_options( &self, config: &Config, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + input: Rc<RefCell<dyn InputInterface>>, keep_vcs_requires_prefer_source: bool, ) -> Result<(bool, bool)> { let mut prefer_source = false; @@ -666,8 +509,8 @@ impl<C: HasBaseCommandData> BaseCommand for C { fn get_platform_requirement_filter( &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - ) -> Result<std::rc::Rc<dyn PlatformRequirementFilterInterface>> { + input: Rc<RefCell<dyn InputInterface>>, + ) -> Result<Rc<dyn PlatformRequirementFilterInterface>> { if !input.borrow().has_option("ignore-platform-reqs") || !input.borrow().has_option("ignore-platform-req") { @@ -733,11 +576,7 @@ impl<C: HasBaseCommandData> BaseCommand for C { parser.parse_name_version_pairs(requirements) } - fn render_table( - &self, - table: Vec<PhpMixed>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) { + fn render_table(&self, table: Vec<PhpMixed>, output: Rc<RefCell<dyn OutputInterface>>) { let mut renderer = Table::new(output); renderer .set_style(PhpMixed::from("compact")) @@ -762,7 +601,7 @@ impl<C: HasBaseCommandData> BaseCommand for C { fn get_audit_format( &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + input: Rc<RefCell<dyn InputInterface>>, opt_name: &str, ) -> Result<String> { if !input.borrow().has_option(opt_name) { @@ -799,7 +638,7 @@ impl<C: HasBaseCommandData> BaseCommand for C { fn create_audit_config( &self, config: &mut Config, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + input: Rc<RefCell<dyn InputInterface>>, ) -> Result<AuditConfig> { // Handle both --audit and --no-audit flags let audit = if input.borrow().has_option("audit") { @@ -854,8 +693,154 @@ impl<C: HasBaseCommandData> BaseCommand for C { } } -// TODO(phase-c): bridge BaseCommand to Symfony Command for trait-object container usage. -// Cannot blanket-impl a foreign trait for a local generic (orphan rule); each concrete -// command must impl symfony Command itself, or a wrapper type must be introduced. This is the -// same orphan-rule blocker as Application::get_default_commands and is gated on the Symfony -// command-registry model, which is an intentional external-package todo!() stub. +/// \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 +/// future overrides) bind correctly. +pub fn base_command_initialize( + cmd: &mut dyn BaseCommand, + input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, +) -> Result<()> { + // initialize a plugin-enabled Composer instance, either local or global + // PHP also ORs in $this->getApplication()->getDisablePluginsByDefault() / + // getDisableScriptsByDefault(). + // TODO(phase-c): the application-default OR-terms need a shared Application handle + // (deferred), so only the input flags are honoured here. + let mut disable_plugins = input + .borrow() + .has_parameter_option(PhpMixed::from(vec!["--no-plugins"]), false); + let mut disable_scripts = input + .borrow() + .has_parameter_option(PhpMixed::from(vec!["--no-scripts"]), false); + + if cmd.is_self_update_command() { + disable_plugins = true; + disable_scripts = true; + } + + let composer = cmd.try_composer(Some(disable_plugins), Some(disable_scripts)); + let io = cmd.get_io(); + + let disable_plugins_kind = if disable_plugins { + crate::factory::DisablePlugins::All + } else { + crate::factory::DisablePlugins::None + }; + let composer = if composer.is_none() { + Factory::create_global(io.clone(), disable_plugins_kind, disable_scripts) + } else { + composer + }; + if let Some(composer) = composer.as_ref() { + let command_name = cmd.get_name().unwrap_or_default(); + let mut pre_command_run_event = PreCommandRunEvent::new( + PluginEvents::PRE_COMMAND_RUN.to_string(), + input.clone(), + command_name, + ); + let pre_command_run_event_name = pre_command_run_event.get_name().to_string(); + let dispatcher = composer.borrow_partial().get_event_dispatcher(); + dispatcher.borrow_mut().dispatch( + Some(&pre_command_run_event_name), + Some(&mut pre_command_run_event), + )?; + } + + if input + .borrow() + .has_parameter_option(PhpMixed::from(vec!["--no-ansi"]), false) + && input.borrow().has_option("no-progress") + { + input + .borrow_mut() + .set_option("no-progress", PhpMixed::Bool(true)); + } + + let env_options: IndexMap<&str, Vec<&str>> = [ + ("COMPOSER_NO_AUDIT", vec!["no-audit"]), + ("COMPOSER_NO_DEV", vec!["no-dev", "update-no-dev"]), + ("COMPOSER_PREFER_STABLE", vec!["prefer-stable"]), + ("COMPOSER_PREFER_LOWEST", vec!["prefer-lowest"]), + ("COMPOSER_MINIMAL_CHANGES", vec!["minimal-changes"]), + ("COMPOSER_WITH_DEPENDENCIES", vec!["with-dependencies"]), + ( + "COMPOSER_WITH_ALL_DEPENDENCIES", + vec!["with-all-dependencies"], + ), + ( + "COMPOSER_NO_SECURITY_BLOCKING", + vec!["no-security-blocking"], + ), + ] + .into_iter() + .collect(); + for (env_name, option_names) in &env_options { + for option_name in option_names { + if true == input.borrow().has_option(option_name) { + if false + == input + .borrow() + .get_option(option_name)? + .as_bool() + .unwrap_or(false) + && Platform::get_env(env_name).map_or(false, |s| !s.is_empty() && s != "0") + { + input + .borrow_mut() + .set_option(option_name, PhpMixed::Bool(true)); + } + } + } + } + + if true == input.borrow().has_option("ignore-platform-reqs") { + if !input + .borrow() + .get_option("ignore-platform-reqs")? + .as_bool() + .unwrap_or(false) + && Platform::get_env("COMPOSER_IGNORE_PLATFORM_REQS") + .map_or(false, |s| !s.is_empty() && s != "0") + { + input + .borrow_mut() + .set_option("ignore-platform-reqs", PhpMixed::Bool(true)); + + io.write_error("<warning>COMPOSER_IGNORE_PLATFORM_REQS is set. You may experience unexpected errors.</warning>"); + } + } + + if true == input.borrow().has_option("ignore-platform-req") + && (!input.borrow().has_option("ignore-platform-reqs") + || !input + .borrow() + .get_option("ignore-platform-reqs")? + .as_bool() + .unwrap_or(false)) + { + let ignore_platform_req_env = Platform::get_env("COMPOSER_IGNORE_PLATFORM_REQ"); + let ignore_str = ignore_platform_req_env.clone().unwrap_or_default(); + if 0 == count(&input.borrow().get_option("ignore-platform-req")?) + && ignore_platform_req_env.is_some() + && "" != ignore_str + { + input.borrow_mut().set_option( + "ignore-platform-req", + PhpMixed::List( + explode(",", &ignore_str) + .into_iter() + .map(|s| Box::new(PhpMixed::String(s))) + .collect(), + ), + ); + + io.write_error(&format!( + "<warning>COMPOSER_IGNORE_PLATFORM_REQ is set to ignore {}. You may experience unexpected errors.</warning>", + ignore_str + )); + } + } + + Ok(()) +} diff --git a/crates/shirabe/src/command/base_config_command.rs b/crates/shirabe/src/command/base_config_command.rs index 4f6fd59..5e77715 100644 --- a/crates/shirabe/src/command/base_config_command.rs +++ b/crates/shirabe/src/command/base_config_command.rs @@ -1,6 +1,6 @@ //! ref: composer/src/Composer/Command/BaseConfigCommand.php -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::{BaseCommand, BaseCommandData}; use crate::config::Config; use crate::config::JsonConfigSource; use crate::factory::Factory; diff --git a/crates/shirabe/src/command/base_dependency_command.rs b/crates/shirabe/src/command/base_dependency_command.rs index 1ca8a76..5424dd1 100644 --- a/crates/shirabe/src/command/base_dependency_command.rs +++ b/crates/shirabe/src/command/base_dependency_command.rs @@ -9,7 +9,7 @@ use shirabe_php_shim::{InvalidArgumentException, PhpMixed, UnexpectedValueExcept use shirabe_semver::constraint::AnyConstraint; use shirabe_semver::constraint::Bound; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::{BaseCommand, BaseCommandData}; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::package::CompletePackageInterface; diff --git a/crates/shirabe/src/command/bump_command.rs b/crates/shirabe/src/command/bump_command.rs index 56548a9..faa1ea7 100644 --- a/crates/shirabe/src/command/bump_command.rs +++ b/crates/shirabe/src/command/bump_command.rs @@ -3,15 +3,24 @@ use crate::io::io_interface; use crate::package::base_package; use anyhow::Result; +use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; +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_shim::{PhpMixed, file_get_contents, file_put_contents, is_writable, strtolower}; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; use crate::factory::Factory; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::json::JsonFile; @@ -32,69 +41,14 @@ impl BumpCommand { const ERROR_GENERIC: i64 = 1; const ERROR_LOCK_OUTDATED: i64 = 2; - pub fn configure(&mut self) { - // TODO(cli-completion): suggest_root_requirement() for `packages` argument - self - .set_name("bump") - .set_description("Increases the lower limit of your composer.json requirements to the currently installed versions") - .set_definition (&[ - InputArgument::new("packages",Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL),"Optional package name(s) to restrict which packages are bumped.",None,).unwrap().into(),InputOption::new("dev-only", Some(PhpMixed::String("D".to_string())), Some(InputOption::VALUE_NONE), "Only bump requirements in \"require-dev\".", None).unwrap().into(), - InputOption::new("no-dev-only", Some(PhpMixed::String("R".to_string())), Some(InputOption::VALUE_NONE), "Only bump requirements in \"require\".", None).unwrap().into(), - InputOption::new("dry-run", None, Some(InputOption::VALUE_NONE), "Outputs the packages to bump, but will not execute anything.", None).unwrap().into(), - ]) - .set_help( - "The <info>bump</info> command increases the lower limit of your composer.json requirements\n\ - to the currently installed versions. This helps to ensure your dependencies do not\n\ - accidentally get downgraded due to some other conflict, and can slightly improve\n\ - dependency resolution performance as it limits the amount of package versions\n\ - Composer has to look at.\n\n\ - Running this blindly on libraries is **NOT** recommended as it will narrow down\n\ - your allowed dependencies, which may cause dependency hell for your users.\n\ - Running it with <info>--dev-only</info> on libraries may be fine however as dev requirements\n\ - are local to the library and do not affect consumers of the package.\n" - ); - } - - pub fn execute( - &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { - let packages_filter: Vec<String> = input - .borrow() - .get_argument("packages")? - .as_list() - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - - let dev_only = input - .borrow() - .get_option("dev-only")? - .as_bool() - .unwrap_or(false); - let no_dev_only = input - .borrow() - .get_option("no-dev-only")? - .as_bool() - .unwrap_or(false); - let dry_run = input - .borrow() - .get_option("dry-run")? - .as_bool() - .unwrap_or(false); - let io = self.get_io().clone(); - self.do_bump( - io, - dev_only, - no_dev_only, - dry_run, - packages_filter, - "--dev-only".to_string(), - ) + pub fn new() -> Self { + let mut command = BumpCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("BumpCommand::configure uses static, valid metadata"); + command } pub fn do_bump( @@ -379,12 +333,121 @@ impl BumpCommand { } } -impl HasBaseCommandData for BumpCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data +impl Command for BumpCommand { + fn configure(&mut self) -> anyhow::Result<()> { + // TODO(cli-completion): suggest_root_requirement() for `packages` argument + self.set_name("bump")?; + self.set_description("Increases the lower limit of your composer.json requirements to the currently installed versions"); + self.set_definition(&[ + InputArgument::new( + "packages", + Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), + "Optional package name(s) to restrict which packages are bumped.", + None, + ) + .unwrap() + .into(), + InputOption::new( + "dev-only", + Some(PhpMixed::String("D".to_string())), + Some(InputOption::VALUE_NONE), + "Only bump requirements in \"require-dev\".", + None, + ) + .unwrap() + .into(), + InputOption::new( + "no-dev-only", + Some(PhpMixed::String("R".to_string())), + Some(InputOption::VALUE_NONE), + "Only bump requirements in \"require\".", + None, + ) + .unwrap() + .into(), + InputOption::new( + "dry-run", + None, + Some(InputOption::VALUE_NONE), + "Outputs the packages to bump, but will not execute anything.", + None, + ) + .unwrap() + .into(), + ]); + self.set_help( + "The <info>bump</info> command increases the lower limit of your composer.json requirements\n\ + to the currently installed versions. This helps to ensure your dependencies do not\n\ + accidentally get downgraded due to some other conflict, and can slightly improve\n\ + dependency resolution performance as it limits the amount of package versions\n\ + Composer has to look at.\n\n\ + Running this blindly on libraries is **NOT** recommended as it will narrow down\n\ + your allowed dependencies, which may cause dependency hell for your users.\n\ + Running it with <info>--dev-only</info> on libraries may be fine however as dev requirements\n\ + are local to the library and do not affect consumers of the package.\n" + ); + Ok(()) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + fn execute( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + let packages_filter: Vec<String> = input + .borrow() + .get_argument("packages")? + .as_list() + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + + let dev_only = input + .borrow() + .get_option("dev-only")? + .as_bool() + .unwrap_or(false); + let no_dev_only = input + .borrow() + .get_option("no-dev-only")? + .as_bool() + .unwrap_or(false); + let dry_run = input + .borrow() + .get_option("dry-run")? + .as_bool() + .unwrap_or(false); + let io = self.get_io().clone(); + self.do_bump( + io, + dev_only, + no_dev_only, + dry_run, + packages_filter, + "--dev-only".to_string(), + ) + } + + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for BumpCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/check_platform_reqs_command.rs b/crates/shirabe/src/command/check_platform_reqs_command.rs index 658d049..333c37f 100644 --- a/crates/shirabe/src/command/check_platform_reqs_command.rs +++ b/crates/shirabe/src/command/check_platform_reqs_command.rs @@ -2,14 +2,22 @@ use anyhow::Result; 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_shim::{PhpMixed, array_merge_map, strip_tags}; use shirabe_semver::constraint::AnyConstraint; use shirabe_semver::constraint::SimpleConstraint; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::json::JsonFile; @@ -33,27 +41,160 @@ pub struct CheckPlatformReqsCommand { } impl CheckPlatformReqsCommand { - pub fn configure(&mut self) { - self - .set_name("check-platform-reqs") - .set_description("Check that platform requirements are satisfied") - .set_definition(&[ - InputOption::new("no-dev", None, Some(InputOption::VALUE_NONE), "Disables checking of require-dev packages requirements.", None).unwrap().into(), - InputOption::new("lock", None, Some(InputOption::VALUE_NONE), "Checks requirements only from the lock file, not from installed packages.", None).unwrap().into(), - InputOption::new("format", Some(shirabe_php_shim::PhpMixed::String("f".to_string())), Some(InputOption::VALUE_REQUIRED), "Format of the output: text or json", Some(shirabe_php_shim::PhpMixed::String("text".to_string()))).unwrap().into(), - ]) - .set_help( - "Checks that your PHP and extensions versions match the platform requirements of the installed packages.\n\n\ - Unlike update/install, this command will ignore config.platform settings and check the real platform packages so you can be certain you have the required platform dependencies.\n\n\ - <info>php composer.phar check-platform-reqs</info>\n\n" - ); + pub fn new() -> Self { + let mut command = CheckPlatformReqsCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("CheckPlatformReqsCommand::configure uses static, valid metadata"); + command + } + + fn print_table( + &mut self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + results: &[CheckResult], + format: &str, + ) { + let io = self.get_io(); + + if format == "json" { + let rows: Vec<PhpMixed> = results + .iter() + .map(|result| { + let mut row = IndexMap::new(); + row.insert( + "name".to_string(), + Box::new(PhpMixed::String(result.platform_package.clone())), + ); + row.insert( + "version".to_string(), + Box::new(PhpMixed::String(result.version.clone())), + ); + row.insert( + "status".to_string(), + Box::new(PhpMixed::String(strip_tags(&result.status))), + ); + if let Some(link) = &result.link { + let mut failed_req = IndexMap::new(); + failed_req.insert( + "source".to_string(), + Box::new(PhpMixed::String(link.get_source().to_string())), + ); + failed_req.insert( + "type".to_string(), + Box::new(PhpMixed::String(link.get_description().to_string())), + ); + failed_req.insert( + "target".to_string(), + Box::new(PhpMixed::String(link.get_target().to_string())), + ); + failed_req.insert( + "constraint".to_string(), + Box::new(PhpMixed::String(link.get_pretty_constraint().to_string())), + ); + row.insert( + "failed_requirement".to_string(), + Box::new(PhpMixed::Array(failed_req)), + ); + } else { + row.insert("failed_requirement".to_string(), Box::new(PhpMixed::Null)); + } + let provider_str = strip_tags(&result.provider); + row.insert( + "provider".to_string(), + Box::new(if provider_str.is_empty() { + PhpMixed::Null + } else { + PhpMixed::String(provider_str) + }), + ); + PhpMixed::Array(row) + }) + .collect(); + + io.write(&JsonFile::encode(&PhpMixed::List( + rows.into_iter().map(Box::new).collect(), + ))); + } else { + let rows: Vec<PhpMixed> = results + .iter() + .map(|result| { + PhpMixed::List(vec![ + Box::new(PhpMixed::String(result.platform_package.clone())), + Box::new(PhpMixed::String(result.version.clone())), + Box::new(if let Some(link) = &result.link { + PhpMixed::String(format!( + "{} {} {} ({})", + link.get_source(), + link.get_description(), + link.get_target(), + link.get_pretty_constraint(), + )) + } else { + PhpMixed::String(String::new()) + }), + Box::new(PhpMixed::String( + format!("{} {}", result.status, result.provider) + .trim_end() + .to_string(), + )), + ]) + }) + .collect(); + + self.render_table(rows, output); + } + } +} + +impl Command for CheckPlatformReqsCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("check-platform-reqs")?; + self.set_description("Check that platform requirements are satisfied"); + self.set_definition(&[ + InputOption::new( + "no-dev", + None, + Some(InputOption::VALUE_NONE), + "Disables checking of require-dev packages requirements.", + None, + ) + .unwrap() + .into(), + InputOption::new( + "lock", + None, + Some(InputOption::VALUE_NONE), + "Checks requirements only from the lock file, not from installed packages.", + None, + ) + .unwrap() + .into(), + InputOption::new( + "format", + Some(shirabe_php_shim::PhpMixed::String("f".to_string())), + Some(InputOption::VALUE_REQUIRED), + "Format of the output: text or json", + Some(shirabe_php_shim::PhpMixed::String("text".to_string())), + ) + .unwrap() + .into(), + ]); + self.set_help( + "Checks that your PHP and extensions versions match the platform requirements of the installed packages.\n\n\ + Unlike update/install, this command will ignore config.platform settings and check the real platform packages so you can be certain you have the required platform dependencies.\n\n\ + <info>php composer.phar check-platform-reqs</info>\n\n" + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let composer = self.require_composer(None, None)?; let mut composer = crate::command::composer_full_mut(&composer); let io = self.get_io(); @@ -257,110 +398,23 @@ impl CheckPlatformReqsCommand { Ok(exit_code) } - fn print_table( + fn initialize( &mut self, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - results: &[CheckResult], - format: &str, - ) { - let io = self.get_io(); - - if format == "json" { - let rows: Vec<PhpMixed> = results - .iter() - .map(|result| { - let mut row = IndexMap::new(); - row.insert( - "name".to_string(), - Box::new(PhpMixed::String(result.platform_package.clone())), - ); - row.insert( - "version".to_string(), - Box::new(PhpMixed::String(result.version.clone())), - ); - row.insert( - "status".to_string(), - Box::new(PhpMixed::String(strip_tags(&result.status))), - ); - if let Some(link) = &result.link { - let mut failed_req = IndexMap::new(); - failed_req.insert( - "source".to_string(), - Box::new(PhpMixed::String(link.get_source().to_string())), - ); - failed_req.insert( - "type".to_string(), - Box::new(PhpMixed::String(link.get_description().to_string())), - ); - failed_req.insert( - "target".to_string(), - Box::new(PhpMixed::String(link.get_target().to_string())), - ); - failed_req.insert( - "constraint".to_string(), - Box::new(PhpMixed::String(link.get_pretty_constraint().to_string())), - ); - row.insert( - "failed_requirement".to_string(), - Box::new(PhpMixed::Array(failed_req)), - ); - } else { - row.insert("failed_requirement".to_string(), Box::new(PhpMixed::Null)); - } - let provider_str = strip_tags(&result.provider); - row.insert( - "provider".to_string(), - Box::new(if provider_str.is_empty() { - PhpMixed::Null - } else { - PhpMixed::String(provider_str) - }), - ); - PhpMixed::Array(row) - }) - .collect(); - - io.write(&JsonFile::encode(&PhpMixed::List( - rows.into_iter().map(Box::new).collect(), - ))); - } else { - let rows: Vec<PhpMixed> = results - .iter() - .map(|result| { - PhpMixed::List(vec![ - Box::new(PhpMixed::String(result.platform_package.clone())), - Box::new(PhpMixed::String(result.version.clone())), - Box::new(if let Some(link) = &result.link { - PhpMixed::String(format!( - "{} {} {} ({})", - link.get_source(), - link.get_description(), - link.get_target(), - link.get_pretty_constraint(), - )) - } else { - PhpMixed::String(String::new()) - }), - Box::new(PhpMixed::String( - format!("{} {}", result.status, result.provider) - .trim_end() - .to_string(), - )), - ]) - }) - .collect(); - - self.render_table(rows, output); - } + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); } -impl HasBaseCommandData for CheckPlatformReqsCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data +impl BaseCommand for CheckPlatformReqsCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/clear_cache_command.rs b/crates/shirabe/src/command/clear_cache_command.rs index 5dbfaa5..31ba953 100644 --- a/crates/shirabe/src/command/clear_cache_command.rs +++ b/crates/shirabe/src/command/clear_cache_command.rs @@ -1,13 +1,25 @@ //! ref: composer/src/Composer/Command/ClearCacheCommand.php -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; -use crate::composer; -use crate::composer::ComposerHandle; -use crate::console::input::InputOption; -use crate::factory::Factory; +use anyhow::Result; 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_shim::PhpMixed; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::advisory::AuditConfig; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; +use crate::composer; +use crate::composer::PartialComposerHandle; +use crate::config::Config; +use crate::console::input::InputOption; +use crate::factory::Factory; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; +use crate::io::IOInterface; #[derive(Debug)] pub struct ClearCacheCommand { @@ -15,9 +27,21 @@ pub struct ClearCacheCommand { } impl ClearCacheCommand { - pub fn configure(&mut self) { - self.set_name("clear-cache"); - self.set_aliases(&["clearcache".to_string(), "cc".to_string()]); + pub fn new() -> Self { + let mut command = ClearCacheCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("ClearCacheCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for ClearCacheCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("clear-cache")?; + self.set_aliases(vec!["clearcache".to_string(), "cc".to_string()])?; self.set_description("Clears composer's internal package cache"); self.set_definition(&[InputOption::new( "gc", @@ -33,12 +57,13 @@ impl ClearCacheCommand { cache directory.\n\n\ Read more at https://getcomposer.org/doc/03-cli.md#clear-cache-clearcache-cc", ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + _input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { // PHP: $config = $this->tryComposer()?->getConfig() ?? Factory::createConfig(); then // iterate the cache-* paths and clear/gc each via Cache. @@ -52,14 +77,24 @@ impl ClearCacheCommand { let _ = Factory::create_config(None, None); todo!("ClearCacheCommand::execute pending try_composer + Config sharing model") } -} -impl HasBaseCommandData for ClearCacheCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for ClearCacheCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/config_command.rs b/crates/shirabe/src/command/config_command.rs index e055fee..0243d6a 100644 --- a/crates/shirabe/src/command/config_command.rs +++ b/crates/shirabe/src/command/config_command.rs @@ -1,10 +1,12 @@ //! ref: composer/src/Composer/Command/ConfigCommand.php use crate::io::io_interface; +use anyhow::Result; use indexmap::IndexMap; use crate::console::input::InputOption; use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; +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_shim::{ @@ -14,15 +16,20 @@ use shirabe_php_shim::{ is_bool, is_dir, is_numeric, is_object, is_string, json_encode, key, sort, sprintf, str_replace, str_starts_with, strpos, strtolower, system, touch, var_export, }; +use std::cell::RefCell; +use std::rc::Rc; +use crate::advisory::AuditConfig; use crate::advisory::Auditor; use crate::command::BaseConfigCommand; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::{BaseCommand, BaseCommandData}; +use crate::composer::PartialComposerHandle; use crate::config::Config; use crate::config::ConfigSourceInterface; use crate::config::JsonConfigSource; use crate::console::input::InputArgument; use crate::factory::Factory; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::json::JsonEncodeOptions; @@ -61,12 +68,31 @@ impl ConfigCommand { "suggest", "extra", ]; +} + +impl ConfigCommand { + pub fn new() -> Self { + let mut command = ConfigCommand { + base_command_data: BaseCommandData::new(None), + config: None, + config_file: None, + config_source: None, + auth_config_file: None, + auth_config_source: None, + }; + command + .configure() + .expect("ConfigCommand::configure uses static, valid metadata"); + command + } +} - pub(crate) fn configure(&mut self) { +impl Command for ConfigCommand { + fn configure(&mut self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_setting_keys() for `setting-key` argument - self.set_name("config") - .set_description("Sets config options") - .set_definition(&[ + self.set_name("config")?; + self.set_description("Sets config options"); + self.set_definition(&[ InputOption::new("global", Some(PhpMixed::String("g".to_string())), Some(InputOption::VALUE_NONE), "Apply command to the global config file", None).unwrap().into(), InputOption::new("editor", Some(PhpMixed::String("e".to_string())), Some(InputOption::VALUE_NONE), "Open editor", None).unwrap().into(), InputOption::new("auth", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Affect auth config file (only used for --editor)", None).unwrap().into(), @@ -80,8 +106,8 @@ impl ConfigCommand { InputOption::new("source", None, Some(InputOption::VALUE_NONE), "Display where the config value is loaded from", None).unwrap().into(), InputArgument::new("setting-key", None, "Setting key", None).unwrap().into(), InputArgument::new("setting-value", Some(InputArgument::IS_ARRAY), "Setting value", None).unwrap().into(), - ]) - .set_help( + ]); + self.set_help( "This command allows you to edit composer config settings and repositories\n\ in either the local composer.json file or the global config.json file.\n\n\ Additionally it lets you edit most properties in the local composer.json.\n\n\ @@ -116,14 +142,19 @@ impl ConfigCommand { \t<comment>%command.full_name% --editor --global</comment>\n\n\ Read more at https://getcomposer.org/doc/03-cli.md#config", ); + Ok(()) } - pub(crate) fn initialize( + fn initialize( &mut self, input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, ) -> anyhow::Result<()> { - BaseCommand::initialize(self, input.clone(), output)?; + <Self as crate::command::base_config_command::BaseConfigCommand>::initialize( + self, + input.clone(), + output, + )?; let auth_config_file = self.get_auth_config_file(input.clone(), &*self.config.as_ref().unwrap().borrow())?; @@ -176,7 +207,7 @@ impl ConfigCommand { Ok(()) } - pub(crate) fn execute( + fn execute( &mut self, input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, @@ -1230,6 +1261,20 @@ impl ConfigCommand { .into()) } + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for ConfigCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); +} + +impl ConfigCommand { pub(crate) fn handle_single_value( &mut self, key: &str, @@ -2188,13 +2233,3 @@ impl BaseConfigCommand for ConfigCommand { self.config_source = source; } } - -impl HasBaseCommandData for ConfigCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data - } - - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } -} diff --git a/crates/shirabe/src/command/create_project_command.rs b/crates/shirabe/src/command/create_project_command.rs index 15a64df..b1ef8ec 100644 --- a/crates/shirabe/src/command/create_project_command.rs +++ b/crates/shirabe/src/command/create_project_command.rs @@ -4,6 +4,7 @@ use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; use shirabe_external_packages::seld::signal::SignalHandler; +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_external_packages::symfony::finder::Finder; @@ -12,9 +13,13 @@ use shirabe_php_shim::{ UnexpectedValueException, array_pop, chdir, explode_with_limit, file_exists, getcwd, implode, is_dir, is_file, mkdir, realpath, rtrim, strtolower, unlink, }; +use std::cell::RefCell; +use std::rc::Rc; +use crate::advisory::AuditConfig; use crate::advisory::Auditor; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; use crate::composer::PartialComposerHandle; use crate::config::Config; use crate::config::ConfigSourceInterface; @@ -57,12 +62,24 @@ pub struct CreateProjectCommand { } impl CreateProjectCommand { - fn configure(&mut self) { + pub fn new() -> Self { + let mut command = CreateProjectCommand { + base_command_data: BaseCommandData::new(None), + suggested_packages_reporter: None, + }; + command + .configure() + .expect("CreateProjectCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for CreateProjectCommand { + fn configure(&mut self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_prefer_install / suggest_available_package - self - .set_name("create-project") - .set_description("Creates new project from a package into given directory") - .set_definition(&[ + self.set_name("create-project")?; + self.set_description("Creates new project from a package into given directory"); + self.set_definition(&[ InputArgument::new("package", Some(InputArgument::OPTIONAL), "Package name to be installed", None).unwrap().into(), InputArgument::new("directory", Some(InputArgument::OPTIONAL), "Directory where the files should be created", None).unwrap().into(), InputArgument::new("version", Some(InputArgument::OPTIONAL), "Version, will default to latest", None).unwrap().into(), @@ -88,8 +105,8 @@ impl CreateProjectCommand { InputOption::new("ignore-platform-req", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Ignore a specific platform requirement (php & ext- packages).", None).unwrap().into(), InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages).", None).unwrap().into(), InputOption::new("ask", None, Some(InputOption::VALUE_NONE), "Whether to ask for project directory.", None).unwrap().into(), - ]) - .set_help( + ]); + self.set_help( "The <info>create-project</info> command creates a new project from a given\n\ package into a new directory. If executed without params and in a directory\n\ with a composer.json file it installs the packages for the current project.\n\n\ @@ -106,13 +123,14 @@ impl CreateProjectCommand { can pass the <info>'--repository=https://myrepository.org'</info> flag.\n\n\ Read more at https://getcomposer.org/doc/03-cli.md#create-project" ); + Ok(()) } fn execute( &mut self, input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + ) -> anyhow::Result<i64> { let config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?)); let io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = self.get_io(); @@ -234,6 +252,28 @@ impl CreateProjectCommand { ) } + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for CreateProjectCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); +} + +impl CreateProjectCommand { /// @param string|array<string>|null $repositories /// /// @throws \Exception @@ -996,13 +1036,3 @@ impl CreateProjectCommand { Ok(installed_from_vcs) } } - -impl HasBaseCommandData for CreateProjectCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data - } - - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } -} diff --git a/crates/shirabe/src/command/depends_command.rs b/crates/shirabe/src/command/depends_command.rs index 5c77b51..3b139a9 100644 --- a/crates/shirabe/src/command/depends_command.rs +++ b/crates/shirabe/src/command/depends_command.rs @@ -1,12 +1,25 @@ //! ref: composer/src/Composer/Command/DependsCommand.php +use anyhow::Result; +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_shim::PhpMixed; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::advisory::AuditConfig; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; use crate::command::BaseDependencyCommand; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::base_command::base_command_initialize; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; -use shirabe_external_packages::symfony::console::input::InputInterface; -use shirabe_external_packages::symfony::console::output::OutputInterface; #[derive(Debug)] pub struct DependsCommand { @@ -16,61 +29,15 @@ pub struct DependsCommand { } impl DependsCommand { - pub fn configure(&mut self) { - // TODO(cli-completion): suggest_installed_package(true, true) for `package` argument - self.set_name("depends") - .set_aliases(&["why".to_string()]) - .set_description("Shows which packages cause the given package to be installed") - .set_definition(&[ - InputArgument::new( - crate::command::ARGUMENT_PACKAGE, - Some(InputArgument::REQUIRED), - "Package to inspect", - None, - ) - .unwrap() - .into(), - InputOption::new( - crate::command::OPTION_RECURSIVE, - Some(shirabe_php_shim::PhpMixed::String("r".to_string())), - Some(InputOption::VALUE_NONE), - "Recursively resolves up to the root package", - None, - ) - .unwrap() - .into(), - InputOption::new( - crate::command::OPTION_TREE, - Some(shirabe_php_shim::PhpMixed::String("t".to_string())), - Some(InputOption::VALUE_NONE), - "Prints the results as a nested tree", - None, - ) - .unwrap() - .into(), - InputOption::new( - "locked", - None, - Some(InputOption::VALUE_NONE), - "Read dependency information from composer.lock", - None, - ) - .unwrap() - .into(), - ]) - .set_help( - "Displays detailed information about where a package is referenced.\n\n\ - <info>php composer.phar depends composer/composer</info>\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#depends-why", - ); - } - - pub fn execute( - &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<i64> { - self.do_execute(input, output, false) + pub fn new() -> Self { + let mut command = DependsCommand { + base_command_data: BaseCommandData::new(None), + colors: Vec::new(), + }; + command + .configure() + .expect("DependsCommand::configure uses static, valid metadata"); + command } } @@ -84,12 +51,82 @@ impl BaseDependencyCommand for DependsCommand { } } -impl HasBaseCommandData for DependsCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data +impl Command for DependsCommand { + fn configure(&mut self) -> anyhow::Result<()> { + // TODO(cli-completion): suggest_installed_package(true, true) for `package` argument + self.set_name("depends")?; + self.set_aliases(vec!["why".to_string()])?; + self.set_description("Shows which packages cause the given package to be installed"); + self.set_definition(&[ + InputArgument::new( + crate::command::ARGUMENT_PACKAGE, + Some(InputArgument::REQUIRED), + "Package to inspect", + None, + ) + .unwrap() + .into(), + InputOption::new( + crate::command::OPTION_RECURSIVE, + Some(shirabe_php_shim::PhpMixed::String("r".to_string())), + Some(InputOption::VALUE_NONE), + "Recursively resolves up to the root package", + None, + ) + .unwrap() + .into(), + InputOption::new( + crate::command::OPTION_TREE, + Some(shirabe_php_shim::PhpMixed::String("t".to_string())), + Some(InputOption::VALUE_NONE), + "Prints the results as a nested tree", + None, + ) + .unwrap() + .into(), + InputOption::new( + "locked", + None, + Some(InputOption::VALUE_NONE), + "Read dependency information from composer.lock", + None, + ) + .unwrap() + .into(), + ]); + self.set_help( + "Displays detailed information about where a package is referenced.\n\n\ + <info>php composer.phar depends composer/composer</info>\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#depends-why", + ); + Ok(()) + } + + fn execute( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + self.do_execute(input, output, false) + } + + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for DependsCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/diagnose_command.rs b/crates/shirabe/src/command/diagnose_command.rs index 56dffc1..252208d 100644 --- a/crates/shirabe/src/command/diagnose_command.rs +++ b/crates/shirabe/src/command/diagnose_command.rs @@ -1,9 +1,11 @@ //! ref: composer/src/Composer/Command/DiagnoseCommand.php +use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; use shirabe_external_packages::composer::xdebug_handler::XdebugHandler; +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_external_packages::symfony::process::ExecutableFinder; @@ -17,14 +19,20 @@ use shirabe_php_shim::{ is_array, is_string, key, max_i64, ob_get_clean, ob_start, phpinfo, reset, rtrim, sprintf, str_contains, str_replace, str_starts_with, strpos, strstr, strtolower, trim, version_compare, }; +use std::cell::RefCell; +use std::rc::Rc; +use crate::advisory::AuditConfig; use crate::advisory::Auditor; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; use crate::composer; use crate::composer::ComposerHandle; +use crate::composer::PartialComposerHandle; use crate::config::Config; use crate::downloader::TransportException; use crate::factory::Factory; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::BufferIO; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; @@ -62,18 +70,33 @@ pub struct DiagnoseCommand { } impl DiagnoseCommand { - pub(crate) fn configure(&mut self) { - self - .set_name("diagnose") - .set_description("Diagnoses the system to identify common errors") - .set_help( - "The <info>diagnose</info> command checks common errors to help debugging problems.\n\n\ - The process exit code will be 1 in case of warnings and 2 for errors.\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#diagnose", - ); + pub fn new() -> Self { + let mut command = DiagnoseCommand { + base_command_data: BaseCommandData::new(None), + http_downloader: None, + process: None, + exit_code: 0, + }; + command + .configure() + .expect("DiagnoseCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for DiagnoseCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("diagnose")?; + self.set_description("Diagnoses the system to identify common errors"); + self.set_help( + "The <info>diagnose</info> command checks common errors to help debugging problems.\n\n\ + The process exit code will be 1 in case of warnings and 2 for errors.\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#diagnose", + ); + Ok(()) } - pub(crate) fn execute( + fn execute( &mut self, input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, @@ -424,6 +447,28 @@ impl DiagnoseCommand { Ok(self.exit_code) } + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for DiagnoseCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); +} + +impl DiagnoseCommand { fn check_composer_schema(&mut self) -> anyhow::Result<PhpMixed> { let validator = ConfigValidator::new(self.get_io().clone()); let (errors, _, warnings) = validator.validate(&Factory::get_composer_file()?, 0, 0); @@ -1448,13 +1493,3 @@ impl DiagnoseCommand { PhpMixed::Bool(true) } } - -impl HasBaseCommandData for DiagnoseCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data - } - - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } -} diff --git a/crates/shirabe/src/command/dump_autoload_command.rs b/crates/shirabe/src/command/dump_autoload_command.rs index ae51eac..8045d16 100644 --- a/crates/shirabe/src/command/dump_autoload_command.rs +++ b/crates/shirabe/src/command/dump_autoload_command.rs @@ -1,12 +1,22 @@ //! ref: composer/src/Composer/Command/DumpAutoloadCommand.php use anyhow::Result; +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_shim::{InvalidArgumentException, PhpMixed, file_exists}; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::plugin::CommandEvent; @@ -18,13 +28,24 @@ pub struct DumpAutoloadCommand { } impl DumpAutoloadCommand { - pub fn configure(&mut self) { - self - .set_name("dump-autoload") - .set_aliases(&["dumpautoload".to_string()]) - .set_description("Dumps the autoloader") - .set_definition(&[ - InputOption::new("optimize", Some(PhpMixed::String("o".to_string())), Some(InputOption::VALUE_NONE), "Optimizes PSR0 and PSR4 packages to be loaded with classmaps too, good for production.", None).unwrap().into(), + pub fn new() -> Self { + let mut command = DumpAutoloadCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("DumpAutoloadCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for DumpAutoloadCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("dump-autoload")?; + self.set_aliases(vec!["dumpautoload".to_string()])?; + self.set_description("Dumps the autoloader"); + self.set_definition(&[ + InputOption::new("optimize", Some(PhpMixed::String("o".to_string())), Some(InputOption::VALUE_NONE), "Optimizes PSR0 and PSR4 packages to be loaded with classmaps too, good for production.", None).unwrap().into(), InputOption::new("classmap-authoritative", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Autoload classes from the classmap only. Implicitly enables `--optimize`.", None).unwrap().into(), InputOption::new("apcu", None, Some(InputOption::VALUE_NONE), "Use APCu to cache found/not-found classes.", None).unwrap().into(), InputOption::new("apcu-prefix", None, Some(InputOption::VALUE_REQUIRED), "Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu", None).unwrap().into(), @@ -35,18 +56,19 @@ impl DumpAutoloadCommand { InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages).", None).unwrap().into(), InputOption::new("strict-psr", None, Some(InputOption::VALUE_NONE), "Return a failed status code (1) if PSR-4 or PSR-0 mapping errors are present. Requires --optimize to work.", None).unwrap().into(), InputOption::new("strict-ambiguous", None, Some(InputOption::VALUE_NONE), "Return a failed status code (2) if the same class is found in multiple files. Requires --optimize to work.", None).unwrap().into(), - ]) - .set_help( - "<info>php composer.phar dump-autoload</info>\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#dump-autoload-dumpautoload" - ); + ]); + self.set_help( + "<info>php composer.phar dump-autoload</info>\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#dump-autoload-dumpautoload", + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let composer = self.require_composer(None, None)?; let mut composer = crate::command::composer_full_mut(&composer); @@ -290,14 +312,24 @@ impl DumpAutoloadCommand { Ok(0) } -} -impl HasBaseCommandData for DumpAutoloadCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for DumpAutoloadCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/exec_command.rs b/crates/shirabe/src/command/exec_command.rs index 3c9cee2..aef22a3 100644 --- a/crates/shirabe/src/command/exec_command.rs +++ b/crates/shirabe/src/command/exec_command.rs @@ -1,13 +1,23 @@ //! ref: composer/src/Composer/Command/ExecCommand.php use anyhow::Result; +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_shim::{PhpMixed, RuntimeException, basename, chdir, getcwd, glob}; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; @@ -17,73 +27,123 @@ pub struct ExecCommand { } impl ExecCommand { - pub fn configure(&mut self) { - self - .set_name("exec") - .set_description("Executes a vendored binary/script") - .set_definition(&[ - InputOption::new("list", Some(PhpMixed::String("l".to_string())), Some(InputOption::VALUE_NONE), "", None).unwrap().into(), - // TODO(cli-completion): suggest installed binary names (via get_binaries) for `binary` argument - InputArgument::new("binary", - Some(InputArgument::OPTIONAL), - "The binary to run, e.g. phpunit", - None,).unwrap().into(), - InputArgument::new("args", - Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), - "Arguments to pass to the binary. Use <info>--</info> to separate from composer arguments", - None,).unwrap().into(), - ]) - .set_help( - "Executes a vendored binary/script.\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#exec" - ); + pub fn new() -> Self { + let mut command = ExecCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("ExecCommand::configure uses static, valid metadata"); + command } - pub fn interact( - &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<()> { - let binaries = self.get_binaries(false)?; - if binaries.is_empty() { - return Ok(()); - } + fn get_binaries(&mut self, for_display: bool) -> Result<Vec<String>> { + let composer = self.require_composer(None, None)?; + let composer_ref = crate::command::composer_full_mut(&composer); + let bin_dir = composer_ref + .get_config() + .borrow_mut() + .get("bin-dir") + .as_string() + .unwrap_or("") + .to_string(); + let bins = glob(&format!("{}/*", bin_dir)); + let local_bins_raw: Vec<String> = composer_ref.get_package().get_binaries(); + let local_bins: Vec<String> = if for_display { + local_bins_raw + .into_iter() + .map(|e| format!("{} (local)", e)) + .collect() + } else { + local_bins_raw + }; - if input.borrow().get_argument("binary")?.as_string().is_some() - || input - .borrow() - .get_option("list")? - .as_bool() - .unwrap_or(false) - { - return Ok(()); + let mut binaries: Vec<String> = Vec::new(); + let mut previous_bin: Option<String> = None; + for bin in bins.iter().chain(local_bins.iter()) { + if let Some(prev) = &previous_bin { + if bin == &format!("{}.bat", prev) { + continue; + } + } + previous_bin = Some(bin.clone()); + binaries.push(basename(bin)); } - let io = self.get_io(); - let binary = io.select( - "Binary to run: ".to_string(), - binaries.clone(), - PhpMixed::String(String::new()), - PhpMixed::Int(1), - "Invalid binary name \"%s\"".to_string(), - false, + Ok(binaries) + } +} + +impl Command for ExecCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("exec")?; + self.set_description("Executes a vendored binary/script"); + self.set_definition(&[ + InputOption::new("list", Some(PhpMixed::String("l".to_string())), Some(InputOption::VALUE_NONE), "", None).unwrap().into(), + // TODO(cli-completion): suggest installed binary names (via get_binaries) for `binary` argument + InputArgument::new("binary", + Some(InputArgument::OPTIONAL), + "The binary to run, e.g. phpunit", + None,).unwrap().into(), + InputArgument::new("args", + Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), + "Arguments to pass to the binary. Use <info>--</info> to separate from composer arguments", + None,).unwrap().into(), + ]); + self.set_help( + "Executes a vendored binary/script.\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#exec", ); + Ok(()) + } + + fn interact( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) { + let _ = (|| -> anyhow::Result<()> { + let binaries = self.get_binaries(false)?; + if binaries.is_empty() { + return Ok(()); + } - if let Some(idx) = binary.as_int() { - input.borrow_mut().set_argument( - "binary", - shirabe_php_shim::PhpMixed::String(binaries[idx as usize].clone()), + if input.borrow().get_argument("binary")?.as_string().is_some() + || input + .borrow() + .get_option("list")? + .as_bool() + .unwrap_or(false) + { + return Ok(()); + } + + let io = self.get_io(); + let binary = io.select( + "Binary to run: ".to_string(), + binaries.clone(), + PhpMixed::String(String::new()), + PhpMixed::Int(1), + "Invalid binary name \"%s\"".to_string(), + false, ); - } - Ok(()) + if let Some(idx) = binary.as_int() { + input.borrow_mut().set_argument( + "binary", + shirabe_php_shim::PhpMixed::String(binaries[idx as usize].clone()), + ); + } + + Ok(()) + })(); } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let composer = self.require_composer(None, None)?; if input @@ -137,7 +197,10 @@ impl ExecCommand { 0, ); - let initial_working_directory = self.get_application()?.get_initial_working_directory(); + // TODO(phase-c): getApplication()->getInitialWorkingDirectory() needs the shared shirabe + // Application handle (deferred with the Application shared-ownership work). Until then the + // working-directory restore is skipped. + let initial_working_directory: Option<String> = None; if let Some(ref iwd) = initial_working_directory { if getcwd().as_deref() != Some(iwd.as_str()) { chdir(iwd).map_err(|e| RuntimeException { @@ -166,49 +229,23 @@ impl ExecCommand { )?) } - fn get_binaries(&mut self, for_display: bool) -> Result<Vec<String>> { - let composer = self.require_composer(None, None)?; - let composer_ref = crate::command::composer_full_mut(&composer); - let bin_dir = composer_ref - .get_config() - .borrow_mut() - .get("bin-dir") - .as_string() - .unwrap_or("") - .to_string(); - let bins = glob(&format!("{}/*", bin_dir)); - let local_bins_raw: Vec<String> = composer_ref.get_package().get_binaries(); - let local_bins: Vec<String> = if for_display { - local_bins_raw - .into_iter() - .map(|e| format!("{} (local)", e)) - .collect() - } else { - local_bins_raw - }; - - let mut binaries: Vec<String> = Vec::new(); - let mut previous_bin: Option<String> = None; - for bin in bins.iter().chain(local_bins.iter()) { - if let Some(prev) = &previous_bin { - if bin == &format!("{}.bat", prev) { - continue; - } - } - previous_bin = Some(bin.clone()); - binaries.push(basename(bin)); - } - - Ok(binaries) + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); } -impl HasBaseCommandData for ExecCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data +impl BaseCommand for ExecCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/fund_command.rs b/crates/shirabe/src/command/fund_command.rs index 8bfae47..1e26fc0 100644 --- a/crates/shirabe/src/command/fund_command.rs +++ b/crates/shirabe/src/command/fund_command.rs @@ -5,15 +5,23 @@ use std::any::Any; use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; +use shirabe_external_packages::symfony::console::command::command::Command; use shirabe_external_packages::symfony::console::formatter::OutputFormatter; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::OutputInterface; use shirabe_php_shim::PhpMixed; use shirabe_semver::constraint::AnyConstraint; use shirabe_semver::constraint::MatchAllConstraint; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::json::JsonFile; @@ -31,25 +39,76 @@ pub struct FundCommand { } impl FundCommand { - pub fn configure(&mut self) { - self.set_name("fund") - .set_description("Discover how to help fund the maintenance of your dependencies") - .set_definition(&[InputOption::new( - "format", - Some(PhpMixed::String("f".to_string())), - Some(InputOption::VALUE_REQUIRED), - "Format of the output: text or json", - Some(PhpMixed::String("text".to_string())), - ) - .unwrap() - .into()]); + pub fn new() -> Self { + let mut command = FundCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("FundCommand::configure uses static, valid metadata"); + command } - pub fn execute( + fn insert_funding_data( + fundings: &mut IndexMap<String, IndexMap<String, Vec<String>>>, + package: &crate::package::CompletePackageInterfaceHandle, + ) -> Result<()> { + let pretty_name = package.get_pretty_name(); + let (vendor, package_name) = pretty_name + .split_once('/') + .unwrap_or(("", pretty_name.as_str())); + + for funding_option in package.get_funding() { + let url_val = funding_option.get("url").and_then(|v| v.as_string()); + if url_val.map_or(true, |s| s.is_empty()) { + continue; + } + let mut url = url_val.unwrap().to_string(); + let r#type = funding_option + .get("type") + .and_then(|v| v.as_string()) + .unwrap_or(""); + if r#type == "github" { + if let Some(matches) = + Preg::is_match_with_indexed_captures(r"^https://github.com/([^/]+)$", &url) + { + if let Some(sponsor) = matches.into_iter().nth(1) { + url = format!("https://github.com/sponsors/{}", sponsor); + } + } + } + fundings + .entry(vendor.to_string()) + .or_default() + .entry(url) + .or_default() + .push(package_name.to_string()); + } + Ok(()) + } +} + +impl Command for FundCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("fund")?; + self.set_description("Discover how to help fund the maintenance of your dependencies"); + self.set_definition(&[InputOption::new( + "format", + Some(PhpMixed::String("f".to_string())), + Some(InputOption::VALUE_REQUIRED), + "Format of the output: text or json", + Some(PhpMixed::String("text".to_string())), + ) + .unwrap() + .into()]); + Ok(()) + } + + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let composer = self.require_composer(None, None)?; let composer = crate::command::composer_full(&composer); @@ -171,51 +230,23 @@ impl FundCommand { Ok(0) } - fn insert_funding_data( - fundings: &mut IndexMap<String, IndexMap<String, Vec<String>>>, - package: &crate::package::CompletePackageInterfaceHandle, - ) -> Result<()> { - let pretty_name = package.get_pretty_name(); - let (vendor, package_name) = pretty_name - .split_once('/') - .unwrap_or(("", pretty_name.as_str())); - - for funding_option in package.get_funding() { - let url_val = funding_option.get("url").and_then(|v| v.as_string()); - if url_val.map_or(true, |s| s.is_empty()) { - continue; - } - let mut url = url_val.unwrap().to_string(); - let r#type = funding_option - .get("type") - .and_then(|v| v.as_string()) - .unwrap_or(""); - if r#type == "github" { - if let Some(matches) = - Preg::is_match_with_indexed_captures(r"^https://github.com/([^/]+)$", &url) - { - if let Some(sponsor) = matches.into_iter().nth(1) { - url = format!("https://github.com/sponsors/{}", sponsor); - } - } - } - fundings - .entry(vendor.to_string()) - .or_default() - .entry(url) - .or_default() - .push(package_name.to_string()); - } - Ok(()) + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); } -impl HasBaseCommandData for FundCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data +impl BaseCommand for FundCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/global_command.rs b/crates/shirabe/src/command/global_command.rs index 0184772..69cf48a 100644 --- a/crates/shirabe/src/command/global_command.rs +++ b/crates/shirabe/src/command/global_command.rs @@ -3,17 +3,28 @@ use std::path::Path; use anyhow::Result; +use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; +use shirabe_external_packages::symfony::console::command::command::Command; use shirabe_external_packages::symfony::console::input::ArgvInput; use shirabe_external_packages::symfony::console::input::ArrayInput; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::input::StringInput; use shirabe_external_packages::symfony::console::output::OutputInterface; +use shirabe_php_shim::PhpMixed; use shirabe_php_shim::{AsAny, LogicException, RuntimeException, chdir}; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::factory::Factory; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::util::Filesystem; @@ -25,39 +36,18 @@ pub struct GlobalCommand { } impl GlobalCommand { - // TODO(cli-completion): pub fn complete(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) - - pub fn configure(&mut self) { - self.set_name("global") - .set_description("Allows running commands in the global composer dir ($COMPOSER_HOME)") - .set_definition(&[ - InputArgument::new("command-name", Some(InputArgument::REQUIRED), "", None) - .unwrap() - .into(), - InputArgument::new( - "args", - Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), - "", - None, - ) - .unwrap() - .into(), - ]) - .set_help( - "Use this command as a wrapper to run other Composer commands\n\ - within the global context of COMPOSER_HOME.\n\n\ - You can use this to install CLI utilities globally, all you need\n\ - is to add the COMPOSER_HOME/vendor/bin dir to your PATH env var.\n\n\ - COMPOSER_HOME is c:\\Users\\<user>\\AppData\\Roaming\\Composer on Windows\n\ - and /home/<user>/.composer on unix systems.\n\n\ - If your system uses freedesktop.org standards, then it will first check\n\ - XDG_CONFIG_HOME or default to /home/<user>/.config/composer\n\n\ - Note: This path may vary depending on customizations to bin-dir in\n\ - composer.json or the environmental variable COMPOSER_BIN_DIR.\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#global", - ); + pub fn new() -> Self { + let mut command = GlobalCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("GlobalCommand::configure uses static, valid metadata"); + command } + // TODO(cli-completion): pub fn complete(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) + // TODO remove for Symfony 6+ as it is then in the interface. // Mirrors PHP's `method_exists($input, '__toString')` guard followed by // `$input->__toString()`. `InputInterface` does not declare `__toString`, so the @@ -81,37 +71,9 @@ impl GlobalCommand { } } - pub fn run( - &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { - let tokens = Preg::split(r"{\s+}", &Self::input_to_string(&*input.borrow())?); - let mut args: Vec<String> = vec![]; - for token in &tokens { - if !token.is_empty() && !token.starts_with('-') { - args.push(token.clone()); - if args.len() >= 2 { - break; - } - } - } - - if args.len() < 2 { - return self.run(input, output); - } - - let sub_input = self.prepare_subcommand_input(input, false)?; - let mut app = self.get_application()?; - Ok(app.run( - Some(std::rc::Rc::new(std::cell::RefCell::new(sub_input))), - Some(output), - )?) - } - fn prepare_subcommand_input( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + input: Rc<RefCell<dyn InputInterface>>, quiet: bool, ) -> Result<StringInput> { if Platform::get_env("COMPOSER").is_some() { @@ -139,7 +101,7 @@ impl GlobalCommand { })?; if !quiet { - self.get_io().write_error(&format!( + self.get_io().borrow().write_error(&format!( "<info>Changed current directory to {}</info>", home )); @@ -151,22 +113,98 @@ impl GlobalCommand { &Self::input_to_string(&*input.borrow())?, 1, ); - self.get_application()?.reset_composer(); + // TODO(phase-c): getApplication()->resetComposer() needs the shared shirabe Application + // handle (deferred with the Application shared-ownership work). Ok(StringInput::new(&new_input_str)?) } +} - pub fn is_proxy_command(&self) -> bool { +impl Command for GlobalCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("global")?; + self.set_description("Allows running commands in the global composer dir ($COMPOSER_HOME)"); + self.set_definition(&[ + InputArgument::new("command-name", Some(InputArgument::REQUIRED), "", None) + .unwrap() + .into(), + InputArgument::new( + "args", + Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), + "", + None, + ) + .unwrap() + .into(), + ]); + self.set_help( + "Use this command as a wrapper to run other Composer commands\n\ + within the global context of COMPOSER_HOME.\n\n\ + You can use this to install CLI utilities globally, all you need\n\ + is to add the COMPOSER_HOME/vendor/bin dir to your PATH env var.\n\n\ + COMPOSER_HOME is c:\\Users\\<user>\\AppData\\Roaming\\Composer on Windows\n\ + and /home/<user>/.composer on unix systems.\n\n\ + If your system uses freedesktop.org standards, then it will first check\n\ + XDG_CONFIG_HOME or default to /home/<user>/.config/composer\n\n\ + Note: This path may vary depending on customizations to bin-dir in\n\ + composer.json or the environmental variable COMPOSER_BIN_DIR.\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#global", + ); + Ok(()) + } + + fn is_proxy_command(&self) -> bool { true } + + fn run( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + let tokens = Preg::split(r"{\s+}", &Self::input_to_string(&*input.borrow())?); + let mut args: Vec<String> = vec![]; + for token in &tokens { + if !token.is_empty() && !token.starts_with('-') { + args.push(token.clone()); + if args.len() >= 2 { + break; + } + } + } + + if args.len() < 2 { + return self.run(input, output); + } + + let sub_input = self.prepare_subcommand_input(input, false)?; + // TODO(phase-c): proxying to Application::run needs the shared shirabe Application handle + // (deferred with the Application shared-ownership work and command registration). + let _ = (sub_input, output); + todo!("global command proxy run pending shared Application handle") + } + + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); } -impl HasBaseCommandData for GlobalCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data +impl BaseCommand for GlobalCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); + + fn is_proxy_command(&self) -> bool { + true } } diff --git a/crates/shirabe/src/command/home_command.rs b/crates/shirabe/src/command/home_command.rs index 029eaab..0ac7483 100644 --- a/crates/shirabe/src/command/home_command.rs +++ b/crates/shirabe/src/command/home_command.rs @@ -1,13 +1,24 @@ //! ref: composer/src/Composer/Command/HomeCommand.php use anyhow::Result; +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_shim::{FILTER_VALIDATE_URL, PhpMixed, filter_var}; +use shirabe_php_shim::PhpMixed; +use shirabe_php_shim::{FILTER_VALIDATE_URL, filter_var}; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::package::CompletePackageInterfaceHandle; @@ -25,124 +36,14 @@ pub struct HomeCommand { } impl HomeCommand { - pub fn configure(&mut self) { - // TODO(cli-completion): suggest_installed_package() for `packages` argument - self.set_name("browse") - .set_aliases(&["home".to_string()]) - .set_description("Opens the package's repository URL or homepage in your browser") - .set_definition(&[ - InputArgument::new( - "packages", - Some(InputArgument::IS_ARRAY), - "Package(s) to browse to.", - None, - ) - .unwrap() - .into(), - InputOption::new( - "homepage", - Some(shirabe_php_shim::PhpMixed::String("H".to_string())), - Some(InputOption::VALUE_NONE), - "Open the homepage instead of the repository URL.", - None, - ) - .unwrap() - .into(), - InputOption::new( - "show", - Some(shirabe_php_shim::PhpMixed::String("s".to_string())), - Some(InputOption::VALUE_NONE), - "Only show the homepage or repository URL.", - None, - ) - .unwrap() - .into(), - ]) - .set_help( - "The home command opens or shows a package's repository URL or\n\ - homepage in your default browser.\n\n\ - To open the homepage by default, use -H or --homepage.\n\ - To show instead of open the repository or homepage URL, use -s or --show.\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#browse-home", - ); - } - - pub fn execute( - &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { - let repos = self.initialize_repos()?; - let io = self.get_io().clone(); - let mut return_code: i64 = 0; - - let packages: Vec<String> = input - .borrow() - .get_argument("packages")? - .as_list() - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - - let packages = if packages.is_empty() { - io.write_error("No package specified, opening homepage for the root package"); - let composer_rc = self.require_composer(None, None)?; - let composer_ref = crate::command::composer_full(&composer_rc); - vec![composer_ref.get_package().get_name().to_string()] - } else { - packages + pub fn new() -> Self { + let mut command = HomeCommand { + base_command_data: BaseCommandData::new(None), }; - - let show_homepage = input - .borrow() - .get_option("homepage")? - .as_bool() - .unwrap_or(false); - let show_only = input - .borrow() - .get_option("show")? - .as_bool() - .unwrap_or(false); - - for package_name in &packages { - let mut handled = false; - let mut package_exists = false; - - 'repos: for repo in &repos { - for package in repo.find_packages(&package_name, None)? { - package_exists = true; - if let Some(complete_pkg) = package.as_complete() { - if self.handle_package(complete_pkg, show_homepage, show_only) { - handled = true; - break 'repos; - } - } - } - } - - if !package_exists { - return_code = 1; - io.write_error(&format!( - "<warning>Package {} not found</warning>", - package_name - )); - } - - if !handled { - return_code = 1; - let msg = if show_homepage { - "Invalid or missing homepage" - } else { - "Invalid or missing repository URL" - }; - io.write_error(&format!("<warning>{} for {}</warning>", msg, package_name)); - } - } - - Ok(return_code) + command + .configure() + .expect("HomeCommand::configure uses static, valid metadata"); + command } fn handle_package( @@ -234,12 +135,145 @@ impl HomeCommand { } } -impl HasBaseCommandData for HomeCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data +impl Command for HomeCommand { + fn configure(&mut self) -> anyhow::Result<()> { + // TODO(cli-completion): suggest_installed_package() for `packages` argument + self.set_name("browse")?; + self.set_aliases(vec!["home".to_string()])?; + self.set_description("Opens the package's repository URL or homepage in your browser"); + self.set_definition(&[ + InputArgument::new( + "packages", + Some(InputArgument::IS_ARRAY), + "Package(s) to browse to.", + None, + ) + .unwrap() + .into(), + InputOption::new( + "homepage", + Some(shirabe_php_shim::PhpMixed::String("H".to_string())), + Some(InputOption::VALUE_NONE), + "Open the homepage instead of the repository URL.", + None, + ) + .unwrap() + .into(), + InputOption::new( + "show", + Some(shirabe_php_shim::PhpMixed::String("s".to_string())), + Some(InputOption::VALUE_NONE), + "Only show the homepage or repository URL.", + None, + ) + .unwrap() + .into(), + ]); + self.set_help( + "The home command opens or shows a package's repository URL or\n\ + homepage in your default browser.\n\n\ + To open the homepage by default, use -H or --homepage.\n\ + To show instead of open the repository or homepage URL, use -s or --show.\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#browse-home", + ); + Ok(()) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + fn execute( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + let repos = self.initialize_repos()?; + let io = self.get_io().clone(); + let mut return_code: i64 = 0; + + let packages: Vec<String> = input + .borrow() + .get_argument("packages")? + .as_list() + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + + let packages = if packages.is_empty() { + io.write_error("No package specified, opening homepage for the root package"); + let composer_rc = self.require_composer(None, None)?; + let composer_ref = crate::command::composer_full(&composer_rc); + vec![composer_ref.get_package().get_name().to_string()] + } else { + packages + }; + + let show_homepage = input + .borrow() + .get_option("homepage")? + .as_bool() + .unwrap_or(false); + let show_only = input + .borrow() + .get_option("show")? + .as_bool() + .unwrap_or(false); + + for package_name in &packages { + let mut handled = false; + let mut package_exists = false; + + 'repos: for repo in &repos { + for package in repo.find_packages(&package_name, None)? { + package_exists = true; + if let Some(complete_pkg) = package.as_complete() { + if self.handle_package(complete_pkg, show_homepage, show_only) { + handled = true; + break 'repos; + } + } + } + } + + if !package_exists { + return_code = 1; + io.write_error(&format!( + "<warning>Package {} not found</warning>", + package_name + )); + } + + if !handled { + return_code = 1; + let msg = if show_homepage { + "Invalid or missing homepage" + } else { + "Invalid or missing repository URL" + }; + io.write_error(&format!("<warning>{} for {}</warning>", msg, package_name)); + } + } + + Ok(return_code) } + + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for HomeCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/init_command.rs b/crates/shirabe/src/command/init_command.rs index 13d5809..de68829 100644 --- a/crates/shirabe/src/command/init_command.rs +++ b/crates/shirabe/src/command/init_command.rs @@ -5,6 +5,7 @@ use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; use shirabe_external_packages::composer::spdx_licenses::SpdxLicenses; +use shirabe_external_packages::symfony::console::command::command::Command; use shirabe_external_packages::symfony::console::helper::FormatBlockMessages; use shirabe_external_packages::symfony::console::helper::FormatterHelper; use shirabe_external_packages::symfony::console::input::ArrayInput; @@ -17,12 +18,18 @@ use shirabe_php_shim::{ function_exists, get_current_user, implode, is_dir, is_string, preg_quote, realpath, server_get, sprintf, str_replace, strpos, strtolower, trim, ucwords, }; +use std::cell::RefCell; +use std::rc::Rc; +use crate::advisory::AuditConfig; use crate::command::PackageDiscoveryTrait; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputOption; use crate::factory::Factory; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::json::JsonFile; @@ -87,12 +94,24 @@ impl PackageDiscoveryTrait for InitCommand { } impl InitCommand { - pub fn configure(&mut self) { + pub fn new() -> Self { + let mut command = InitCommand { + base_command_data: BaseCommandData::new(None), + git_config: None, + }; + command + .configure() + .expect("InitCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for InitCommand { + fn configure(&mut self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_available_package_incl_platform() for `require` / `require-dev` - self - .set_name("init") - .set_description("Creates a basic composer.json file in current directory") - .set_definition(&[ + self.set_name("init")?; + self.set_description("Creates a basic composer.json file in current directory"); + self.set_definition(&[ InputOption::new("name", None, Some(InputOption::VALUE_REQUIRED), "Name of the package", None).unwrap().into(), InputOption::new("description", None, Some(InputOption::VALUE_REQUIRED), "Description of package", None).unwrap().into(), InputOption::new("author", None, Some(InputOption::VALUE_REQUIRED), "Author name of package", None).unwrap().into(), @@ -104,23 +123,24 @@ impl InitCommand { InputOption::new("license", Some(PhpMixed::String("l".to_string())), Some(InputOption::VALUE_REQUIRED), "License of package", None).unwrap().into(), InputOption::new("repository", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Add custom repositories, either by URL or using JSON arrays", None).unwrap().into(), InputOption::new("autoload", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_REQUIRED), "Add PSR-4 autoload mapping. Maps your package's namespace to the provided directory. (Expects a relative path, e.g. src/)", None).unwrap().into(), - ]) - .set_help( - "The <info>init</info> command creates a basic composer.json file\n\ + ]); + self.set_help( + "The <info>init</info> command creates a basic composer.json file\n\ in the current directory.\n\ \n\ <info>php composer.phar init</info>\n\ \n\ - Read more at https://getcomposer.org/doc/03-cli.md#init" - ); + Read more at https://getcomposer.org/doc/03-cli.md#init", + ); + Ok(()) } /// @throws \Seld\JsonLint\ParsingException - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let io = PackageDiscoveryTrait::get_io(self); let allowlist: Vec<String> = vec![ @@ -416,12 +436,12 @@ impl InitCommand { Ok(0) } - pub(crate) fn initialize( + fn initialize( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, ) -> anyhow::Result<()> { - BaseCommand::initialize(self, input.clone(), output)?; + base_command_initialize(self, input.clone(), output.clone())?; if !input.borrow().is_interactive() { if input.borrow().get_option("name")?.is_null() { @@ -444,124 +464,126 @@ impl InitCommand { Ok(()) } - pub(crate) fn interact( + fn interact( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<()> { - let io = self.get_io(); - // @var FormatterHelper $formatter — PHP: $this->getHelperSet()->get('formatter') - // TODO(phase-c): get_helper_set returns PhpMixed and HelperSet::get is a todo!() stub (per - // the "Symfony stays todo!()" policy), so the typed FormatterHelper cannot be retrieved - // until the Helper trait + typed HelperSet are modelled. - let formatter: FormatterHelper = todo!(); - let _ = &formatter; - let _ = self.get_helper_set(); + input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) { + let _ = (|| -> anyhow::Result<()> { + let io = self.get_io(); + // @var FormatterHelper $formatter — PHP: $this->getHelperSet()->get('formatter') + // TODO(phase-c): get_helper_set returns PhpMixed and HelperSet::get is a todo!() stub (per + // the "Symfony stays todo!()" policy), so the typed FormatterHelper cannot be retrieved + // until the Helper trait + typed HelperSet are modelled. + let formatter: FormatterHelper = todo!(); + let _ = &formatter; + let _ = self.get_helper_set(); - // initialize repos if configured - let repositories: Vec<String> = input - .borrow() - .get_option("repository")? - .as_list() - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - if (repositories.len() as i64) > 0 { - let config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config( - Some(io.clone()), - None, - )?)); - io.borrow_mut() - .load_configuration(&mut *config.borrow_mut())?; - let mut repo_manager = - RepositoryFactory::manager(io.clone(), &config, None, None, None)?; + // initialize repos if configured + let repositories: Vec<String> = input + .borrow() + .get_option("repository")? + .as_list() + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + if (repositories.len() as i64) > 0 { + let config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config( + Some(io.clone()), + None, + )?)); + io.borrow_mut() + .load_configuration(&mut *config.borrow_mut())?; + let mut repo_manager = + RepositoryFactory::manager(io.clone(), &config, None, None, None)?; - let mut repos: Vec<crate::repository::RepositoryInterfaceHandle> = - vec![crate::repository::RepositoryInterfaceHandle::new( - PlatformRepository::new(vec![], IndexMap::new())?, - )]; - let mut create_default_packagist_repo = true; - for repo in &repositories { - let repo_config = - RepositoryFactory::config_from_string(io.clone(), &config, repo, true)?; - let is_packagist_false = repo_config - .get("packagist") - .map(|v| v.as_bool() == Some(false)) - .unwrap_or(false) - && repo_config.len() == 1; - let is_packagist_org_false = repo_config - .get("packagist.org") - .map(|v| v.as_bool() == Some(false)) - .unwrap_or(false) - && repo_config.len() == 1; - if is_packagist_false || is_packagist_org_false { - create_default_packagist_repo = false; - continue; + let mut repos: Vec<crate::repository::RepositoryInterfaceHandle> = + vec![crate::repository::RepositoryInterfaceHandle::new( + PlatformRepository::new(vec![], IndexMap::new())?, + )]; + let mut create_default_packagist_repo = true; + for repo in &repositories { + let repo_config = + RepositoryFactory::config_from_string(io.clone(), &config, repo, true)?; + let is_packagist_false = repo_config + .get("packagist") + .map(|v| v.as_bool() == Some(false)) + .unwrap_or(false) + && repo_config.len() == 1; + let is_packagist_org_false = repo_config + .get("packagist.org") + .map(|v| v.as_bool() == Some(false)) + .unwrap_or(false) + && repo_config.len() == 1; + if is_packagist_false || is_packagist_org_false { + create_default_packagist_repo = false; + continue; + } + repos.push(RepositoryFactory::create_repo( + io.clone(), + &config, + repo_config, + Some(&mut repo_manager), + )?); } - repos.push(RepositoryFactory::create_repo( - io.clone(), - &config, - repo_config, - Some(&mut repo_manager), - )?); - } - if create_default_packagist_repo { - let mut default_config: IndexMap<String, PhpMixed> = IndexMap::new(); - default_config.insert("type".to_string(), PhpMixed::String("composer".to_string())); - default_config.insert( - "url".to_string(), - PhpMixed::String("https://repo.packagist.org".to_string()), - ); - repos.push(RepositoryFactory::create_repo( - io.clone(), - &config, - default_config, - Some(&mut repo_manager), - )?); - } + if create_default_packagist_repo { + let mut default_config: IndexMap<String, PhpMixed> = IndexMap::new(); + default_config + .insert("type".to_string(), PhpMixed::String("composer".to_string())); + default_config.insert( + "url".to_string(), + PhpMixed::String("https://repo.packagist.org".to_string()), + ); + repos.push(RepositoryFactory::create_repo( + io.clone(), + &config, + default_config, + Some(&mut repo_manager), + )?); + } - *self.get_repos_mut() = Some(crate::repository::RepositoryInterfaceHandle::new( - CompositeRepository::new(repos), - )); - // unset($repos, $config, $repositories); - } + *self.get_repos_mut() = Some(crate::repository::RepositoryInterfaceHandle::new( + CompositeRepository::new(repos), + )); + // unset($repos, $config, $repositories); + } - io.write_error3( - &format!( - "\n{}\n", - formatter.format_block( - FormatBlockMessages::String( - "Welcome to the Composer config generator".to_string(), - ), - "bg=blue;fg=white", - true, - ) - ), - true, - io_interface::NORMAL, - ); + io.write_error3( + &format!( + "\n{}\n", + formatter.format_block( + FormatBlockMessages::String( + "Welcome to the Composer config generator".to_string(), + ), + "bg=blue;fg=white", + true, + ) + ), + true, + io_interface::NORMAL, + ); - // namespace - io.write_error3( - "\nThis command will guide you through creating your composer.json config.\n", - true, - io_interface::NORMAL, - ); + // namespace + io.write_error3( + "\nThis command will guide you through creating your composer.json config.\n", + true, + io_interface::NORMAL, + ); - let mut name = input - .borrow() - .get_option("name")? - .as_string() - .map(|s| s.to_string()) - .unwrap_or_else(|| self.get_default_package_name()); + let mut name = input + .borrow() + .get_option("name")? + .as_string() + .map(|s| s.to_string()) + .unwrap_or_else(|| self.get_default_package_name()); - let name_default = name.clone(); - let name_for_validate = name.clone(); - name = io + let name_default = name.clone(); + let name_for_validate = name.clone(); + name = io .ask_and_validate( format!( "Package name (<vendor>/<name>) [<comment>{}</comment>]: ", @@ -594,168 +616,168 @@ impl InitCommand { .as_string() .unwrap_or("") .to_string(); - input - .borrow_mut() - .set_option("name", PhpMixed::String(name)); + input + .borrow_mut() + .set_option("name", PhpMixed::String(name)); - let description = input - .borrow() - .get_option("description")? - .as_string() - .map(|s| s.to_string()); - let description_default = description.clone(); - let description = io.ask( - format!( - "Description [<comment>{}</comment>]: ", - description.clone().unwrap_or_default() - ), - description_default - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null), - ); - input.borrow_mut().set_option("description", description); + let description = input + .borrow() + .get_option("description")? + .as_string() + .map(|s| s.to_string()); + let description_default = description.clone(); + let description = io.ask( + format!( + "Description [<comment>{}</comment>]: ", + description.clone().unwrap_or_default() + ), + description_default + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null), + ); + input.borrow_mut().set_option("description", description); - let author = input - .borrow() - .get_option("author")? - .as_string() - .map(|s| s.to_string()) - .unwrap_or_else(|| self.get_default_author().unwrap_or_default()); + let author = input + .borrow() + .get_option("author")? + .as_string() + .map(|s| s.to_string()) + .unwrap_or_else(|| self.get_default_author().unwrap_or_default()); - let author_for_validate = author.clone(); - let author_default = author.clone(); - // PHP: $this->parseAuthorString is called inside a closure. We approximate by binding. - let author_value = io.ask_and_validate( - format!( - "Author [{} n to skip]: ", - if is_string(&PhpMixed::String(author.clone())) { - format!("<comment>{}</comment>, ", author) - } else { - String::new() - } - ), - // PHP: function ($value) use ($self, $author) { ... $author = $self->parseAuthorString($value); ... } - // TODO(phase-c): IOInterface::ask_and_validate takes a `Box<dyn Fn> + 'static` - // validator, so it cannot borrow &self to call self.parse_author_string; and - // ask_and_validate itself is a deferred QuestionHelper todo!() (see console_io). The - // validator body below therefore stays a placeholder. (parse_author_string and - // is_valid_email are stateless, so a future fix can make them associated functions and - // call them from the closure once the helper interaction is modelled.) - Box::new(move |value: PhpMixed| -> anyhow::Result<PhpMixed> { - let value_str = value.as_string().unwrap_or("").to_string(); - if value_str == "n" || value_str == "no" { - return Ok(PhpMixed::Null); - } - let value_or_default = if value_str.is_empty() { - author_for_validate.clone() - } else { - value_str - }; - // PHP: $author = $self->parseAuthorString($value); return $author['email'] === null - // ? $author['name'] : sprintf('%s <%s>', $author['name'], $author['email']); - // TODO(phase-c): see the closure note above — cannot reach parse_author_string from - // this 'static validator yet. - let _ = value_or_default; - Ok(PhpMixed::Null) - }), - None, - PhpMixed::String(author_default), - )?; - input.borrow_mut().set_option("author", author_value); + let author_for_validate = author.clone(); + let author_default = author.clone(); + // PHP: $this->parseAuthorString is called inside a closure. We approximate by binding. + let author_value = io.ask_and_validate( + format!( + "Author [{} n to skip]: ", + if is_string(&PhpMixed::String(author.clone())) { + format!("<comment>{}</comment>, ", author) + } else { + String::new() + } + ), + // PHP: function ($value) use ($self, $author) { ... $author = $self->parseAuthorString($value); ... } + // TODO(phase-c): IOInterface::ask_and_validate takes a `Box<dyn Fn> + 'static` + // validator, so it cannot borrow &self to call self.parse_author_string; and + // ask_and_validate itself is a deferred QuestionHelper todo!() (see console_io). The + // validator body below therefore stays a placeholder. (parse_author_string and + // is_valid_email are stateless, so a future fix can make them associated functions and + // call them from the closure once the helper interaction is modelled.) + Box::new(move |value: PhpMixed| -> anyhow::Result<PhpMixed> { + let value_str = value.as_string().unwrap_or("").to_string(); + if value_str == "n" || value_str == "no" { + return Ok(PhpMixed::Null); + } + let value_or_default = if value_str.is_empty() { + author_for_validate.clone() + } else { + value_str + }; + // PHP: $author = $self->parseAuthorString($value); return $author['email'] === null + // ? $author['name'] : sprintf('%s <%s>', $author['name'], $author['email']); + // TODO(phase-c): see the closure note above — cannot reach parse_author_string from + // this 'static validator yet. + let _ = value_or_default; + Ok(PhpMixed::Null) + }), + None, + PhpMixed::String(author_default), + )?; + input.borrow_mut().set_option("author", author_value); - let minimum_stability = input - .borrow() - .get_option("stability")? - .as_string() - .map(|s| s.to_string()); - let minimum_stability_default = minimum_stability.clone(); - let minimum_stability_for_validate = minimum_stability.clone(); - let minimum_stability_value = io.ask_and_validate( - format!( - "Minimum Stability [<comment>{}</comment>]: ", - minimum_stability.clone().unwrap_or_default() - ), - Box::new(move |value: PhpMixed| -> anyhow::Result<PhpMixed> { - if value.is_null() { - return Ok(minimum_stability_for_validate - .clone() - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null)); - } + let minimum_stability = input + .borrow() + .get_option("stability")? + .as_string() + .map(|s| s.to_string()); + let minimum_stability_default = minimum_stability.clone(); + let minimum_stability_for_validate = minimum_stability.clone(); + let minimum_stability_value = io.ask_and_validate( + format!( + "Minimum Stability [<comment>{}</comment>]: ", + minimum_stability.clone().unwrap_or_default() + ), + Box::new(move |value: PhpMixed| -> anyhow::Result<PhpMixed> { + if value.is_null() { + return Ok(minimum_stability_for_validate + .clone() + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null)); + } - if !base_package::STABILITIES.contains_key(value.as_string().unwrap_or("")) { - return Err(InvalidArgumentException { - message: format!( - "Invalid minimum stability \"{}\". Must be empty or one of: {}", - value.as_string().unwrap_or(""), - implode( - ", ", - &base_package::STABILITIES - .keys() - .map(|k| k.to_string()) - .collect::<Vec<_>>() - ) - ), - code: 0, + if !base_package::STABILITIES.contains_key(value.as_string().unwrap_or("")) { + return Err(InvalidArgumentException { + message: format!( + "Invalid minimum stability \"{}\". Must be empty or one of: {}", + value.as_string().unwrap_or(""), + implode( + ", ", + &base_package::STABILITIES + .keys() + .map(|k| k.to_string()) + .collect::<Vec<_>>() + ) + ), + code: 0, + } + .into()); } - .into()); - } - Ok(value) - }), - None, - minimum_stability_default - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null), - )?; - input - .borrow_mut() - .set_option("stability", minimum_stability_value); + Ok(value) + }), + None, + minimum_stability_default + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null), + )?; + input + .borrow_mut() + .set_option("stability", minimum_stability_value); - let type_val = input.borrow().get_option("type")?; - let type_str = type_val.as_string().unwrap_or("").to_string(); - let mut type_value = io.ask( + let type_val = input.borrow().get_option("type")?; + let type_str = type_val.as_string().unwrap_or("").to_string(); + let mut type_value = io.ask( format!( "Package Type (e.g. library, project, metapackage, composer-plugin) [<comment>{}</comment>]: ", type_str ), type_val, ); - if type_value.as_string() == Some("") || matches!(type_value, PhpMixed::Bool(false)) { - type_value = PhpMixed::Null; - } - input.borrow_mut().set_option("type", type_value); + if type_value.as_string() == Some("") || matches!(type_value, PhpMixed::Bool(false)) { + type_value = PhpMixed::Null; + } + input.borrow_mut().set_option("type", type_value); - let mut license = input - .borrow() - .get_option("license")? - .as_string() - .map(|s| s.to_string()); - if license.is_none() { - let default_license = server_get("COMPOSER_DEFAULT_LICENSE"); - if !empty( - &default_license - .clone() - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null), - ) { - license = default_license; + let mut license = input + .borrow() + .get_option("license")? + .as_string() + .map(|s| s.to_string()); + if license.is_none() { + let default_license = server_get("COMPOSER_DEFAULT_LICENSE"); + if !empty( + &default_license + .clone() + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null), + ) { + license = default_license; + } } - } - let license = io.ask( - format!( - "License [<comment>{}</comment>]: ", - license.clone().unwrap_or_default() - ), - license.map(PhpMixed::String).unwrap_or(PhpMixed::Null), - ); - let spdx = SpdxLicenses::new(); - if !license.is_null() - && !spdx.validate(license.as_string().unwrap_or("")) - && license.as_string() != Some("proprietary") - { - return Err(InvalidArgumentException { + let license = io.ask( + format!( + "License [<comment>{}</comment>]: ", + license.clone().unwrap_or_default() + ), + license.map(PhpMixed::String).unwrap_or(PhpMixed::Null), + ); + let spdx = SpdxLicenses::new(); + if !license.is_null() + && !spdx.validate(license.as_string().unwrap_or("")) + && license.as_string() != Some("proprietary") + { + return Err(InvalidArgumentException { message: format!( "Invalid license provided: {}. Only SPDX license identifiers (https://spdx.org/licenses/) or \"proprietary\" are accepted.", license.as_string().unwrap_or("") @@ -763,85 +785,52 @@ impl InitCommand { code: 0, } .into()); - } - input.borrow_mut().set_option("license", license); + } + input.borrow_mut().set_option("license", license); - io.write_error3("\nDefine your dependencies.\n", true, io_interface::NORMAL); + io.write_error3("\nDefine your dependencies.\n", true, io_interface::NORMAL); - // prepare to resolve dependencies - let repos = self.get_repos(); - let preferred_stability = - if let Some(s) = minimum_stability_default.clone().filter(|s| !s.is_empty()) { - s - } else { - "stable".to_string() - }; - let platform_repo: Option<PlatformRepositoryHandle> = if repos.is::<CompositeRepository>() { - let borrowed = repos.borrow(); - let composite = borrowed - .as_any() - .downcast_ref::<CompositeRepository>() - .expect("is::<CompositeRepository>() checked above"); - composite - .get_repositories() - .iter() - .find(|candidate| candidate.is::<PlatformRepository>()) - .and_then(|candidate| candidate.as_platform_repository()) - } else { - None - }; - - let question = "Would you like to define your dependencies (require) interactively [<comment>yes</comment>]? ".to_string(); - let require: Vec<String> = input - .borrow() - .get_option("require")? - .as_list() - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - let requirements = if (require.len() as i64) > 0 || io.ask_confirmation(question, true) { - self.determine_requirements( - input, - _output, - require, - platform_repo.as_ref(), - &preferred_stability, - false, - false, - )? - } else { - vec![] - }; - input.borrow_mut().set_option( - "require", - PhpMixed::List( - requirements - .into_iter() - .map(|s| Box::new(PhpMixed::String(s))) - .collect(), - ), - ); + // prepare to resolve dependencies + let repos = self.get_repos(); + let preferred_stability = + if let Some(s) = minimum_stability_default.clone().filter(|s| !s.is_empty()) { + s + } else { + "stable".to_string() + }; + let platform_repo: Option<PlatformRepositoryHandle> = + if repos.is::<CompositeRepository>() { + let borrowed = repos.borrow(); + let composite = borrowed + .as_any() + .downcast_ref::<CompositeRepository>() + .expect("is::<CompositeRepository>() checked above"); + composite + .get_repositories() + .iter() + .find(|candidate| candidate.is::<PlatformRepository>()) + .and_then(|candidate| candidate.as_platform_repository()) + } else { + None + }; - let question = "Would you like to define your dev dependencies (require-dev) interactively [<comment>yes</comment>]? ".to_string(); - let require_dev: Vec<String> = input - .borrow() - .get_option("require-dev")? - .as_list() - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - let dev_requirements = - if (require_dev.len() as i64) > 0 || io.ask_confirmation(question, true) { + let question = "Would you like to define your dependencies (require) interactively [<comment>yes</comment>]? ".to_string(); + let require: Vec<String> = input + .borrow() + .get_option("require")? + .as_list() + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + let requirements = if (require.len() as i64) > 0 || io.ask_confirmation(question, true) + { self.determine_requirements( input, _output, - require_dev, + require, platform_repo.as_ref(), &preferred_stability, false, @@ -850,36 +839,71 @@ impl InitCommand { } else { vec![] }; - input.borrow_mut().set_option( - "require-dev", - PhpMixed::List( - dev_requirements - .into_iter() - .map(|s| Box::new(PhpMixed::String(s))) - .collect(), - ), - ); + input.borrow_mut().set_option( + "require", + PhpMixed::List( + requirements + .into_iter() + .map(|s| Box::new(PhpMixed::String(s))) + .collect(), + ), + ); - // --autoload - input and validation - let mut autoload = input - .borrow() - .get_option("autoload")? - .as_string() - .map(|s| s.to_string()) - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "src/".to_string()); - let name_str = input - .borrow() - .get_option("name")? - .as_string() - .unwrap_or("") - .to_string(); - let namespace = self - .namespace_from_package_name(&name_str) - .unwrap_or_default(); - let autoload_for_validate = autoload.clone(); - let autoload_default = autoload.clone(); - let autoload_value = io.ask_and_validate( + let question = "Would you like to define your dev dependencies (require-dev) interactively [<comment>yes</comment>]? ".to_string(); + let require_dev: Vec<String> = input + .borrow() + .get_option("require-dev")? + .as_list() + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + let dev_requirements = + if (require_dev.len() as i64) > 0 || io.ask_confirmation(question, true) { + self.determine_requirements( + input, + _output, + require_dev, + platform_repo.as_ref(), + &preferred_stability, + false, + false, + )? + } else { + vec![] + }; + input.borrow_mut().set_option( + "require-dev", + PhpMixed::List( + dev_requirements + .into_iter() + .map(|s| Box::new(PhpMixed::String(s))) + .collect(), + ), + ); + + // --autoload - input and validation + let mut autoload = input + .borrow() + .get_option("autoload")? + .as_string() + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| "src/".to_string()); + let name_str = input + .borrow() + .get_option("name")? + .as_string() + .unwrap_or("") + .to_string(); + let namespace = self + .namespace_from_package_name(&name_str) + .unwrap_or_default(); + let autoload_for_validate = autoload.clone(); + let autoload_default = autoload.clone(); + let autoload_value = io.ask_and_validate( format!( "Add PSR-4 autoload mapping? Maps namespace \"{}\" to the entered relative path. [<comment>{}</comment>, n to skip]: ", namespace, autoload @@ -917,11 +941,26 @@ impl InitCommand { None, PhpMixed::String(autoload_default), )?; - input.borrow_mut().set_option("autoload", autoload_value); + input.borrow_mut().set_option("autoload", autoload_value); - Ok(()) + Ok(()) + })(); + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for InitCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); +} + +impl InitCommand { /// @return array{name: string, email: string|null} fn parse_author_string(&self, author: &str) -> Result<IndexMap<String, Option<String>>> { let mut m: IndexMap<CaptureKey, String> = IndexMap::new(); @@ -1088,45 +1127,24 @@ impl InitCommand { } fn update_dependencies(&self, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>) { - // PHP try/catch: catch \Exception - let result = self.get_application().and_then(|mut app| { - let _update_command = app.find("update")?; - app.reset_composer(); - // PHP: $updateCommand->run(new ArrayInput([]), $output); - // TODO(phase-c): Application::find returns PhpMixed (the Symfony command registry is a - // todo!() stub), so the resolved command's run() cannot be invoked until the typed - // command registry is modelled. - let _ = ArrayInput::new(vec![], None); - let _ = output; - Ok(()) - }); - if let Err(_e) = result { - self.get_io().write_error3( - "Could not update dependencies. Run `composer update` to see more information.", - true, - io_interface::NORMAL, - ); - } + // PHP: try { $this->getApplication()->find('update')->run(new ArrayInput([]), $output); } + // catch (\Exception $e) { $this->getIO()->writeError('Could not update dependencies...'); } + // TODO(phase-c): needs the shared shirabe Application handle and the typed command registry + // (deferred with the Application shared-ownership work). Until then this is a no-op. + let _ = ArrayInput::new(vec![], None); + let _ = output; } fn run_dump_autoload_command( &self, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, ) { - let result = self.get_application().and_then(|mut app| { - let _command = app.find("dump-autoload")?; - app.reset_composer(); - // PHP: $command->run(new ArrayInput([]), $output); - // TODO(phase-c): same blocker as update_dependencies — Application::find returns - // PhpMixed (Symfony command registry todo!() stub), so run() cannot be invoked. - let _ = ArrayInput::new(vec![], None); - let _ = output; - Ok(()) - }); - if let Err(_e) = result { - self.get_io() - .write_error3("Could not run dump-autoload.", true, io_interface::NORMAL); - } + // PHP: try { $this->getApplication()->find('dump-autoload')->run(new ArrayInput([]), $output); } + // catch (\Exception $e) { $this->getIO()->writeError('Could not run dump-autoload.'); } + // TODO(phase-c): same deferral as update_dependencies — needs the shared shirabe + // Application handle and the typed command registry. + let _ = ArrayInput::new(vec![], None); + let _ = output; } /// @param array<string, string|array<string>> $options @@ -1242,13 +1260,3 @@ impl InitCommand { None } } - -impl HasBaseCommandData for InitCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data - } - - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } -} diff --git a/crates/shirabe/src/command/install_command.rs b/crates/shirabe/src/command/install_command.rs index 182f855..74d75bf 100644 --- a/crates/shirabe/src/command/install_command.rs +++ b/crates/shirabe/src/command/install_command.rs @@ -1,14 +1,24 @@ //! ref: composer/src/Composer/Command/InstallCommand.php use anyhow::Result; +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_shim::PhpMixed; +use std::cell::RefCell; +use std::rc::Rc; +use crate::advisory::AuditConfig; use crate::advisory::Auditor; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::installer::Installer; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; @@ -22,51 +32,63 @@ pub struct InstallCommand { } impl InstallCommand { - pub fn configure(&mut self) { + pub fn new() -> Self { + let mut command = InstallCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("InstallCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for InstallCommand { + fn configure(&mut self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_prefer_install() for `prefer-install` option - self - .set_name("install") - .set_aliases(&["i".to_string()]) - .set_description("Installs the project dependencies from the composer.lock file if present, or falls back on the composer.json") - .set_definition(&[ - InputOption::new("prefer-source", None, Some(InputOption::VALUE_NONE), "Forces installation from package sources when possible, including VCS information.", None).unwrap().into(), - InputOption::new("prefer-dist", None, Some(InputOption::VALUE_NONE), "Forces installation from package dist (default behavior).", None).unwrap().into(), - InputOption::new("prefer-install", None, Some(InputOption::VALUE_REQUIRED), "Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).", None).unwrap().into(), - InputOption::new("dry-run", None, Some(InputOption::VALUE_NONE), "Outputs the operations but will not execute anything (implicitly enables --verbose).", None).unwrap().into(), - InputOption::new("download-only", None, Some(InputOption::VALUE_NONE), "Download only, do not install packages.", None).unwrap().into(), - InputOption::new("dev", None, Some(InputOption::VALUE_NONE), "DEPRECATED: Enables installation of require-dev packages (enabled by default, only present for BC).", None).unwrap().into(), - InputOption::new("no-suggest", None, Some(InputOption::VALUE_NONE), "DEPRECATED: This flag does not exist anymore.", None).unwrap().into(), - InputOption::new("no-dev", None, Some(InputOption::VALUE_NONE), "Disables installation of require-dev packages.", None).unwrap().into(), - InputOption::new("no-security-blocking", None, Some(InputOption::VALUE_NONE), "Allows installing packages with security advisories or that are abandoned (can also be set via the COMPOSER_NO_SECURITY_BLOCKING=1 env var). Only applies when no lock file is present.", None).unwrap().into(), - InputOption::new("no-autoloader", None, Some(InputOption::VALUE_NONE), "Skips autoloader generation", None).unwrap().into(), - InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(), - InputOption::new("no-install", None, Some(InputOption::VALUE_NONE), "Do not use, only defined here to catch misuse of the install command.", None).unwrap().into(), - InputOption::new("audit", None, Some(InputOption::VALUE_NONE), "Run an audit after installation is complete.", None).unwrap().into(), - InputOption::new("audit-format", None, Some(InputOption::VALUE_REQUIRED), "Audit output format. Must be \"table\", \"plain\", \"json\", or \"summary\".", Some(PhpMixed::String(Auditor::FORMAT_SUMMARY.to_string()))).unwrap().into(), - InputOption::new("verbose", Some(PhpMixed::String("v|vv|vvv".to_string())), Some(InputOption::VALUE_NONE), "Shows more details including new commits pulled in when updating packages.", None).unwrap().into(), - InputOption::new("optimize-autoloader", Some(PhpMixed::String("o".to_string())), Some(InputOption::VALUE_NONE), "Optimize autoloader during autoloader dump", None).unwrap().into(), - InputOption::new("classmap-authoritative", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.", None).unwrap().into(), - InputOption::new("apcu-autoloader", None, Some(InputOption::VALUE_NONE), "Use APCu to cache found/not-found classes.", None).unwrap().into(), - InputOption::new("apcu-autoloader-prefix", None, Some(InputOption::VALUE_REQUIRED), "Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader", None).unwrap().into(), - InputOption::new("ignore-platform-req", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Ignore a specific platform requirement (php & ext- packages).", None).unwrap().into(), - InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages).", None).unwrap().into(), - InputArgument::new("packages", Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), "Should not be provided, use composer require instead to add a given package to composer.json.", None).unwrap().into(), - ]) - .set_help( - "The <info>install</info> command reads the composer.lock file from\n\ - the current directory, processes it, and downloads and installs all the\n\ - libraries and dependencies outlined in that file. If the file does not\n\ - exist it will look for composer.json and do the same.\n\n\ - <info>php composer.phar install</info>\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#install-i" - ); + self.set_name("install")?; + self.set_aliases(vec!["i".to_string()])?; + self.set_description("Installs the project dependencies from the composer.lock file if present, or falls back on the composer.json"); + self.set_definition(&[ + InputOption::new("prefer-source", None, Some(InputOption::VALUE_NONE), "Forces installation from package sources when possible, including VCS information.", None).unwrap().into(), + InputOption::new("prefer-dist", None, Some(InputOption::VALUE_NONE), "Forces installation from package dist (default behavior).", None).unwrap().into(), + InputOption::new("prefer-install", None, Some(InputOption::VALUE_REQUIRED), "Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).", None).unwrap().into(), + InputOption::new("dry-run", None, Some(InputOption::VALUE_NONE), "Outputs the operations but will not execute anything (implicitly enables --verbose).", None).unwrap().into(), + InputOption::new("download-only", None, Some(InputOption::VALUE_NONE), "Download only, do not install packages.", None).unwrap().into(), + InputOption::new("dev", None, Some(InputOption::VALUE_NONE), "DEPRECATED: Enables installation of require-dev packages (enabled by default, only present for BC).", None).unwrap().into(), + InputOption::new("no-suggest", None, Some(InputOption::VALUE_NONE), "DEPRECATED: This flag does not exist anymore.", None).unwrap().into(), + InputOption::new("no-dev", None, Some(InputOption::VALUE_NONE), "Disables installation of require-dev packages.", None).unwrap().into(), + InputOption::new("no-security-blocking", None, Some(InputOption::VALUE_NONE), "Allows installing packages with security advisories or that are abandoned (can also be set via the COMPOSER_NO_SECURITY_BLOCKING=1 env var). Only applies when no lock file is present.", None).unwrap().into(), + InputOption::new("no-autoloader", None, Some(InputOption::VALUE_NONE), "Skips autoloader generation", None).unwrap().into(), + InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(), + InputOption::new("no-install", None, Some(InputOption::VALUE_NONE), "Do not use, only defined here to catch misuse of the install command.", None).unwrap().into(), + InputOption::new("audit", None, Some(InputOption::VALUE_NONE), "Run an audit after installation is complete.", None).unwrap().into(), + InputOption::new("audit-format", None, Some(InputOption::VALUE_REQUIRED), "Audit output format. Must be \"table\", \"plain\", \"json\", or \"summary\".", Some(PhpMixed::String(Auditor::FORMAT_SUMMARY.to_string()))).unwrap().into(), + InputOption::new("verbose", Some(PhpMixed::String("v|vv|vvv".to_string())), Some(InputOption::VALUE_NONE), "Shows more details including new commits pulled in when updating packages.", None).unwrap().into(), + InputOption::new("optimize-autoloader", Some(PhpMixed::String("o".to_string())), Some(InputOption::VALUE_NONE), "Optimize autoloader during autoloader dump", None).unwrap().into(), + InputOption::new("classmap-authoritative", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.", None).unwrap().into(), + InputOption::new("apcu-autoloader", None, Some(InputOption::VALUE_NONE), "Use APCu to cache found/not-found classes.", None).unwrap().into(), + InputOption::new("apcu-autoloader-prefix", None, Some(InputOption::VALUE_REQUIRED), "Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader", None).unwrap().into(), + InputOption::new("ignore-platform-req", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Ignore a specific platform requirement (php & ext- packages).", None).unwrap().into(), + InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages).", None).unwrap().into(), + InputArgument::new("packages", Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), "Should not be provided, use composer require instead to add a given package to composer.json.", None).unwrap().into(), + ]); + self.set_help( + "The <info>install</info> command reads the composer.lock file from\n\ + the current directory, processes it, and downloads and installs all the\n\ + libraries and dependencies outlined in that file. If the file does not\n\ + exist it will look for composer.json and do the same.\n\n\ + <info>php composer.phar install</info>\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#install-i", + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let io = self.get_io().clone(); if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { @@ -242,14 +264,24 @@ impl InstallCommand { install.run() } -} -impl HasBaseCommandData for InstallCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for InstallCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/licenses_command.rs b/crates/shirabe/src/command/licenses_command.rs index 5be2b41..56aa042 100644 --- a/crates/shirabe/src/command/licenses_command.rs +++ b/crates/shirabe/src/command/licenses_command.rs @@ -4,6 +4,7 @@ use std::any::Any; use anyhow::Result; use indexmap::IndexMap; +use shirabe_external_packages::symfony::console::command::command::Command; use shirabe_external_packages::symfony::console::formatter::OutputFormatter; use shirabe_external_packages::symfony::console::helper::Table; use shirabe_external_packages::symfony::console::input::InputInterface; @@ -11,9 +12,16 @@ use shirabe_external_packages::symfony::console::output::OutputInterface; use shirabe_external_packages::symfony::console::style::StyleInterface; use shirabe_external_packages::symfony::console::style::SymfonyStyle; use shirabe_php_shim::{PhpMixed, RuntimeException, UnexpectedValueException}; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::json::JsonFile; @@ -34,52 +42,65 @@ pub struct LicensesCommand { } impl LicensesCommand { - pub fn configure(&mut self) { - self.set_name("licenses") - .set_description("Shows information about licenses of dependencies") - .set_definition(&[ - InputOption::new( - "format", - Some(PhpMixed::String("f".to_string())), - Some(InputOption::VALUE_REQUIRED), - "Format of the output: text, json or summary", - Some(PhpMixed::String("text".to_string())), - ) - .unwrap() - .into(), - InputOption::new( - "no-dev", - None, - Some(InputOption::VALUE_NONE), - "Disables search in require-dev packages.", - None, - ) - .unwrap() - .into(), - InputOption::new( - "locked", - None, - Some(InputOption::VALUE_NONE), - "Shows licenses from the lock file instead of installed packages.", - None, - ) - .unwrap() - .into(), - ]) - .set_help( - "The license command displays detailed information about the licenses of\n\ - the installed dependencies.\n\n\ - Use --locked to show licenses from composer.lock instead of what's currently\n\ - installed in the vendor directory.\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#licenses", - ); + pub fn new() -> Self { + let mut command = LicensesCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("LicensesCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for LicensesCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("licenses")?; + self.set_description("Shows information about licenses of dependencies"); + self.set_definition(&[ + InputOption::new( + "format", + Some(PhpMixed::String("f".to_string())), + Some(InputOption::VALUE_REQUIRED), + "Format of the output: text, json or summary", + Some(PhpMixed::String("text".to_string())), + ) + .unwrap() + .into(), + InputOption::new( + "no-dev", + None, + Some(InputOption::VALUE_NONE), + "Disables search in require-dev packages.", + None, + ) + .unwrap() + .into(), + InputOption::new( + "locked", + None, + Some(InputOption::VALUE_NONE), + "Shows licenses from the lock file instead of installed packages.", + None, + ) + .unwrap() + .into(), + ]); + self.set_help( + "The license command displays detailed information about the licenses of\n\ + the installed dependencies.\n\n\ + Use --locked to show licenses from composer.lock instead of what's currently\n\ + installed in the vendor directory.\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#licenses", + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let composer = self.require_composer(None, None)?; let mut composer = crate::command::composer_full_mut(&composer); @@ -339,14 +360,24 @@ impl LicensesCommand { Ok(0) } -} -impl HasBaseCommandData for LicensesCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for LicensesCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/outdated_command.rs b/crates/shirabe/src/command/outdated_command.rs index c7ea773..0627901 100644 --- a/crates/shirabe/src/command/outdated_command.rs +++ b/crates/shirabe/src/command/outdated_command.rs @@ -1,15 +1,25 @@ //! ref: composer/src/Composer/Command/OutdatedCommand.php -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; -use crate::console::input::InputArgument; -use crate::console::input::InputOption; -use crate::io::IOInterface; use anyhow::Result; use indexmap::IndexMap; +use shirabe_external_packages::symfony::console::command::command::Command; use shirabe_external_packages::symfony::console::input::ArrayInput; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::OutputInterface; use shirabe_php_shim::PhpMixed; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::advisory::AuditConfig; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; +use crate::composer::PartialComposerHandle; +use crate::config::Config; +use crate::console::input::InputArgument; +use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; +use crate::io::IOInterface; #[derive(Debug)] pub struct OutdatedCommand { @@ -17,13 +27,24 @@ pub struct OutdatedCommand { } impl OutdatedCommand { - pub fn configure(&mut self) { + pub fn new() -> Self { + let mut command = OutdatedCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("OutdatedCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for OutdatedCommand { + fn configure(&mut self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_installed_package(false, false) for `package` argument and `--ignore` option - self - .set_name("outdated") - .set_description("Shows a list of installed packages that have updates available, including their latest version") - .set_definition(&[ - InputArgument::new("package", Some(InputArgument::OPTIONAL), "Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.", None).unwrap().into(), + self.set_name("outdated")?; + self.set_description("Shows a list of installed packages that have updates available, including their latest version"); + self.set_definition(&[ + InputArgument::new("package", Some(InputArgument::OPTIONAL), "Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.", None).unwrap().into(), InputOption::new("outdated", Some(PhpMixed::String("o".to_string())), Some(InputOption::VALUE_NONE), "Show only packages that are outdated (this is the default, but present here for compat with `show`", None).unwrap().into(), InputOption::new("all", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Show all installed packages with their latest versions", None).unwrap().into(), InputOption::new("locked", None, Some(InputOption::VALUE_NONE), "Shows updates for packages from the lock file, regardless of what is currently in vendor dir", None).unwrap().into(), @@ -38,24 +59,25 @@ impl OutdatedCommand { InputOption::new("no-dev", None, Some(InputOption::VALUE_NONE), "Disables search in require-dev packages.", None).unwrap().into(), InputOption::new("ignore-platform-req", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Ignore a specific platform requirement (php & ext- packages). Use with the --outdated option", None).unwrap().into(), InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages). Use with the --outdated option", None).unwrap().into(), - ]) - .set_help( - "The outdated command is just a proxy for `composer show -l`\n\n\ - The color coding (or signage if you have ANSI colors disabled) for dependency versions is as such:\n\n\ - - <info>green</info> (=): Dependency is in the latest version and is up to date.\n\ - - <comment>yellow</comment> (~): Dependency has a new version available that includes backwards\n \ - compatibility breaks according to semver, so upgrade when you can but it\n \ - may involve work.\n\ - - <highlight>red</highlight> (!): Dependency has a new version that is semver-compatible and you should upgrade it.\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#outdated" - ); + ]); + self.set_help( + "The outdated command is just a proxy for `composer show -l`\n\n\ + The color coding (or signage if you have ANSI colors disabled) for dependency versions is as such:\n\n\ + - <info>green</info> (=): Dependency is in the latest version and is up to date.\n\ + - <comment>yellow</comment> (~): Dependency has a new version available that includes backwards\n \ + compatibility breaks according to semver, so upgrade when you can but it\n \ + may involve work.\n\ + - <highlight>red</highlight> (!): Dependency has a new version that is semver-compatible and you should upgrade it.\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#outdated" + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let mut args: IndexMap<String, PhpMixed> = IndexMap::new(); args.insert("command".to_string(), PhpMixed::String("show".to_string())); args.insert("--latest".to_string(), PhpMixed::Bool(true)); @@ -185,22 +207,34 @@ impl OutdatedCommand { None, )?; - let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = - std::rc::Rc::new(std::cell::RefCell::new(input)); - self.get_application()?.run(Some(input), Some(output)) + let input: Rc<RefCell<dyn InputInterface>> = Rc::new(RefCell::new(input)); + // TODO(phase-c): proxying to ShowCommand via Application::run needs the shared shirabe + // Application handle (deferred with the Application shared-ownership work and registration). + let _ = (input, output); + todo!("outdated command proxy run pending shared Application handle") } - pub fn is_proxy_command(&self) -> bool { - true + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); } -impl HasBaseCommandData for OutdatedCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data +impl BaseCommand for OutdatedCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + fn is_proxy_command(&self) -> bool { + true } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/prohibits_command.rs b/crates/shirabe/src/command/prohibits_command.rs index 0df87fd..02aaf33 100644 --- a/crates/shirabe/src/command/prohibits_command.rs +++ b/crates/shirabe/src/command/prohibits_command.rs @@ -1,12 +1,24 @@ //! ref: composer/src/Composer/Command/ProhibitsCommand.php +use anyhow::Result; +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_shim::PhpMixed; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::advisory::AuditConfig; use crate::command::BaseDependencyCommand; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; -use shirabe_external_packages::symfony::console::input::InputInterface; -use shirabe_external_packages::symfony::console::output::OutputInterface; #[derive(Debug)] pub struct ProhibitsCommand { @@ -16,69 +28,15 @@ pub struct ProhibitsCommand { } impl ProhibitsCommand { - pub fn configure(&mut self) { - // TODO(cli-completion): suggest_available_package() for `package` argument - self.set_name("prohibits") - .set_aliases(&["why-not".to_string()]) - .set_description("Shows which packages prevent the given package from being installed") - .set_definition(&[ - InputArgument::new( - <Self as BaseDependencyCommand>::ARGUMENT_PACKAGE, - Some(InputArgument::REQUIRED), - "Package to inspect", - None, - ) - .unwrap() - .into(), - InputArgument::new( - <Self as BaseDependencyCommand>::ARGUMENT_CONSTRAINT, - Some(InputArgument::REQUIRED), - "Version constraint, which version you expected to be installed", - None, - ) - .unwrap() - .into(), - InputOption::new( - <Self as BaseDependencyCommand>::OPTION_RECURSIVE, - Some(shirabe_php_shim::PhpMixed::String("r".to_string())), - Some(InputOption::VALUE_NONE), - "Recursively resolves up to the root package", - None, - ) - .unwrap() - .into(), - InputOption::new( - <Self as BaseDependencyCommand>::OPTION_TREE, - Some(shirabe_php_shim::PhpMixed::String("t".to_string())), - Some(InputOption::VALUE_NONE), - "Prints the results as a nested tree", - None, - ) - .unwrap() - .into(), - InputOption::new( - "locked", - None, - Some(InputOption::VALUE_NONE), - "Read dependency information from composer.lock", - None, - ) - .unwrap() - .into(), - ]) - .set_help( - "Displays detailed information about why a package cannot be installed.\n\n\ - <info>php composer.phar prohibits composer/composer</info>\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#prohibits-why-not", - ); - } - - pub fn execute( - &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<i64> { - self.do_execute(input, output, true) + pub fn new() -> Self { + let mut command = ProhibitsCommand { + base_command_data: BaseCommandData::new(None), + colors: Vec::new(), + }; + command + .configure() + .expect("ProhibitsCommand::configure uses static, valid metadata"); + command } } @@ -92,12 +50,90 @@ impl BaseDependencyCommand for ProhibitsCommand { } } -impl HasBaseCommandData for ProhibitsCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data +impl Command for ProhibitsCommand { + fn configure(&mut self) -> anyhow::Result<()> { + // TODO(cli-completion): suggest_available_package() for `package` argument + self.set_name("prohibits")?; + self.set_aliases(vec!["why-not".to_string()])?; + self.set_description("Shows which packages prevent the given package from being installed"); + self.set_definition(&[ + InputArgument::new( + <Self as BaseDependencyCommand>::ARGUMENT_PACKAGE, + Some(InputArgument::REQUIRED), + "Package to inspect", + None, + ) + .unwrap() + .into(), + InputArgument::new( + <Self as BaseDependencyCommand>::ARGUMENT_CONSTRAINT, + Some(InputArgument::REQUIRED), + "Version constraint, which version you expected to be installed", + None, + ) + .unwrap() + .into(), + InputOption::new( + <Self as BaseDependencyCommand>::OPTION_RECURSIVE, + Some(shirabe_php_shim::PhpMixed::String("r".to_string())), + Some(InputOption::VALUE_NONE), + "Recursively resolves up to the root package", + None, + ) + .unwrap() + .into(), + InputOption::new( + <Self as BaseDependencyCommand>::OPTION_TREE, + Some(shirabe_php_shim::PhpMixed::String("t".to_string())), + Some(InputOption::VALUE_NONE), + "Prints the results as a nested tree", + None, + ) + .unwrap() + .into(), + InputOption::new( + "locked", + None, + Some(InputOption::VALUE_NONE), + "Read dependency information from composer.lock", + None, + ) + .unwrap() + .into(), + ]); + self.set_help( + "Displays detailed information about why a package cannot be installed.\n\n\ + <info>php composer.phar prohibits composer/composer</info>\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#prohibits-why-not", + ); + Ok(()) + } + + fn execute( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + self.do_execute(input, output, true) + } + + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for ProhibitsCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/reinstall_command.rs b/crates/shirabe/src/command/reinstall_command.rs index 5053492..9c71f01 100644 --- a/crates/shirabe/src/command/reinstall_command.rs +++ b/crates/shirabe/src/command/reinstall_command.rs @@ -3,18 +3,29 @@ use std::any::Any; use anyhow::Result; +use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; +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_shim::InvalidArgumentException; +use shirabe_php_shim::PhpMixed; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputDefinitionItem; use crate::console::input::InputOption; use crate::dependency_resolver::Transaction; use crate::dependency_resolver::operation::InstallOperation; use crate::dependency_resolver::operation::UninstallOperation; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::package::base_package; @@ -29,41 +40,53 @@ pub struct ReinstallCommand { } impl ReinstallCommand { - pub fn configure(&mut self) { + pub fn new() -> Self { + let mut command = ReinstallCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("ReinstallCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for ReinstallCommand { + fn configure(&mut self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_prefer_install / suggest_installed_package_types / suggest_installed_package - self - .set_name("reinstall") - .set_description("Uninstalls and reinstalls the given package names") - .set_definition(&[ - InputOption::new("prefer-source", None, Some(InputOption::VALUE_NONE), "Forces installation from package sources when possible, including VCS information.", None).unwrap().into(), - InputOption::new("prefer-dist", None, Some(InputOption::VALUE_NONE), "Forces installation from package dist (default behavior).", None).unwrap().into(), - InputOption::new("prefer-install", None, Some(InputOption::VALUE_REQUIRED), "Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).", None).unwrap().into(), - InputOption::new("no-autoloader", None, Some(InputOption::VALUE_NONE), "Skips autoloader generation", None).unwrap().into(), - InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(), - InputOption::new("optimize-autoloader", Some(shirabe_php_shim::PhpMixed::String("o".to_string())), Some(InputOption::VALUE_NONE), "Optimize autoloader during autoloader dump", None).unwrap().into(), - InputOption::new("classmap-authoritative", Some(shirabe_php_shim::PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.", None).unwrap().into(), - InputOption::new("apcu-autoloader", None, Some(InputOption::VALUE_NONE), "Use APCu to cache found/not-found classes.", None).unwrap().into(), - InputOption::new("apcu-autoloader-prefix", None, Some(InputOption::VALUE_REQUIRED), "Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader", None).unwrap().into(), - InputOption::new("ignore-platform-req", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Ignore a specific platform requirement (php & ext- packages).", None).unwrap().into(), - InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages).", None).unwrap().into(), - InputOption::new("type", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Filter packages to reinstall by type(s)", None).unwrap().into(), - InputArgument::new("packages", Some(InputArgument::IS_ARRAY), "List of package names to reinstall, can include a wildcard (*) to match any substring.", None).unwrap().into(), - ]) - .set_help( - "The <info>reinstall</info> command looks up installed packages by name,\n\ - uninstalls them and reinstalls them. This lets you do a clean install\n\ - of a package if you messed with its files, or if you wish to change\n\ - the installation type using --prefer-install.\n\n\ - <info>php composer.phar reinstall acme/foo \"acme/bar-*\"</info>\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#reinstall" - ); + self.set_name("reinstall")?; + self.set_description("Uninstalls and reinstalls the given package names"); + self.set_definition(&[ + InputOption::new("prefer-source", None, Some(InputOption::VALUE_NONE), "Forces installation from package sources when possible, including VCS information.", None).unwrap().into(), + InputOption::new("prefer-dist", None, Some(InputOption::VALUE_NONE), "Forces installation from package dist (default behavior).", None).unwrap().into(), + InputOption::new("prefer-install", None, Some(InputOption::VALUE_REQUIRED), "Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).", None).unwrap().into(), + InputOption::new("no-autoloader", None, Some(InputOption::VALUE_NONE), "Skips autoloader generation", None).unwrap().into(), + InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(), + InputOption::new("optimize-autoloader", Some(shirabe_php_shim::PhpMixed::String("o".to_string())), Some(InputOption::VALUE_NONE), "Optimize autoloader during autoloader dump", None).unwrap().into(), + InputOption::new("classmap-authoritative", Some(shirabe_php_shim::PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.", None).unwrap().into(), + InputOption::new("apcu-autoloader", None, Some(InputOption::VALUE_NONE), "Use APCu to cache found/not-found classes.", None).unwrap().into(), + InputOption::new("apcu-autoloader-prefix", None, Some(InputOption::VALUE_REQUIRED), "Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader", None).unwrap().into(), + InputOption::new("ignore-platform-req", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Ignore a specific platform requirement (php & ext- packages).", None).unwrap().into(), + InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages).", None).unwrap().into(), + InputOption::new("type", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Filter packages to reinstall by type(s)", None).unwrap().into(), + InputArgument::new("packages", Some(InputArgument::IS_ARRAY), "List of package names to reinstall, can include a wildcard (*) to match any substring.", None).unwrap().into(), + ]); + self.set_help( + "The <info>reinstall</info> command looks up installed packages by name,\n\ + uninstalls them and reinstalls them. This lets you do a clean install\n\ + of a package if you messed with its files, or if you wish to change\n\ + the installation type using --prefer-install.\n\n\ + <info>php composer.phar reinstall acme/foo \"acme/bar-*\"</info>\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#reinstall", + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let composer = self.require_composer(None, None)?; let composer = crate::command::composer_full(&composer); let io = self.get_io(); @@ -319,14 +342,24 @@ impl ReinstallCommand { Ok(0) } -} -impl HasBaseCommandData for ReinstallCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for ReinstallCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/remove_command.rs b/crates/shirabe/src/command/remove_command.rs index 3604c90..1ee32ac 100644 --- a/crates/shirabe/src/command/remove_command.rs +++ b/crates/shirabe/src/command/remove_command.rs @@ -1,14 +1,22 @@ //! ref: composer/src/Composer/Command/RemoveCommand.php +use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; +use shirabe_external_packages::symfony::console::command::command::Command; use shirabe_external_packages::symfony::console::exception::InvalidArgumentException; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::OutputInterface; use shirabe_php_shim::{PhpMixed, UnexpectedValueException, array_map, strtolower}; +use std::cell::RefCell; +use std::rc::Rc; +use crate::advisory::AuditConfig; use crate::advisory::Auditor; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::config::ConfigSourceInterface; use crate::config::JsonConfigSource; use crate::console::input::InputArgument; @@ -16,6 +24,7 @@ use crate::console::input::InputOption; use crate::dependency_resolver::Request; use crate::dependency_resolver::UpdateAllowTransitiveDeps; use crate::factory::Factory; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::installer::Installer; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; @@ -31,13 +40,24 @@ pub struct RemoveCommand { } impl RemoveCommand { - pub fn configure(&mut self) { + pub fn new() -> Self { + let mut command = RemoveCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("RemoveCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for RemoveCommand { + fn configure(&mut self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_root_requirement() for `packages` argument - self - .set_name("remove") - .set_aliases(&["rm".to_string(), "uninstall".to_string()]) - .set_description("Removes a package from the require or require-dev") - .set_definition(&[ + self.set_name("remove")?; + self.set_aliases(vec!["rm".to_string(), "uninstall".to_string()])?; + self.set_description("Removes a package from the require or require-dev"); + self.set_definition(&[ InputArgument::new("packages", Some(InputArgument::IS_ARRAY), "Packages that should be removed.", @@ -147,19 +167,20 @@ impl RemoveCommand { Some(InputOption::VALUE_REQUIRED), "Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader", None,).unwrap().into(), - ]) - .set_help( - "The <info>remove</info> command removes a package from the current\n\ + ]); + self.set_help( + "The <info>remove</info> command removes a package from the current\n\ list of installed packages\n\n\ <info>php composer.phar remove</info>\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#remove-rm" - ); + Read more at https://getcomposer.org/doc/03-cli.md#remove-rm", + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { if input .borrow() @@ -693,14 +714,24 @@ impl RemoveCommand { Ok(status) } -} -impl HasBaseCommandData for RemoveCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for RemoveCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/repository_command.rs b/crates/shirabe/src/command/repository_command.rs index 417266f..284b523 100644 --- a/crates/shirabe/src/command/repository_command.rs +++ b/crates/shirabe/src/command/repository_command.rs @@ -1,21 +1,28 @@ //! ref: composer/src/Composer/Command/RepositoryCommand.php +use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; +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_shim::{ InvalidArgumentException, PHP_URL_HOST, PhpMixed, RuntimeException, parse_url, strtolower, }; +use std::cell::RefCell; +use std::rc::Rc; +use crate::advisory::AuditConfig; use crate::command::BaseConfigCommand; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::{BaseCommand, BaseCommandData}; +use crate::composer::PartialComposerHandle; use crate::config::Config; use crate::config::ConfigSourceInterface; use crate::config::JsonConfigSource; use crate::console::input::InputArgument; use crate::console::input::InputOption; use crate::factory::Factory; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::json::JsonFile; @@ -30,43 +37,201 @@ pub struct RepositoryCommand { } impl RepositoryCommand { - pub fn configure(&mut self) { + pub fn new() -> Self { + let mut command = RepositoryCommand { + base_command_data: BaseCommandData::new(None), + config: None, + config_file: None, + config_source: None, + }; + command + .configure() + .expect("RepositoryCommand::configure uses static, valid metadata"); + command + } + + fn list_repositories(&mut self, mut repos: IndexMap<String, PhpMixed>) { + let io = self.get_io(); + + let mut packagist_present = false; + for (_key, repo) in &repos { + if let PhpMixed::Array(ref repo_map) = *repo { + let has_type_and_url = + repo_map.contains_key("type") && repo_map.contains_key("url"); + let is_composer_type = + repo_map.get("type").and_then(|v| v.as_string()) == Some("composer"); + let url_host_ends_with_packagist = repo_map + .get("url") + .and_then(|v| v.as_string()) + .map(|url| { + parse_url(url, PHP_URL_HOST) + .as_string() + .unwrap_or("") + .ends_with("packagist.org") + }) + .unwrap_or(false); + if has_type_and_url && is_composer_type && url_host_ends_with_packagist { + packagist_present = true; + break; + } + } + } + if !packagist_present { + let mut packagist_entry = IndexMap::new(); + packagist_entry.insert("packagist.org".to_string(), Box::new(PhpMixed::Bool(false))); + repos.insert(repos.len().to_string(), PhpMixed::Array(packagist_entry)); + } + + if repos.is_empty() { + io.write("No repositories configured"); + return; + } + + for (key, repo) in &repos { + if matches!(*repo, PhpMixed::Bool(false)) { + io.write(&format!("[{}] <info>disabled</info>", key)); + continue; + } + + if let PhpMixed::Array(ref repo_map) = *repo { + if repo_map.len() == 1 { + if let Some(first_val) = repo_map.values().next() { + if matches!(**first_val, PhpMixed::Bool(false)) { + let first_key = repo_map.keys().next().unwrap(); + io.write(&format!("[{}] <info>disabled</info>", first_key)); + continue; + } + } + } + + let name = repo_map + .get("name") + .and_then(|v| v.as_string()) + .unwrap_or(key.as_str()); + let r#type = repo_map + .get("type") + .and_then(|v| v.as_string()) + .unwrap_or("unknown"); + let url = repo_map + .get("url") + .and_then(|v| v.as_string()) + .map(|s| s.to_string()) + .unwrap_or_else(|| JsonFile::encode(repo)); + io.write(&format!("[{}] <info>{}</info> {}", name, r#type, url)); + } + } + } + + // TODO(cli-completion): fn suggest_type_for_add() + // TODO(cli-completion): fn suggest_repo_names(&self) +} + +impl Command for RepositoryCommand { + fn configure(&mut self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_repo_names() / suggest_type_for_add() - self - .set_name("repository") - .set_aliases(&["repo".to_string()]) - .set_description("Manages repositories") - .set_definition(&[ - InputOption::new("global", Some(PhpMixed::String("g".to_string())), Some(InputOption::VALUE_NONE), "Apply command to the global config file", None).unwrap().into(), - InputOption::new("file", Some(PhpMixed::String("f".to_string())), Some(InputOption::VALUE_REQUIRED), "If you want to choose a different composer.json or config.json", None).unwrap().into(), - InputOption::new("append", None, Some(InputOption::VALUE_NONE), "When adding a repository, append it (lower priority) instead of prepending it", None).unwrap().into(), - InputOption::new("before", None, Some(InputOption::VALUE_REQUIRED), "When adding a repository, insert it before the given repository name", None).unwrap().into(), - InputOption::new("after", None, Some(InputOption::VALUE_REQUIRED), "When adding a repository, insert it after the given repository name", None).unwrap().into(), - InputArgument::new("action", Some(InputArgument::OPTIONAL), "Action to perform: list, add, remove, set-url, get-url, enable, disable", Some(PhpMixed::String("list".to_string()))).unwrap().into(), - InputArgument::new("name", Some(InputArgument::OPTIONAL), "Repository name (or special name packagist.org for enable/disable)", None).unwrap().into(), - InputArgument::new("arg1", Some(InputArgument::OPTIONAL), "Type for add, or new URL for set-url, or JSON config for add", None).unwrap().into(), - InputArgument::new("arg2", Some(InputArgument::OPTIONAL), "URL for add (if not using JSON)", None).unwrap().into(), - ]) - .set_help( - "This command lets you manage repositories in your composer.json.\n\n\ - Examples:\n \ - composer repo list\n \ - composer repo add foo vcs https://github.com/acme/foo\n \ - composer repo add bar composer https://repo.packagist.com/bar\n \ - composer repo add zips '{\"type\":\"artifact\",\"url\":\"/path/to/dir/with/zips\"}'\n \ - composer repo add baz vcs https://example.org --before foo\n \ - composer repo add qux vcs https://example.org --after bar\n \ - composer repo remove foo\n \ - composer repo set-url foo https://git.example.org/acme/foo\n \ - composer repo get-url foo\n \ - composer repo disable packagist.org\n \ - composer repo enable packagist.org\n\n\ - Use --global/-g to alter the global config.json instead.\n\ - Use --file to alter a specific file." - ); + self.set_name("repository")?; + self.set_aliases(vec!["repo".to_string()])?; + self.set_description("Manages repositories"); + self.set_definition(&[ + InputOption::new( + "global", + Some(PhpMixed::String("g".to_string())), + Some(InputOption::VALUE_NONE), + "Apply command to the global config file", + None, + ) + .unwrap() + .into(), + InputOption::new( + "file", + Some(PhpMixed::String("f".to_string())), + Some(InputOption::VALUE_REQUIRED), + "If you want to choose a different composer.json or config.json", + None, + ) + .unwrap() + .into(), + InputOption::new( + "append", + None, + Some(InputOption::VALUE_NONE), + "When adding a repository, append it (lower priority) instead of prepending it", + None, + ) + .unwrap() + .into(), + InputOption::new( + "before", + None, + Some(InputOption::VALUE_REQUIRED), + "When adding a repository, insert it before the given repository name", + None, + ) + .unwrap() + .into(), + InputOption::new( + "after", + None, + Some(InputOption::VALUE_REQUIRED), + "When adding a repository, insert it after the given repository name", + None, + ) + .unwrap() + .into(), + InputArgument::new( + "action", + Some(InputArgument::OPTIONAL), + "Action to perform: list, add, remove, set-url, get-url, enable, disable", + Some(PhpMixed::String("list".to_string())), + ) + .unwrap() + .into(), + InputArgument::new( + "name", + Some(InputArgument::OPTIONAL), + "Repository name (or special name packagist.org for enable/disable)", + None, + ) + .unwrap() + .into(), + InputArgument::new( + "arg1", + Some(InputArgument::OPTIONAL), + "Type for add, or new URL for set-url, or JSON config for add", + None, + ) + .unwrap() + .into(), + InputArgument::new( + "arg2", + Some(InputArgument::OPTIONAL), + "URL for add (if not using JSON)", + None, + ) + .unwrap() + .into(), + ]); + self.set_help( + "This command lets you manage repositories in your composer.json.\n\n\ + Examples:\n \ + composer repo list\n \ + composer repo add foo vcs https://github.com/acme/foo\n \ + composer repo add bar composer https://repo.packagist.com/bar\n \ + composer repo add zips '{\"type\":\"artifact\",\"url\":\"/path/to/dir/with/zips\"}'\n \ + composer repo add baz vcs https://example.org --before foo\n \ + composer repo add qux vcs https://example.org --after bar\n \ + composer repo remove foo\n \ + composer repo set-url foo https://git.example.org/acme/foo\n \ + composer repo get-url foo\n \ + composer repo disable packagist.org\n \ + composer repo enable packagist.org\n\n\ + Use --global/-g to alter the global config.json instead.\n\ + Use --file to alter a specific file.", + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, @@ -336,80 +501,27 @@ impl RepositoryCommand { } } - fn list_repositories(&mut self, mut repos: IndexMap<String, PhpMixed>) { - let io = self.get_io(); - - let mut packagist_present = false; - for (_key, repo) in &repos { - if let PhpMixed::Array(ref repo_map) = *repo { - let has_type_and_url = - repo_map.contains_key("type") && repo_map.contains_key("url"); - let is_composer_type = - repo_map.get("type").and_then(|v| v.as_string()) == Some("composer"); - let url_host_ends_with_packagist = repo_map - .get("url") - .and_then(|v| v.as_string()) - .map(|url| { - parse_url(url, PHP_URL_HOST) - .as_string() - .unwrap_or("") - .ends_with("packagist.org") - }) - .unwrap_or(false); - if has_type_and_url && is_composer_type && url_host_ends_with_packagist { - packagist_present = true; - break; - } - } - } - if !packagist_present { - let mut packagist_entry = IndexMap::new(); - packagist_entry.insert("packagist.org".to_string(), Box::new(PhpMixed::Bool(false))); - repos.insert(repos.len().to_string(), PhpMixed::Array(packagist_entry)); - } - - if repos.is_empty() { - io.write("No repositories configured"); - return; - } - - for (key, repo) in &repos { - if matches!(*repo, PhpMixed::Bool(false)) { - io.write(&format!("[{}] <info>disabled</info>", key)); - continue; - } + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + <Self as crate::command::base_config_command::BaseConfigCommand>::initialize( + self, input, output, + ) + } - if let PhpMixed::Array(ref repo_map) = *repo { - if repo_map.len() == 1 { - if let Some(first_val) = repo_map.values().next() { - if matches!(**first_val, PhpMixed::Bool(false)) { - let first_key = repo_map.keys().next().unwrap(); - io.write(&format!("[{}] <info>disabled</info>", first_key)); - continue; - } - } - } + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} - let name = repo_map - .get("name") - .and_then(|v| v.as_string()) - .unwrap_or(key.as_str()); - let r#type = repo_map - .get("type") - .and_then(|v| v.as_string()) - .unwrap_or("unknown"); - let url = repo_map - .get("url") - .and_then(|v| v.as_string()) - .map(|s| s.to_string()) - .unwrap_or_else(|| JsonFile::encode(repo)); - io.write(&format!("[{}] <info>{}</info> {}", name, r#type, url)); - } - } +impl BaseCommand for RepositoryCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } - // TODO(cli-completion): fn suggest_type_for_add() - // TODO(cli-completion): fn suggest_repo_names(&self) + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } impl BaseConfigCommand for RepositoryCommand { @@ -441,13 +553,3 @@ impl BaseConfigCommand for RepositoryCommand { self.config_source = source; } } - -impl HasBaseCommandData for RepositoryCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data - } - - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } -} diff --git a/crates/shirabe/src/command/require_command.rs b/crates/shirabe/src/command/require_command.rs index e1cbba4..a82f858 100644 --- a/crates/shirabe/src/command/require_command.rs +++ b/crates/shirabe/src/command/require_command.rs @@ -5,6 +5,7 @@ use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; use shirabe_external_packages::seld::signal::SignalHandler; +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_shim::{ @@ -12,16 +13,22 @@ use shirabe_php_shim::{ array_keys, array_map, array_merge, array_unique, count, empty, file_exists, file_get_contents, file_put_contents, filesize, implode, is_writable, sprintf, strtolower, unlink, }; +use std::cell::RefCell; +use std::rc::Rc; +use crate::advisory::AuditConfig; use crate::advisory::Auditor; use crate::command::PackageDiscoveryTrait; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; use crate::dependency_resolver::Request; use crate::dependency_resolver::UpdateAllowTransitiveDeps; use crate::factory::Factory; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::installer::Installer; use crate::installer::InstallerEvents; use crate::io::IOInterface; @@ -60,6 +67,26 @@ pub struct RequireCommand { dependency_resolution_completed: bool, } +impl RequireCommand { + pub fn new() -> Self { + let mut command = RequireCommand { + base_command_data: BaseCommandData::new(None), + newly_created: false, + first_require: false, + json: None, + file: String::new(), + composer_backup: String::new(), + lock: String::new(), + lock_backup: None, + dependency_resolution_completed: false, + }; + command + .configure() + .expect("RequireCommand::configure uses static, valid metadata"); + command + } +} + impl PackageDiscoveryTrait for RequireCommand { fn get_repos_mut(&mut self) -> &mut Option<crate::repository::RepositoryInterfaceHandle> { todo!() @@ -101,60 +128,60 @@ impl PackageDiscoveryTrait for RequireCommand { } } -impl RequireCommand { - pub fn configure(&mut self) { +impl Command for RequireCommand { + fn configure(&mut self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_available_package_incl_platform / suggest_prefer_install - self - .set_name("require") - .set_aliases(&["r".to_string()]) - .set_description("Adds required packages to your composer.json and installs them") - .set_definition(&[ - InputArgument::new("packages", Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), "Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or \"foo/bar 1.0.0\"", None).unwrap().into(), - InputOption::new("dev", None, Some(InputOption::VALUE_NONE), "Add requirement to require-dev.", None).unwrap().into(), - InputOption::new("dry-run", None, Some(InputOption::VALUE_NONE), "Outputs the operations but will not execute anything (implicitly enables --verbose).", None).unwrap().into(), - InputOption::new("prefer-source", None, Some(InputOption::VALUE_NONE), "Forces installation from package sources when possible, including VCS information.", None).unwrap().into(), - InputOption::new("prefer-dist", None, Some(InputOption::VALUE_NONE), "Forces installation from package dist (default behavior).", None).unwrap().into(), - InputOption::new("prefer-install", None, Some(InputOption::VALUE_REQUIRED), "Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).", None).unwrap().into(), - InputOption::new("fixed", None, Some(InputOption::VALUE_NONE), "Write fixed version to the composer.json.", None).unwrap().into(), - InputOption::new("no-suggest", None, Some(InputOption::VALUE_NONE), "DEPRECATED: This flag does not exist anymore.", None).unwrap().into(), - InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(), - InputOption::new("no-update", None, Some(InputOption::VALUE_NONE), "Disables the automatic update of the dependencies (implies --no-install).", None).unwrap().into(), - InputOption::new("no-install", None, Some(InputOption::VALUE_NONE), "Skip the install step after updating the composer.lock file.", None).unwrap().into(), - InputOption::new("no-audit", None, Some(InputOption::VALUE_NONE), "Skip the audit step after updating the composer.lock file (can also be set via the COMPOSER_NO_AUDIT=1 env var).", None).unwrap().into(), - InputOption::new("audit-format", None, Some(InputOption::VALUE_REQUIRED), "Audit output format. Must be \"table\", \"plain\", \"json\", or \"summary\".", Some(PhpMixed::String(Auditor::FORMAT_SUMMARY.to_string()))).unwrap().into(), - InputOption::new("no-security-blocking", None, Some(InputOption::VALUE_NONE), "Allows installing packages with security advisories or that are abandoned (can also be set via the COMPOSER_NO_SECURITY_BLOCKING=1 env var).", None).unwrap().into(), - InputOption::new("update-no-dev", None, Some(InputOption::VALUE_NONE), "Run the dependency update with the --no-dev option.", None).unwrap().into(), - InputOption::new("update-with-dependencies", Some(PhpMixed::String("w".to_string())), Some(InputOption::VALUE_NONE), "Allows inherited dependencies to be updated, except those that are root requirements (can also be set via the COMPOSER_WITH_DEPENDENCIES=1 env var).", None).unwrap().into(), - InputOption::new("update-with-all-dependencies", Some(PhpMixed::String("W".to_string())), Some(InputOption::VALUE_NONE), "Allows all inherited dependencies to be updated, including those that are root requirements (can also be set via the COMPOSER_WITH_ALL_DEPENDENCIES=1 env var).", None).unwrap().into(), - InputOption::new("with-dependencies", None, Some(InputOption::VALUE_NONE), "Alias for --update-with-dependencies", None).unwrap().into(), - InputOption::new("with-all-dependencies", None, Some(InputOption::VALUE_NONE), "Alias for --update-with-all-dependencies", None).unwrap().into(), - InputOption::new("ignore-platform-req", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Ignore a specific platform requirement (php & ext- packages).", None).unwrap().into(), - InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages).", None).unwrap().into(), - InputOption::new("prefer-stable", None, Some(InputOption::VALUE_NONE), "Prefer stable versions of dependencies (can also be set via the COMPOSER_PREFER_STABLE=1 env var).", None).unwrap().into(), - InputOption::new("prefer-lowest", None, Some(InputOption::VALUE_NONE), "Prefer lowest versions of dependencies (can also be set via the COMPOSER_PREFER_LOWEST=1 env var).", None).unwrap().into(), - InputOption::new("minimal-changes", Some(PhpMixed::String("m".to_string())), Some(InputOption::VALUE_NONE), "During an update with -w/-W, only perform absolutely necessary changes to transitive dependencies (can also be set via the COMPOSER_MINIMAL_CHANGES=1 env var).", None).unwrap().into(), - InputOption::new("sort-packages", None, Some(InputOption::VALUE_NONE), "Sorts packages when adding/updating a new dependency", None).unwrap().into(), - InputOption::new("optimize-autoloader", Some(PhpMixed::String("o".to_string())), Some(InputOption::VALUE_NONE), "Optimize autoloader during autoloader dump", None).unwrap().into(), - InputOption::new("classmap-authoritative", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.", None).unwrap().into(), - InputOption::new("apcu-autoloader", None, Some(InputOption::VALUE_NONE), "Use APCu to cache found/not-found classes.", None).unwrap().into(), - InputOption::new("apcu-autoloader-prefix", None, Some(InputOption::VALUE_REQUIRED), "Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader", None).unwrap().into(), - ]) - .set_help( - "The require command adds required packages to your composer.json and installs them.\n\ - \n\ - If you do not specify a package, composer will prompt you to search for a package, and given results, provide a list of\n\ - matches to require.\n\ - \n\ - If you do not specify a version constraint, composer will choose a suitable one based on the available package versions.\n\ - \n\ - If you do not want to install the new dependencies immediately you can call it with --no-update\n\ - \n\ - Read more at https://getcomposer.org/doc/03-cli.md#require-r" - ); + self.set_name("require")?; + self.set_aliases(vec!["r".to_string()])?; + self.set_description("Adds required packages to your composer.json and installs them"); + self.set_definition(&[ + InputArgument::new("packages", Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), "Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or \"foo/bar 1.0.0\"", None).unwrap().into(), + InputOption::new("dev", None, Some(InputOption::VALUE_NONE), "Add requirement to require-dev.", None).unwrap().into(), + InputOption::new("dry-run", None, Some(InputOption::VALUE_NONE), "Outputs the operations but will not execute anything (implicitly enables --verbose).", None).unwrap().into(), + InputOption::new("prefer-source", None, Some(InputOption::VALUE_NONE), "Forces installation from package sources when possible, including VCS information.", None).unwrap().into(), + InputOption::new("prefer-dist", None, Some(InputOption::VALUE_NONE), "Forces installation from package dist (default behavior).", None).unwrap().into(), + InputOption::new("prefer-install", None, Some(InputOption::VALUE_REQUIRED), "Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).", None).unwrap().into(), + InputOption::new("fixed", None, Some(InputOption::VALUE_NONE), "Write fixed version to the composer.json.", None).unwrap().into(), + InputOption::new("no-suggest", None, Some(InputOption::VALUE_NONE), "DEPRECATED: This flag does not exist anymore.", None).unwrap().into(), + InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(), + InputOption::new("no-update", None, Some(InputOption::VALUE_NONE), "Disables the automatic update of the dependencies (implies --no-install).", None).unwrap().into(), + InputOption::new("no-install", None, Some(InputOption::VALUE_NONE), "Skip the install step after updating the composer.lock file.", None).unwrap().into(), + InputOption::new("no-audit", None, Some(InputOption::VALUE_NONE), "Skip the audit step after updating the composer.lock file (can also be set via the COMPOSER_NO_AUDIT=1 env var).", None).unwrap().into(), + InputOption::new("audit-format", None, Some(InputOption::VALUE_REQUIRED), "Audit output format. Must be \"table\", \"plain\", \"json\", or \"summary\".", Some(PhpMixed::String(Auditor::FORMAT_SUMMARY.to_string()))).unwrap().into(), + InputOption::new("no-security-blocking", None, Some(InputOption::VALUE_NONE), "Allows installing packages with security advisories or that are abandoned (can also be set via the COMPOSER_NO_SECURITY_BLOCKING=1 env var).", None).unwrap().into(), + InputOption::new("update-no-dev", None, Some(InputOption::VALUE_NONE), "Run the dependency update with the --no-dev option.", None).unwrap().into(), + InputOption::new("update-with-dependencies", Some(PhpMixed::String("w".to_string())), Some(InputOption::VALUE_NONE), "Allows inherited dependencies to be updated, except those that are root requirements (can also be set via the COMPOSER_WITH_DEPENDENCIES=1 env var).", None).unwrap().into(), + InputOption::new("update-with-all-dependencies", Some(PhpMixed::String("W".to_string())), Some(InputOption::VALUE_NONE), "Allows all inherited dependencies to be updated, including those that are root requirements (can also be set via the COMPOSER_WITH_ALL_DEPENDENCIES=1 env var).", None).unwrap().into(), + InputOption::new("with-dependencies", None, Some(InputOption::VALUE_NONE), "Alias for --update-with-dependencies", None).unwrap().into(), + InputOption::new("with-all-dependencies", None, Some(InputOption::VALUE_NONE), "Alias for --update-with-all-dependencies", None).unwrap().into(), + InputOption::new("ignore-platform-req", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Ignore a specific platform requirement (php & ext- packages).", None).unwrap().into(), + InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages).", None).unwrap().into(), + InputOption::new("prefer-stable", None, Some(InputOption::VALUE_NONE), "Prefer stable versions of dependencies (can also be set via the COMPOSER_PREFER_STABLE=1 env var).", None).unwrap().into(), + InputOption::new("prefer-lowest", None, Some(InputOption::VALUE_NONE), "Prefer lowest versions of dependencies (can also be set via the COMPOSER_PREFER_LOWEST=1 env var).", None).unwrap().into(), + InputOption::new("minimal-changes", Some(PhpMixed::String("m".to_string())), Some(InputOption::VALUE_NONE), "During an update with -w/-W, only perform absolutely necessary changes to transitive dependencies (can also be set via the COMPOSER_MINIMAL_CHANGES=1 env var).", None).unwrap().into(), + InputOption::new("sort-packages", None, Some(InputOption::VALUE_NONE), "Sorts packages when adding/updating a new dependency", None).unwrap().into(), + InputOption::new("optimize-autoloader", Some(PhpMixed::String("o".to_string())), Some(InputOption::VALUE_NONE), "Optimize autoloader during autoloader dump", None).unwrap().into(), + InputOption::new("classmap-authoritative", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.", None).unwrap().into(), + InputOption::new("apcu-autoloader", None, Some(InputOption::VALUE_NONE), "Use APCu to cache found/not-found classes.", None).unwrap().into(), + InputOption::new("apcu-autoloader-prefix", None, Some(InputOption::VALUE_REQUIRED), "Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader", None).unwrap().into(), + ]); + self.set_help( + "The require command adds required packages to your composer.json and installs them.\n\ + \n\ + If you do not specify a package, composer will prompt you to search for a package, and given results, provide a list of\n\ + matches to require.\n\ + \n\ + If you do not specify a version constraint, composer will choose a suitable one based on the available package versions.\n\ + \n\ + If you do not want to install the new dependencies immediately you can call it with --no-update\n\ + \n\ + Read more at https://getcomposer.org/doc/03-cli.md#require-r" + ); + Ok(()) } /// @throws \Seld\JsonLint\ParsingException - pub fn execute( + fn execute( &mut self, input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, @@ -636,6 +663,35 @@ impl RequireCommand { result } + fn interact( + &mut self, + _input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) { + } + + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for RequireCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); +} + +impl RequireCommand { /// @param array<string, string> $newRequirements /// @return string[] fn get_inconsistent_require_keys( @@ -1232,13 +1288,6 @@ impl RequireCommand { true } - pub(crate) fn interact( - &self, - _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) { - } - fn revert_composer_file(&mut self) { if self.newly_created { let msg = format!( @@ -1271,13 +1320,3 @@ impl RequireCommand { } } } - -impl HasBaseCommandData for RequireCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data - } - - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } -} diff --git a/crates/shirabe/src/command/run_script_command.rs b/crates/shirabe/src/command/run_script_command.rs index 199dbab..db6463e 100644 --- a/crates/shirabe/src/command/run_script_command.rs +++ b/crates/shirabe/src/command/run_script_command.rs @@ -2,13 +2,23 @@ use anyhow::Result; 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_shim::{InvalidArgumentException, PhpMixed, RuntimeException}; +use shirabe_php_shim::PhpMixed; +use shirabe_php_shim::{InvalidArgumentException, RuntimeException}; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::script::Event as ScriptEvent; @@ -25,11 +35,8 @@ pub struct RunScriptCommand { impl RunScriptCommand { pub fn new() -> Self { - Self { - base_command_data: BaseCommandData { - composer: None, - io: None, - }, + let mut command = RunScriptCommand { + base_command_data: BaseCommandData::new(None), script_events: vec![ ScriptEvents::PRE_INSTALL_CMD, ScriptEvents::POST_INSTALL_CMD, @@ -44,120 +51,179 @@ impl RunScriptCommand { ScriptEvents::PRE_AUTOLOAD_DUMP, ScriptEvents::POST_AUTOLOAD_DUMP, ], - } - } - - pub fn configure(&mut self) { - self.set_name("run-script") - .set_aliases(&["run".to_string()]) - .set_description("Runs the scripts defined in composer.json") - .set_definition(&[ - // TODO(cli-completion): script-name completion was provided via a closure suggesting runtime script names - InputArgument::new( - "script", - Some(InputArgument::OPTIONAL), - "Script name to run.", - None, - ) - .unwrap() - .into(), - InputArgument::new( - "args", - Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), - "", - None, - ) - .unwrap() - .into(), - InputOption::new( - "timeout", - None, - Some(InputOption::VALUE_REQUIRED), - "Sets script timeout in seconds, or 0 for never.", - None, - ) - .unwrap() - .into(), - InputOption::new( - "dev", - None, - Some(InputOption::VALUE_NONE), - "Sets the dev mode.", - None, - ) - .unwrap() - .into(), - InputOption::new( - "no-dev", - None, - Some(InputOption::VALUE_NONE), - "Disables the dev mode.", - None, - ) - .unwrap() - .into(), - InputOption::new( - "list", - Some(PhpMixed::String("l".to_string())), - Some(InputOption::VALUE_NONE), - "List scripts.", - None, - ) - .unwrap() - .into(), - ]) - .set_help( - "The <info>run-script</info> command runs scripts defined in composer.json:\n\n\ - <info>php composer.phar run-script post-update-cmd</info>\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#run-script-run", - ); + }; + command + .configure() + .expect("RunScriptCommand::configure uses static, valid metadata"); + command } - pub fn interact( - &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<()> { + fn list_scripts(&mut self, output: Rc<RefCell<dyn OutputInterface>>) -> Result<i64> { let scripts = self.get_scripts()?; if scripts.is_empty() { - return Ok(()); + return Ok(0); } - if input.borrow().get_argument("script")?.as_string().is_some() - || input - .borrow() - .get_option("list")? - .as_bool() - .unwrap_or(false) - { - return Ok(()); + let io = self.get_io(); + io.write_error("<info>scripts:</info>"); + let table: Vec<PhpMixed> = scripts + .iter() + .map(|(name, desc)| { + PhpMixed::List(vec![ + Box::new(PhpMixed::String(format!(" {}", name))), + Box::new(PhpMixed::String(desc.clone())), + ]) + }) + .collect(); + + self.render_table(table, output); + + Ok(0) + } + + fn get_scripts(&mut self) -> Result<Vec<(String, String)>> { + let composer = self.require_composer(None, None)?; + let scripts = crate::command::composer_full(&composer) + .get_package() + .get_scripts(); + drop(composer); + if scripts.is_empty() { + return Ok(vec![]); } - let mut options = indexmap::IndexMap::new(); - for script in &scripts { - options.insert(script.0.clone(), script.1.clone()); + let mut result: Vec<(String, String)> = vec![]; + for (name, _script) in scripts { + // PHP: $cmd = $this->getApplication()->find($name); $description = $cmd->getDescription(); + // TODO(phase-c): Application::find returns PhpMixed (the Symfony command registry is a + // todo!() stub) and get_application() is itself deferred, so the resolved command's + // getDescription() cannot be read; the description stays empty until the typed command + // registry is modelled. + // get_application() is deferred (returns the Symfony `dyn Application` back-reference, + // not the shirabe Application with find()); the description stays empty until the + // shared Application handle and typed command registry are modelled. + let description = String::new(); + result.push((name, description)); } - let io = self.get_io(); - let script = io.select( - "Script to run: ".to_string(), - options.keys().cloned().collect(), - PhpMixed::String(String::new()), - PhpMixed::Int(1), - "Invalid script name \"%s\"".to_string(), - false, + Ok(result) + } +} + +impl Command for RunScriptCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("run-script")?; + self.set_aliases(vec!["run".to_string()])?; + self.set_description("Runs the scripts defined in composer.json"); + self.set_definition(&[ + // TODO(cli-completion): script-name completion was provided via a closure suggesting runtime script names + InputArgument::new( + "script", + Some(InputArgument::OPTIONAL), + "Script name to run.", + None, + ) + .unwrap() + .into(), + InputArgument::new( + "args", + Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), + "", + None, + ) + .unwrap() + .into(), + InputOption::new( + "timeout", + None, + Some(InputOption::VALUE_REQUIRED), + "Sets script timeout in seconds, or 0 for never.", + None, + ) + .unwrap() + .into(), + InputOption::new( + "dev", + None, + Some(InputOption::VALUE_NONE), + "Sets the dev mode.", + None, + ) + .unwrap() + .into(), + InputOption::new( + "no-dev", + None, + Some(InputOption::VALUE_NONE), + "Disables the dev mode.", + None, + ) + .unwrap() + .into(), + InputOption::new( + "list", + Some(PhpMixed::String("l".to_string())), + Some(InputOption::VALUE_NONE), + "List scripts.", + None, + ) + .unwrap() + .into(), + ]); + self.set_help( + "The <info>run-script</info> command runs scripts defined in composer.json:\n\n\ + <info>php composer.phar run-script post-update-cmd</info>\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#run-script-run", ); + Ok(()) + } + + fn interact( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) { + let _ = (|| -> anyhow::Result<()> { + let scripts = self.get_scripts()?; + if scripts.is_empty() { + return Ok(()); + } - input.borrow_mut().set_argument("script", script)?; + if input.borrow().get_argument("script")?.as_string().is_some() + || input + .borrow() + .get_option("list")? + .as_bool() + .unwrap_or(false) + { + return Ok(()); + } - Ok(()) + let mut options = indexmap::IndexMap::new(); + for script in &scripts { + options.insert(script.0.clone(), script.1.clone()); + } + + let io = self.get_io(); + let script = io.select( + "Script to run: ".to_string(), + options.keys().cloned().collect(), + PhpMixed::String(String::new()), + PhpMixed::Int(1), + "Invalid script name \"%s\"".to_string(), + false, + ); + + input.borrow_mut().set_argument("script", script)?; + + Ok(()) + })(); } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { if input .borrow() .get_option("list")? @@ -253,64 +319,23 @@ impl RunScriptCommand { .dispatch_script(&script, dev_mode, args, IndexMap::new())?) } - fn list_scripts( + fn initialize( &mut self, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { - let scripts = self.get_scripts()?; - if scripts.is_empty() { - return Ok(0); - } - - let io = self.get_io(); - io.write_error("<info>scripts:</info>"); - let table: Vec<PhpMixed> = scripts - .iter() - .map(|(name, desc)| { - PhpMixed::List(vec![ - Box::new(PhpMixed::String(format!(" {}", name))), - Box::new(PhpMixed::String(desc.clone())), - ]) - }) - .collect(); - - self.render_table(table, output); - - Ok(0) + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn get_scripts(&mut self) -> Result<Vec<(String, String)>> { - let composer = self.require_composer(None, None)?; - let scripts = crate::command::composer_full(&composer) - .get_package() - .get_scripts(); - drop(composer); - if scripts.is_empty() { - return Ok(vec![]); - } - - let mut result: Vec<(String, String)> = vec![]; - for (name, _script) in scripts { - // PHP: $cmd = $this->getApplication()->find($name); $description = $cmd->getDescription(); - // TODO(phase-c): Application::find returns PhpMixed (the Symfony command registry is a - // todo!() stub) and get_application() is itself deferred, so the resolved command's - // getDescription() cannot be read; the description stays empty until the typed command - // registry is modelled. - let _ = self.get_application()?.find(&name); - let description = String::new(); - result.push((name, description)); - } - - Ok(result) - } + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); } -impl HasBaseCommandData for RunScriptCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data +impl BaseCommand for RunScriptCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/script_alias_command.rs b/crates/shirabe/src/command/script_alias_command.rs index 95c80fb..e740c4b 100644 --- a/crates/shirabe/src/command/script_alias_command.rs +++ b/crates/shirabe/src/command/script_alias_command.rs @@ -1,15 +1,26 @@ //! ref: composer/src/Composer/Command/ScriptAliasCommand.php -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; -use crate::console::input::InputArgument; -use crate::console::input::InputOption; -use crate::io::IOInterface; -use crate::util::Platform; use anyhow::Result; +use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; +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_shim::{InvalidArgumentException, LogicException, PhpMixed, is_string}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::advisory::AuditConfig; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; +use crate::composer::PartialComposerHandle; +use crate::config::Config; +use crate::console::input::InputArgument; +use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; +use crate::io::IOInterface; +use crate::util::Platform; #[derive(Debug)] pub struct ScriptAliasCommand { @@ -43,64 +54,67 @@ impl ScriptAliasCommand { // the command's name/definition/application state and ignoreValidationErrors() flips a flag // on it. Composer's BaseCommand carries no such Symfony Command state yet (the Symfony // Command base is an intentional todo!() stub), so there is nothing to initialize here. - Ok(Self { - base_command_data: BaseCommandData { - composer: None, - io: None, - }, + let mut command = Self { + base_command_data: BaseCommandData::new(None), script, description, aliases, - }) + }; + command + .configure() + .expect("ScriptAliasCommand::configure uses constructor-provided metadata"); + Ok(command) } +} - pub fn configure(&mut self) { - let script = self.script.clone(); +impl Command for ScriptAliasCommand { + fn configure(&mut self) -> anyhow::Result<()> { + let name = self.script.clone(); + self.set_name(&name)?; let description = self.description.clone(); - let aliases = self.aliases.clone(); - self.set_name(&script) - .set_description(&description) - .set_aliases(&aliases) - .set_definition(&[ - InputOption::new( - "dev", - None, - Some(InputOption::VALUE_NONE), - "Sets the dev mode.", - None, - ) - .unwrap() - .into(), - InputOption::new( - "no-dev", - None, - Some(InputOption::VALUE_NONE), - "Disables the dev mode.", - None, - ) - .unwrap() - .into(), - InputArgument::new( - "args", - Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), - "", - None, - ) - .unwrap() - .into(), - ]) - .set_help( - "The <info>run-script</info> command runs scripts defined in composer.json:\n\n\ - <info>php composer.phar run-script post-update-cmd</info>\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#run-script-run", - ); + self.set_description(&description); + self.set_aliases(self.aliases.clone())?; + self.set_definition(&[ + InputOption::new( + "dev", + None, + Some(InputOption::VALUE_NONE), + "Sets the dev mode.", + None, + ) + .unwrap() + .into(), + InputOption::new( + "no-dev", + None, + Some(InputOption::VALUE_NONE), + "Disables the dev mode.", + None, + ) + .unwrap() + .into(), + InputArgument::new( + "args", + Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), + "", + None, + ) + .unwrap() + .into(), + ]); + self.set_help( + "The <info>run-script</info> command runs scripts defined in composer.json:\n\n\ + <info>php composer.phar run-script post-update-cmd</info>\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#run-script-run", + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let composer = self.require_composer(None, None)?; let dispatcher = crate::command::composer_full(&composer) .get_event_dispatcher() @@ -151,14 +165,24 @@ impl ScriptAliasCommand { .borrow_mut() .dispatch_script(&self.script, dev_mode, args_value, flags)?) } -} -impl HasBaseCommandData for ScriptAliasCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for ScriptAliasCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/search_command.rs b/crates/shirabe/src/command/search_command.rs index b2fb1df..794d8e0 100644 --- a/crates/shirabe/src/command/search_command.rs +++ b/crates/shirabe/src/command/search_command.rs @@ -1,8 +1,23 @@ //! ref: composer/src/Composer/Command/SearchCommand.php -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use anyhow::Result; +use indexmap::IndexMap; +use shirabe_external_packages::symfony::console::command::command::Command; +use shirabe_external_packages::symfony::console::formatter::OutputFormatter; +use shirabe_external_packages::symfony::console::input::InputInterface; +use shirabe_external_packages::symfony::console::output::OutputInterface; +use shirabe_php_shim::{InvalidArgumentException, PhpMixed, implode, in_array, preg_quote}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::advisory::AuditConfig; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::json::JsonFile; @@ -12,12 +27,6 @@ use crate::repository::CompositeRepository; use crate::repository::PlatformRepository; use crate::repository::RepositoryInterfaceHandle; use crate::repository::repository_interface::{self, RepositoryInterface}; -use anyhow::Result; -use indexmap::IndexMap; -use shirabe_external_packages::symfony::console::formatter::OutputFormatter; -use shirabe_external_packages::symfony::console::input::InputInterface; -use shirabe_external_packages::symfony::console::output::OutputInterface; -use shirabe_php_shim::{InvalidArgumentException, PhpMixed, implode, in_array, preg_quote}; #[derive(Debug)] pub struct SearchCommand { @@ -25,29 +34,80 @@ pub struct SearchCommand { } impl SearchCommand { - pub fn configure(&mut self) { - self - .set_name("search") - .set_description("Searches for packages") - .set_definition(&[ - InputOption::new("only-name", Some(PhpMixed::String("N".to_string())), Some(InputOption::VALUE_NONE), "Search only in package names", None).unwrap().into(), - InputOption::new("only-vendor", Some(PhpMixed::String("O".to_string())), Some(InputOption::VALUE_NONE), "Search only for vendor / organization names, returns only \"vendor\" as result", None).unwrap().into(), - InputOption::new("type", Some(PhpMixed::String("t".to_string())), Some(InputOption::VALUE_REQUIRED), "Search for a specific package type", None).unwrap().into(), - InputOption::new("format", Some(PhpMixed::String("f".to_string())), Some(InputOption::VALUE_REQUIRED), "Format of the output: text or json", Some(PhpMixed::String("text".to_string()))).unwrap().into(), - InputArgument::new("tokens", Some(InputArgument::IS_ARRAY | InputArgument::REQUIRED), "tokens to search for", None).unwrap().into(), - ]) - .set_help( - "The search command searches for packages by its name\n\ - <info>php composer.phar search symfony composer</info>\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#search" - ); + pub fn new() -> Self { + let mut command = SearchCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("SearchCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for SearchCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("search")?; + self.set_description("Searches for packages"); + self.set_definition(&[ + InputOption::new( + "only-name", + Some(PhpMixed::String("N".to_string())), + Some(InputOption::VALUE_NONE), + "Search only in package names", + None, + ) + .unwrap() + .into(), + InputOption::new( + "only-vendor", + Some(PhpMixed::String("O".to_string())), + Some(InputOption::VALUE_NONE), + "Search only for vendor / organization names, returns only \"vendor\" as result", + None, + ) + .unwrap() + .into(), + InputOption::new( + "type", + Some(PhpMixed::String("t".to_string())), + Some(InputOption::VALUE_REQUIRED), + "Search for a specific package type", + None, + ) + .unwrap() + .into(), + InputOption::new( + "format", + Some(PhpMixed::String("f".to_string())), + Some(InputOption::VALUE_REQUIRED), + "Format of the output: text or json", + Some(PhpMixed::String("text".to_string())), + ) + .unwrap() + .into(), + InputArgument::new( + "tokens", + Some(InputArgument::IS_ARRAY | InputArgument::REQUIRED), + "tokens to search for", + None, + ) + .unwrap() + .into(), + ]); + self.set_help( + "The search command searches for packages by its name\n\ + <info>php composer.phar search symfony composer</info>\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#search", + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let platform_repo = PlatformRepository::new4(vec![], IndexMap::new(), None, None)?; let io = self.get_io(); @@ -209,14 +269,24 @@ impl SearchCommand { Ok(0) } -} -impl HasBaseCommandData for SearchCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for SearchCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/self_update_command.rs b/crates/shirabe/src/command/self_update_command.rs index 8c7baeb..a32f931 100644 --- a/crates/shirabe/src/command/self_update_command.rs +++ b/crates/shirabe/src/command/self_update_command.rs @@ -1,15 +1,26 @@ //! ref: composer/src/Composer/Command/SelfUpdateCommand.php -use crate::io::io_interface; use anyhow::Result; +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_shim::PhpMixed; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; +use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; +use crate::io::io_interface; #[derive(Debug)] pub struct SelfUpdateCommand { @@ -17,43 +28,55 @@ pub struct SelfUpdateCommand { } impl SelfUpdateCommand { - pub fn configure(&mut self) { - self - .set_name("self-update") - .set_aliases(&["selfupdate".to_string()]) - .set_description("Updates composer.phar to the latest version") - .set_definition(&[ - InputOption::new("rollback", Some(PhpMixed::String("r".to_string())), Some(InputOption::VALUE_NONE), "Revert to an older installation of composer", None).unwrap().into(), - InputOption::new("clean-backups", None, Some(InputOption::VALUE_NONE), "Delete old backups during an update. This makes the current version of composer the only backup available after the update", None).unwrap().into(), - InputArgument::new("version", Some(InputArgument::OPTIONAL), "The version to update to", None).unwrap().into(), - InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(), - InputOption::new("update-keys", None, Some(InputOption::VALUE_NONE), "Prompt user for a key update", None).unwrap().into(), - InputOption::new("stable", None, Some(InputOption::VALUE_NONE), "Force an update to the stable channel", None).unwrap().into(), - InputOption::new("preview", None, Some(InputOption::VALUE_NONE), "Force an update to the preview channel", None).unwrap().into(), - InputOption::new("snapshot", None, Some(InputOption::VALUE_NONE), "Force an update to the snapshot channel", None).unwrap().into(), - InputOption::new("1", None, Some(InputOption::VALUE_NONE), "Force an update to the stable channel, but only use 1.x versions", None).unwrap().into(), - InputOption::new("2", None, Some(InputOption::VALUE_NONE), "Force an update to the stable channel, but only use 2.x versions", None).unwrap().into(), - InputOption::new("2.2", None, Some(InputOption::VALUE_NONE), "Force an update to the stable channel, but only use 2.2.x LTS versions", None).unwrap().into(), - InputOption::new("set-channel-only", None, Some(InputOption::VALUE_NONE), "Only store the channel as the default one and then exit", None).unwrap().into(), - ]) - .set_help( - "The <info>self-update</info> command checks getcomposer.org for newer\n\ - versions of composer and if found, installs the latest.\n\ - \n\ - <info>php composer.phar self-update</info>\n\ - \n\ - Read more at https://getcomposer.org/doc/03-cli.md#self-update-selfupdate" - ); + pub fn new() -> Self { + let mut command = SelfUpdateCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("SelfUpdateCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for SelfUpdateCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("self-update")?; + self.set_aliases(vec!["selfupdate".to_string()])?; + self.set_description("Updates composer.phar to the latest version"); + self.set_definition(&[ + InputOption::new("rollback", Some(PhpMixed::String("r".to_string())), Some(InputOption::VALUE_NONE), "Revert to an older installation of composer", None).unwrap().into(), + InputOption::new("clean-backups", None, Some(InputOption::VALUE_NONE), "Delete old backups during an update. This makes the current version of composer the only backup available after the update", None).unwrap().into(), + InputArgument::new("version", Some(InputArgument::OPTIONAL), "The version to update to", None).unwrap().into(), + InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(), + InputOption::new("update-keys", None, Some(InputOption::VALUE_NONE), "Prompt user for a key update", None).unwrap().into(), + InputOption::new("stable", None, Some(InputOption::VALUE_NONE), "Force an update to the stable channel", None).unwrap().into(), + InputOption::new("preview", None, Some(InputOption::VALUE_NONE), "Force an update to the preview channel", None).unwrap().into(), + InputOption::new("snapshot", None, Some(InputOption::VALUE_NONE), "Force an update to the snapshot channel", None).unwrap().into(), + InputOption::new("1", None, Some(InputOption::VALUE_NONE), "Force an update to the stable channel, but only use 1.x versions", None).unwrap().into(), + InputOption::new("2", None, Some(InputOption::VALUE_NONE), "Force an update to the stable channel, but only use 2.x versions", None).unwrap().into(), + InputOption::new("2.2", None, Some(InputOption::VALUE_NONE), "Force an update to the stable channel, but only use 2.2.x LTS versions", None).unwrap().into(), + InputOption::new("set-channel-only", None, Some(InputOption::VALUE_NONE), "Only store the channel as the default one and then exit", None).unwrap().into(), + ]); + self.set_help( + "The <info>self-update</info> command checks getcomposer.org for newer\n\ + versions of composer and if found, installs the latest.\n\ + \n\ + <info>php composer.phar self-update</info>\n\ + \n\ + Read more at https://getcomposer.org/doc/03-cli.md#self-update-selfupdate", + ); + Ok(()) } /// The self-update mechanism does not apply to Shirabe: the update flow /// differs fundamentally from the PHP phar-based one, and no release has /// been published yet. The command is therefore disabled. - pub fn execute( + fn execute( &mut self, - _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + _input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let io = self.get_io(); io.write_error3( "<error>The self-update command is not available in Shirabe.</error>", @@ -63,17 +86,27 @@ impl SelfUpdateCommand { Ok(1) } -} -impl HasBaseCommandData for SelfUpdateCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for SelfUpdateCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); + fn is_self_update_command(&self) -> bool { true } diff --git a/crates/shirabe/src/command/show_command.rs b/crates/shirabe/src/command/show_command.rs index 05b6b3e..520e716 100644 --- a/crates/shirabe/src/command/show_command.rs +++ b/crates/shirabe/src/command/show_command.rs @@ -1,9 +1,11 @@ //! ref: composer/src/Composer/Command/ShowCommand.php +use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; use shirabe_external_packages::composer::semver::Semver; use shirabe_external_packages::composer::spdx_licenses::SpdxLicenses; +use shirabe_external_packages::symfony::console::command::command::Command; use shirabe_external_packages::symfony::console::formatter::OutputFormatter; use shirabe_external_packages::symfony::console::formatter::OutputFormatterStyle; use shirabe_external_packages::symfony::console::input::InputInterface; @@ -13,11 +15,16 @@ use shirabe_php_shim::{ array_search, date, date_format_to_strftime, extension_loaded, in_array, realpath, strtolower, version_compare, }; +use std::cell::RefCell; +use std::rc::Rc; use shirabe_semver::constraint::AnyConstraint; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputOption; use crate::dependency_resolver::DefaultPolicy; use crate::dependency_resolver::PolicyInterface; @@ -63,26 +70,40 @@ pub struct ShowCommand { } impl ShowCommand { - pub fn configure(&mut self) { - self.set_name("show") - .set_aliases(&["info".to_string()]) - .set_description("Shows information about packages") - .set_definition(&[ - // TODO(cli-completion): wire up suggest_package_based_on_mode / suggest_installed_package closures here. - ]) - .set_help( - "The show command displays detailed information about a package, or\n\ - lists all packages available.\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#show-info", - ); + pub fn new() -> Self { + let mut command = ShowCommand { + base_command_data: BaseCommandData::new(None), + version_parser: VersionParser::new(), + colors: Vec::new(), + repository_set: None, + }; + command + .configure() + .expect("ShowCommand::configure uses static, valid metadata"); + command } +} - // TODO(cli-completion): pub fn suggest_package_based_on_mode(&self) -> Box<dyn Fn(&CompletionInput) -> Vec<String>> +impl Command for ShowCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("show")?; + self.set_aliases(vec!["info".to_string()])?; + self.set_description("Shows information about packages"); + self.set_definition(&[ + // TODO(cli-completion): wire up suggest_package_based_on_mode / suggest_installed_package closures here. + ]); + self.set_help( + "The show command displays detailed information about a package, or\n\ + lists all packages available.\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#show-info", + ); + Ok(()) + } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { self.version_parser = VersionParser::new(); if input.borrow().get_option("tree")?.as_bool() == Some(true) { @@ -1345,6 +1366,30 @@ impl ShowCommand { Ok(exit_code) } + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for ShowCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); +} + +impl ShowCommand { + // TODO(cli-completion): pub fn suggest_package_based_on_mode(&self) -> Box<dyn Fn(&CompletionInput) -> Vec<String>> + fn print_packages( &mut self, packages: &[IndexMap<String, PhpMixed>], @@ -2859,13 +2904,3 @@ struct ViewMetaData { write_latest: bool, write_release_date: bool, } - -impl HasBaseCommandData for ShowCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data - } - - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } -} diff --git a/crates/shirabe/src/command/status_command.rs b/crates/shirabe/src/command/status_command.rs index bbff681..9115caf 100644 --- a/crates/shirabe/src/command/status_command.rs +++ b/crates/shirabe/src/command/status_command.rs @@ -2,11 +2,21 @@ use anyhow::Result; 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_shim::PhpMixed; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::package::dumper::ArrayDumper; @@ -27,62 +37,14 @@ impl StatusCommand { const EXIT_CODE_UNPUSHED_CHANGES: i64 = 2; const EXIT_CODE_VERSION_CHANGES: i64 = 4; - pub fn configure(&mut self) { - self - .set_name("status") - .set_description("Shows a list of locally modified packages") - .set_definition(&[ - InputOption::new("verbose", Some(shirabe_php_shim::PhpMixed::String("v|vv|vvv".to_string())), Some(InputOption::VALUE_NONE), "Show modified files for each directory that contains changes.", None).unwrap().into(), - ]) - .set_help( - "The status command displays a list of dependencies that have\nbeen modified locally.\n\nRead more at https://getcomposer.org/doc/03-cli.md#status" - ); - } - - pub fn execute( - &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { - let composer_rc = self.require_composer(None, None)?; - { - let composer = crate::command::composer_full(&composer_rc); - - // TODO(plugin): dispatch CommandEvent - let command_event = - CommandEvent::new(PluginEvents::COMMAND, "status", input.clone(), output); - composer - .get_event_dispatcher() - .borrow_mut() - .dispatch(Some(command_event.get_name()), None); - - composer - .get_event_dispatcher() - .borrow_mut() - .dispatch_script( - ScriptEvents::PRE_STATUS_CMD, - true, - vec![], - indexmap::IndexMap::new(), - ); - } - - let exit_code = self.do_execute(input)?; - - { - let composer = crate::command::composer_full(&composer_rc); - composer - .get_event_dispatcher() - .borrow_mut() - .dispatch_script( - ScriptEvents::POST_STATUS_CMD, - true, - vec![], - indexmap::IndexMap::new(), - ); - } - - Ok(exit_code) + pub fn new() -> Self { + let mut command = StatusCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("StatusCommand::configure uses static, valid metadata"); + command } fn do_execute( @@ -357,12 +319,88 @@ impl StatusCommand { } } -impl HasBaseCommandData for StatusCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data +impl Command for StatusCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("status")?; + self.set_description("Shows a list of locally modified packages"); + self.set_definition(&[InputOption::new( + "verbose", + Some(shirabe_php_shim::PhpMixed::String("v|vv|vvv".to_string())), + Some(InputOption::VALUE_NONE), + "Show modified files for each directory that contains changes.", + None, + ) + .unwrap() + .into()]); + self.set_help( + "The status command displays a list of dependencies that have\nbeen modified locally.\n\nRead more at https://getcomposer.org/doc/03-cli.md#status" + ); + Ok(()) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + fn execute( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + let composer_rc = self.require_composer(None, None)?; + { + let composer = crate::command::composer_full(&composer_rc); + + // TODO(plugin): dispatch CommandEvent + let command_event = + CommandEvent::new(PluginEvents::COMMAND, "status", input.clone(), output); + composer + .get_event_dispatcher() + .borrow_mut() + .dispatch(Some(command_event.get_name()), None); + + composer + .get_event_dispatcher() + .borrow_mut() + .dispatch_script( + ScriptEvents::PRE_STATUS_CMD, + true, + vec![], + indexmap::IndexMap::new(), + ); + } + + let exit_code = self.do_execute(input)?; + + { + let composer = crate::command::composer_full(&composer_rc); + composer + .get_event_dispatcher() + .borrow_mut() + .dispatch_script( + ScriptEvents::POST_STATUS_CMD, + true, + vec![], + indexmap::IndexMap::new(), + ); + } + + Ok(exit_code) + } + + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for StatusCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/suggests_command.rs b/crates/shirabe/src/command/suggests_command.rs index 393874f..344887e 100644 --- a/crates/shirabe/src/command/suggests_command.rs +++ b/crates/shirabe/src/command/suggests_command.rs @@ -1,8 +1,13 @@ //! ref: composer/src/Composer/Command/SuggestsCommand.php -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::installer::SuggestedPackagesReporter; use crate::io::IOInterface; use crate::repository::InstalledRepository; @@ -12,9 +17,12 @@ use crate::repository::RepositoryInterfaceHandle; use crate::repository::RootPackageRepository; use anyhow::Result; 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_shim::{PhpMixed, empty, in_array}; +use std::cell::RefCell; +use std::rc::Rc; #[derive(Debug)] pub struct SuggestsCommand { @@ -22,28 +30,87 @@ pub struct SuggestsCommand { } impl SuggestsCommand { - pub fn configure(&mut self) { + pub fn new() -> Self { + let mut command = SuggestsCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("SuggestsCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for SuggestsCommand { + fn configure(&mut self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_installed_package() for `packages` argument - self - .set_name("suggests") - .set_description("Shows package suggestions") - .set_definition(&[ - InputOption::new("by-package", None, Some(InputOption::VALUE_NONE), "Groups output by suggesting package (default)", None).unwrap().into(), - InputOption::new("by-suggestion", None, Some(InputOption::VALUE_NONE), "Groups output by suggested package", None).unwrap().into(), - InputOption::new("all", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Show suggestions from all dependencies, including transitive ones", None).unwrap().into(), - InputOption::new("list", None, Some(InputOption::VALUE_NONE), "Show only list of suggested package names", None).unwrap().into(), - InputOption::new("no-dev", None, Some(InputOption::VALUE_NONE), "Exclude suggestions from require-dev packages", None).unwrap().into(), - InputArgument::new("packages", Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), "Packages that you want to list suggestions from.", None).unwrap().into(), - ]) - .set_help( - "\nThe <info>%command.name%</info> command shows a sorted list of suggested packages.\n\nRead more at https://getcomposer.org/doc/03-cli.md#suggests", - ); + self.set_name("suggests")?; + self.set_description("Shows package suggestions"); + self.set_definition(&[ + InputOption::new( + "by-package", + None, + Some(InputOption::VALUE_NONE), + "Groups output by suggesting package (default)", + None, + ) + .unwrap() + .into(), + InputOption::new( + "by-suggestion", + None, + Some(InputOption::VALUE_NONE), + "Groups output by suggested package", + None, + ) + .unwrap() + .into(), + InputOption::new( + "all", + Some(PhpMixed::String("a".to_string())), + Some(InputOption::VALUE_NONE), + "Show suggestions from all dependencies, including transitive ones", + None, + ) + .unwrap() + .into(), + InputOption::new( + "list", + None, + Some(InputOption::VALUE_NONE), + "Show only list of suggested package names", + None, + ) + .unwrap() + .into(), + InputOption::new( + "no-dev", + None, + Some(InputOption::VALUE_NONE), + "Exclude suggestions from require-dev packages", + None, + ) + .unwrap() + .into(), + InputArgument::new( + "packages", + Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), + "Packages that you want to list suggestions from.", + None, + ) + .unwrap() + .into(), + ]); + self.set_help( + "\nThe <info>%command.name%</info> command shows a sorted list of suggested packages.\n\nRead more at https://getcomposer.org/doc/03-cli.md#suggests", + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + input: Rc<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, ) -> Result<i64> { let composer = self.require_composer(None, None)?; let mut composer = crate::command::composer_full_mut(&composer); @@ -145,14 +212,24 @@ impl SuggestsCommand { Ok(0) } -} -impl HasBaseCommandData for SuggestsCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for SuggestsCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } diff --git a/crates/shirabe/src/command/update_command.rs b/crates/shirabe/src/command/update_command.rs index 26b05f5..873b244 100644 --- a/crates/shirabe/src/command/update_command.rs +++ b/crates/shirabe/src/command/update_command.rs @@ -5,6 +5,7 @@ use crate::package::base_package; use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; +use shirabe_external_packages::symfony::console::command::command::Command; use shirabe_external_packages::symfony::console::helper::Table; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::OutputInterface; @@ -14,14 +15,20 @@ use shirabe_php_shim::{ }; use shirabe_semver::constraint::MultiConstraint; use shirabe_semver::intervals::Intervals; +use std::cell::RefCell; +use std::rc::Rc; +use crate::advisory::AuditConfig; use crate::advisory::Auditor; use crate::command::BumpCommand; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::command::base_command::base_command_initialize; +use crate::command::{BaseCommand, BaseCommandData}; use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; use crate::dependency_resolver::request::{self, Request, UpdateAllowTransitiveDeps}; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::installer::Installer; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; @@ -43,40 +50,52 @@ pub struct UpdateCommand { } impl UpdateCommand { - pub fn configure(&mut self) { + pub fn new() -> Self { + let mut command = UpdateCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("UpdateCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for UpdateCommand { + fn configure(&mut self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_installed_package(false, true) / suggest_prefer_install - self - .set_name("update") - .set_aliases(&["u".to_string(), "upgrade".to_string()]) - .set_description("Updates your dependencies to the latest version according to composer.json, and updates the composer.lock file") - // TODO(phase-c): populate with InputArgument/InputOption entries (see PHP UpdateCommand); - // blocked on the symfony InputDefinition entry modeling. - .set_definition(&[]) - .set_help( - "The <info>update</info> command reads the composer.json file from the\n\ - current directory, processes it, and updates, removes or installs all the\n\ - dependencies.\n\n\ - <info>php composer.phar update</info>\n\n\ - To limit the update operation to a few packages, you can list the package(s)\n\ - you want to update as such:\n\n\ - <info>php composer.phar update vendor/package1 foo/mypackage [...]</info>\n\n\ - You may also use an asterisk (*) pattern to limit the update operation to package(s)\n\ - from a specific vendor:\n\n\ - <info>php composer.phar update vendor/package1 foo/* [...]</info>\n\n\ - To run an update with more restrictive constraints you can use:\n\n\ - <info>php composer.phar update --with vendor/package:1.0.*</info>\n\n\ - To run a partial update with more restrictive constraints you can use the shorthand:\n\n\ - <info>php composer.phar update vendor/package:1.0.*</info>\n\n\ - To select packages names interactively with auto-completion use <info>-i</info>.\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#update-u-upgrade\n", - ); + self.set_name("update")?; + self.set_aliases(vec!["u".to_string(), "upgrade".to_string()])?; + self.set_description("Updates your dependencies to the latest version according to composer.json, and updates the composer.lock file"); + // TODO(phase-c): populate with InputArgument/InputOption entries (see PHP UpdateCommand); + // blocked on the symfony InputDefinition entry modeling. + self.set_definition(&[]); + self.set_help( + "The <info>update</info> command reads the composer.json file from the\n\ + current directory, processes it, and updates, removes or installs all the\n\ + dependencies.\n\n\ + <info>php composer.phar update</info>\n\n\ + To limit the update operation to a few packages, you can list the package(s)\n\ + you want to update as such:\n\n\ + <info>php composer.phar update vendor/package1 foo/mypackage [...]</info>\n\n\ + You may also use an asterisk (*) pattern to limit the update operation to package(s)\n\ + from a specific vendor:\n\n\ + <info>php composer.phar update vendor/package1 foo/* [...]</info>\n\n\ + To run an update with more restrictive constraints you can use:\n\n\ + <info>php composer.phar update --with vendor/package:1.0.*</info>\n\n\ + To run a partial update with more restrictive constraints you can use the shorthand:\n\n\ + <info>php composer.phar update vendor/package:1.0.*</info>\n\n\ + To select packages names interactively with auto-completion use <info>-i</info>.\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#update-u-upgrade\n", + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let io = self.get_io().clone(); if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { io.write_error3( @@ -491,7 +510,7 @@ impl UpdateCommand { true, io_interface::NORMAL, ); - let mut bump_command = BumpCommand::new(None); + let mut bump_command = BumpCommand::new(); bump_command.set_composer(composer_handle.clone()); result = bump_command.do_bump( io.clone(), @@ -520,6 +539,28 @@ impl UpdateCommand { Ok(result) } + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for UpdateCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); +} + +impl UpdateCommand { /// @param array<string> $packages /// @return array<string> fn get_packages_interactively( @@ -717,13 +758,3 @@ impl UpdateCommand { ) } } - -impl HasBaseCommandData for UpdateCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data - } - - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } -} diff --git a/crates/shirabe/src/command/validate_command.rs b/crates/shirabe/src/command/validate_command.rs index 1d9d036..5fc8f3d 100644 --- a/crates/shirabe/src/command/validate_command.rs +++ b/crates/shirabe/src/command/validate_command.rs @@ -1,13 +1,24 @@ //! ref: composer/src/Composer/Command/ValidateCommand.php use anyhow::Result; +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_shim::PhpMixed; +use std::cell::RefCell; +use std::rc::Rc; -use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; +use crate::advisory::AuditConfig; +use crate::command::BaseCommand; +use crate::command::BaseCommandData; +use crate::command::base_command::base_command_initialize; +use crate::composer::PartialComposerHandle; +use crate::config::Config; use crate::console::input::InputArgument; use crate::console::input::InputOption; use crate::factory::Factory; +use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::package::loader::ValidatingArrayLoader; @@ -22,97 +33,110 @@ pub struct ValidateCommand { } impl ValidateCommand { - pub fn configure(&mut self) { - self.set_name("validate") - .set_description("Validates a composer.json and composer.lock") - .set_definition(&[ - InputOption::new( - "no-check-all", - None, - Some(InputOption::VALUE_NONE), - "Do not validate requires for overly strict/loose constraints", - None, - ) - .unwrap() - .into(), - InputOption::new( - "check-lock", - None, - Some(InputOption::VALUE_NONE), - "Check if lock file is up to date (even when config.lock is false)", - None, - ) - .unwrap() - .into(), - InputOption::new( - "no-check-lock", - None, - Some(InputOption::VALUE_NONE), - "Do not check if lock file is up to date", - None, - ) - .unwrap() - .into(), - InputOption::new( - "no-check-publish", - None, - Some(InputOption::VALUE_NONE), - "Do not check for publish errors", - None, - ) - .unwrap() - .into(), - InputOption::new( - "no-check-version", - None, - Some(InputOption::VALUE_NONE), - "Do not report a warning if the version field is present", - None, - ) - .unwrap() - .into(), - InputOption::new( - "with-dependencies", - Some(shirabe_php_shim::PhpMixed::String("A".to_string())), - Some(InputOption::VALUE_NONE), - "Also validate the composer.json of all installed dependencies", - None, - ) - .unwrap() - .into(), - InputOption::new( - "strict", - None, - Some(InputOption::VALUE_NONE), - "Return a non-zero exit code for warnings as well as errors", - None, - ) - .unwrap() - .into(), - InputArgument::new( - "file", - Some(InputArgument::OPTIONAL), - "path to composer.json file", - None, - ) - .unwrap() - .into(), - ]) - .set_help( - "The validate command validates a given composer.json and composer.lock\n\n\ - Exit codes in case of errors are:\n\ - 1 validation warning(s), only when --strict is given\n\ - 2 validation error(s)\n\ - 3 file unreadable or missing\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#validate", - ); + pub fn new() -> Self { + let mut command = ValidateCommand { + base_command_data: BaseCommandData::new(None), + }; + command + .configure() + .expect("ValidateCommand::configure uses static, valid metadata"); + command + } +} + +impl Command for ValidateCommand { + fn configure(&mut self) -> anyhow::Result<()> { + self.set_name("validate")?; + self.set_description("Validates a composer.json and composer.lock"); + self.set_definition(&[ + InputOption::new( + "no-check-all", + None, + Some(InputOption::VALUE_NONE), + "Do not validate requires for overly strict/loose constraints", + None, + ) + .unwrap() + .into(), + InputOption::new( + "check-lock", + None, + Some(InputOption::VALUE_NONE), + "Check if lock file is up to date (even when config.lock is false)", + None, + ) + .unwrap() + .into(), + InputOption::new( + "no-check-lock", + None, + Some(InputOption::VALUE_NONE), + "Do not check if lock file is up to date", + None, + ) + .unwrap() + .into(), + InputOption::new( + "no-check-publish", + None, + Some(InputOption::VALUE_NONE), + "Do not check for publish errors", + None, + ) + .unwrap() + .into(), + InputOption::new( + "no-check-version", + None, + Some(InputOption::VALUE_NONE), + "Do not report a warning if the version field is present", + None, + ) + .unwrap() + .into(), + InputOption::new( + "with-dependencies", + Some(shirabe_php_shim::PhpMixed::String("A".to_string())), + Some(InputOption::VALUE_NONE), + "Also validate the composer.json of all installed dependencies", + None, + ) + .unwrap() + .into(), + InputOption::new( + "strict", + None, + Some(InputOption::VALUE_NONE), + "Return a non-zero exit code for warnings as well as errors", + None, + ) + .unwrap() + .into(), + InputArgument::new( + "file", + Some(InputArgument::OPTIONAL), + "path to composer.json file", + None, + ) + .unwrap() + .into(), + ]); + self.set_help( + "The validate command validates a given composer.json and composer.lock\n\n\ + Exit codes in case of errors are:\n\ + 1 validation warning(s), only when --strict is given\n\ + 2 validation error(s)\n\ + 3 file unreadable or missing\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#validate", + ); + Ok(()) } - pub fn execute( + fn execute( &mut self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Result<i64> { + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { let file = input .borrow() .get_argument("file")? @@ -279,6 +303,28 @@ impl ValidateCommand { Ok(exit_code.max(event_code)) } + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for ValidateCommand { + fn command_data_mut( + &mut self, + ) -> &mut shirabe_external_packages::symfony::console::command::command::CommandData { + self.base_command_data.command_data_mut() + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); +} + +impl ValidateCommand { fn output_result( &self, io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, @@ -372,13 +418,3 @@ impl ValidateCommand { } } } - -impl HasBaseCommandData for ValidateCommand { - fn base_command_data(&self) -> &BaseCommandData { - &self.base_command_data - } - - fn base_command_data_mut(&mut self) -> &mut BaseCommandData { - &mut self.base_command_data - } -} diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs index 37c0bb0..591114a 100644 --- a/crates/shirabe/src/console/application.rs +++ b/crates/shirabe/src/console/application.rs @@ -144,6 +144,10 @@ pub struct Application { disable_scripts_by_default: bool, /// Store the initial working directory at startup time initial_working_directory: Option<String>, + /// Self-reference used to hand commands their owning application (PHP's `$command->setApplication($this)`). + /// Set by `new_shared`; the run flow threads the upgraded `Rc` so command callbacks never + /// re-borrow the application while it is already borrowed. + me: std::rc::Weak<std::cell::RefCell<Application>>, } impl Application { @@ -212,6 +216,7 @@ impl Application { disable_plugins_by_default: false, disable_scripts_by_default: false, initial_working_directory, + me: std::rc::Weak::new(), }; if defined("SIGINT") && SignalRegistry::is_supported() { this.signal_registry = Some(SignalRegistry::new()); @@ -225,8 +230,92 @@ impl Application { this } + /// Builds the application inside a shared `Rc<RefCell<…>>` and registers the default commands. + /// + /// Commands hold a back-pointer to their application, which shares this very `RefCell`. So the + /// whole run flow is driven through the `Rc` (never a long-lived `borrow_mut`), and command + /// registration happens here — before any borrow is taken — so `set_application` can borrow the + /// application without a re-entrant conflict. + pub fn new_shared( + name: String, + version: String, + ) -> anyhow::Result<std::rc::Rc<std::cell::RefCell<Application>>> { + let application = + std::rc::Rc::new(std::cell::RefCell::new(Application::new(name, version))); + application.borrow_mut().me = std::rc::Rc::downgrade(&application); + Application::init_shared(&application)?; + Ok(application) + } + + /// Registers the default commands on a shared application (PHP's lazy `init()`), executed once + /// with no application borrow held so each `set_application` can borrow back safely. + fn init_shared( + application: &std::rc::Rc<std::cell::RefCell<Application>>, + ) -> anyhow::Result<()> { + { + let mut app = application.borrow_mut(); + if app.initialized { + return Ok(()); + } + app.initialized = true; + } + + let commands = application.borrow().get_default_commands(); + for command in commands { + Application::add_shared(application, command)?; + } + + Ok(()) + } + + /// Adds a command object to a shared application (PHP's `add()`), without holding an application + /// borrow while calling into the command, so `set_application` can borrow back safely. + pub fn add_shared( + application: &std::rc::Rc<std::cell::RefCell<Application>>, + command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>, + ) -> anyhow::Result<Option<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> { + command.borrow_mut().set_application(Some( + application.clone() as std::rc::Rc<std::cell::RefCell<dyn BaseApplication>> + )); + + if !command.borrow().is_enabled() { + command.borrow_mut().set_application(None); + + return Ok(None); + } + + // if (!$command instanceof LazyCommand) { $command->getDefinition(); } + command.borrow().get_definition(); + + if command.borrow().get_name().is_none() { + return Err(ConsoleLogicException(shirabe_php_shim::LogicException { + message: format!( + "The command defined in \"{}\" cannot have an empty name.", + PhpMixed::from(shirabe_php_shim::get_debug_type_obj(&command)), + ), + code: 0, + }) + .into()); + } + + let name = command.borrow().get_name().unwrap(); + application + .borrow_mut() + .commands + .insert(name, command.clone()); + + for alias in command.borrow().get_aliases() { + application + .borrow_mut() + .commands + .insert(alias, command.clone()); + } + + Ok(Some(command)) + } + pub fn run( - &mut self, + application: &std::rc::Rc<std::cell::RefCell<Application>>, input: Option<std::rc::Rc<std::cell::RefCell<dyn InputInterface>>>, output: Option<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>, ) -> anyhow::Result<i64> { @@ -238,18 +327,18 @@ impl Application { ), }; - self.base_run(input, output) + Application::base_run(application, input, output) } pub fn do_run( - &mut self, + application: &std::rc::Rc<std::cell::RefCell<Application>>, input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { - self.disable_plugins_by_default = input + application.borrow_mut().disable_plugins_by_default = input .borrow() .has_parameter_option(PhpMixed::from(vec!["--no-plugins"]), false); - self.disable_scripts_by_default = input + application.borrow_mut().disable_scripts_by_default = input .borrow() .has_parameter_option(PhpMixed::from(vec!["--no-scripts"]), false); @@ -271,21 +360,23 @@ impl Application { ); let helper_set = std::rc::Rc::new(std::cell::RefCell::new(HelperSet::default())); HelperSet::new(&helper_set, helpers); - self.io = std::rc::Rc::new(std::cell::RefCell::new(ConsoleIO::new( + application.borrow_mut().io = std::rc::Rc::new(std::cell::RefCell::new(ConsoleIO::new( input.clone(), output.clone(), helper_set.borrow().clone(), ))); + // Cache the IO so the rest of the flow does not re-borrow the application; command callbacks + // (e.g. mergeApplicationDefinition) need the application's RefCell free. + let io = application.borrow().io.clone(); // Register error handler again to pass it the IO instance - ErrorHandler::register(Some(self.io.clone())); + ErrorHandler::register(Some(io.clone())); if input .borrow() .has_parameter_option(PhpMixed::from(vec!["--no-cache"]), false) { - self.io - .write_error3("Disabling cache usage", true, io_interface::DEBUG); + io.write_error3("Disabling cache usage", true, io_interface::DEBUG); Platform::put_env( "COMPOSER_CACHE_DIR", if Platform::is_windows() { @@ -297,14 +388,14 @@ impl Application { } // switch working dir - let new_work_dir = self.get_new_working_dir(input.clone())?; + let new_work_dir = application.borrow().get_new_working_dir(input.clone())?; let mut old_working_dir: Option<String> = None; if let Some(ref nwd) = new_work_dir { old_working_dir = Some(Platform::get_cwd(true).unwrap_or_default()); chdir(nwd); - self.initial_working_directory = getcwd(); + application.borrow_mut().initial_working_directory = getcwd(); let cwd = Platform::get_cwd(true).unwrap_or_default(); - self.io.write_error3( + io.write_error3( &format!( "Changed CWD to {}", if !cwd.is_empty() { @@ -320,17 +411,14 @@ impl Application { // determine command name to be executed without including plugin commands let mut command_name: Option<String> = Some(String::new()); - let raw_command_name = self.get_command_name_before_binding(input.clone())?; + let raw_command_name = application + .borrow_mut() + .get_command_name_before_binding(input.clone())?; if let Some(ref raw) = raw_command_name { - match self.find(raw) { + let find_result = application.borrow_mut().find(raw); + match find_result { Ok(cmd) => { - // TODO(phase-c): the Symfony Application stub keeps its command registry as - // PhpMixed with a todo!() find(), per the "Symfony stays todo!()" policy. - // Reading the resolved command's getName() needs the full typed-command - // registry, which lives in the external-package stub; until that is modelled - // we cannot recover the bound command name here. - let _ = cmd; - command_name = Some(String::new()); + command_name = cmd.borrow().get_name(); } Err(e) => { if e.downcast_ref::<CommandNotFoundException>().is_some() { @@ -355,7 +443,8 @@ impl Application { "create-project".to_string(), "outdated".to_string(), ]; - let use_parent_dir_if_no_json_available = self.get_use_parent_dir_config_value(); + let use_parent_dir_if_no_json_available = + application.borrow().get_use_parent_dir_config_value(); let no_composer_json_commands_pm = PhpMixed::List( no_composer_json_commands .iter() @@ -402,18 +491,18 @@ impl Application { Factory::get_composer_file().unwrap_or_default() )) { if use_parent_dir_if_no_json_available.as_bool() != Some(true) - && !self.io.is_interactive() + && !io.is_interactive() { - self.io.write_error(&format!("<info>No composer.json in current directory, to use the one at {} run interactively or set config.use-parent-dir to true</info>", dir)); + io.write_error(&format!("<info>No composer.json in current directory, to use the one at {} run interactively or set config.use-parent-dir to true</info>", dir)); break; } if use_parent_dir_if_no_json_available.as_bool() == Some(true) - || self.io.ask_confirmation(format!("<info>No composer.json in current directory, do you want to use the one at {}?</info> [<comment>y,n</comment>]? ", dir), true) + || io.ask_confirmation(format!("<info>No composer.json in current directory, do you want to use the one at {}?</info> [<comment>y,n</comment>]? ", dir), true) { if use_parent_dir_if_no_json_available.as_bool() == Some(true) { - self.io.write_error(&format!("<info>No composer.json in current directory, changing working directory to {}</info>", dir)); + io.write_error(&format!("<info>No composer.json in current directory, changing working directory to {}</info>", dir)); } else { - self.io.write_error("<info>Always want to use the parent dir? Use \"composer config --global use-parent-dir true\" to change the default.</info>"); + io.write_error("<info>Always want to use the parent dir? Use \"composer config --global use-parent-dir true\" to change the default.</info>"); } old_working_dir = Some(Platform::get_cwd(true).unwrap_or_default()); chdir(&dir); @@ -433,7 +522,7 @@ impl Application { // Clobber sudo credentials if COMPOSER_ALLOW_SUPERUSER is not set before loading plugins if needs_sudo_check { - is_non_allowed_root = self.is_running_as_root(); + is_non_allowed_root = application.borrow().is_running_as_root(); if is_non_allowed_root { let uid: i64 = Platform::get_env("SUDO_UID") @@ -482,15 +571,17 @@ impl Application { || command_name.as_deref() == Some("run-script") || raw_command_name != command_name; - if may_need_plugin_command && !self.disable_plugins_by_default && !self.has_plugin_commands + if may_need_plugin_command + && !application.borrow().disable_plugins_by_default + && !application.borrow().has_plugin_commands { // at this point plugins are needed, so if we are running as root and it is not allowed we need to prompt // if interactive, and abort otherwise if is_non_allowed_root { - self.io.write_error("<warning>Do not run Composer as root/super user! See https://getcomposer.org/root for details</warning>"); + io.write_error("<warning>Do not run Composer as root/super user! See https://getcomposer.org/root for details</warning>"); - if self.io.is_interactive() - && self.io.ask_confirmation( + if io.is_interactive() + && io.ask_confirmation( "<info>Continue as root/super user</info> [<comment>yes</comment>]? " .to_string(), true, @@ -499,7 +590,7 @@ impl Application { // avoid a second prompt later is_non_allowed_root = false; } else { - self.io.write_error("<warning>Aborting as no plugin should be loaded if running as super user is not explicitly allowed</warning>"); + io.write_error("<warning>Aborting as no plugin should be loaded if running as super user is not explicitly allowed</warning>"); return Ok(1); } @@ -510,9 +601,10 @@ impl Application { // because get_plugin_commands borrows &mut self, conflicting with io. let mut plugin_warnings: Vec<String> = Vec::new(); match (|| -> anyhow::Result<()> { - for command in self.get_plugin_commands()? { + let plugin_commands = application.borrow_mut().get_plugin_commands()?; + for command in plugin_commands { let cmd_name = command.get_name().unwrap_or_default(); - if self.has(&cmd_name) { + if application.borrow_mut().has(&cmd_name) { // TODO(plugin): PHP uses get_class($command) for the skipped-command class // name. Plugin command discovery (get_plugin_commands) is unimplemented, so // this loop never runs; wire the concrete class name with the plugin API. @@ -539,7 +631,7 @@ impl Application { let line = details.line; - let mut ghe = GithubActionError::new(self.io.clone()); + let mut ghe = GithubActionError::new(io.clone()); ghe.emit(&pe.message, file.as_deref(), line); return Err(e); @@ -549,33 +641,39 @@ impl Application { } } for warning in &plugin_warnings { - self.io.write_error(warning); + io.write_error(warning); } - self.has_plugin_commands = true; + application.borrow_mut().has_plugin_commands = true; } - if !self.disable_plugins_by_default && is_non_allowed_root && !self.io.is_interactive() { - self.io.write_error("<error>Composer plugins have been disabled for safety in this non-interactive session.</error>"); - self.io.write_error("<error>Set COMPOSER_ALLOW_SUPERUSER=1 if you want to allow plugins to run as root/super user.</error>"); - self.disable_plugins_by_default = true; + if !application.borrow().disable_plugins_by_default + && is_non_allowed_root + && !io.is_interactive() + { + io.write_error("<error>Composer plugins have been disabled for safety in this non-interactive session.</error>"); + io.write_error("<error>Set COMPOSER_ALLOW_SUPERUSER=1 if you want to allow plugins to run as root/super user.</error>"); + application.borrow_mut().disable_plugins_by_default = true; } // determine command name to be executed incl plugin commands, and check if it's a proxy command - let is_proxy_command = false; - if let Some(ref name) = self.get_command_name_before_binding(input.clone())? { - if let Ok(command) = self.find(name) { - // TODO(phase-c): same blocker as the earlier find() call — the Symfony command - // registry is a PhpMixed/todo!() stub, so the resolved command's name and its - // isProxyCommand() flag cannot be recovered until the typed-command registry is - // modelled in the external package. - let _ = command; - command_name = Some(String::new()); + let mut is_proxy_command = false; + let resolved_command_name = application + .borrow_mut() + .get_command_name_before_binding(input.clone())?; + if let Some(ref name) = resolved_command_name { + let find_result = application.borrow_mut().find(name); + if let Ok(command) = find_result { + command_name = command.borrow().get_name(); + // PHP: $command instanceof Command\BaseCommand && $command->isProxyCommand(). + // The Symfony `Command` trait exposes `is_proxy_command` (default false) so the + // `dyn SymfonyCommand` registry can answer this; Composer proxy commands override it. + is_proxy_command = command.borrow().is_proxy_command(); } } if !is_proxy_command { - self.io.write_error3( + io.write_error3( &format!( "Running {} ({}) with {} on {}", composer::get_version(), @@ -596,13 +694,13 @@ impl Application { ); if PHP_VERSION_ID < 70205 { - self.io.write_error(&format!("<warning>Composer supports PHP 7.2.5 and above, you will most likely encounter problems with your PHP {}. Upgrading is strongly recommended but you can use Composer 2.2.x LTS as a fallback.</warning>", PHP_VERSION)); + io.write_error(&format!("<warning>Composer supports PHP 7.2.5 and above, you will most likely encounter problems with your PHP {}. Upgrading is strongly recommended but you can use Composer 2.2.x LTS as a fallback.</warning>", PHP_VERSION)); } if XdebugHandler::is_xdebug_active() && Platform::get_env("COMPOSER_DISABLE_XDEBUG_WARN").is_none() { - self.io.write_error("<warning>Composer is operating slower than normal because you have Xdebug enabled. See https://getcomposer.org/xdebug</warning>"); + io.write_error("<warning>Composer is operating slower than normal because you have Xdebug enabled. See https://getcomposer.org/xdebug</warning>"); } if defined("COMPOSER_DEV_WARNING_TIME") @@ -610,7 +708,7 @@ impl Application { && command_name.as_deref() != Some("selfupdate") && time() > shirabe_php_shim::composer_dev_warning_time() { - self.io.write_error(&format!( + io.write_error(&format!( "<warning>Warning: This development build of Composer is over 60 days old. It is recommended to update it by running \"{} self-update\" to get the latest version.</warning>", shirabe_php_shim::server_get("PHP_SELF").unwrap_or_default(), )); @@ -621,10 +719,10 @@ impl Application { && command_name.as_deref() != Some("selfupdate") && command_name.as_deref() != Some("_complete") { - self.io.write_error("<warning>Do not run Composer as root/super user! See https://getcomposer.org/root for details</warning>"); + io.write_error("<warning>Do not run Composer as root/super user! See https://getcomposer.org/root for details</warning>"); - if self.io.is_interactive() { - if !self.io.ask_confirmation( + if io.is_interactive() { + if !io.ask_confirmation( "<info>Continue as root/super user</info> [<comment>yes</comment>]? " .to_string(), true, @@ -660,7 +758,7 @@ impl Application { .ok() .flatten(); if let Some(msg) = tempfile_msg { - self.io.write_error(&msg); + io.write_error(&msg); } // add non-standard scripts as own commands @@ -677,8 +775,8 @@ impl Application { str_replace("-", "_", &strtoupper(script)) ); if !defined(&script_event_const) { - if self.has(script) { - self.io.write_error(&format!("<warning>A script named {} would override a Composer command and has been skipped</warning>", script)); + if application.borrow_mut().has(script) { + io.write_error(&format!("<warning>A script named {} would override a Composer command and has been skipped</warning>", script)); } else { let mut description = format!( "Runs the {} script as defined in composer.json", @@ -708,7 +806,9 @@ impl Application { }) .unwrap_or_default(); - if let Some(composer) = self.get_composer(false, None, None)? { + let composer_opt = + application.borrow_mut().get_composer(false, None, None)?; + if let Some(composer) = composer_opt { let composer = crate::command::composer_full(&composer); let root_package = composer.get_package(); let generator = composer.get_autoload_generator().clone(); @@ -753,7 +853,7 @@ impl Application { "Symfony\\Component\\Console\\SingleCommandApplication", true, ) { - self.io.write_error(&format!("<warning>The script named {} extends SingleCommandApplication which is not compatible with Composer 2.9+, make sure you extend Symfony\\Component\\Console\\Command instead.</warning>", script)); + io.write_error(&format!("<warning>The script named {} extends SingleCommandApplication which is not compatible with Composer 2.9+, make sure you extend Symfony\\Component\\Console\\Command instead.</warning>", script)); } let mut cmd = shirabe_php_shim::instantiate_class( &dummy_str, @@ -821,17 +921,17 @@ impl Application { let _ = start_time.unwrap(); } - let result: i64 = self.base_do_run(input.clone(), output.clone())?; + let result: i64 = Application::base_do_run(application, input.clone(), output.clone())?; if input .borrow() .has_parameter_option(PhpMixed::from(vec!["--version", "-V"]), true) { - self.io.write_error(&format!( + io.write_error(&format!( "<info>PHP</info> version <comment>{}</comment> ({})", PHP_VERSION, PHP_BINARY, )); - self.io.write_error( + io.write_error( "Run the \"diagnose\" command to get more detailed diagnostics output.", ); } @@ -848,7 +948,7 @@ impl Application { } if let Some(st) = start_time { - self.io.write_error(&format!( + io.write_error(&format!( "<info>Memory usage: {}MiB (peak: {}MiB), time: {}s</info>", round((memory_get_usage() as f64) / 1024.0 / 1024.0, 2), round((memory_get_peak_usage(false) as f64) / 1024.0 / 1024.0, 2), @@ -863,12 +963,12 @@ impl Application { Ok(r) => Ok(r), Err(e) => { if let Some(see) = e.downcast_ref::<ScriptExecutionException>() { - if self.get_disable_plugins_by_default() - && self.is_running_as_root() - && !self.io.is_interactive() + if application.borrow().get_disable_plugins_by_default() + && application.borrow().is_running_as_root() + && !io.is_interactive() { - self.io.write_error3("<error>Plugins have been disabled automatically as you are running as root, this may be the cause of the script failure.</error>", true, io_interface::QUIET); - self.io.write_error3( + io.write_error3("<error>Plugins have been disabled automatically as you are running as root, this may be the cause of the script failure.</error>", true, io_interface::QUIET); + io.write_error3( "<error>See also https://getcomposer.org/root</error>", true, io_interface::QUIET, @@ -877,10 +977,10 @@ impl Application { Ok(see.get_code()) } else { - let mut ghe = GithubActionError::new(self.io.clone()); + let mut ghe = GithubActionError::new(io.clone()); ghe.emit(&e.to_string(), None, None); - self.hint_common_errors(&e, output); + application.borrow_mut().hint_common_errors(&e, output); // override TransportException's code for the purpose of parent::run() using it as process exit code // as http error codes are all beyond the 255 range of permitted exit codes @@ -1146,14 +1246,43 @@ impl Application { pub(crate) fn get_default_commands( &self, ) -> Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> { - // PHP: array_merge(parent::getDefaultCommands(), [new AboutCommand(), ...]). - // TODO(phase-c): the composer commands implement the shirabe BaseCommand trait, not the - // Symfony SymfonyCommand trait, and the orphan rule forbids a blanket - // `impl<C: HasBaseCommandData> SymfonyCommand for C`. Each command needs its own `impl - // SymfonyCommand` - // (or a wrapper) before they can be merged with `base_get_default_commands()` and returned; - // the parent list (`base_get_default_commands`) is likewise a stub. - vec![] + let mut commands = self.base_get_default_commands(); + let composer_commands: Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> = vec![ + std::rc::Rc::new(std::cell::RefCell::new(AboutCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(ConfigCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(DependsCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(ProhibitsCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(InitCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(InstallCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(CreateProjectCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(UpdateCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(SearchCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(ValidateCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(AuditCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(ShowCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(SuggestsCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(RequireCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(DumpAutoloadCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(StatusCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(ArchiveCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(DiagnoseCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(RunScriptCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(LicensesCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(GlobalCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(ClearCacheCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(RemoveCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(HomeCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(ExecCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(OutdatedCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(CheckPlatformReqsCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(FundCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(ReinstallCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(BumpCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(RepositoryCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new(SelfUpdateCommand::new())), + ]; + commands.extend(composer_commands); + commands } /// This ensures we can find the correct command name even if a global input option is present before it @@ -1311,13 +1440,17 @@ impl Application { /// Runs the current application (Symfony base; `parent::run`). pub fn base_run( - &mut self, + application: &std::rc::Rc<std::cell::RefCell<Application>>, input: Option<std::rc::Rc<std::cell::RefCell<dyn InputInterface>>>, output: Option<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>, ) -> anyhow::Result<i64> { if shirabe_php_shim::function_exists("putenv") { - shirabe_php_shim::putenv(&format!("LINES={}", self.terminal.get_height())); - shirabe_php_shim::putenv(&format!("COLUMNS={}", self.terminal.get_width())); + let (height, width) = { + let app = application.borrow(); + (app.terminal.get_height(), app.terminal.get_width()) + }; + shirabe_php_shim::putenv(&format!("LINES={}", height)); + shirabe_php_shim::putenv(&format!("COLUMNS={}", width)); } let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = match input { @@ -1347,9 +1480,9 @@ impl Application { }; let result = (|| -> anyhow::Result<i64> { - self.configure_io(&input, &output)?; + application.borrow_mut().configure_io(&input, &output)?; - let exit_code = self.do_run(input.clone(), output.clone())?; + let exit_code = Application::do_run(application, input.clone(), output.clone())?; Ok(exit_code) })(); @@ -1357,11 +1490,11 @@ impl Application { let mut exit_code = match result { Ok(exit_code) => exit_code, Err(e) => { - if !self.catch_exceptions { + if !application.borrow().catch_exceptions { return Err(e); } - render_exception(self, &e, &output); + render_exception(&application.borrow(), &e, &output); // $exitCode = $e->getCode(); // is_numeric($exitCode) ? max(1, (int) $exitCode) : 1 @@ -1379,7 +1512,7 @@ impl Application { // finally: handler restore. See TODO above; no-op here. - if self.auto_exit { + if application.borrow().auto_exit { if exit_code > 255 { exit_code = 255; } @@ -1392,7 +1525,7 @@ impl Application { /// Runs the current application (Symfony base; `parent::doRun`). pub fn base_do_run( - &mut self, + application: &std::rc::Rc<std::cell::RefCell<Application>>, input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { @@ -1403,15 +1536,17 @@ impl Application { ]), true, ) { + let long_version = application.borrow().get_long_version(); output .borrow() - .writeln(&[self.get_long_version()], output_interface::OUTPUT_NORMAL); + .writeln(&[long_version], output_interface::OUTPUT_NORMAL); return Ok(0); } // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument. - match input.borrow_mut().bind(&self.get_definition().borrow()) { + let definition = application.borrow_mut().get_definition(); + match input.borrow_mut().bind(&definition.borrow()) { Ok(()) => {} Err(e) => { // Errors must be ignored, full binding/validation happens later when the command is known. @@ -1422,7 +1557,7 @@ impl Application { } let mut input = input; - let mut name = self.get_command_name(&*input.borrow()); + let mut name = application.borrow().get_command_name(&*input.borrow()); if input.borrow().has_parameter_option( PhpMixed::from(vec![ PhpMixed::from("--help".to_string()), @@ -1432,29 +1567,30 @@ impl Application { ) { if name.is_none() { name = Some("help".to_string()); + let default_command = application.borrow().default_command.clone(); input = std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new( vec![( PhpMixed::from("command_name".to_string()), - PhpMixed::from(self.default_command.clone()), + PhpMixed::from(default_command), )], None, )?)); } else { - self.want_helps = true; + application.borrow_mut().want_helps = true; } } let name = match name { Some(name) => name, None => { - let name = self.default_command.clone(); - let definition = self.get_definition(); + let name = application.borrow().default_command.clone(); + let definition = application.borrow_mut().get_definition(); let command_description = definition .borrow() .get_argument(&PhpMixed::from("command".to_string()))? .get_description() .to_string(); - let _new_command_argument = InputArgument::new( + let new_command_argument = InputArgument::new( "command".to_string(), Some(InputArgument::OPTIONAL), command_description, @@ -1462,24 +1598,29 @@ impl Application { )?; // $definition->setArguments(array_merge($definition->getArguments(), // ['command' => new InputArgument('command', InputArgument::OPTIONAL, ...)])) - // TODO(review): get_arguments() yields Rc<InputArgument> (shared, non-Clone) while - // set_arguments() consumes owned InputArgument values. Re-building the merged - // argument list requires an InputArgument clone/ownership strategy not yet present. - definition.borrow_mut().set_arguments(todo!( - "merge existing arguments with the new 'command' argument" - ))?; + let mut merged_arguments: Vec<InputArgument> = Vec::new(); + let mut replaced_command = false; + for (key, argument) in definition.borrow().get_arguments() { + if key == "command" { + merged_arguments.push(new_command_argument.clone()); + replaced_command = true; + } else { + merged_arguments.push((**argument).clone()); + } + } + if !replaced_command { + merged_arguments.push(new_command_argument); + } + definition.borrow_mut().set_arguments(merged_arguments)?; name } }; let command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>; - let find_result = - (|| -> anyhow::Result<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> { - self.running_command = None; - // the command name MUST be the first element of the input - self.find(&name) - })(); + application.borrow_mut().running_command = None; + // the command name MUST be the first element of the input + let find_result = application.borrow_mut().find(&name); match find_result { Ok(c) => { @@ -1528,7 +1669,7 @@ impl Application { return Ok(1); } - command = self.find(&alternative)?; + command = application.borrow_mut().find(&alternative)?; } } @@ -1538,9 +1679,16 @@ impl Application { // needs a design decision about how lazy commands are represented. let _ = std::marker::PhantomData::<LazyCommand>; - self.running_command = Some(command.clone()); - let exit_code = self.do_run_command(command.clone(), input.clone(), output.clone())?; - self.running_command = None; + application.borrow_mut().running_command = Some(command.clone()); + // do_run_command invokes the command's run(), which calls back into the application + // (mergeApplicationDefinition etc.); it must run with no application borrow held. + let exit_code = Application::do_run_command( + application, + command.clone(), + input.clone(), + output.clone(), + )?; + application.borrow_mut().running_command = None; Ok(exit_code) } @@ -2393,7 +2541,7 @@ impl Application { /// /// Returns 0 if everything went fine, or an error code. pub fn do_run_command( - &mut self, + application: &std::rc::Rc<std::cell::RefCell<Application>>, command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>, input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, @@ -2408,14 +2556,14 @@ impl Application { } } - if !self.signals_to_dispatch_event.is_empty() { + if !application.borrow().signals_to_dispatch_event.is_empty() { // $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [] // TODO(review): SymfonyCommand is not a SignalableCommandInterface here; downcast needed. let command_signals: Vec<i64> = Vec::new(); let _ = std::marker::PhantomData::<dyn SignalableCommandInterface>; if !command_signals.is_empty() { - if self.signal_registry.is_none() { + if application.borrow().signal_registry.is_none() { return Err(ConsoleRuntimeException(shirabe_php_shim::RuntimeException { message: "Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that \"pcntl_*\" functions are not disabled by your php.ini's \"disable_functions\" directive.".to_string(), code: 0, @@ -2440,10 +2588,7 @@ impl Application { } } - command.borrow_mut().run( - &mut *borrow_input_mut(&input), - &mut *borrow_output_mut(&output), - ) + command.borrow_mut().run(input.clone(), output.clone()) } /// Gets the name of the command based on input. @@ -2540,18 +2685,21 @@ impl Application { pub fn base_get_default_commands( &self, ) -> Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> { - // return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()]; - // TODO(review): HelpCommand/ListCommand/CompleteCommand/DumpCompletionCommand are ported as - // distinct structs (not subtypes of the concrete `SymfonyCommand` struct), so they cannot populate - // a Vec<Rc<RefCell<dyn SymfonyCommand>>>. Reconciling SymfonyCommand subclassing with Rust requires the - // command-hierarchy design decision (see also `add`/`find`/LazyCommand handling). - let _ = std::marker::PhantomData::<( - shirabe_external_packages::symfony::console::command::help_command::HelpCommand, - shirabe_external_packages::symfony::console::command::list_command::ListCommand, - shirabe_external_packages::symfony::console::command::complete_command::CompleteCommand, - shirabe_external_packages::symfony::console::command::dump_completion_command::DumpCompletionCommand, - )>; - todo!("construct default commands once SymfonyCommand-subclass representation is decided") + use shirabe_external_packages::symfony::console::command::complete_command::CompleteCommand; + use shirabe_external_packages::symfony::console::command::dump_completion_command::DumpCompletionCommand; + use shirabe_external_packages::symfony::console::command::help_command::HelpCommand; + use shirabe_external_packages::symfony::console::command::list_command::ListCommand; + + vec![ + std::rc::Rc::new(std::cell::RefCell::new(HelpCommand::new())) + as std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>, + std::rc::Rc::new(std::cell::RefCell::new(ListCommand::new())), + std::rc::Rc::new(std::cell::RefCell::new( + CompleteCommand::new(IndexMap::new()) + .expect("CompleteCommand with default outputs is valid"), + )), + std::rc::Rc::new(std::cell::RefCell::new(DumpCompletionCommand::new())), + ] } /// Gets the default helper set with the helpers that should always be available. diff --git a/crates/shirabe/src/console/input/input_argument.rs b/crates/shirabe/src/console/input/input_argument.rs index 81f38ac..53576dd 100644 --- a/crates/shirabe/src/console/input/input_argument.rs +++ b/crates/shirabe/src/console/input/input_argument.rs @@ -29,4 +29,10 @@ impl InputArgument { )?; Ok(Self { inner }) } + + /// Unwraps to the underlying Symfony `InputArgument` (used when forwarding a Composer-typed + /// definition to the Symfony command state). + pub(crate) fn to_base(&self) -> BaseInputArgument { + self.inner.clone() + } } diff --git a/crates/shirabe/src/console/input/input_option.rs b/crates/shirabe/src/console/input/input_option.rs index 3298be0..c7b2af5 100644 --- a/crates/shirabe/src/console/input/input_option.rs +++ b/crates/shirabe/src/console/input/input_option.rs @@ -30,4 +30,10 @@ impl InputOption { BaseInputOption::new(name, shortcut, mode, description.to_string(), default_mixed)?; Ok(Self { inner }) } + + /// Unwraps to the underlying Symfony `InputOption` (used when forwarding a Composer-typed + /// definition to the Symfony command state). + pub(crate) fn to_base(&self) -> BaseInputOption { + self.inner.clone() + } } diff --git a/crates/shirabe/src/console/input/mod.rs b/crates/shirabe/src/console/input/mod.rs index 093d2f4..9141f0c 100644 --- a/crates/shirabe/src/console/input/mod.rs +++ b/crates/shirabe/src/console/input/mod.rs @@ -9,6 +9,21 @@ pub enum InputDefinitionItem { Option(input_option::InputOption), } +impl InputDefinitionItem { + /// Converts to the Symfony-typed definition item accepted by `CommandData::set_definition`. + pub(crate) fn to_definition_item( + &self, + ) -> shirabe_external_packages::symfony::console::input::input_definition::DefinitionItem { + use shirabe_external_packages::symfony::console::input::input_definition::DefinitionItem; + match self { + InputDefinitionItem::Argument(argument) => { + DefinitionItem::InputArgument(argument.to_base()) + } + InputDefinitionItem::Option(option) => DefinitionItem::InputOption(option.to_base()), + } + } +} + impl From<input_argument::InputArgument> for InputDefinitionItem { fn from(value: input_argument::InputArgument) -> Self { Self::Argument(value) diff --git a/crates/shirabe/src/main.rs b/crates/shirabe/src/main.rs index 6281ddd..f179c52 100644 --- a/crates/shirabe/src/main.rs +++ b/crates/shirabe/src/main.rs @@ -14,8 +14,14 @@ fn main() { ); // run the command application - let mut application = Application::new("Composer".to_string(), String::new()); - let result = application.run(None, None); + let application = match Application::new_shared("Composer".to_string(), String::new()) { + Ok(application) => application, + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + }; + let result = Application::run(&application, None, None); run_shutdown_functions(); if let Err(e) = result { eprintln!("{}", e); |
