diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-07 07:26:48 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-07 07:26:48 +0900 |
| commit | f749a47804cd296a3059cd3f8079c62dbaa5fdc0 (patch) | |
| tree | a84d5d40f6f9eea2a83355a273d0fb57214a864a | |
| parent | e7f83b74e8f8c12b4a1b0f9f613387b03858dbdd (diff) | |
| download | php-shirabe-f749a47804cd296a3059cd3f8079c62dbaa5fdc0.tar.gz php-shirabe-f749a47804cd296a3059cd3f8079c62dbaa5fdc0.tar.zst php-shirabe-f749a47804cd296a3059cd3f8079c62dbaa5fdc0.zip | |
refactor: merge split inherent impl blocks into one per type
Enable clippy::multiple_inherent_impl and fix the 21 sites it reports.
Types whose inherent methods were spread across two or three impl blocks
now keep them in a single block; only the impl headers move, no method
bodies change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
19 files changed, 4507 insertions, 4548 deletions
@@ -60,4 +60,5 @@ diverging_sub_expression = "allow" if_same_then_else = "allow" large_enum_variant = "allow" module_inception = "allow" +multiple_inherent_impl = "warn" type_complexity = "allow" diff --git a/crates/shirabe-external-packages/src/symfony/console/input/array_input.rs b/crates/shirabe-external-packages/src/symfony/console/input/array_input.rs index 1ae7069d..9a19b839 100644 --- a/crates/shirabe-external-packages/src/symfony/console/input/array_input.rs +++ b/crates/shirabe-external-packages/src/symfony/console/input/array_input.rs @@ -130,67 +130,7 @@ impl ArrayInput { default } -} - -/// Returns a stringified representation of the args passed to the command. -impl std::fmt::Display for ArrayInput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut params: Vec<String> = vec![]; - for (param, val) in &self.parameters { - // $param && \is_string($param) && '-' === $param[0] - let is_option_key = - matches!(param, PhpMixed::String(s) if !s.is_empty() && s.as_bytes()[0] == b'-'); - if is_option_key { - let param = param.as_string().unwrap(); - let glue = if param.as_bytes().get(1) == Some(&b'-') { - "=" - } else { - " " - }; - if let PhpMixed::List(list) = val { - for v in list { - let v = shirabe_php_shim::php_to_string(v); - params.push(format!( - "{}{}", - param, - if !v.is_empty() { - format!("{}{}", glue, self.inner.escape_token(&v)) - } else { - String::new() - } - )); - } - } else { - let val = shirabe_php_shim::php_to_string(val); - params.push(format!( - "{}{}", - param, - if !val.is_empty() { - format!("{}{}", glue, self.inner.escape_token(&val)) - } else { - String::new() - } - )); - } - } else if let PhpMixed::List(list) = val { - let escaped: Vec<String> = list - .iter() - .map(|v| self.inner.escape_token(&shirabe_php_shim::php_to_string(v))) - .collect(); - params.push(shirabe_php_shim::implode(" ", &escaped)); - } else { - params.push( - self.inner - .escape_token(&shirabe_php_shim::php_to_string(val)), - ); - } - } - - write!(f, "{}", shirabe_php_shim::implode(" ", ¶ms)) - } -} -impl ArrayInput { fn parse(&mut self) -> anyhow::Result<()> { // Clone to avoid borrowing self while mutating; PHP iterates over a copy semantically. let parameters = self.parameters.clone(); @@ -296,6 +236,64 @@ impl ArrayInput { } } +/// Returns a stringified representation of the args passed to the command. +impl std::fmt::Display for ArrayInput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut params: Vec<String> = vec![]; + for (param, val) in &self.parameters { + // $param && \is_string($param) && '-' === $param[0] + let is_option_key = + matches!(param, PhpMixed::String(s) if !s.is_empty() && s.as_bytes()[0] == b'-'); + if is_option_key { + let param = param.as_string().unwrap(); + let glue = if param.as_bytes().get(1) == Some(&b'-') { + "=" + } else { + " " + }; + if let PhpMixed::List(list) = val { + for v in list { + let v = shirabe_php_shim::php_to_string(v); + params.push(format!( + "{}{}", + param, + if !v.is_empty() { + format!("{}{}", glue, self.inner.escape_token(&v)) + } else { + String::new() + } + )); + } + } else { + let val = shirabe_php_shim::php_to_string(val); + params.push(format!( + "{}{}", + param, + if !val.is_empty() { + format!("{}{}", glue, self.inner.escape_token(&val)) + } else { + String::new() + } + )); + } + } else if let PhpMixed::List(list) = val { + let escaped: Vec<String> = list + .iter() + .map(|v| self.inner.escape_token(&shirabe_php_shim::php_to_string(v))) + .collect(); + params.push(shirabe_php_shim::implode(" ", &escaped)); + } else { + params.push( + self.inner + .escape_token(&shirabe_php_shim::php_to_string(val)), + ); + } + } + + write!(f, "{}", shirabe_php_shim::implode(" ", ¶ms)) + } +} + impl InputInterface for ArrayInput { fn dup(&self) -> std::rc::Rc<std::cell::RefCell<dyn InputInterface>> { std::rc::Rc::new(std::cell::RefCell::new(self.clone())) diff --git a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs index 9097faff..b62c2565 100644 --- a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs +++ b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs @@ -479,9 +479,7 @@ impl SymfonyStyle { as Box<dyn Fn(Option<PhpMixed>) -> Result<PhpMixed, InvalidArgumentException>> }) } -} -impl SymfonyStyle { /// {@inheritdoc} pub fn writeln(&mut self, messages: PhpMixed, r#type: i64) { let messages: Vec<PhpMixed> = if !shirabe_php_shim::is_iterable(&messages) { diff --git a/crates/shirabe-php-shim/src/zip.rs b/crates/shirabe-php-shim/src/zip.rs index 56f02db7..6a088419 100644 --- a/crates/shirabe-php-shim/src/zip.rs +++ b/crates/shirabe-php-shim/src/zip.rs @@ -265,9 +265,7 @@ impl ZipArchive { _ => String::new(), } } -} -impl ZipArchive { pub const CREATE: i64 = 1; pub const OPSYS_UNIX: i64 = 3; pub const ER_SEEK: i64 = 4; diff --git a/crates/shirabe/src/command/archive_command.rs b/crates/shirabe/src/command/archive_command.rs index 7a24dacc..e749d13f 100644 --- a/crates/shirabe/src/command/archive_command.rs +++ b/crates/shirabe/src/command/archive_command.rs @@ -80,155 +80,7 @@ impl ArchiveCommand { .expect("ArchiveCommand::configure uses static, valid metadata"); command } -} - -impl Command for ArchiveCommand { - fn configure(&self) -> anyhow::Result<()> { - self.set_name("archive")?; - self.set_description("Creates an archive of this composer package"); - self.set_definition(&[ - InputArgument::new5("package", Some(InputArgument::OPTIONAL), "The package to archive instead of the current project", None, self.suggest_available_package(99)).unwrap().into(), - InputArgument::new("version", Some(InputArgument::OPTIONAL), "A version constraint to find the package to archive", None).unwrap().into(), - InputOption::new6("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, SuggestedValues::List(Self::FORMATS.iter().map(|s| s.to_string()).collect())).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>shirabe archive [--format=zip] [--dir=/foo] [--file=filename] [package [version]]</info>\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#archive" - ); - Ok(()) - } - - fn execute( - &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<i64> { - let composer = self.try_composer(None, None); - - let config = if let Some(ref composer) = composer { - let config = composer.borrow_partial().get_config(); - // TODO(plugin): dispatch CommandEvent - let command_event = - CommandEvent::new(PluginEvents::COMMAND, "archive", input.clone(), output); - let event_dispatcher = composer.borrow_partial().get_event_dispatcher(); - event_dispatcher - .borrow_mut() - .dispatch(Some(command_event.get_name()), None); - event_dispatcher.borrow_mut().dispatch_script( - ScriptEvents::PRE_ARCHIVE_CMD, - true, - vec![], - indexmap::IndexMap::new(), - ); - config - } else { - std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?)) - }; - - let format = input - .borrow() - .get_option("format")? - .as_string() - .map(|s| s.to_string()) - .unwrap_or_else(|| { - config - .borrow_mut() - .get("archive-format") - .as_string() - .unwrap_or("tar") - .to_string() - }); - - let dir = input - .borrow() - .get_option("dir")? - .as_string() - .map(|s| s.to_string()) - .unwrap_or_else(|| { - config - .borrow_mut() - .get("archive-dir") - .as_string() - .unwrap_or(".") - .to_string() - }); - let io = self.get_io().clone(); - let return_code = self.archive( - io.clone(), - &config, - input - .borrow() - .get_argument("package")? - .as_string() - .map(|s| s.to_string()), - input - .borrow() - .get_argument("version")? - .as_string() - .map(|s| s.to_string()), - &format, - &dir, - input - .borrow() - .get_option("file")? - .as_string() - .map(|s| s.to_string()), - input - .borrow() - .get_option("ignore-filters")? - .as_bool() - .unwrap_or(false), - composer.as_ref(), - )?; - - if return_code == 0 - && let Some(ref composer) = composer - { - composer - .borrow_partial() - .get_event_dispatcher() - .borrow_mut() - .dispatch_script( - ScriptEvents::POST_ARCHIVE_CMD, - true, - vec![], - indexmap::IndexMap::new(), - ); - } - - Ok(return_code) - } - - fn initialize( - &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<()> { - if self.test_hooks.borrow().skip_initialize { - return Ok(()); - } - base_command_initialize(self, input, output) - } - - fn complete( - &self, - input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, - suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, - ) -> anyhow::Result<()> { - crate::command::base_command::base_command_complete(self, input, suggestions) - } - - shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); -} - -impl ArchiveCommand { /// For testing only: makes `initialize` a no-op (PHPUnit `onlyMethods(['initialize'])`). pub fn __test_skip_initialize(&self) { self.test_hooks.borrow_mut().skip_initialize = true; @@ -244,17 +96,7 @@ impl ArchiveCommand { pub fn __test_archive_calls(&self) -> Vec<ArchiveCallRecord> { self.test_hooks.borrow().archive_calls.clone() } -} - -impl BaseCommand for ArchiveCommand { - fn base_command_data(&self) -> &crate::command::BaseCommandData { - &self.base_command_data - } - - crate::delegate_base_command_trait_impls_to_inner!(base_command_data); -} -impl ArchiveCommand { #[allow(clippy::too_many_arguments, reason = "to keep PHP signature")] pub fn archive( &self, @@ -474,3 +316,157 @@ impl ArchiveCommand { Ok(Some(complete)) } } + +impl Command for ArchiveCommand { + fn configure(&self) -> anyhow::Result<()> { + self.set_name("archive")?; + self.set_description("Creates an archive of this composer package"); + self.set_definition(&[ + InputArgument::new5("package", Some(InputArgument::OPTIONAL), "The package to archive instead of the current project", None, self.suggest_available_package(99)).unwrap().into(), + InputArgument::new("version", Some(InputArgument::OPTIONAL), "A version constraint to find the package to archive", None).unwrap().into(), + InputOption::new6("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, SuggestedValues::List(Self::FORMATS.iter().map(|s| s.to_string()).collect())).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>shirabe archive [--format=zip] [--dir=/foo] [--file=filename] [package [version]]</info>\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#archive" + ); + Ok(()) + } + + fn execute( + &self, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + let composer = self.try_composer(None, None); + + let config = if let Some(ref composer) = composer { + let config = composer.borrow_partial().get_config(); + // TODO(plugin): dispatch CommandEvent + let command_event = + CommandEvent::new(PluginEvents::COMMAND, "archive", input.clone(), output); + let event_dispatcher = composer.borrow_partial().get_event_dispatcher(); + event_dispatcher + .borrow_mut() + .dispatch(Some(command_event.get_name()), None); + event_dispatcher.borrow_mut().dispatch_script( + ScriptEvents::PRE_ARCHIVE_CMD, + true, + vec![], + indexmap::IndexMap::new(), + ); + config + } else { + std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?)) + }; + + let format = input + .borrow() + .get_option("format")? + .as_string() + .map(|s| s.to_string()) + .unwrap_or_else(|| { + config + .borrow_mut() + .get("archive-format") + .as_string() + .unwrap_or("tar") + .to_string() + }); + + let dir = input + .borrow() + .get_option("dir")? + .as_string() + .map(|s| s.to_string()) + .unwrap_or_else(|| { + config + .borrow_mut() + .get("archive-dir") + .as_string() + .unwrap_or(".") + .to_string() + }); + + let io = self.get_io().clone(); + let return_code = self.archive( + io.clone(), + &config, + input + .borrow() + .get_argument("package")? + .as_string() + .map(|s| s.to_string()), + input + .borrow() + .get_argument("version")? + .as_string() + .map(|s| s.to_string()), + &format, + &dir, + input + .borrow() + .get_option("file")? + .as_string() + .map(|s| s.to_string()), + input + .borrow() + .get_option("ignore-filters")? + .as_bool() + .unwrap_or(false), + composer.as_ref(), + )?; + + if return_code == 0 + && let Some(ref composer) = composer + { + composer + .borrow_partial() + .get_event_dispatcher() + .borrow_mut() + .dispatch_script( + ScriptEvents::POST_ARCHIVE_CMD, + true, + vec![], + indexmap::IndexMap::new(), + ); + } + + Ok(return_code) + } + + fn initialize( + &self, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + if self.test_hooks.borrow().skip_initialize { + return Ok(()); + } + base_command_initialize(self, input, output) + } + + fn complete( + &self, + input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, + suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, + ) -> anyhow::Result<()> { + crate::command::base_command::base_command_complete(self, input, suggestions) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for ArchiveCommand { + fn base_command_data(&self) -> &crate::command::BaseCommandData { + &self.base_command_data + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); +} diff --git a/crates/shirabe/src/command/audit_command.rs b/crates/shirabe/src/command/audit_command.rs index 082afdc2..0eee0036 100644 --- a/crates/shirabe/src/command/audit_command.rs +++ b/crates/shirabe/src/command/audit_command.rs @@ -44,6 +44,60 @@ impl AuditCommand { .expect("AuditCommand::configure uses static, valid metadata"); command } + + fn get_packages( + &self, + composer: &PartialComposerHandle, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + ) -> anyhow::Result<Vec<crate::package::PackageInterfaceHandle>> { + let composer = crate::composer::composer_full(composer); + if input + .borrow() + .get_option("locked")? + .as_bool() + .unwrap_or(false) + { + let locker = composer.get_locker().clone(); + let mut locker = locker.borrow_mut(); + if !locker.is_locked() { + return Err(UnexpectedValueException { + message: "Valid composer.json and composer.lock files are required to run this command with --locked".to_string(), + code: 0, + }.into()); + } + let locked_repo = locker.get_locked_repository( + !input + .borrow() + .get_option("no-dev")? + .as_bool() + .unwrap_or(false), + )?; + return locked_repo.borrow_mut().get_packages(); + } + + let root_pkg = composer.get_package(); + let local_repo = composer + .get_repository_manager() + .borrow() + .get_local_repository(); + let mut installed_repo = InstalledRepository::new(vec![local_repo]); + + if input + .borrow() + .get_option("no-dev")? + .as_bool() + .unwrap_or(false) + { + return Ok(RepositoryUtils::filter_required_packages( + &installed_repo.get_packages()?, + root_pkg.clone().into(), + false, + vec![], + )); + } + + installed_repo.get_packages() + } } impl Command for AuditCommand { @@ -255,59 +309,3 @@ impl BaseCommand for AuditCommand { crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } - -impl AuditCommand { - fn get_packages( - &self, - composer: &PartialComposerHandle, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - ) -> anyhow::Result<Vec<crate::package::PackageInterfaceHandle>> { - let composer = crate::composer::composer_full(composer); - if input - .borrow() - .get_option("locked")? - .as_bool() - .unwrap_or(false) - { - let locker = composer.get_locker().clone(); - let mut locker = locker.borrow_mut(); - if !locker.is_locked() { - return Err(UnexpectedValueException { - message: "Valid composer.json and composer.lock files are required to run this command with --locked".to_string(), - code: 0, - }.into()); - } - let locked_repo = locker.get_locked_repository( - !input - .borrow() - .get_option("no-dev")? - .as_bool() - .unwrap_or(false), - )?; - return locked_repo.borrow_mut().get_packages(); - } - - let root_pkg = composer.get_package(); - let local_repo = composer - .get_repository_manager() - .borrow() - .get_local_repository(); - let mut installed_repo = InstalledRepository::new(vec![local_repo]); - - if input - .borrow() - .get_option("no-dev")? - .as_bool() - .unwrap_or(false) - { - return Ok(RepositoryUtils::filter_required_packages( - &installed_repo.get_packages()?, - root_pkg.clone().into(), - false, - vec![], - )); - } - - installed_repo.get_packages() - } -} diff --git a/crates/shirabe/src/command/config_command.rs b/crates/shirabe/src/command/config_command.rs index c4409f20..2727f36b 100644 --- a/crates/shirabe/src/command/config_command.rs +++ b/crates/shirabe/src/command/config_command.rs @@ -61,15 +61,7 @@ impl ConfigCommand { "suggest", "extra", ]; -} - -impl Default for ConfigCommand { - fn default() -> Self { - Self::new() - } -} -impl ConfigCommand { pub fn new() -> Self { let command = ConfigCommand { base_command_data: BaseCommandData::new(None), @@ -84,6 +76,372 @@ impl ConfigCommand { .expect("ConfigCommand::configure uses static, valid metadata"); command } + + pub(crate) fn handle_single_value( + &self, + key: &str, + callbacks: &(ValidatorFn, NormalizerFn), + values: &[String], + method: &str, + ) -> anyhow::Result<()> { + let (validator, normalizer) = callbacks; + if 1 != values.len() { + return Err(RuntimeException { + message: "You can only pass one value. Example: shirabe config process-timeout 300" + .to_string(), + code: 0, + } + .into()); + } + + let validation = validator(&PhpMixed::String(values[0].clone())); + if validation.as_bool() != Some(true) { + let suffix = if !validation.is_null() && validation.as_bool() != Some(false) { + format!(" ({})", validation.as_string().unwrap_or("")) + } else { + String::new() + }; + return Err(RuntimeException { + message: format!("\"{}\" is an invalid value{}", values[0].clone(), suffix), + code: 0, + } + .into()); + } + + let normalized_value = normalizer(&PhpMixed::String(values[0].clone())); + + if key == "disable-tls" { + let config = self.config.borrow().as_ref().unwrap().clone(); + if !normalized_value.as_bool().unwrap_or(false) + && config + .borrow() + .get("disable-tls") + .as_bool() + .unwrap_or(false) + { + self.get_io().write_error( + "<info>You are now running Composer with SSL/TLS protection enabled.</info>", + ); + } else if normalized_value.as_bool().unwrap_or(false) + && !config + .borrow() + .get("disable-tls") + .as_bool() + .unwrap_or(false) + { + self.get_io().write_error("<warning>You are now running Composer with SSL/TLS protection disabled.</warning>"); + } + } + + let mut config_source = self.config_source.borrow_mut(); + let config_source = config_source.as_mut().unwrap(); + match method { + "addConfigSetting" => config_source.add_config_setting(key, normalized_value)?, + "addProperty" => config_source.add_property(key, normalized_value)?, + _ => unreachable!(), + } + Ok(()) + } + + pub(crate) fn handle_multi_value( + &self, + key: &str, + callbacks: &(ValidatorFn, NormalizerFn), + values: &[String], + method: &str, + ) -> anyhow::Result<()> { + let (validator, normalizer) = callbacks; + let values_mixed = + PhpMixed::List(values.iter().map(|s| PhpMixed::String(s.clone())).collect()); + let validation = validator(&values_mixed); + if validation.as_bool() != Some(true) { + let suffix = if !validation.is_null() && validation.as_bool() != Some(false) { + format!(" ({})", validation.as_string().unwrap_or("")) + } else { + String::new() + }; + return Err(RuntimeException { + message: format!( + "{} is an invalid value{}", + PhpMixed::from(json_encode(&values_mixed).ok()), + suffix + ), + code: 0, + } + .into()); + } + + let mut config_source = self.config_source.borrow_mut(); + let config_source = config_source.as_mut().unwrap(); + match method { + "addConfigSetting" => { + config_source.add_config_setting(key, normalizer(&values_mixed))? + } + "addProperty" => config_source.add_property(key, normalizer(&values_mixed))?, + _ => unreachable!(), + } + Ok(()) + } + + /// Display the contents of the file in a pretty formatted way + pub(crate) fn list_configuration( + &self, + contents: PhpMixed, + raw_contents: PhpMixed, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + k: Option<String>, + show_source: bool, + ) { + let orig_k = k.clone(); + let contents_arr = contents.as_array().cloned().unwrap_or_default(); + let raw_contents_arr = raw_contents.as_array().cloned().unwrap_or_default(); + let mut k = k; + for (key, value) in &contents_arr { + if k.is_none() && !matches!(key.as_str(), "config" | "repositories") { + continue; + } + + let raw_val = raw_contents_arr.get(key).cloned().unwrap_or(PhpMixed::Null); + + let value_inner = value.clone(); + + if is_array(&value_inner) + && (!is_numeric(&key_first_key(&value_inner).unwrap_or_default().into()) + || (key == "repositories" && k.is_none())) + { + let mut new_k = k.clone().unwrap_or_default(); + new_k.push_str(&Preg::replace( + php_regex!("{^config\\.}"), + "", + &format!("{}.", key), + )); + k = Some(new_k); + self.list_configuration( + value_inner, + raw_val, + output.clone(), + k.clone(), + show_source, + ); + k = orig_k.clone(); + + continue; + } + + let value_display: String = if is_array(&value_inner) { + let arr_strs: Vec<String> = value_inner + .as_list() + .map(|l| { + l.iter() + .map(|val| { + if is_array(val) { + json_encode(val).unwrap_or_default() + } else { + val.as_string().unwrap_or("").to_string() + } + }) + .collect::<Vec<_>>() + }) + .unwrap_or_default(); + format!("[{}]", implode(", ", &arr_strs)) + } else if is_bool(&value_inner) { + var_export(&value_inner, true) + } else { + value_inner.as_string().unwrap_or("").to_string() + }; + + let source = if show_source { + format!( + " ({})", + self.config + .borrow() + .as_ref() + .unwrap() + .borrow_mut() + .get_source_of_value(&format!("{}{}", k.clone().unwrap_or_default(), key)) + ) + } else { + String::new() + }; + + let link: String = + if k.is_some() && strpos(k.as_ref().unwrap(), "repositories") == Some(0) { + "https://getcomposer.org/doc/05-repositories.md".to_string() + } else { + let id_source = if k.as_deref() == Some("") || k.is_none() { + key.clone() + } else { + k.clone().unwrap() + }; + let id = Preg::replace(php_regex!("{\\..*$}"), "", &id_source); + let id = Preg::replace( + php_regex!("{[^a-z0-9]}i"), + "-", + &strtolower(&shirabe_php_shim::trim(&id, Some(" \t\n\r\0\u{0B}"))), + ); + let id = Preg::replace(php_regex!("{-+}"), "-", &id); + format!("https://getcomposer.org/doc/06-config.md#{}", id) + }; + if is_string(&raw_val) + && raw_val + .as_string() + .map(|s| s.to_string()) + .unwrap_or_default() + != value_display + { + self.get_io().write3( + &format!( + "[<fg=yellow;href={}>{}{}</>] <info>{} ({})</info>{}", + link, + k.clone().unwrap_or_default(), + key, + raw_val.as_string().unwrap_or(""), + value_display, + source + ), + true, + io_interface::QUIET, + ); + } else { + self.get_io().write3( + &format!( + "[<fg=yellow;href={}>{}{}</>] <info>{}</info>{}", + link, + k.clone().unwrap_or_default(), + key, + value_display, + source + ), + true, + io_interface::QUIET, + ); + } + } + } + + /// Suggest setting-keys, while taking given options in account. + fn suggest_setting_keys(&self) -> crate::console::input::SuggestedValues { + crate::console::input::SuggestedValues::Closure(Box::new(|this, input, _suggestions| { + if input.get_option("list")?.to_bool() + || input.get_option("editor")?.to_bool() + || input.get_option("auth")?.to_bool() + { + return Ok(vec![]); + } + + let this = this + .as_any() + .downcast_ref::<ConfigCommand>() + .expect("suggestSettingKeys is bound to ConfigCommand"); + // PHP passes the CompletionInput itself; the accessors only read from it, so a + // clone behind a fresh handle is equivalent. + let input_handle: std::rc::Rc< + std::cell::RefCell< + dyn shirabe_external_packages::symfony::console::input::InputInterface, + >, + > = std::rc::Rc::new(std::cell::RefCell::new(input.clone())); + + // initialize configuration + let mut config = Factory::create_config(None, None)?; + + // load configuration + let config_file = JsonFile::new( + this.get_composer_config_file(input_handle.clone(), &config)?, + None, + None, + )?; + if config_file.exists() { + let path = config_file.get_path().to_string(); + let data = config_file.read()?.as_array().cloned().unwrap_or_default(); + config.merge(&data, &path); + } + + // load auth-configuration + let auth_config_file = JsonFile::new( + this.get_auth_config_file(input_handle.clone(), &config)?, + None, + None, + )?; + if auth_config_file.exists() { + let path = auth_config_file.get_path().to_string(); + let mut data = IndexMap::new(); + data.insert("config".to_string(), auth_config_file.read()?); + config.merge(&data, &path); + } + + // collect all configuration setting-keys + let raw_config = config.raw(); + let mut keys = flatten_setting_keys( + raw_config.get("config").cloned().unwrap_or(PhpMixed::Null), + "", + ); + keys.extend(flatten_setting_keys( + raw_config + .get("repositories") + .cloned() + .unwrap_or(PhpMixed::Null), + "repositories.", + )); + + // if unsetting … + if input.get_option("unset")?.to_bool() { + // … keep only the currently customized setting-keys … + let sources = [ + config_file.get_path().to_string(), + auth_config_file.get_path().to_string(), + ]; + keys.retain(|key| sources.contains(&config.get_source_of_value(key))); + + // … else if showing or setting a value … + } else { + // … add all configurable package-properties, no matter if it exist + keys.extend( + Self::CONFIGURABLE_PACKAGE_PROPERTIES + .iter() + .map(|property| property.to_string()), + ); + + // it would be nice to distinguish between showing and setting + // a value, but that makes the implementation much more complex + // and partially impossible because symfony's implementation + // does not complete arguments followed by other arguments + } + + // add all existing configurable package-properties + if config_file.exists() { + let properties: IndexMap<String, PhpMixed> = config_file + .read()? + .as_array() + .cloned() + .unwrap_or_default() + .into_iter() + .filter(|(key, _)| { + Self::CONFIGURABLE_PACKAGE_PROPERTIES.contains(&key.as_str()) + }) + .collect(); + + keys.extend(flatten_setting_keys(PhpMixed::Array(properties), "")); + } + + // filter settings-keys by completion value + let completion_value = input.get_completion_value(); + + if !completion_value.is_empty() { + keys.retain(|key| key.starts_with(&completion_value)); + } + + keys.sort(); + + keys.dedup(); + Ok(keys) + })) + } +} + +impl Default for ConfigCommand { + fn default() -> Self { + Self::new() + } } impl Command for ConfigCommand { @@ -1275,368 +1633,6 @@ impl BaseCommand for ConfigCommand { crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } -impl ConfigCommand { - pub(crate) fn handle_single_value( - &self, - key: &str, - callbacks: &(ValidatorFn, NormalizerFn), - values: &[String], - method: &str, - ) -> anyhow::Result<()> { - let (validator, normalizer) = callbacks; - if 1 != values.len() { - return Err(RuntimeException { - message: "You can only pass one value. Example: shirabe config process-timeout 300" - .to_string(), - code: 0, - } - .into()); - } - - let validation = validator(&PhpMixed::String(values[0].clone())); - if validation.as_bool() != Some(true) { - let suffix = if !validation.is_null() && validation.as_bool() != Some(false) { - format!(" ({})", validation.as_string().unwrap_or("")) - } else { - String::new() - }; - return Err(RuntimeException { - message: format!("\"{}\" is an invalid value{}", values[0].clone(), suffix), - code: 0, - } - .into()); - } - - let normalized_value = normalizer(&PhpMixed::String(values[0].clone())); - - if key == "disable-tls" { - let config = self.config.borrow().as_ref().unwrap().clone(); - if !normalized_value.as_bool().unwrap_or(false) - && config - .borrow() - .get("disable-tls") - .as_bool() - .unwrap_or(false) - { - self.get_io().write_error( - "<info>You are now running Composer with SSL/TLS protection enabled.</info>", - ); - } else if normalized_value.as_bool().unwrap_or(false) - && !config - .borrow() - .get("disable-tls") - .as_bool() - .unwrap_or(false) - { - self.get_io().write_error("<warning>You are now running Composer with SSL/TLS protection disabled.</warning>"); - } - } - - let mut config_source = self.config_source.borrow_mut(); - let config_source = config_source.as_mut().unwrap(); - match method { - "addConfigSetting" => config_source.add_config_setting(key, normalized_value)?, - "addProperty" => config_source.add_property(key, normalized_value)?, - _ => unreachable!(), - } - Ok(()) - } - - pub(crate) fn handle_multi_value( - &self, - key: &str, - callbacks: &(ValidatorFn, NormalizerFn), - values: &[String], - method: &str, - ) -> anyhow::Result<()> { - let (validator, normalizer) = callbacks; - let values_mixed = - PhpMixed::List(values.iter().map(|s| PhpMixed::String(s.clone())).collect()); - let validation = validator(&values_mixed); - if validation.as_bool() != Some(true) { - let suffix = if !validation.is_null() && validation.as_bool() != Some(false) { - format!(" ({})", validation.as_string().unwrap_or("")) - } else { - String::new() - }; - return Err(RuntimeException { - message: format!( - "{} is an invalid value{}", - PhpMixed::from(json_encode(&values_mixed).ok()), - suffix - ), - code: 0, - } - .into()); - } - - let mut config_source = self.config_source.borrow_mut(); - let config_source = config_source.as_mut().unwrap(); - match method { - "addConfigSetting" => { - config_source.add_config_setting(key, normalizer(&values_mixed))? - } - "addProperty" => config_source.add_property(key, normalizer(&values_mixed))?, - _ => unreachable!(), - } - Ok(()) - } - - /// Display the contents of the file in a pretty formatted way - pub(crate) fn list_configuration( - &self, - contents: PhpMixed, - raw_contents: PhpMixed, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - k: Option<String>, - show_source: bool, - ) { - let orig_k = k.clone(); - let contents_arr = contents.as_array().cloned().unwrap_or_default(); - let raw_contents_arr = raw_contents.as_array().cloned().unwrap_or_default(); - let mut k = k; - for (key, value) in &contents_arr { - if k.is_none() && !matches!(key.as_str(), "config" | "repositories") { - continue; - } - - let raw_val = raw_contents_arr.get(key).cloned().unwrap_or(PhpMixed::Null); - - let value_inner = value.clone(); - - if is_array(&value_inner) - && (!is_numeric(&key_first_key(&value_inner).unwrap_or_default().into()) - || (key == "repositories" && k.is_none())) - { - let mut new_k = k.clone().unwrap_or_default(); - new_k.push_str(&Preg::replace( - php_regex!("{^config\\.}"), - "", - &format!("{}.", key), - )); - k = Some(new_k); - self.list_configuration( - value_inner, - raw_val, - output.clone(), - k.clone(), - show_source, - ); - k = orig_k.clone(); - - continue; - } - - let value_display: String = if is_array(&value_inner) { - let arr_strs: Vec<String> = value_inner - .as_list() - .map(|l| { - l.iter() - .map(|val| { - if is_array(val) { - json_encode(val).unwrap_or_default() - } else { - val.as_string().unwrap_or("").to_string() - } - }) - .collect::<Vec<_>>() - }) - .unwrap_or_default(); - format!("[{}]", implode(", ", &arr_strs)) - } else if is_bool(&value_inner) { - var_export(&value_inner, true) - } else { - value_inner.as_string().unwrap_or("").to_string() - }; - - let source = if show_source { - format!( - " ({})", - self.config - .borrow() - .as_ref() - .unwrap() - .borrow_mut() - .get_source_of_value(&format!("{}{}", k.clone().unwrap_or_default(), key)) - ) - } else { - String::new() - }; - - let link: String = - if k.is_some() && strpos(k.as_ref().unwrap(), "repositories") == Some(0) { - "https://getcomposer.org/doc/05-repositories.md".to_string() - } else { - let id_source = if k.as_deref() == Some("") || k.is_none() { - key.clone() - } else { - k.clone().unwrap() - }; - let id = Preg::replace(php_regex!("{\\..*$}"), "", &id_source); - let id = Preg::replace( - php_regex!("{[^a-z0-9]}i"), - "-", - &strtolower(&shirabe_php_shim::trim(&id, Some(" \t\n\r\0\u{0B}"))), - ); - let id = Preg::replace(php_regex!("{-+}"), "-", &id); - format!("https://getcomposer.org/doc/06-config.md#{}", id) - }; - if is_string(&raw_val) - && raw_val - .as_string() - .map(|s| s.to_string()) - .unwrap_or_default() - != value_display - { - self.get_io().write3( - &format!( - "[<fg=yellow;href={}>{}{}</>] <info>{} ({})</info>{}", - link, - k.clone().unwrap_or_default(), - key, - raw_val.as_string().unwrap_or(""), - value_display, - source - ), - true, - io_interface::QUIET, - ); - } else { - self.get_io().write3( - &format!( - "[<fg=yellow;href={}>{}{}</>] <info>{}</info>{}", - link, - k.clone().unwrap_or_default(), - key, - value_display, - source - ), - true, - io_interface::QUIET, - ); - } - } - } - - /// Suggest setting-keys, while taking given options in account. - fn suggest_setting_keys(&self) -> crate::console::input::SuggestedValues { - crate::console::input::SuggestedValues::Closure(Box::new(|this, input, _suggestions| { - if input.get_option("list")?.to_bool() - || input.get_option("editor")?.to_bool() - || input.get_option("auth")?.to_bool() - { - return Ok(vec![]); - } - - let this = this - .as_any() - .downcast_ref::<ConfigCommand>() - .expect("suggestSettingKeys is bound to ConfigCommand"); - // PHP passes the CompletionInput itself; the accessors only read from it, so a - // clone behind a fresh handle is equivalent. - let input_handle: std::rc::Rc< - std::cell::RefCell< - dyn shirabe_external_packages::symfony::console::input::InputInterface, - >, - > = std::rc::Rc::new(std::cell::RefCell::new(input.clone())); - - // initialize configuration - let mut config = Factory::create_config(None, None)?; - - // load configuration - let config_file = JsonFile::new( - this.get_composer_config_file(input_handle.clone(), &config)?, - None, - None, - )?; - if config_file.exists() { - let path = config_file.get_path().to_string(); - let data = config_file.read()?.as_array().cloned().unwrap_or_default(); - config.merge(&data, &path); - } - - // load auth-configuration - let auth_config_file = JsonFile::new( - this.get_auth_config_file(input_handle.clone(), &config)?, - None, - None, - )?; - if auth_config_file.exists() { - let path = auth_config_file.get_path().to_string(); - let mut data = IndexMap::new(); - data.insert("config".to_string(), auth_config_file.read()?); - config.merge(&data, &path); - } - - // collect all configuration setting-keys - let raw_config = config.raw(); - let mut keys = flatten_setting_keys( - raw_config.get("config").cloned().unwrap_or(PhpMixed::Null), - "", - ); - keys.extend(flatten_setting_keys( - raw_config - .get("repositories") - .cloned() - .unwrap_or(PhpMixed::Null), - "repositories.", - )); - - // if unsetting … - if input.get_option("unset")?.to_bool() { - // … keep only the currently customized setting-keys … - let sources = [ - config_file.get_path().to_string(), - auth_config_file.get_path().to_string(), - ]; - keys.retain(|key| sources.contains(&config.get_source_of_value(key))); - - // … else if showing or setting a value … - } else { - // … add all configurable package-properties, no matter if it exist - keys.extend( - Self::CONFIGURABLE_PACKAGE_PROPERTIES - .iter() - .map(|property| property.to_string()), - ); - - // it would be nice to distinguish between showing and setting - // a value, but that makes the implementation much more complex - // and partially impossible because symfony's implementation - // does not complete arguments followed by other arguments - } - - // add all existing configurable package-properties - if config_file.exists() { - let properties: IndexMap<String, PhpMixed> = config_file - .read()? - .as_array() - .cloned() - .unwrap_or_default() - .into_iter() - .filter(|(key, _)| { - Self::CONFIGURABLE_PACKAGE_PROPERTIES.contains(&key.as_str()) - }) - .collect(); - - keys.extend(flatten_setting_keys(PhpMixed::Array(properties), "")); - } - - // filter settings-keys by completion value - let completion_value = input.get_completion_value(); - - if !completion_value.is_empty() { - keys.retain(|key| key.starts_with(&completion_value)); - } - - keys.sort(); - - keys.dedup(); - Ok(keys) - })) - } -} - // PHP signature: function ($val): bool / ($val) -> bool/string pub type ValidatorFn = Box<dyn Fn(&PhpMixed) -> PhpMixed>; pub type NormalizerFn = Box<dyn Fn(&PhpMixed) -> PhpMixed>; diff --git a/crates/shirabe/src/command/create_project_command.rs b/crates/shirabe/src/command/create_project_command.rs index 260685c7..dcf0e0f8 100644 --- a/crates/shirabe/src/command/create_project_command.rs +++ b/crates/shirabe/src/command/create_project_command.rs @@ -81,213 +81,7 @@ impl CreateProjectCommand { .expect("CreateProjectCommand::configure uses static, valid metadata"); command } -} - -impl Command for CreateProjectCommand { - fn configure(&self) -> anyhow::Result<()> { - self.set_name("create-project")?; - self.set_description("Creates new project from a package into given directory"); - self.set_definition(&[ - InputArgument::new5("package", Some(InputArgument::OPTIONAL), "Package name to be installed", None, self.suggest_available_package(99)).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(), - InputOption::new("stability", Some(PhpMixed::String("s".to_string())), Some(InputOption::VALUE_REQUIRED), "Minimum-stability allowed (unless a version is specified).", 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::new6("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, self.suggest_prefer_install()).unwrap().into(), - InputOption::new("repository", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Add custom repositories to look the package up, either by URL or using JSON arrays", None).unwrap().into(), - InputOption::new("repository-url", None, Some(InputOption::VALUE_REQUIRED), "DEPRECATED: Use --repository instead.", None).unwrap().into(), - InputOption::new("add-repository", None, Some(InputOption::VALUE_NONE), "Add the custom repository in the composer.json. If a lock file is present it will be deleted and an update will be run instead of install.", None).unwrap().into(), - InputOption::new("dev", None, Some(InputOption::VALUE_NONE), "Enables installation of require-dev packages (enabled by default, only present for BC).", None).unwrap().into(), - InputOption::new("no-dev", None, Some(InputOption::VALUE_NONE), "Disables installation of require-dev packages.", None).unwrap().into(), - InputOption::new("no-custom-installers", None, Some(InputOption::VALUE_NONE), "DEPRECATED: Use no-plugins instead.", None).unwrap().into(), - InputOption::new("no-scripts", None, Some(InputOption::VALUE_NONE), "Whether to prevent execution of all defined scripts in the root package.", None).unwrap().into(), - InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(), - InputOption::new("no-secure-http", None, Some(InputOption::VALUE_NONE), "Disable the secure-http config option temporarily while installing the root package. Use at your own risk. Using this flag is a bad idea.", None).unwrap().into(), - InputOption::new("keep-vcs", None, Some(InputOption::VALUE_NONE), "Whether to prevent deleting the vcs folder.", None).unwrap().into(), - InputOption::new("remove-vcs", None, Some(InputOption::VALUE_NONE), "Whether to force deletion of the vcs folder without prompting.", None).unwrap().into(), - InputOption::new("no-install", None, Some(InputOption::VALUE_NONE), "Whether to skip installation of the package dependencies.", None).unwrap().into(), - InputOption::new("no-audit", None, Some(InputOption::VALUE_NONE), "Whether to skip auditing of the installed package dependencies (can also be set via the COMPOSER_NO_AUDIT=1 env var).", None).unwrap().into(), - InputOption::new6("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())), SuggestedValues::List(Auditor::FORMATS.iter().map(|s| s.to_string()).collect())).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("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(), - ]); - 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\ - You can use this command to bootstrap new projects or setup a clean\n\ - version-controlled installation for developers of your project.\n\n\ - <info>shirabe create-project vendor/project target-directory [version]</info>\n\n\ - You can also specify the version with the package name using = or : as separator.\n\n\ - <info>shirabe create-project vendor/project:version target-directory</info>\n\n\ - To install unstable packages, either specify the version you want, or use the\n\ - --stability=dev (where dev can be one of RC, beta, alpha or dev).\n\n\ - To setup a developer workable version you should create the project using the source\n\ - controlled code by appending the <info>'--prefer-source'</info> flag.\n\n\ - To install a package from another repository than the default one you\n\ - 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( - &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> 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(); - - let (prefer_source, prefer_dist) = - self.get_preferred_install_options(&config.borrow(), input.clone(), true)?; - - if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { - io.write_error("<warning>You are using the deprecated option \"dev\". Dev packages are installed by default now.</warning>"); - } - if input - .borrow() - .get_option("no-custom-installers")? - .as_bool() - .unwrap_or(false) - { - io.write_error("<warning>You are using the deprecated option \"no-custom-installers\". Use \"no-plugins\" instead.</warning>"); - input - .borrow_mut() - .set_option("no-plugins", PhpMixed::Bool(true)); - } - - if input.borrow().is_interactive() - && input.borrow().get_option("ask")?.as_bool().unwrap_or(false) - { - let package = input.borrow().get_argument("package")?; - if package.is_null() { - return Err(RuntimeException { - message: "Not enough arguments (missing: \"package\").".to_string(), - code: 0, - } - .into()); - } - let mut parts = - explode_with_limit("/", &strtolower(package.as_string().unwrap_or("")), 2); - let prompt = format!( - "New project directory [<comment>{}</comment>]: ", - array_pop(&mut parts).unwrap_or_default() - ); - input - .borrow_mut() - .set_argument("directory", io.ask(prompt, PhpMixed::Null)?); - } - - let repository_opt = input.borrow().get_option("repository")?; - let repository_url_opt = input.borrow().get_option("repository-url")?; - let repositories = if repository_opt - .as_list() - .map(|l| !l.is_empty()) - .unwrap_or(false) - { - Some(repository_opt) - } else { - Some(repository_url_opt) - }; - - self.install_project( - io, - config, - input.clone(), - input - .borrow() - .get_argument("package")? - .as_string() - .map(|s| s.to_string()), - input - .borrow() - .get_argument("directory")? - .as_string() - .map(|s| s.to_string()), - input - .borrow() - .get_argument("version")? - .as_string() - .map(|s| s.to_string()), - input - .borrow() - .get_option("stability")? - .as_string() - .map(|s| s.to_string()), - prefer_source, - prefer_dist, - !input - .borrow() - .get_option("no-dev")? - .as_bool() - .unwrap_or(false), - repositories, - input - .borrow() - .get_option("no-plugins")? - .as_bool() - .unwrap_or(false), - input - .borrow() - .get_option("no-scripts")? - .as_bool() - .unwrap_or(false), - input - .borrow() - .get_option("no-progress")? - .as_bool() - .unwrap_or(false), - input - .borrow() - .get_option("no-install")? - .as_bool() - .unwrap_or(false), - Some(self.get_platform_requirement_filter(input.clone())?), - !input - .borrow() - .get_option("no-secure-http")? - .as_bool() - .unwrap_or(false), - input - .borrow() - .get_option("add-repository")? - .as_bool() - .unwrap_or(false), - ) - } - - fn initialize( - &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<()> { - base_command_initialize(self, input, output) - } - - fn complete( - &self, - input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, - suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, - ) -> anyhow::Result<()> { - crate::command::base_command::base_command_complete(self, input, suggestions) - } - - shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); -} - -impl BaseCommand for CreateProjectCommand { - fn base_command_data(&self) -> &crate::command::BaseCommandData { - &self.base_command_data - } - - crate::delegate_base_command_trait_impls_to_inner!(base_command_data); -} -impl CreateProjectCommand { /// @throws \Exception #[allow(clippy::too_many_arguments)] pub fn install_project( @@ -1057,3 +851,207 @@ impl CreateProjectCommand { Ok(installed_from_vcs) } } + +impl Command for CreateProjectCommand { + fn configure(&self) -> anyhow::Result<()> { + self.set_name("create-project")?; + self.set_description("Creates new project from a package into given directory"); + self.set_definition(&[ + InputArgument::new5("package", Some(InputArgument::OPTIONAL), "Package name to be installed", None, self.suggest_available_package(99)).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(), + InputOption::new("stability", Some(PhpMixed::String("s".to_string())), Some(InputOption::VALUE_REQUIRED), "Minimum-stability allowed (unless a version is specified).", 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::new6("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, self.suggest_prefer_install()).unwrap().into(), + InputOption::new("repository", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Add custom repositories to look the package up, either by URL or using JSON arrays", None).unwrap().into(), + InputOption::new("repository-url", None, Some(InputOption::VALUE_REQUIRED), "DEPRECATED: Use --repository instead.", None).unwrap().into(), + InputOption::new("add-repository", None, Some(InputOption::VALUE_NONE), "Add the custom repository in the composer.json. If a lock file is present it will be deleted and an update will be run instead of install.", None).unwrap().into(), + InputOption::new("dev", None, Some(InputOption::VALUE_NONE), "Enables installation of require-dev packages (enabled by default, only present for BC).", None).unwrap().into(), + InputOption::new("no-dev", None, Some(InputOption::VALUE_NONE), "Disables installation of require-dev packages.", None).unwrap().into(), + InputOption::new("no-custom-installers", None, Some(InputOption::VALUE_NONE), "DEPRECATED: Use no-plugins instead.", None).unwrap().into(), + InputOption::new("no-scripts", None, Some(InputOption::VALUE_NONE), "Whether to prevent execution of all defined scripts in the root package.", None).unwrap().into(), + InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(), + InputOption::new("no-secure-http", None, Some(InputOption::VALUE_NONE), "Disable the secure-http config option temporarily while installing the root package. Use at your own risk. Using this flag is a bad idea.", None).unwrap().into(), + InputOption::new("keep-vcs", None, Some(InputOption::VALUE_NONE), "Whether to prevent deleting the vcs folder.", None).unwrap().into(), + InputOption::new("remove-vcs", None, Some(InputOption::VALUE_NONE), "Whether to force deletion of the vcs folder without prompting.", None).unwrap().into(), + InputOption::new("no-install", None, Some(InputOption::VALUE_NONE), "Whether to skip installation of the package dependencies.", None).unwrap().into(), + InputOption::new("no-audit", None, Some(InputOption::VALUE_NONE), "Whether to skip auditing of the installed package dependencies (can also be set via the COMPOSER_NO_AUDIT=1 env var).", None).unwrap().into(), + InputOption::new6("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())), SuggestedValues::List(Auditor::FORMATS.iter().map(|s| s.to_string()).collect())).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("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(), + ]); + 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\ + You can use this command to bootstrap new projects or setup a clean\n\ + version-controlled installation for developers of your project.\n\n\ + <info>shirabe create-project vendor/project target-directory [version]</info>\n\n\ + You can also specify the version with the package name using = or : as separator.\n\n\ + <info>shirabe create-project vendor/project:version target-directory</info>\n\n\ + To install unstable packages, either specify the version you want, or use the\n\ + --stability=dev (where dev can be one of RC, beta, alpha or dev).\n\n\ + To setup a developer workable version you should create the project using the source\n\ + controlled code by appending the <info>'--prefer-source'</info> flag.\n\n\ + To install a package from another repository than the default one you\n\ + 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( + &self, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> 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(); + + let (prefer_source, prefer_dist) = + self.get_preferred_install_options(&config.borrow(), input.clone(), true)?; + + if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { + io.write_error("<warning>You are using the deprecated option \"dev\". Dev packages are installed by default now.</warning>"); + } + if input + .borrow() + .get_option("no-custom-installers")? + .as_bool() + .unwrap_or(false) + { + io.write_error("<warning>You are using the deprecated option \"no-custom-installers\". Use \"no-plugins\" instead.</warning>"); + input + .borrow_mut() + .set_option("no-plugins", PhpMixed::Bool(true)); + } + + if input.borrow().is_interactive() + && input.borrow().get_option("ask")?.as_bool().unwrap_or(false) + { + let package = input.borrow().get_argument("package")?; + if package.is_null() { + return Err(RuntimeException { + message: "Not enough arguments (missing: \"package\").".to_string(), + code: 0, + } + .into()); + } + let mut parts = + explode_with_limit("/", &strtolower(package.as_string().unwrap_or("")), 2); + let prompt = format!( + "New project directory [<comment>{}</comment>]: ", + array_pop(&mut parts).unwrap_or_default() + ); + input + .borrow_mut() + .set_argument("directory", io.ask(prompt, PhpMixed::Null)?); + } + + let repository_opt = input.borrow().get_option("repository")?; + let repository_url_opt = input.borrow().get_option("repository-url")?; + let repositories = if repository_opt + .as_list() + .map(|l| !l.is_empty()) + .unwrap_or(false) + { + Some(repository_opt) + } else { + Some(repository_url_opt) + }; + + self.install_project( + io, + config, + input.clone(), + input + .borrow() + .get_argument("package")? + .as_string() + .map(|s| s.to_string()), + input + .borrow() + .get_argument("directory")? + .as_string() + .map(|s| s.to_string()), + input + .borrow() + .get_argument("version")? + .as_string() + .map(|s| s.to_string()), + input + .borrow() + .get_option("stability")? + .as_string() + .map(|s| s.to_string()), + prefer_source, + prefer_dist, + !input + .borrow() + .get_option("no-dev")? + .as_bool() + .unwrap_or(false), + repositories, + input + .borrow() + .get_option("no-plugins")? + .as_bool() + .unwrap_or(false), + input + .borrow() + .get_option("no-scripts")? + .as_bool() + .unwrap_or(false), + input + .borrow() + .get_option("no-progress")? + .as_bool() + .unwrap_or(false), + input + .borrow() + .get_option("no-install")? + .as_bool() + .unwrap_or(false), + Some(self.get_platform_requirement_filter(input.clone())?), + !input + .borrow() + .get_option("no-secure-http")? + .as_bool() + .unwrap_or(false), + input + .borrow() + .get_option("add-repository")? + .as_bool() + .unwrap_or(false), + ) + } + + fn initialize( + &self, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + fn complete( + &self, + input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, + suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, + ) -> anyhow::Result<()> { + crate::command::base_command::base_command_complete(self, input, suggestions) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for CreateProjectCommand { + fn base_command_data(&self) -> &crate::command::BaseCommandData { + &self.base_command_data + } + + 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 1674e10e..8483eb56 100644 --- a/crates/shirabe/src/command/diagnose_command.rs +++ b/crates/shirabe/src/command/diagnose_command.rs @@ -77,396 +77,7 @@ impl DiagnoseCommand { .expect("DiagnoseCommand::configure uses static, valid metadata"); command } -} - -impl Command for DiagnoseCommand { - fn configure(&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(()) - } - - fn execute( - &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<i64> { - let mut composer = self.try_composer(None, None); - let io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = self.get_io().clone(); - - let config: std::rc::Rc<std::cell::RefCell<Config>>; - if let Some(ref mut c) = composer { - let c = crate::composer::composer_full(c); - config = c.get_config(); - - let command_event = CommandEvent::new6( - PluginEvents::COMMAND, - "diagnose", - input, - output, - vec![], - IndexMap::new(), - ); - c.get_event_dispatcher() - .borrow_mut() - .dispatch(Some(command_event.get_name()), None); - *self.process.borrow_mut() = Some( - c.get_loop() - .borrow() - .get_process_executor() - .map(std::rc::Rc::clone) - .unwrap_or_else(|| { - std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some( - io.clone(), - )))) - }), - ); - } else { - config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?)); - - *self.process.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new( - ProcessExecutor::new(Some(io.clone())), - ))); - } - let mut config_inner = IndexMap::new(); - config_inner.insert("secure-http".to_string(), PhpMixed::Bool(false)); - let mut secure_http_wrap: IndexMap<String, PhpMixed> = IndexMap::new(); - secure_http_wrap.insert("config".to_string(), PhpMixed::Array(config_inner)); - let config = config; - config - .borrow_mut() - .merge(&secure_http_wrap, Config::SOURCE_COMMAND); - let _ = config.borrow_mut().prohibit_url_by_config( - "http://repo.packagist.org", - Some(std::rc::Rc::new(std::cell::RefCell::new(NullIO::new()))), - &IndexMap::new(), - ); - - *self.http_downloader.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new( - Factory::create_http_downloader(io.clone(), &config, indexmap::IndexMap::new())?, - ))); - - if strpos(file!(), "phar:") == Some(0) { - io.write_no_newline("Checking pubkeys: "); - let r = self.check_pub_keys(&config.borrow())?; - self.output_result(r); - - io.write_no_newline("Checking Composer version: "); - let r = self.check_version(&config)?; - self.output_result(r); - } - - io.write(&format!( - "Composer version: <comment>{}</comment>", - composer::get_version() - )); - - io.write_no_newline("Checking Composer and its dependencies for vulnerabilities: "); - let r = self.check_composer_audit(&config)?; - self.output_result(r); - - let platform_overrides = config - .borrow_mut() - .get("platform") - .as_array() - .cloned() - .unwrap_or_default(); - let platform_overrides_unboxed: indexmap::IndexMap<String, PhpMixed> = - platform_overrides.into_iter().collect(); - let mut platform_repo = - PlatformRepository::new(vec![], platform_overrides_unboxed).unwrap(); - let php_pkg = <PlatformRepository as crate::repository::RepositoryInterface>::find_package( - &mut platform_repo, - "php", - crate::repository::FindPackageConstraint::String("*".to_string()), - )? - .unwrap(); - let mut php_version = php_pkg.get_pretty_version(); - if let Some(cp) = php_pkg.as_complete() - && str_contains(&cp.get_description().unwrap_or_default(), "overridden") - { - php_version = format!( - "{} - {}", - php_version, - cp.get_description().unwrap_or_default() - ); - } - - io.write(&format!("PHP version: <comment>{}</comment>", php_version)); - - let diagnostics = shirabe_php_rpc::get_diagnostics(); - - if let Some(php_binary) = &diagnostics.php_binary { - io.write(&format!( - "PHP binary path: <comment>{}</comment>", - php_binary - )); - } - - io.write(&format!( - "OpenSSL version: {}", - match &diagnostics.openssl_version_text { - Some(text) => format!("<comment>{}</comment>", text), - None => "<error>missing</error>".to_string(), - } - )); - io.write(&format!("curl version: {}", self.get_curl_version())); - - let finder = ExecutableFinder::new(); - let has_system_unzip = finder.find("unzip", None, &[]).is_some(); - let mut bin_7zip = String::new(); - let has_system_7zip = if finder - .find("7z", None, &["C:\\Program Files\\7-Zip".to_string()]) - .is_some() - { - bin_7zip = "7z".to_string(); - true - } else if !Platform::is_windows() && finder.find("7zz", None, &[]).is_some() { - bin_7zip = "7zz".to_string(); - true - } else if !Platform::is_windows() && finder.find("7za", None, &[]).is_some() { - bin_7zip = "7za".to_string(); - true - } else { - false - }; - - io.write(&format!( - "zip: {}, {}, {}{}", - if diagnostics.extension_loaded("zip") { - "<comment>extension present</comment>" - } else { - "<comment>extension not loaded</comment>" - }, - if has_system_unzip { - "<comment>unzip present</comment>".to_string() - } else { - "<comment>unzip not available</comment>".to_string() - }, - if has_system_7zip { - format!("<comment>7-Zip present ({})</comment>", bin_7zip) - } else { - "<comment>7-Zip not available</comment>".to_string() - }, - if (has_system_7zip || has_system_unzip) && !diagnostics.function_exists("proc_open") { - ", <warning>proc_open is disabled or not present, unzip/7-z will not be usable</warning>" - } else { - "" - } - )); - - if let Some(ref mut c) = composer { - let c = crate::composer::composer_full(c); - io.write(&format!( - "Active plugins: {}", - implode( - ", ", - &c.get_plugin_manager().borrow().get_registered_plugins() - ) - )); - - io.write_no_newline("Checking composer.json: "); - let r = self.check_composer_schema()?; - self.output_result(r); - - if c.get_locker().borrow_mut().is_locked() { - io.write_no_newline("Checking composer.lock: "); - let locker = c.get_locker().clone(); - let locker = locker.borrow(); - let r = self.check_composer_lock_schema(&*locker)?; - self.output_result(r); - } - } - - io.write_no_newline("Checking platform settings: "); - let r = self.check_platform()?; - self.output_result(r); - - io.write_no_newline("Checking git settings: "); - let r = self.check_git(); - self.output_result(PhpMixed::String(r)); - - io.write_no_newline("Checking http connectivity to packagist: "); - let r = self.check_http("http", &config)?; - self.output_result(r); - - io.write_no_newline("Checking https connectivity to packagist: "); - let r = self.check_http("https", &config)?; - self.output_result(r); - - let repositories = config.borrow().get_repositories(); - for repo in repositories { - let repo_arr = repo.1.as_array().cloned().unwrap_or_default(); - if repo_arr.get("type").and_then(|v| v.as_string()) == Some("composer") - && repo_arr.get("url").is_some() - { - let repo_arr_unboxed: indexmap::IndexMap<String, PhpMixed> = repo_arr - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - let composer_repo = ComposerRepository::new( - repo_arr_unboxed, - self.get_io().clone(), - &config.borrow(), - self.http_downloader.borrow().clone().unwrap(), - None, - ) - .unwrap(); - // PHP: ReflectionMethod($composerRepo, 'getPackagesJsonUrl') - // We surface the same internal call by directly invoking the equivalent method. - // TODO(plugin): support reflection-based access if plugin code requires it. - let url = composer_repo.get_packages_json_url(); - if !str_starts_with(&url, "http") { - continue; - } - if str_starts_with(&url, "https://repo.packagist.org") { - continue; - } - io.write_no_newline(&format!( - "Checking connectivity to {}: ", - repo_arr - .get("url") - .and_then(|v| v.as_string()) - .unwrap_or("") - )); - let r = self.check_composer_repo(&url, &config)?; - self.output_result(r); - } - } - - let protos: Vec<&str> = if config.borrow_mut().get("disable-tls").as_bool() == Some(true) { - vec!["http"] - } else { - vec!["http", "https"] - }; - let proxy_check_result: anyhow::Result<(), anyhow::Error> = (|| -> anyhow::Result<()> { - for proto in &protos { - // Compute the proxy under a short-lived lock: `check_http_proxy` below transitively - // re-enters `ProxyManager::get_instance()` (via HttpDownloader -> CurlDownloader / - // RemoteFilesystem), and `std::sync::Mutex` is not reentrant, so the guard must not - // still be held when that call happens. - let proxy = ProxyManager::get_instance() - .as_ref() - .unwrap() - .get_proxy_for_request(&format!("{}://repo.packagist.org", proto)) - .map_err(|e| anyhow::anyhow!(e))?; - if !proxy.get_status(None)?.is_empty() { - let r#type = if proxy.is_secure() { "HTTPS" } else { "HTTP" }; - io.write_no_newline(&format!("Checking {} proxy with {}: ", r#type, proto)); - let r = self.check_http_proxy(&proxy, proto)?; - self.output_result(r); - } - } - Ok(()) - })(); - if let Err(e) = proxy_check_result { - if let Some(_te) = e.downcast_ref::<TransportException>() { - io.write_no_newline("Checking HTTP proxy: "); - let status = self.check_connectivity_and_composer_network_http_enablement(); - self.output_result(if is_string(&status) { - status - } else { - PhpMixed::String(format!("<error>[{}] {}</error>", get_class_err(&e), e)) - }); - } else { - return Err(e); - } - } - - let oauth = config - .borrow_mut() - .get("github-oauth") - .as_array() - .cloned() - .unwrap_or_default(); - if oauth.len() as i64 > 0 { - for (domain, token) in &oauth { - io.write_no_newline(&format!("Checking {} oauth access: ", domain)); - let r = self.check_github_oauth(domain, token.as_string().unwrap_or(""))?; - self.output_result(r); - } - } else { - io.write_no_newline("Checking github.com rate limit: "); - match self.get_github_rate_limit("github.com", None) { - Ok(rate) => { - if !is_array(&rate) { - self.output_result(rate); - } else if let Some(arr) = rate.as_array() { - let remaining = arr.get("remaining").and_then(|v| v.as_int()).unwrap_or(0); - let limit = arr.get("limit").and_then(|v| v.as_int()).unwrap_or(0); - if 10 > remaining { - io.write("<warning>WARNING</warning>"); - io.write(&format!( - "<comment>GitHub has a rate limit on their API. You currently have <options=bold>{}</options=bold> out of <options=bold>{}</options=bold> requests left.\nSee https://developer.github.com/v3/#rate-limiting and also\n https://getcomposer.org/doc/articles/troubleshooting.md#api-rate-limit-and-oauth-tokens</comment>", - remaining, limit, - )); - } else { - self.output_result(PhpMixed::Bool(true)); - } - } - } - Err(e) => { - if let Some(te) = e.downcast_ref::<TransportException>() { - if te.get_code() == 401 { - self.output_result(PhpMixed::String("<comment>The oauth token for github.com seems invalid, run \"composer config --global --unset github-oauth.github.com\" to remove it</comment>".to_string())); - } else { - self.output_result(PhpMixed::String(format!( - "<error>[{}] {}</error>", - get_class_err(&e), - e - ))); - } - } else { - self.output_result(PhpMixed::String(format!( - "<error>[{}] {}</error>", - get_class_err(&e), - e - ))); - } - } - } - } - - io.write_no_newline("Checking disk free space: "); - let r = self.check_disk_space(&config.borrow()); - self.output_result(r); - - Ok(self.exit_code.get()) - } - - fn initialize( - &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<()> { - base_command_initialize(self, input, output) - } - - fn complete( - &self, - input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, - suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, - ) -> anyhow::Result<()> { - crate::command::base_command::base_command_complete(self, input, suggestions) - } - - shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); -} - -impl BaseCommand for DiagnoseCommand { - fn base_command_data(&self) -> &crate::command::BaseCommandData { - &self.base_command_data - } - - crate::delegate_base_command_trait_impls_to_inner!(base_command_data); -} -impl DiagnoseCommand { fn check_composer_schema(&self) -> anyhow::Result<PhpMixed> { let validator = ConfigValidator::new(self.get_io().clone()); let (errors, _, warnings) = validator.validate(&Factory::get_composer_file()?, 0, 0); @@ -1511,3 +1122,390 @@ impl DiagnoseCommand { PhpMixed::Bool(true) } } + +impl Command for DiagnoseCommand { + fn configure(&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(()) + } + + fn execute( + &self, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + let mut composer = self.try_composer(None, None); + let io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = self.get_io().clone(); + + let config: std::rc::Rc<std::cell::RefCell<Config>>; + if let Some(ref mut c) = composer { + let c = crate::composer::composer_full(c); + config = c.get_config(); + + let command_event = CommandEvent::new6( + PluginEvents::COMMAND, + "diagnose", + input, + output, + vec![], + IndexMap::new(), + ); + c.get_event_dispatcher() + .borrow_mut() + .dispatch(Some(command_event.get_name()), None); + *self.process.borrow_mut() = Some( + c.get_loop() + .borrow() + .get_process_executor() + .map(std::rc::Rc::clone) + .unwrap_or_else(|| { + std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some( + io.clone(), + )))) + }), + ); + } else { + config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?)); + + *self.process.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new( + ProcessExecutor::new(Some(io.clone())), + ))); + } + let mut config_inner = IndexMap::new(); + config_inner.insert("secure-http".to_string(), PhpMixed::Bool(false)); + let mut secure_http_wrap: IndexMap<String, PhpMixed> = IndexMap::new(); + secure_http_wrap.insert("config".to_string(), PhpMixed::Array(config_inner)); + let config = config; + config + .borrow_mut() + .merge(&secure_http_wrap, Config::SOURCE_COMMAND); + let _ = config.borrow_mut().prohibit_url_by_config( + "http://repo.packagist.org", + Some(std::rc::Rc::new(std::cell::RefCell::new(NullIO::new()))), + &IndexMap::new(), + ); + + *self.http_downloader.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new( + Factory::create_http_downloader(io.clone(), &config, indexmap::IndexMap::new())?, + ))); + + if strpos(file!(), "phar:") == Some(0) { + io.write_no_newline("Checking pubkeys: "); + let r = self.check_pub_keys(&config.borrow())?; + self.output_result(r); + + io.write_no_newline("Checking Composer version: "); + let r = self.check_version(&config)?; + self.output_result(r); + } + + io.write(&format!( + "Composer version: <comment>{}</comment>", + composer::get_version() + )); + + io.write_no_newline("Checking Composer and its dependencies for vulnerabilities: "); + let r = self.check_composer_audit(&config)?; + self.output_result(r); + + let platform_overrides = config + .borrow_mut() + .get("platform") + .as_array() + .cloned() + .unwrap_or_default(); + let platform_overrides_unboxed: indexmap::IndexMap<String, PhpMixed> = + platform_overrides.into_iter().collect(); + let mut platform_repo = + PlatformRepository::new(vec![], platform_overrides_unboxed).unwrap(); + let php_pkg = <PlatformRepository as crate::repository::RepositoryInterface>::find_package( + &mut platform_repo, + "php", + crate::repository::FindPackageConstraint::String("*".to_string()), + )? + .unwrap(); + let mut php_version = php_pkg.get_pretty_version(); + if let Some(cp) = php_pkg.as_complete() + && str_contains(&cp.get_description().unwrap_or_default(), "overridden") + { + php_version = format!( + "{} - {}", + php_version, + cp.get_description().unwrap_or_default() + ); + } + + io.write(&format!("PHP version: <comment>{}</comment>", php_version)); + + let diagnostics = shirabe_php_rpc::get_diagnostics(); + + if let Some(php_binary) = &diagnostics.php_binary { + io.write(&format!( + "PHP binary path: <comment>{}</comment>", + php_binary + )); + } + + io.write(&format!( + "OpenSSL version: {}", + match &diagnostics.openssl_version_text { + Some(text) => format!("<comment>{}</comment>", text), + None => "<error>missing</error>".to_string(), + } + )); + io.write(&format!("curl version: {}", self.get_curl_version())); + + let finder = ExecutableFinder::new(); + let has_system_unzip = finder.find("unzip", None, &[]).is_some(); + let mut bin_7zip = String::new(); + let has_system_7zip = if finder + .find("7z", None, &["C:\\Program Files\\7-Zip".to_string()]) + .is_some() + { + bin_7zip = "7z".to_string(); + true + } else if !Platform::is_windows() && finder.find("7zz", None, &[]).is_some() { + bin_7zip = "7zz".to_string(); + true + } else if !Platform::is_windows() && finder.find("7za", None, &[]).is_some() { + bin_7zip = "7za".to_string(); + true + } else { + false + }; + + io.write(&format!( + "zip: {}, {}, {}{}", + if diagnostics.extension_loaded("zip") { + "<comment>extension present</comment>" + } else { + "<comment>extension not loaded</comment>" + }, + if has_system_unzip { + "<comment>unzip present</comment>".to_string() + } else { + "<comment>unzip not available</comment>".to_string() + }, + if has_system_7zip { + format!("<comment>7-Zip present ({})</comment>", bin_7zip) + } else { + "<comment>7-Zip not available</comment>".to_string() + }, + if (has_system_7zip || has_system_unzip) && !diagnostics.function_exists("proc_open") { + ", <warning>proc_open is disabled or not present, unzip/7-z will not be usable</warning>" + } else { + "" + } + )); + + if let Some(ref mut c) = composer { + let c = crate::composer::composer_full(c); + io.write(&format!( + "Active plugins: {}", + implode( + ", ", + &c.get_plugin_manager().borrow().get_registered_plugins() + ) + )); + + io.write_no_newline("Checking composer.json: "); + let r = self.check_composer_schema()?; + self.output_result(r); + + if c.get_locker().borrow_mut().is_locked() { + io.write_no_newline("Checking composer.lock: "); + let locker = c.get_locker().clone(); + let locker = locker.borrow(); + let r = self.check_composer_lock_schema(&*locker)?; + self.output_result(r); + } + } + + io.write_no_newline("Checking platform settings: "); + let r = self.check_platform()?; + self.output_result(r); + + io.write_no_newline("Checking git settings: "); + let r = self.check_git(); + self.output_result(PhpMixed::String(r)); + + io.write_no_newline("Checking http connectivity to packagist: "); + let r = self.check_http("http", &config)?; + self.output_result(r); + + io.write_no_newline("Checking https connectivity to packagist: "); + let r = self.check_http("https", &config)?; + self.output_result(r); + + let repositories = config.borrow().get_repositories(); + for repo in repositories { + let repo_arr = repo.1.as_array().cloned().unwrap_or_default(); + if repo_arr.get("type").and_then(|v| v.as_string()) == Some("composer") + && repo_arr.get("url").is_some() + { + let repo_arr_unboxed: indexmap::IndexMap<String, PhpMixed> = repo_arr + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + let composer_repo = ComposerRepository::new( + repo_arr_unboxed, + self.get_io().clone(), + &config.borrow(), + self.http_downloader.borrow().clone().unwrap(), + None, + ) + .unwrap(); + // PHP: ReflectionMethod($composerRepo, 'getPackagesJsonUrl') + // We surface the same internal call by directly invoking the equivalent method. + // TODO(plugin): support reflection-based access if plugin code requires it. + let url = composer_repo.get_packages_json_url(); + if !str_starts_with(&url, "http") { + continue; + } + if str_starts_with(&url, "https://repo.packagist.org") { + continue; + } + io.write_no_newline(&format!( + "Checking connectivity to {}: ", + repo_arr + .get("url") + .and_then(|v| v.as_string()) + .unwrap_or("") + )); + let r = self.check_composer_repo(&url, &config)?; + self.output_result(r); + } + } + + let protos: Vec<&str> = if config.borrow_mut().get("disable-tls").as_bool() == Some(true) { + vec!["http"] + } else { + vec!["http", "https"] + }; + let proxy_check_result: anyhow::Result<(), anyhow::Error> = (|| -> anyhow::Result<()> { + for proto in &protos { + // Compute the proxy under a short-lived lock: `check_http_proxy` below transitively + // re-enters `ProxyManager::get_instance()` (via HttpDownloader -> CurlDownloader / + // RemoteFilesystem), and `std::sync::Mutex` is not reentrant, so the guard must not + // still be held when that call happens. + let proxy = ProxyManager::get_instance() + .as_ref() + .unwrap() + .get_proxy_for_request(&format!("{}://repo.packagist.org", proto)) + .map_err(|e| anyhow::anyhow!(e))?; + if !proxy.get_status(None)?.is_empty() { + let r#type = if proxy.is_secure() { "HTTPS" } else { "HTTP" }; + io.write_no_newline(&format!("Checking {} proxy with {}: ", r#type, proto)); + let r = self.check_http_proxy(&proxy, proto)?; + self.output_result(r); + } + } + Ok(()) + })(); + if let Err(e) = proxy_check_result { + if let Some(_te) = e.downcast_ref::<TransportException>() { + io.write_no_newline("Checking HTTP proxy: "); + let status = self.check_connectivity_and_composer_network_http_enablement(); + self.output_result(if is_string(&status) { + status + } else { + PhpMixed::String(format!("<error>[{}] {}</error>", get_class_err(&e), e)) + }); + } else { + return Err(e); + } + } + + let oauth = config + .borrow_mut() + .get("github-oauth") + .as_array() + .cloned() + .unwrap_or_default(); + if oauth.len() as i64 > 0 { + for (domain, token) in &oauth { + io.write_no_newline(&format!("Checking {} oauth access: ", domain)); + let r = self.check_github_oauth(domain, token.as_string().unwrap_or(""))?; + self.output_result(r); + } + } else { + io.write_no_newline("Checking github.com rate limit: "); + match self.get_github_rate_limit("github.com", None) { + Ok(rate) => { + if !is_array(&rate) { + self.output_result(rate); + } else if let Some(arr) = rate.as_array() { + let remaining = arr.get("remaining").and_then(|v| v.as_int()).unwrap_or(0); + let limit = arr.get("limit").and_then(|v| v.as_int()).unwrap_or(0); + if 10 > remaining { + io.write("<warning>WARNING</warning>"); + io.write(&format!( + "<comment>GitHub has a rate limit on their API. You currently have <options=bold>{}</options=bold> out of <options=bold>{}</options=bold> requests left.\nSee https://developer.github.com/v3/#rate-limiting and also\n https://getcomposer.org/doc/articles/troubleshooting.md#api-rate-limit-and-oauth-tokens</comment>", + remaining, limit, + )); + } else { + self.output_result(PhpMixed::Bool(true)); + } + } + } + Err(e) => { + if let Some(te) = e.downcast_ref::<TransportException>() { + if te.get_code() == 401 { + self.output_result(PhpMixed::String("<comment>The oauth token for github.com seems invalid, run \"composer config --global --unset github-oauth.github.com\" to remove it</comment>".to_string())); + } else { + self.output_result(PhpMixed::String(format!( + "<error>[{}] {}</error>", + get_class_err(&e), + e + ))); + } + } else { + self.output_result(PhpMixed::String(format!( + "<error>[{}] {}</error>", + get_class_err(&e), + e + ))); + } + } + } + } + + io.write_no_newline("Checking disk free space: "); + let r = self.check_disk_space(&config.borrow()); + self.output_result(r); + + Ok(self.exit_code.get()) + } + + fn initialize( + &self, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + fn complete( + &self, + input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, + suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, + ) -> anyhow::Result<()> { + crate::command::base_command::base_command_complete(self, input, suggestions) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for DiagnoseCommand { + fn base_command_data(&self) -> &crate::command::BaseCommandData { + &self.base_command_data + } + + 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 99a7de00..f2bf19d7 100644 --- a/crates/shirabe/src/command/init_command.rs +++ b/crates/shirabe/src/command/init_command.rs @@ -84,6 +84,370 @@ impl InitCommand { .expect("InitCommand::configure uses static, valid metadata"); command } + + fn parse_author_string( + &self, + author: &str, + ) -> anyhow::Result<IndexMap<String, Option<String>>> { + let mut m: IndexMap<CaptureKey, String> = IndexMap::new(); + if Preg::is_match3( + php_regex!(r#"/^(?P<name>[- .,\p{L}\p{N}\p{Mn}\'’\"()]+)(?:\s+<(?P<email>.+?)>)?$/u"#), + author, + Some(&mut m), + ) { + let email = m.get(&CaptureKey::ByName("email".to_string())).cloned(); + if let Some(ref email) = email + && !self.is_valid_email(email) + { + return Err(InvalidArgumentException { + message: format!("Invalid email \"{}\"", email), + code: 0, + } + .into()); + } + + let mut result: IndexMap<String, Option<String>> = IndexMap::new(); + result.insert( + "name".to_string(), + Some(trim( + &m.get(&CaptureKey::ByName("name".to_string())) + .cloned() + .unwrap_or_default(), + None, + )), + ); + result.insert("email".to_string(), email); + + return Ok(result); + } + + Err(InvalidArgumentException { + message: "Invalid author string. Must be in the formats: Jane Doe or John Smith <john@example.com>" + .to_string(), + code: 0, + } + .into()) + } + + pub(crate) fn format_authors( + &self, + author: &str, + ) -> anyhow::Result<Vec<IndexMap<String, PhpMixed>>> { + let parsed = self.parse_author_string(author)?; + let mut author_map: IndexMap<String, PhpMixed> = IndexMap::new(); + let name = parsed.get("name").cloned().unwrap_or(None); + let email = parsed.get("email").cloned().unwrap_or(None); + if let Some(name) = name { + author_map.insert("name".to_string(), PhpMixed::String(name)); + } + if let Some(email) = email { + author_map.insert("email".to_string(), PhpMixed::String(email)); + } + + Ok(vec![author_map]) + } + + /// Extract namespace from package's vendor name. + /// + /// new_projects.acme-extra/package-name becomes "NewProjectsAcmeExtra\PackageName" + pub fn namespace_from_package_name(&self, package_name: &str) -> Option<String> { + if package_name.is_empty() || strpos(package_name, "/").is_none() { + return None; + } + + let namespace: Vec<String> = array_map( + |part: &String| { + let part = Preg::replace(php_regex!(r"/[^a-z0-9]/i"), " ", part); + let part = ucwords(&part); + str_replace(" ", "", &part) + }, + &explode("/", package_name), + ); + + Some(implode("\\", &namespace)) + } + + pub(crate) fn get_git_config(&self) -> IndexMap<String, String> { + if self.git_config.borrow().is_some() { + return self.git_config.borrow().clone().unwrap_or_default(); + } + + let mut process = ProcessExecutor::new(Some(self.get_io().clone())); + + let mut output = String::new(); + if process.execute_args( + &["git".to_string(), "config".to_string(), "-l".to_string()], + &mut output, + None, + ) == 0 + { + *self.git_config.borrow_mut() = Some(IndexMap::new()); + let mut m: IndexMap<CaptureKey, Vec<String>> = IndexMap::new(); + if Preg::is_match_all3(php_regex!(r"{^([^=]+)=(.*)$}m"), &output, Some(&mut m)) { + let keys: Vec<String> = m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default(); + let values: Vec<String> = + m.get(&CaptureKey::ByIndex(2)).cloned().unwrap_or_default(); + for (key, value) in keys.iter().zip(values.iter()) { + self.git_config + .borrow_mut() + .as_mut() + .unwrap() + .insert(key.clone(), value.clone()); + } + } + + return self.git_config.borrow().clone().unwrap_or_default(); + } + + *self.git_config.borrow_mut() = Some(IndexMap::new()); + IndexMap::new() + } + + /// Checks the local .gitignore file for the Composer vendor directory. + /// + /// Tested patterns include: + /// "/$vendor" + /// "$vendor" + /// "$vendor/" + /// "/$vendor/" + /// "/$vendor/*" + /// "$vendor/*" + pub(crate) fn has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool { + if !file_exists(ignore_file) { + return false; + } + + let pattern = format!("{{^/?{}(/\\*?)?$}}", preg_quote(vendor, None)); + + let lines = file(ignore_file, FILE_IGNORE_NEW_LINES).unwrap_or_default(); + for line in &lines { + if Preg::is_match(&pattern, line) { + return true; + } + } + + false + } + + pub(crate) fn add_vendor_ignore(&self, ignore_file: &str, vendor: &str) { + let mut contents = String::new(); + if file_exists(ignore_file) { + contents = file_get_contents(ignore_file).unwrap_or_default(); + + if strpos(&contents, "\n") != Some(0) { + contents.push('\n'); + } + } + + file_put_contents(ignore_file, format!("{}{}\n", contents, vendor).as_bytes()); + } + + /// For testing only: invoke the private `parse_author_string`. + pub fn __parse_author_string( + &self, + author: &str, + ) -> anyhow::Result<IndexMap<String, Option<String>>> { + self.parse_author_string(author) + } + + /// For testing only: invoke the crate-private `format_authors`. + pub fn __format_authors( + &self, + author: &str, + ) -> anyhow::Result<Vec<IndexMap<String, PhpMixed>>> { + self.format_authors(author) + } + + /// For testing only: invoke the crate-private `get_git_config`. + pub fn __get_git_config(&self) -> IndexMap<String, String> { + self.get_git_config() + } + + /// For testing only: invoke the crate-private `has_vendor_ignore`. + pub fn __has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool { + self.has_vendor_ignore(ignore_file, vendor) + } + + /// For testing only: invoke the crate-private `add_vendor_ignore`. + pub fn __add_vendor_ignore(&self, ignore_file: &str, vendor: &str) { + self.add_vendor_ignore(ignore_file, vendor) + } + + pub(crate) fn is_valid_email(&self, email: &str) -> bool { + shirabe_php_shim::filter_var_email(email) + } + + fn update_dependencies(&self, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>) { + let result = (|| -> anyhow::Result<i64> { + let application = self + .get_application() + .expect("a Composer command's application is always set"); + let update_command = application.borrow_mut().find("update")?; + self.reset_composer()?; + let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = + std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?)); + let command = update_command.borrow(); + command.run(input, output) + })(); + + if result.is_err() { + self.get_io().borrow().write_error( + "Could not update dependencies. Run `composer update` to see more information.", + ); + } + } + + fn run_dump_autoload_command( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) { + let result = (|| -> anyhow::Result<i64> { + let application = self + .get_application() + .expect("a Composer command's application is always set"); + let command = application.borrow_mut().find("dump-autoload")?; + self.reset_composer()?; + let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = + std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?)); + let command = command.borrow(); + command.run(input, output) + })(); + + if result.is_err() { + self.get_io() + .borrow() + .write_error("Could not run dump-autoload."); + } + } + + fn has_dependencies(&self, options: &IndexMap<String, PhpMixed>) -> bool { + let requires = options.get("require").cloned().unwrap_or(PhpMixed::Null); + let requires_arr_empty = match &requires { + PhpMixed::Array(m) => m.is_empty(), + PhpMixed::List(l) => l.is_empty(), + PhpMixed::Null => true, + _ => false, + }; + let dev_requires = options.get("require-dev").cloned(); + let dev_requires_arr_empty = match &dev_requires { + Some(PhpMixed::Array(m)) => m.is_empty(), + Some(PhpMixed::List(l)) => l.is_empty(), + Some(PhpMixed::Null) | None => true, + _ => false, + }; + + !requires_arr_empty || !dev_requires_arr_empty + } + + fn sanitize_package_name_component(&self, name: &str) -> String { + let name = Preg::replace( + php_regex!(r"{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}"), + "$1$3-$2$4", + name, + ); + let name = strtolower(&name); + let name = Preg::replace(php_regex!(r"{^[_.-]+|[_.-]+$|[^a-z0-9_.-]}u"), "", &name); + + Preg::replace(php_regex!(r"{([_.-]){2,}}u"), "$1", &name) + } + + fn get_default_package_name(&self) -> String { + let git = self.get_git_config(); + let cwd = realpath(".").unwrap_or_default(); + let name = basename(&cwd); + let name = self.sanitize_package_name_component(&name); + + let mut vendor = name.clone(); + let composer_default_vendor = PHP_SERVER + .lock() + .unwrap() + .get("COMPOSER_DEFAULT_VENDOR") + .map(|value| value.to_string_lossy().into_owned()); + let server_username = PHP_SERVER + .lock() + .unwrap() + .get("USERNAME") + .map(|value| value.to_string_lossy().into_owned()); + let server_user = PHP_SERVER + .lock() + .unwrap() + .get("USER") + .map(|value| value.to_string_lossy().into_owned()); + if !empty( + &composer_default_vendor + .clone() + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null), + ) { + vendor = composer_default_vendor.unwrap_or_default(); + } else if git.contains_key("github.user") { + vendor = git.get("github.user").cloned().unwrap_or_default(); + } else if !empty( + &server_username + .clone() + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null), + ) { + vendor = server_username.unwrap_or_default(); + } else if !empty( + &server_user + .clone() + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null), + ) { + vendor = server_user.unwrap_or_default(); + } else if !get_current_user().is_empty() { + vendor = get_current_user(); + } + + let vendor = self.sanitize_package_name_component(&vendor); + + format!("{}/{}", vendor, name) + } + + fn get_default_author(&self) -> Option<String> { + let git = self.get_git_config(); + + let mut author_name: Option<String> = None; + let composer_default_author = PHP_SERVER + .lock() + .unwrap() + .get("COMPOSER_DEFAULT_AUTHOR") + .map(|value| value.to_string_lossy().into_owned()); + if !empty( + &composer_default_author + .clone() + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null), + ) { + author_name = composer_default_author; + } else if git.contains_key("user.name") { + author_name = git.get("user.name").cloned(); + } + + let mut author_email: Option<String> = None; + let composer_default_email = PHP_SERVER + .lock() + .unwrap() + .get("COMPOSER_DEFAULT_EMAIL") + .map(|value| value.to_string_lossy().into_owned()); + if !empty( + &composer_default_email + .clone() + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null), + ) { + author_email = composer_default_email; + } else if git.contains_key("user.email") { + author_email = git.get("user.email").cloned(); + } + + if let (Some(name), Some(email)) = (author_name, author_email) { + return Some(format!("{} <{}>", name, email)); + } + + None + } } impl Command for InitCommand { @@ -921,369 +1285,3 @@ impl BaseCommand for InitCommand { crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } - -impl InitCommand { - fn parse_author_string( - &self, - author: &str, - ) -> anyhow::Result<IndexMap<String, Option<String>>> { - let mut m: IndexMap<CaptureKey, String> = IndexMap::new(); - if Preg::is_match3( - php_regex!(r#"/^(?P<name>[- .,\p{L}\p{N}\p{Mn}\'’\"()]+)(?:\s+<(?P<email>.+?)>)?$/u"#), - author, - Some(&mut m), - ) { - let email = m.get(&CaptureKey::ByName("email".to_string())).cloned(); - if let Some(ref email) = email - && !self.is_valid_email(email) - { - return Err(InvalidArgumentException { - message: format!("Invalid email \"{}\"", email), - code: 0, - } - .into()); - } - - let mut result: IndexMap<String, Option<String>> = IndexMap::new(); - result.insert( - "name".to_string(), - Some(trim( - &m.get(&CaptureKey::ByName("name".to_string())) - .cloned() - .unwrap_or_default(), - None, - )), - ); - result.insert("email".to_string(), email); - - return Ok(result); - } - - Err(InvalidArgumentException { - message: "Invalid author string. Must be in the formats: Jane Doe or John Smith <john@example.com>" - .to_string(), - code: 0, - } - .into()) - } - - pub(crate) fn format_authors( - &self, - author: &str, - ) -> anyhow::Result<Vec<IndexMap<String, PhpMixed>>> { - let parsed = self.parse_author_string(author)?; - let mut author_map: IndexMap<String, PhpMixed> = IndexMap::new(); - let name = parsed.get("name").cloned().unwrap_or(None); - let email = parsed.get("email").cloned().unwrap_or(None); - if let Some(name) = name { - author_map.insert("name".to_string(), PhpMixed::String(name)); - } - if let Some(email) = email { - author_map.insert("email".to_string(), PhpMixed::String(email)); - } - - Ok(vec![author_map]) - } - - /// Extract namespace from package's vendor name. - /// - /// new_projects.acme-extra/package-name becomes "NewProjectsAcmeExtra\PackageName" - pub fn namespace_from_package_name(&self, package_name: &str) -> Option<String> { - if package_name.is_empty() || strpos(package_name, "/").is_none() { - return None; - } - - let namespace: Vec<String> = array_map( - |part: &String| { - let part = Preg::replace(php_regex!(r"/[^a-z0-9]/i"), " ", part); - let part = ucwords(&part); - str_replace(" ", "", &part) - }, - &explode("/", package_name), - ); - - Some(implode("\\", &namespace)) - } - - pub(crate) fn get_git_config(&self) -> IndexMap<String, String> { - if self.git_config.borrow().is_some() { - return self.git_config.borrow().clone().unwrap_or_default(); - } - - let mut process = ProcessExecutor::new(Some(self.get_io().clone())); - - let mut output = String::new(); - if process.execute_args( - &["git".to_string(), "config".to_string(), "-l".to_string()], - &mut output, - None, - ) == 0 - { - *self.git_config.borrow_mut() = Some(IndexMap::new()); - let mut m: IndexMap<CaptureKey, Vec<String>> = IndexMap::new(); - if Preg::is_match_all3(php_regex!(r"{^([^=]+)=(.*)$}m"), &output, Some(&mut m)) { - let keys: Vec<String> = m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default(); - let values: Vec<String> = - m.get(&CaptureKey::ByIndex(2)).cloned().unwrap_or_default(); - for (key, value) in keys.iter().zip(values.iter()) { - self.git_config - .borrow_mut() - .as_mut() - .unwrap() - .insert(key.clone(), value.clone()); - } - } - - return self.git_config.borrow().clone().unwrap_or_default(); - } - - *self.git_config.borrow_mut() = Some(IndexMap::new()); - IndexMap::new() - } - - /// Checks the local .gitignore file for the Composer vendor directory. - /// - /// Tested patterns include: - /// "/$vendor" - /// "$vendor" - /// "$vendor/" - /// "/$vendor/" - /// "/$vendor/*" - /// "$vendor/*" - pub(crate) fn has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool { - if !file_exists(ignore_file) { - return false; - } - - let pattern = format!("{{^/?{}(/\\*?)?$}}", preg_quote(vendor, None)); - - let lines = file(ignore_file, FILE_IGNORE_NEW_LINES).unwrap_or_default(); - for line in &lines { - if Preg::is_match(&pattern, line) { - return true; - } - } - - false - } - - pub(crate) fn add_vendor_ignore(&self, ignore_file: &str, vendor: &str) { - let mut contents = String::new(); - if file_exists(ignore_file) { - contents = file_get_contents(ignore_file).unwrap_or_default(); - - if strpos(&contents, "\n") != Some(0) { - contents.push('\n'); - } - } - - file_put_contents(ignore_file, format!("{}{}\n", contents, vendor).as_bytes()); - } - - /// For testing only: invoke the private `parse_author_string`. - pub fn __parse_author_string( - &self, - author: &str, - ) -> anyhow::Result<IndexMap<String, Option<String>>> { - self.parse_author_string(author) - } - - /// For testing only: invoke the crate-private `format_authors`. - pub fn __format_authors( - &self, - author: &str, - ) -> anyhow::Result<Vec<IndexMap<String, PhpMixed>>> { - self.format_authors(author) - } - - /// For testing only: invoke the crate-private `get_git_config`. - pub fn __get_git_config(&self) -> IndexMap<String, String> { - self.get_git_config() - } - - /// For testing only: invoke the crate-private `has_vendor_ignore`. - pub fn __has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool { - self.has_vendor_ignore(ignore_file, vendor) - } - - /// For testing only: invoke the crate-private `add_vendor_ignore`. - pub fn __add_vendor_ignore(&self, ignore_file: &str, vendor: &str) { - self.add_vendor_ignore(ignore_file, vendor) - } - - pub(crate) fn is_valid_email(&self, email: &str) -> bool { - shirabe_php_shim::filter_var_email(email) - } - - fn update_dependencies(&self, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>) { - let result = (|| -> anyhow::Result<i64> { - let application = self - .get_application() - .expect("a Composer command's application is always set"); - let update_command = application.borrow_mut().find("update")?; - self.reset_composer()?; - let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = - std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?)); - let command = update_command.borrow(); - command.run(input, output) - })(); - - if result.is_err() { - self.get_io().borrow().write_error( - "Could not update dependencies. Run `composer update` to see more information.", - ); - } - } - - fn run_dump_autoload_command( - &self, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) { - let result = (|| -> anyhow::Result<i64> { - let application = self - .get_application() - .expect("a Composer command's application is always set"); - let command = application.borrow_mut().find("dump-autoload")?; - self.reset_composer()?; - let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = - std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?)); - let command = command.borrow(); - command.run(input, output) - })(); - - if result.is_err() { - self.get_io() - .borrow() - .write_error("Could not run dump-autoload."); - } - } - - fn has_dependencies(&self, options: &IndexMap<String, PhpMixed>) -> bool { - let requires = options.get("require").cloned().unwrap_or(PhpMixed::Null); - let requires_arr_empty = match &requires { - PhpMixed::Array(m) => m.is_empty(), - PhpMixed::List(l) => l.is_empty(), - PhpMixed::Null => true, - _ => false, - }; - let dev_requires = options.get("require-dev").cloned(); - let dev_requires_arr_empty = match &dev_requires { - Some(PhpMixed::Array(m)) => m.is_empty(), - Some(PhpMixed::List(l)) => l.is_empty(), - Some(PhpMixed::Null) | None => true, - _ => false, - }; - - !requires_arr_empty || !dev_requires_arr_empty - } - - fn sanitize_package_name_component(&self, name: &str) -> String { - let name = Preg::replace( - php_regex!(r"{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}"), - "$1$3-$2$4", - name, - ); - let name = strtolower(&name); - let name = Preg::replace(php_regex!(r"{^[_.-]+|[_.-]+$|[^a-z0-9_.-]}u"), "", &name); - - Preg::replace(php_regex!(r"{([_.-]){2,}}u"), "$1", &name) - } - - fn get_default_package_name(&self) -> String { - let git = self.get_git_config(); - let cwd = realpath(".").unwrap_or_default(); - let name = basename(&cwd); - let name = self.sanitize_package_name_component(&name); - - let mut vendor = name.clone(); - let composer_default_vendor = PHP_SERVER - .lock() - .unwrap() - .get("COMPOSER_DEFAULT_VENDOR") - .map(|value| value.to_string_lossy().into_owned()); - let server_username = PHP_SERVER - .lock() - .unwrap() - .get("USERNAME") - .map(|value| value.to_string_lossy().into_owned()); - let server_user = PHP_SERVER - .lock() - .unwrap() - .get("USER") - .map(|value| value.to_string_lossy().into_owned()); - if !empty( - &composer_default_vendor - .clone() - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null), - ) { - vendor = composer_default_vendor.unwrap_or_default(); - } else if git.contains_key("github.user") { - vendor = git.get("github.user").cloned().unwrap_or_default(); - } else if !empty( - &server_username - .clone() - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null), - ) { - vendor = server_username.unwrap_or_default(); - } else if !empty( - &server_user - .clone() - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null), - ) { - vendor = server_user.unwrap_or_default(); - } else if !get_current_user().is_empty() { - vendor = get_current_user(); - } - - let vendor = self.sanitize_package_name_component(&vendor); - - format!("{}/{}", vendor, name) - } - - fn get_default_author(&self) -> Option<String> { - let git = self.get_git_config(); - - let mut author_name: Option<String> = None; - let composer_default_author = PHP_SERVER - .lock() - .unwrap() - .get("COMPOSER_DEFAULT_AUTHOR") - .map(|value| value.to_string_lossy().into_owned()); - if !empty( - &composer_default_author - .clone() - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null), - ) { - author_name = composer_default_author; - } else if git.contains_key("user.name") { - author_name = git.get("user.name").cloned(); - } - - let mut author_email: Option<String> = None; - let composer_default_email = PHP_SERVER - .lock() - .unwrap() - .get("COMPOSER_DEFAULT_EMAIL") - .map(|value| value.to_string_lossy().into_owned()); - if !empty( - &composer_default_email - .clone() - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null), - ) { - author_email = composer_default_email; - } else if git.contains_key("user.email") { - author_email = git.get("user.email").cloned(); - } - - if let (Some(name), Some(email)) = (author_name, author_email) { - return Some(format!("{} <{}>", name, email)); - } - - None - } -} diff --git a/crates/shirabe/src/command/require_command.rs b/crates/shirabe/src/command/require_command.rs index d2b8360d..f4410c19 100644 --- a/crates/shirabe/src/command/require_command.rs +++ b/crates/shirabe/src/command/require_command.rs @@ -89,590 +89,7 @@ impl RequireCommand { .expect("RequireCommand::configure uses static, valid metadata"); command } -} - -impl PackageDiscoveryTrait for RequireCommand { - fn get_repos_mut( - &self, - ) -> std::cell::RefMut<'_, Option<crate::repository::RepositoryInterfaceHandle>> { - self.repos.borrow_mut() - } - - fn get_repository_sets_mut( - &self, - ) -> std::cell::RefMut<'_, IndexMap<String, std::rc::Rc<std::cell::RefCell<RepositorySet>>>> - { - self.repository_sets.borrow_mut() - } -} - -impl Command for RequireCommand { - fn configure(&self) -> anyhow::Result<()> { - 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::new5("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, self.suggest_available_package_incl_platform()).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::new6("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, self.suggest_prefer_install()).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::new6("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())), SuggestedValues::List(Auditor::FORMATS.iter().map(|s| s.to_string()).collect())).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 - fn execute( - &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<i64> { - *self.file.borrow_mut() = Factory::get_composer_file()?; - - if input - .borrow() - .get_option("no-suggest")? - .as_bool() - .unwrap_or(false) - { - self.get_io().write_error3("<warning>You are using the deprecated option \"--no-suggest\". It has no effect and will break in Composer 3.</warning>", true, io_interface::NORMAL); - } - - let file = self.file.borrow().clone(); - self.newly_created.set(!file_exists(&file)); - let write_failed = - self.newly_created.get() && file_put_contents(&file, b"{\n}\n").is_none(); - if write_failed { - let msg = format!("<error>{} could not be created.</error>", file); - self.get_io().write_error3(&msg, true, io_interface::NORMAL); - - return Ok(1); - } - if !Filesystem::is_readable(&file) { - let msg = format!("<error>{} is not readable.</error>", file); - self.get_io().write_error3(&msg, true, io_interface::NORMAL); - - return Ok(1); - } - if filesize(&file) == Some(0) { - file_put_contents(&file, b"{\n}\n"); - } - - *self.json.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new(JsonFile::new( - file.clone(), - None, - None, - )?))); - *self.lock.borrow_mut() = Factory::get_lock_file(&file); - let json = self.json.borrow().as_ref().unwrap().clone(); - *self.composer_backup.borrow_mut() = - file_get_contents(json.borrow().get_path()).unwrap_or_default(); - let lock = self.lock.borrow().clone(); - *self.lock_backup.borrow_mut() = if file_exists(&lock) { - file_get_contents(&lock) - } else { - None - }; - - // PHP: function ($signal, $handler) use ($io, $self) { - // $io->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG); - // $self->revertComposerFile(); $handler->exitWithLastSignal(); } - // TODO(phase-c): SignalHandler::create takes a `Box<dyn Fn> + 'static` handler that cannot - // borrow &self, but the body must call self.revert_composer_file() (which mutates the - // command's composer.json backup state) and self.get_io(). Faithfully wiring this needs the - // revert state + io shared into the closure (Rc<RefCell<...>>), i.e. the shared-ownership - // rework of the command — the same pattern as InstallationManager::execute's signal handler. - let signal_handler = SignalHandler::create( - vec![ - SignalHandler::SIGINT.to_string(), - SignalHandler::SIGTERM.to_string(), - SignalHandler::SIGHUP.to_string(), - ], - Box::new(move |signal: String, handler: &SignalHandler| { - let _ = signal; - handler.exit_with_last_signal(); - }), - ); - - // check for writability by writing to the file as is_writable can not be trusted on network-mounts - // see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926 - let file_path = file.clone(); - let backup_contents = self.composer_backup.borrow().clone(); - if !is_writable(&file) - && Silencer::call(|| { - shirabe_php_shim::file_put_contents(&file_path, backup_contents.as_bytes()); - Ok::<bool, anyhow::Error>(false) - }) - .ok() - == Some(false) - { - let msg = format!("<error>{} is not writable.</error>", file); - self.get_io().write_error3(&msg, true, io_interface::NORMAL); - - return Ok(1); - } - - if input.borrow().get_option("fixed")?.as_bool() == Some(true) { - let config = json.borrow_mut().read()?; - - let package_type = if empty(&config.get("type").cloned().unwrap_or(PhpMixed::Null)) { - "library".to_string() - } else { - config - .get("type") - .and_then(|v| v.as_string()) - .unwrap_or("") - .to_string() - }; - - // @see https://github.com/composer/composer/pull/8313#issuecomment-532637955 - if package_type != "project" - && !input.borrow().get_option("dev")?.as_bool().unwrap_or(false) - { - self.get_io().write_error3("<error>The \"--fixed\" option is only allowed for packages with a \"project\" type or for dev dependencies to prevent possible misuses.</error>", true, io_interface::NORMAL); - - if config.get("type").is_none() { - self.get_io().write_error3("<error>If your package is not a library, you can explicitly specify the \"type\" by using \"composer config type project\".</error>", true, io_interface::NORMAL); - } - - return Ok(1); - } - } - - let composer = self.require_composer(None, None)?; - let composer = crate::composer::composer_full(&composer); - let repository_manager = composer.get_repository_manager().clone(); - let repository_manager = repository_manager.borrow(); - let repos = repository_manager.get_repositories(); - - let platform_overrides = composer.get_config().borrow_mut().get("platform"); - let platform_overrides_map: IndexMap<String, PhpMixed> = platform_overrides - .as_array() - .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) - .unwrap_or_default(); - // initialize self.repos as it is used by the PackageDiscoveryTrait - let platform_repo = - PlatformRepositoryHandle::new(PlatformRepository::new(vec![], platform_overrides_map)?); - let mut combined: Vec<crate::repository::RepositoryInterfaceHandle> = - vec![platform_repo.clone().into()]; - for repo in repos { - combined.push(repo.clone()); - } - *self.get_repos_mut() = Some(crate::repository::RepositoryInterfaceHandle::new( - CompositeRepository::new(combined), - )); - - let preferred_stability = if composer.get_package().get_prefer_stable() { - "stable".to_string() - } else { - composer.get_package().get_minimum_stability() - }; - - // Hoist argument computations into locals so no borrow of `input` is held across the - // call: `determine_requirements` may prompt via ConsoleIO, which mutably borrows the - // same input RefCell. - 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(); - // if there is no update, we need to use the best possible version constraint directly as we cannot rely on the solver to guess the best constraint - let no_update = input - .borrow() - .get_option("no-update")? - .as_bool() - .unwrap_or(false); - let fixed = input - .borrow() - .get_option("fixed")? - .as_bool() - .unwrap_or(false); - let requirements_result = self.determine_requirements( - input.clone(), - output.clone(), - packages, - Some(&platform_repo), - &preferred_stability, - no_update, - fixed, - ); - - let requirements = match requirements_result { - Ok(r) => r, - Err(e) => { - if self.newly_created.get() { - self.revert_composer_file(); - - return Err(RuntimeException { - message: format!( - "No composer.json present in the current directory ({}), this may be the cause of the following exception.", - self.file.borrow() - ), - code: 0, - } - .into()); - } - - return Err(e); - } - }; - - let mut requirements = self.format_requirements(requirements)?; - - if !input.borrow().get_option("dev")?.as_bool().unwrap_or(false) - && self.get_io().is_interactive() - && !composer.is_global() - { - let mut dev_packages: Vec<Vec<String>> = vec![]; - let dev_tags: Vec<String> = vec![ - "dev".to_string(), - "testing".to_string(), - "static analysis".to_string(), - ]; - let current_requires_by_key = self.get_packages_by_require_key(); - for (name, _version) in &requirements { - // skip packages which are already in the composer.json as those have already been decided - if current_requires_by_key.contains_key(name) { - continue; - } - - let found_packages: Vec<crate::package::PackageInterfaceHandle> = self - .get_repos() - .find_packages(name, None)? - .into_iter() - .collect(); - let pkg: Option<crate::package::PackageInterfaceHandle> = - PackageSorter::get_most_current_version(found_packages); - let pkg_as_complete: Option<crate::package::CompletePackageInterfaceHandle> = - pkg.as_ref().and_then(|p| p.as_complete()); - if let Some(pkg_complete) = pkg_as_complete { - let lowered: Vec<String> = - array_map(|s: &String| strtolower(s), &pkg_complete.get_keywords()); - let pkg_dev_tags: Vec<String> = array_intersect(&dev_tags, &lowered); - if (pkg_dev_tags.len() as i64) > 0 { - dev_packages.push(pkg_dev_tags); - } - } - let _ = pkg; - } - - if (dev_packages.len() as i64) == (requirements.len() as i64) { - let plural = if (requirements.len() as i64) > 1 { - "s" - } else { - "" - }; - let plural2 = if (requirements.len() as i64) > 1 { - "are" - } else { - "is" - }; - let plural3 = if (requirements.len() as i64) > 1 { - "they are" - } else { - "it is" - }; - let merged: Vec<String> = dev_packages.iter().flatten().cloned().collect(); - let pkg_dev_tags: Vec<String> = array_unique(&merged); - let warn_msg = format!( - "The package{} you required {} recommended to be placed in require-dev (because {} tagged as \"{}\") but you did not use --dev.", - plural, - plural2, - plural3, - implode("\", \"", &pkg_dev_tags), - ); - self.get_io().warning(&warn_msg, &[]); - if self.get_io().ask_confirmation( - "<info>Do you want to re-run the command with --dev?</> [<comment>yes</>]? " - .to_string(), - true, - ) { - input.borrow_mut().set_option("dev", PhpMixed::Bool(true))?; - } - } - - // unset($devPackages, $pkgDevTags); - } - - let mut require_key = if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { - "require-dev" - } else { - "require" - }; - let mut remove_key = if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { - "require" - } else { - "require-dev" - }; - - // check which requirements need the version guessed - let mut requirements_to_guess: Vec<String> = vec![]; - for (package, constraint) in requirements.clone().iter() { - if constraint == "guess" { - requirements.insert(package.clone(), "*".to_string()); - requirements_to_guess.push(package.clone()); - } - } - - // validate requirements format - let version_parser = VersionParser::new(); - for (package, constraint) in &requirements { - if strtolower(package) == composer.get_package().get_name() { - let msg = format!( - "<error>Root package '{}' cannot require itself in its composer.json</error>", - package.clone(), - ); - self.get_io().write_error3(&msg, true, io_interface::NORMAL); - - return Ok(1); - } - if constraint == "self.version" { - continue; - } - version_parser.parse_constraints(constraint)?; - } - - let inconsistent_require_keys = - self.get_inconsistent_require_keys(&requirements, require_key); - if (inconsistent_require_keys.len() as i64) > 0 { - for package in &inconsistent_require_keys { - let warn_msg = format!( - "{} is currently present in the {} key and you ran the command {} the --dev flag, which will move it to the {} key.", - package.clone(), - remove_key, - if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { - "with" - } else { - "without" - }, - require_key, - ); - self.get_io().warning(&warn_msg, &[]); - } - - if self.get_io().is_interactive() { - let q1 = format!( - "<info>Do you want to move {}?</info> [<comment>no</comment>]? ", - if (inconsistent_require_keys.len() as i64) > 1 { - "these requirements" - } else { - "this requirement" - }, - ); - if !self.get_io().ask_confirmation(q1, false) { - let q2 = format!( - "<info>Do you want to re-run the command {} --dev?</info> [<comment>yes</comment>]? ", - if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { - "without" - } else { - "with" - }, - ); - if !self.get_io().ask_confirmation(q2, true) { - return Ok(0); - } - - input.borrow_mut().set_option("dev", PhpMixed::Bool(true))?; - std::mem::swap(&mut require_key, &mut remove_key); - } - } - } - - let sort_packages = input - .borrow() - .get_option("sort-packages")? - .as_bool() - .unwrap_or(false) - || composer - .get_config() - .borrow() - .get("sort-packages") - .as_bool() - .unwrap_or(false); - - self.first_require.set(self.newly_created.get()); - if !self.first_require.get() { - let composer_definition = json.borrow_mut().read()?; - let require_count = composer_definition - .get("require") - .and_then(|v| v.as_array()) - .map(|m| m.len() as i64) - .unwrap_or(0); - let require_dev_count = composer_definition - .get("require-dev") - .and_then(|v| v.as_array()) - .map(|m| m.len() as i64) - .unwrap_or(0); - if require_count == 0 && require_dev_count == 0 { - self.first_require.set(true); - } - } - - if !input - .borrow() - .get_option("dry-run")? - .as_bool() - .unwrap_or(false) - { - self.update_file(&json, &requirements, require_key, remove_key, sort_packages); - } - - let updated_msg = format!( - "<info>{} has been {}</info>", - file, - if self.newly_created.get() { - "created" - } else { - "updated" - } - ); - self.get_io() - .write_error3(&updated_msg, true, io_interface::NORMAL); - - if input - .borrow() - .get_option("no-update")? - .as_bool() - .unwrap_or(false) - { - return Ok(0); - } - - composer - .get_plugin_manager() - .borrow_mut() - .deactivate_installed_plugins()?; - - let io = self.get_io().clone(); - let do_update_result = self.do_update( - input.clone(), - output, - io, - &requirements, - require_key, - remove_key, - ); - let dry_run = input - .borrow() - .get_option("dry-run")? - .as_bool() - .unwrap_or(false); - - let result = match do_update_result { - Ok(result) => { - let final_result = if result == 0 && (requirements_to_guess.len() as i64) > 0 { - let fixed = input - .borrow() - .get_option("fixed")? - .as_bool() - .unwrap_or(false); - self.update_requirements_after_resolution( - &requirements_to_guess, - require_key, - remove_key, - sort_packages, - dry_run, - fixed, - )? - } else { - result - }; - Ok(final_result) - } - Err(e) => { - if !self.dependency_resolution_completed.get() { - self.revert_composer_file(); - } - Err(e) - } - }; - - // finally - if dry_run && self.newly_created.get() { - // @unlink($this->json->getPath()); - unlink(json.borrow().get_path()); - } - signal_handler.unregister(); - - result - } - - fn interact( - &self, - _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) { - } - - fn initialize( - &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<()> { - base_command_initialize(self, input, output) - } - - fn complete( - &self, - input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, - suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, - ) -> anyhow::Result<()> { - crate::command::base_command::base_command_complete(self, input, suggestions) - } - - shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); -} - -impl BaseCommand for RequireCommand { - fn base_command_data(&self) -> &crate::command::BaseCommandData { - &self.base_command_data - } - - crate::delegate_base_command_trait_impls_to_inner!(base_command_data); -} -impl RequireCommand { fn get_inconsistent_require_keys( &self, new_requirements: &IndexMap<String, String>, @@ -1302,3 +719,584 @@ impl RequireCommand { } } } + +impl PackageDiscoveryTrait for RequireCommand { + fn get_repos_mut( + &self, + ) -> std::cell::RefMut<'_, Option<crate::repository::RepositoryInterfaceHandle>> { + self.repos.borrow_mut() + } + + fn get_repository_sets_mut( + &self, + ) -> std::cell::RefMut<'_, IndexMap<String, std::rc::Rc<std::cell::RefCell<RepositorySet>>>> + { + self.repository_sets.borrow_mut() + } +} + +impl Command for RequireCommand { + fn configure(&self) -> anyhow::Result<()> { + 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::new5("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, self.suggest_available_package_incl_platform()).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::new6("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, self.suggest_prefer_install()).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::new6("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())), SuggestedValues::List(Auditor::FORMATS.iter().map(|s| s.to_string()).collect())).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 + fn execute( + &self, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + *self.file.borrow_mut() = Factory::get_composer_file()?; + + if input + .borrow() + .get_option("no-suggest")? + .as_bool() + .unwrap_or(false) + { + self.get_io().write_error3("<warning>You are using the deprecated option \"--no-suggest\". It has no effect and will break in Composer 3.</warning>", true, io_interface::NORMAL); + } + + let file = self.file.borrow().clone(); + self.newly_created.set(!file_exists(&file)); + let write_failed = + self.newly_created.get() && file_put_contents(&file, b"{\n}\n").is_none(); + if write_failed { + let msg = format!("<error>{} could not be created.</error>", file); + self.get_io().write_error3(&msg, true, io_interface::NORMAL); + + return Ok(1); + } + if !Filesystem::is_readable(&file) { + let msg = format!("<error>{} is not readable.</error>", file); + self.get_io().write_error3(&msg, true, io_interface::NORMAL); + + return Ok(1); + } + if filesize(&file) == Some(0) { + file_put_contents(&file, b"{\n}\n"); + } + + *self.json.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new(JsonFile::new( + file.clone(), + None, + None, + )?))); + *self.lock.borrow_mut() = Factory::get_lock_file(&file); + let json = self.json.borrow().as_ref().unwrap().clone(); + *self.composer_backup.borrow_mut() = + file_get_contents(json.borrow().get_path()).unwrap_or_default(); + let lock = self.lock.borrow().clone(); + *self.lock_backup.borrow_mut() = if file_exists(&lock) { + file_get_contents(&lock) + } else { + None + }; + + // PHP: function ($signal, $handler) use ($io, $self) { + // $io->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG); + // $self->revertComposerFile(); $handler->exitWithLastSignal(); } + // TODO(phase-c): SignalHandler::create takes a `Box<dyn Fn> + 'static` handler that cannot + // borrow &self, but the body must call self.revert_composer_file() (which mutates the + // command's composer.json backup state) and self.get_io(). Faithfully wiring this needs the + // revert state + io shared into the closure (Rc<RefCell<...>>), i.e. the shared-ownership + // rework of the command — the same pattern as InstallationManager::execute's signal handler. + let signal_handler = SignalHandler::create( + vec![ + SignalHandler::SIGINT.to_string(), + SignalHandler::SIGTERM.to_string(), + SignalHandler::SIGHUP.to_string(), + ], + Box::new(move |signal: String, handler: &SignalHandler| { + let _ = signal; + handler.exit_with_last_signal(); + }), + ); + + // check for writability by writing to the file as is_writable can not be trusted on network-mounts + // see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926 + let file_path = file.clone(); + let backup_contents = self.composer_backup.borrow().clone(); + if !is_writable(&file) + && Silencer::call(|| { + shirabe_php_shim::file_put_contents(&file_path, backup_contents.as_bytes()); + Ok::<bool, anyhow::Error>(false) + }) + .ok() + == Some(false) + { + let msg = format!("<error>{} is not writable.</error>", file); + self.get_io().write_error3(&msg, true, io_interface::NORMAL); + + return Ok(1); + } + + if input.borrow().get_option("fixed")?.as_bool() == Some(true) { + let config = json.borrow_mut().read()?; + + let package_type = if empty(&config.get("type").cloned().unwrap_or(PhpMixed::Null)) { + "library".to_string() + } else { + config + .get("type") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string() + }; + + // @see https://github.com/composer/composer/pull/8313#issuecomment-532637955 + if package_type != "project" + && !input.borrow().get_option("dev")?.as_bool().unwrap_or(false) + { + self.get_io().write_error3("<error>The \"--fixed\" option is only allowed for packages with a \"project\" type or for dev dependencies to prevent possible misuses.</error>", true, io_interface::NORMAL); + + if config.get("type").is_none() { + self.get_io().write_error3("<error>If your package is not a library, you can explicitly specify the \"type\" by using \"composer config type project\".</error>", true, io_interface::NORMAL); + } + + return Ok(1); + } + } + + let composer = self.require_composer(None, None)?; + let composer = crate::composer::composer_full(&composer); + let repository_manager = composer.get_repository_manager().clone(); + let repository_manager = repository_manager.borrow(); + let repos = repository_manager.get_repositories(); + + let platform_overrides = composer.get_config().borrow_mut().get("platform"); + let platform_overrides_map: IndexMap<String, PhpMixed> = platform_overrides + .as_array() + .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) + .unwrap_or_default(); + // initialize self.repos as it is used by the PackageDiscoveryTrait + let platform_repo = + PlatformRepositoryHandle::new(PlatformRepository::new(vec![], platform_overrides_map)?); + let mut combined: Vec<crate::repository::RepositoryInterfaceHandle> = + vec![platform_repo.clone().into()]; + for repo in repos { + combined.push(repo.clone()); + } + *self.get_repos_mut() = Some(crate::repository::RepositoryInterfaceHandle::new( + CompositeRepository::new(combined), + )); + + let preferred_stability = if composer.get_package().get_prefer_stable() { + "stable".to_string() + } else { + composer.get_package().get_minimum_stability() + }; + + // Hoist argument computations into locals so no borrow of `input` is held across the + // call: `determine_requirements` may prompt via ConsoleIO, which mutably borrows the + // same input RefCell. + 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(); + // if there is no update, we need to use the best possible version constraint directly as we cannot rely on the solver to guess the best constraint + let no_update = input + .borrow() + .get_option("no-update")? + .as_bool() + .unwrap_or(false); + let fixed = input + .borrow() + .get_option("fixed")? + .as_bool() + .unwrap_or(false); + let requirements_result = self.determine_requirements( + input.clone(), + output.clone(), + packages, + Some(&platform_repo), + &preferred_stability, + no_update, + fixed, + ); + + let requirements = match requirements_result { + Ok(r) => r, + Err(e) => { + if self.newly_created.get() { + self.revert_composer_file(); + + return Err(RuntimeException { + message: format!( + "No composer.json present in the current directory ({}), this may be the cause of the following exception.", + self.file.borrow() + ), + code: 0, + } + .into()); + } + + return Err(e); + } + }; + + let mut requirements = self.format_requirements(requirements)?; + + if !input.borrow().get_option("dev")?.as_bool().unwrap_or(false) + && self.get_io().is_interactive() + && !composer.is_global() + { + let mut dev_packages: Vec<Vec<String>> = vec![]; + let dev_tags: Vec<String> = vec![ + "dev".to_string(), + "testing".to_string(), + "static analysis".to_string(), + ]; + let current_requires_by_key = self.get_packages_by_require_key(); + for (name, _version) in &requirements { + // skip packages which are already in the composer.json as those have already been decided + if current_requires_by_key.contains_key(name) { + continue; + } + + let found_packages: Vec<crate::package::PackageInterfaceHandle> = self + .get_repos() + .find_packages(name, None)? + .into_iter() + .collect(); + let pkg: Option<crate::package::PackageInterfaceHandle> = + PackageSorter::get_most_current_version(found_packages); + let pkg_as_complete: Option<crate::package::CompletePackageInterfaceHandle> = + pkg.as_ref().and_then(|p| p.as_complete()); + if let Some(pkg_complete) = pkg_as_complete { + let lowered: Vec<String> = + array_map(|s: &String| strtolower(s), &pkg_complete.get_keywords()); + let pkg_dev_tags: Vec<String> = array_intersect(&dev_tags, &lowered); + if (pkg_dev_tags.len() as i64) > 0 { + dev_packages.push(pkg_dev_tags); + } + } + let _ = pkg; + } + + if (dev_packages.len() as i64) == (requirements.len() as i64) { + let plural = if (requirements.len() as i64) > 1 { + "s" + } else { + "" + }; + let plural2 = if (requirements.len() as i64) > 1 { + "are" + } else { + "is" + }; + let plural3 = if (requirements.len() as i64) > 1 { + "they are" + } else { + "it is" + }; + let merged: Vec<String> = dev_packages.iter().flatten().cloned().collect(); + let pkg_dev_tags: Vec<String> = array_unique(&merged); + let warn_msg = format!( + "The package{} you required {} recommended to be placed in require-dev (because {} tagged as \"{}\") but you did not use --dev.", + plural, + plural2, + plural3, + implode("\", \"", &pkg_dev_tags), + ); + self.get_io().warning(&warn_msg, &[]); + if self.get_io().ask_confirmation( + "<info>Do you want to re-run the command with --dev?</> [<comment>yes</>]? " + .to_string(), + true, + ) { + input.borrow_mut().set_option("dev", PhpMixed::Bool(true))?; + } + } + + // unset($devPackages, $pkgDevTags); + } + + let mut require_key = if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { + "require-dev" + } else { + "require" + }; + let mut remove_key = if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { + "require" + } else { + "require-dev" + }; + + // check which requirements need the version guessed + let mut requirements_to_guess: Vec<String> = vec![]; + for (package, constraint) in requirements.clone().iter() { + if constraint == "guess" { + requirements.insert(package.clone(), "*".to_string()); + requirements_to_guess.push(package.clone()); + } + } + + // validate requirements format + let version_parser = VersionParser::new(); + for (package, constraint) in &requirements { + if strtolower(package) == composer.get_package().get_name() { + let msg = format!( + "<error>Root package '{}' cannot require itself in its composer.json</error>", + package.clone(), + ); + self.get_io().write_error3(&msg, true, io_interface::NORMAL); + + return Ok(1); + } + if constraint == "self.version" { + continue; + } + version_parser.parse_constraints(constraint)?; + } + + let inconsistent_require_keys = + self.get_inconsistent_require_keys(&requirements, require_key); + if (inconsistent_require_keys.len() as i64) > 0 { + for package in &inconsistent_require_keys { + let warn_msg = format!( + "{} is currently present in the {} key and you ran the command {} the --dev flag, which will move it to the {} key.", + package.clone(), + remove_key, + if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { + "with" + } else { + "without" + }, + require_key, + ); + self.get_io().warning(&warn_msg, &[]); + } + + if self.get_io().is_interactive() { + let q1 = format!( + "<info>Do you want to move {}?</info> [<comment>no</comment>]? ", + if (inconsistent_require_keys.len() as i64) > 1 { + "these requirements" + } else { + "this requirement" + }, + ); + if !self.get_io().ask_confirmation(q1, false) { + let q2 = format!( + "<info>Do you want to re-run the command {} --dev?</info> [<comment>yes</comment>]? ", + if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { + "without" + } else { + "with" + }, + ); + if !self.get_io().ask_confirmation(q2, true) { + return Ok(0); + } + + input.borrow_mut().set_option("dev", PhpMixed::Bool(true))?; + std::mem::swap(&mut require_key, &mut remove_key); + } + } + } + + let sort_packages = input + .borrow() + .get_option("sort-packages")? + .as_bool() + .unwrap_or(false) + || composer + .get_config() + .borrow() + .get("sort-packages") + .as_bool() + .unwrap_or(false); + + self.first_require.set(self.newly_created.get()); + if !self.first_require.get() { + let composer_definition = json.borrow_mut().read()?; + let require_count = composer_definition + .get("require") + .and_then(|v| v.as_array()) + .map(|m| m.len() as i64) + .unwrap_or(0); + let require_dev_count = composer_definition + .get("require-dev") + .and_then(|v| v.as_array()) + .map(|m| m.len() as i64) + .unwrap_or(0); + if require_count == 0 && require_dev_count == 0 { + self.first_require.set(true); + } + } + + if !input + .borrow() + .get_option("dry-run")? + .as_bool() + .unwrap_or(false) + { + self.update_file(&json, &requirements, require_key, remove_key, sort_packages); + } + + let updated_msg = format!( + "<info>{} has been {}</info>", + file, + if self.newly_created.get() { + "created" + } else { + "updated" + } + ); + self.get_io() + .write_error3(&updated_msg, true, io_interface::NORMAL); + + if input + .borrow() + .get_option("no-update")? + .as_bool() + .unwrap_or(false) + { + return Ok(0); + } + + composer + .get_plugin_manager() + .borrow_mut() + .deactivate_installed_plugins()?; + + let io = self.get_io().clone(); + let do_update_result = self.do_update( + input.clone(), + output, + io, + &requirements, + require_key, + remove_key, + ); + let dry_run = input + .borrow() + .get_option("dry-run")? + .as_bool() + .unwrap_or(false); + + let result = match do_update_result { + Ok(result) => { + let final_result = if result == 0 && (requirements_to_guess.len() as i64) > 0 { + let fixed = input + .borrow() + .get_option("fixed")? + .as_bool() + .unwrap_or(false); + self.update_requirements_after_resolution( + &requirements_to_guess, + require_key, + remove_key, + sort_packages, + dry_run, + fixed, + )? + } else { + result + }; + Ok(final_result) + } + Err(e) => { + if !self.dependency_resolution_completed.get() { + self.revert_composer_file(); + } + Err(e) + } + }; + + // finally + if dry_run && self.newly_created.get() { + // @unlink($this->json->getPath()); + unlink(json.borrow().get_path()); + } + signal_handler.unregister(); + + result + } + + fn interact( + &self, + _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) { + } + + fn initialize( + &self, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + fn complete( + &self, + input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, + suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, + ) -> anyhow::Result<()> { + crate::command::base_command::base_command_complete(self, input, suggestions) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for RequireCommand { + fn base_command_data(&self) -> &crate::command::BaseCommandData { + &self.base_command_data + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); +} diff --git a/crates/shirabe/src/command/show_command.rs b/crates/shirabe/src/command/show_command.rs index fb663d86..44733531 100644 --- a/crates/shirabe/src/command/show_command.rs +++ b/crates/shirabe/src/command/show_command.rs @@ -81,1382 +81,7 @@ impl ShowCommand { .expect("ShowCommand::configure uses static, valid metadata"); command } -} - -impl Command for ShowCommand { - fn configure(&self) -> anyhow::Result<()> { - self.set_name("show")?; - self.set_aliases(vec!["info".to_string()])?; - self.set_description("Shows information about packages"); - let opt_none = |name: &str, shortcut: Option<&str>, description: &str| { - InputOption::new( - name, - shortcut.map(|s| PhpMixed::String(s.to_string())), - Some(InputOption::VALUE_NONE), - description, - None, - ) - .unwrap() - .into() - }; - self.set_definition(&[ - InputArgument::new5( - "package", - Some(InputArgument::OPTIONAL), - "Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.", - None, - self.suggest_package_based_on_mode(), - ) - .unwrap() - .into(), - InputArgument::new( - "version", - Some(InputArgument::OPTIONAL), - "Version or version constraint to inspect", - None, - ) - .unwrap() - .into(), - opt_none("all", None, "List all packages"), - opt_none("locked", None, "List all locked packages"), - opt_none( - "installed", - Some("i"), - "List installed packages only (enabled by default, only present for BC).", - ), - opt_none("platform", Some("p"), "List platform packages only"), - opt_none("available", Some("a"), "List available packages only"), - opt_none("self", Some("s"), "Show the root package information"), - opt_none("name-only", Some("N"), "List package names only"), - opt_none("path", Some("P"), "Show package paths"), - opt_none("tree", Some("t"), "List the dependencies as a tree"), - opt_none("latest", Some("l"), "Show the latest version"), - opt_none( - "outdated", - Some("o"), - "Show the latest version but only for packages that are outdated", - ), - InputOption::new6( - "ignore", - None, - Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), - "Ignore specified package(s). Can contain wildcards (*). Use it with the --outdated option if you don't want to be informed about new versions of some packages.", - None, - self.suggest_installed_package(false, false), - ) - .unwrap() - .into(), - opt_none( - "major-only", - Some("M"), - "Show only packages that have major SemVer-compatible updates. Use with the --latest or --outdated option.", - ), - opt_none( - "minor-only", - Some("m"), - "Show only packages that have minor SemVer-compatible updates. Use with the --latest or --outdated option.", - ), - opt_none( - "patch-only", - None, - "Show only packages that have patch SemVer-compatible updates. Use with the --latest or --outdated option.", - ), - opt_none( - "sort-by-age", - Some("A"), - "Displays the installed version's age, and sorts packages oldest first. Use with the --latest or --outdated option.", - ), - opt_none( - "direct", - Some("D"), - "Shows only packages that are directly required by the root package", - ), - opt_none( - "strict", - None, - "Return a non-zero exit code when there are outdated packages", - ), - InputOption::new6( - "format", - Some(PhpMixed::String("f".to_string())), - Some(InputOption::VALUE_REQUIRED), - "Format of the output: text or json", - Some(PhpMixed::String("text".to_string())), - SuggestedValues::List(vec!["json".to_string(), "text".to_string()]), - ) - .unwrap() - .into(), - opt_none("no-dev", None, "Disables search in require-dev packages."), - 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(), - opt_none( - "ignore-platform-reqs", - None, - "Ignore all platform requirements (php & ext- packages). Use with the --outdated option", - ), - ]); - 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(()) - } - - fn execute( - &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<i64> { - *self.version_parser.borrow_mut() = VersionParser::new(); - if input.borrow().get_option("tree")?.as_bool() == Some(true) { - self.init_styles(output.clone()); - } - - let composer = self.try_composer(None, None); - - if input.borrow().get_option("installed")?.as_bool() == Some(true) - && input.borrow().get_option("self")?.as_bool() != Some(true) - { - self.get_io().write_error("<warning>You are using the deprecated option \"installed\". Only installed packages are shown by default now. The --all option can be used to show all packages.</warning>"); - } - - if input.borrow().get_option("outdated")?.as_bool() == Some(true) { - input - .borrow_mut() - .set_option("latest", PhpMixed::Bool(true)); - } else if input - .borrow() - .get_option("ignore")? - .as_list() - .map_or(0, |l| l.len()) - > 0 - { - self.get_io().write_error("<warning>You are using the option \"ignore\" for action other than \"outdated\", it will be ignored.</warning>"); - } - - if input.borrow().get_option("direct")?.as_bool() == Some(true) - && (input.borrow().get_option("all")?.as_bool() == Some(true) - || input.borrow().get_option("available")?.as_bool() == Some(true) - || input.borrow().get_option("platform")?.as_bool() == Some(true)) - { - self.get_io().write_error("The --direct (-D) option is not usable in combination with --all, --platform (-p) or --available (-a)"); - - return Ok(1); - } - - if input.borrow().get_option("tree")?.as_bool() == Some(true) - && (input.borrow().get_option("all")?.as_bool() == Some(true) - || input.borrow().get_option("available")?.as_bool() == Some(true)) - { - self.get_io().write_error("The --tree (-t) option is not usable in combination with --all or --available (-a)"); - - return Ok(1); - } - - let only_count: usize = [ - input.borrow().get_option("patch-only")?.as_bool() == Some(true), - input.borrow().get_option("minor-only")?.as_bool() == Some(true), - input.borrow().get_option("major-only")?.as_bool() == Some(true), - ] - .iter() - .filter(|b| **b) - .count(); - if only_count > 1 { - self.get_io().write_error( - "Only one of --major-only, --minor-only or --patch-only can be used at once", - ); - - return Ok(1); - } - - if input.borrow().get_option("tree")?.as_bool() == Some(true) - && input.borrow().get_option("latest")?.as_bool() == Some(true) - { - self.get_io().write_error( - "The --tree (-t) option is not usable in combination with --latest (-l)", - ); - - return Ok(1); - } - - if input.borrow().get_option("tree")?.as_bool() == Some(true) - && input.borrow().get_option("path")?.as_bool() == Some(true) - { - self.get_io().write_error( - "The --tree (-t) option is not usable in combination with --path (-P)", - ); - - return Ok(1); - } - - let format = input - .borrow() - .get_option("format")? - .as_string() - .unwrap_or("text") - .to_string(); - if !in_array_loose( - format.clone(), - &[ - PhpMixed::String("text".to_string()), - PhpMixed::String("json".to_string()), - ], - ) { - self.get_io().write_error(&format!( - "Unsupported format \"{}\". See help for supported formats.", - format - )); - - return Ok(1); - } - - let platform_req_filter = self.get_platform_requirement_filter(input.clone())?; - - // init repos - let mut platform_overrides: IndexMap<String, PhpMixed> = IndexMap::new(); - if let Some(ref composer) = composer { - let composer = crate::composer::composer_full(composer); - if let Some(p) = composer - .get_config() - .borrow() - .get("platform") - .as_array() - .cloned() - { - platform_overrides = p.into_iter().collect(); - } - } - let platform_repo = - PlatformRepositoryHandle::new(PlatformRepository::new(vec![], platform_overrides)?); - let mut locked_repo: Option<RepositoryInterfaceHandle> = None; - - // The single-package $package binding from PHP gets surfaced here. - let mut single_package: Option<crate::package::CompletePackageInterfaceHandle> = None; - let mut versions_map: IndexMap<String, String> = IndexMap::new(); - let installed_repo: RepositoryInterfaceHandle; - let repos: RepositoryInterfaceHandle; - - if input.borrow().get_option("self")?.as_bool() == Some(true) - && input.borrow().get_option("installed")?.as_bool() != Some(true) - && input.borrow().get_option("locked")?.as_bool() != Some(true) - { - let composer = self.require_composer(None, None)?; - let package = crate::package::RootPackageInterfaceHandle::dup( - composer.borrow_partial().get_package(), - ); - if input.borrow().get_option("name-only")?.as_bool() == Some(true) { - self.get_io().write(&package.get_name()); - - return Ok(0); - } - if input - .borrow() - .get_argument("package")? - .as_string() - .is_some() - { - return Err(InvalidArgumentException { - message: "You cannot use --self together with a package name".to_string(), - code: 0, - } - .into()); - } - installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ - RepositoryInterfaceHandle::new(RootPackageRepository::new(package.clone())), - ])); - repos = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ - RepositoryInterfaceHandle::new(RootPackageRepository::new(package.clone())), - ])); - single_package = Some(package.into()); - } else if input.borrow().get_option("platform")?.as_bool() == Some(true) { - installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ - platform_repo.clone().into(), - ])); - repos = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ - platform_repo.clone().into(), - ])); - } else if input.borrow().get_option("available")?.as_bool() == Some(true) { - let mut ir = InstalledRepository::new(vec![platform_repo.clone().into()]); - if let Some(ref composer) = composer { - let composer = crate::composer::composer_full(composer); - repos = RepositoryInterfaceHandle::new(CompositeRepository::new( - composer - .get_repository_manager() - .borrow() - .get_repositories() - .to_vec(), - )); - ir.add_repository( - composer - .get_repository_manager() - .borrow() - .get_local_repository(), - ); - installed_repo = RepositoryInterfaceHandle::new(ir); - } else { - let default_repos = - RepositoryFactory::default_repos_with_default_manager(self.get_io())?; - let names: Vec<String> = default_repos.keys().cloned().collect(); - repos = RepositoryInterfaceHandle::new(CompositeRepository::new( - default_repos.into_values().collect(), - )); - self.get_io().write_error(&format!( - "No composer.json found in the current directory, showing available packages from {}", - names.join(", ") - )); - installed_repo = RepositoryInterfaceHandle::new(ir); - } - } else if input.borrow().get_option("all")?.as_bool() == Some(true) && composer.is_some() { - let composer_ref = crate::composer::composer_full(composer.as_ref().unwrap()); - let local_repo = composer_ref - .get_repository_manager() - .borrow() - .get_local_repository(); - let locker_rc = composer_ref.get_locker().clone(); - let mut locker = locker_rc.borrow_mut(); - if locker.is_locked() { - let lr_handle: RepositoryInterfaceHandle = - locker.get_locked_repository(true)?.into(); - installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ - lr_handle.clone(), - local_repo, - platform_repo.clone().into(), - ])); - locked_repo = Some(lr_handle); - } else { - installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ - local_repo, - platform_repo.clone().into(), - ])); - } - let mut composite_input: Vec<RepositoryInterfaceHandle> = - vec![RepositoryInterfaceHandle::new(FilterRepository::new( - installed_repo.clone(), - { - let mut m = IndexMap::new(); - m.insert("canonical".to_string(), PhpMixed::Bool(false)); - m - }, - )?)]; - for r in composer_ref - .get_repository_manager() - .borrow() - .get_repositories() - { - composite_input.push(r.clone()); - } - repos = RepositoryInterfaceHandle::new(CompositeRepository::new(composite_input)); - } else if input.borrow().get_option("all")?.as_bool() == Some(true) { - let default_repos = - RepositoryFactory::default_repos_with_default_manager(self.get_io())?; - let names: Vec<String> = default_repos.keys().cloned().collect(); - self.get_io().write_error(&format!( - "No composer.json found in the current directory, showing available packages from {}", - names.join(", ") - )); - installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ - platform_repo.clone().into(), - ])); - let mut composite_input: Vec<RepositoryInterfaceHandle> = vec![installed_repo.clone()]; - for (_k, v) in default_repos.into_iter() { - composite_input.push(v); - } - repos = RepositoryInterfaceHandle::new(CompositeRepository::new(composite_input)); - } else if input.borrow().get_option("locked")?.as_bool() == Some(true) { - if composer.is_none() - || !crate::composer::composer_full(composer.as_ref().unwrap()) - .get_locker() - .borrow_mut() - .is_locked() - { - return Err(UnexpectedValueException { - message: "A valid composer.json and composer.lock files is required to run this command with --locked".to_string(), - code: 0, - } - .into()); - } - let composer_ref = crate::composer::composer_full(composer.as_ref().unwrap()); - let locker_rc = composer_ref.get_locker().clone(); - let mut locker = locker_rc.borrow_mut(); - let lr = locker.get_locked_repository( - input.borrow().get_option("no-dev")?.as_bool() != Some(true), - )?; - if input.borrow().get_option("self")?.as_bool() == Some(true) { - lr.add_package( - crate::package::RootPackageInterfaceHandle::dup(composer_ref.get_package()) - .into(), - )?; - } - let lr_handle: RepositoryInterfaceHandle = lr.into(); - locked_repo = Some(lr_handle.clone()); - let new_repo = - RepositoryInterfaceHandle::new(InstalledRepository::new(vec![lr_handle])); - installed_repo = new_repo.clone(); - repos = new_repo; - } else { - // --installed / default case - let composer_local_owned; - let _guard_from_existing; - let composer_local = match composer.as_ref() { - Some(c) => { - _guard_from_existing = crate::composer::composer_full(c); - &*_guard_from_existing - } - None => { - composer_local_owned = self.require_composer(None, None)?; - _guard_from_existing = crate::composer::composer_full(&composer_local_owned); - &*_guard_from_existing - } - }; - let root_pkg = composer_local.get_package(); - - let root_repo: RepositoryInterfaceHandle = - if input.borrow().get_option("self")?.as_bool() == Some(true) { - RepositoryInterfaceHandle::new(RootPackageRepository::new( - crate::package::RootPackageInterfaceHandle::dup( - composer_local.get_package(), - ), - )) - } else { - RepositoryInterfaceHandle::new(InstalledArrayRepository::new()?) - }; - if input.borrow().get_option("no-dev")?.as_bool() == Some(true) { - let local_packages = composer_local - .get_repository_manager() - .borrow() - .get_local_repository() - .get_packages()?; - let packages = RepositoryUtils::filter_required_packages( - &local_packages, - root_pkg.clone().into(), - false, - Vec::new(), - ); - let cloned: Vec<crate::package::PackageInterfaceHandle> = packages - .iter() - .map(crate::package::PackageInterfaceHandle::dup) - .collect(); - let new_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ - root_repo, - RepositoryInterfaceHandle::new(InstalledArrayRepository::new_with_packages( - cloned, - )?), - ])); - installed_repo = new_repo.clone(); - repos = new_repo; - } else { - let repository_manager = composer_local.get_repository_manager().clone(); - let repository_manager = repository_manager.borrow(); - let lr = repository_manager.get_local_repository(); - installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ - root_repo.clone(), - lr.clone(), - ])); - repos = - RepositoryInterfaceHandle::new(InstalledRepository::new(vec![root_repo, lr])); - } - - if installed_repo.get_packages()?.is_empty() { - let has_non_platform_reqs = |reqs: &IndexMap<String, Link>| -> bool { - reqs.keys() - .any(|name| !PlatformRepository::is_platform_package(name)) - }; - - if has_non_platform_reqs(&root_pkg.get_requires()) - || has_non_platform_reqs(&root_pkg.get_dev_requires()) - { - // Borrow is local; release composer_local borrow first. - let _ = root_pkg; - self.get_io().write_error("<warning>No dependencies installed. Try running composer install or update.</warning>"); - } - } - } - - if let Some(ref composer) = composer { - let composer = crate::composer::composer_full(composer); - let mut command_event = CommandEvent::new6( - PluginEvents::COMMAND, - "show", - input.clone(), - output, - vec![], - IndexMap::new(), - ); - let command_event_name = command_event.get_name().to_string(); - composer - .get_event_dispatcher() - .borrow_mut() - .dispatch(Some(&command_event_name), Some(&mut command_event))?; - } - - if input.borrow().get_option("latest")?.as_bool() == Some(true) && composer.is_none() { - self.get_io().write_error( - "No composer.json found in the current directory, disabling \"latest\" option", - ); - input - .borrow_mut() - .set_option("latest", PhpMixed::Bool(false)); - } - - let package_filter: Option<String> = input - .borrow() - .get_argument("package")? - .as_string() - .map(|s| s.to_string()); - - // show single package or single version - if let Some(ref pkg) = single_package { - versions_map.insert(pkg.get_pretty_version(), pkg.get_version()); - } else if let Some(ref pf) = package_filter - && !pf.contains('*') - { - let (matched_package, vers) = self.get_package( - &installed_repo, - &repos, - pf, - input.borrow().get_argument("version")?, - )?; - - if let Some(ref pkg) = matched_package - && input.borrow().get_option("direct")?.as_bool() == Some(true) - && !in_array_strict( - pkg.get_name(), - &self - .get_root_requires() - .into_iter() - .map(PhpMixed::String) - .collect::<Vec<_>>(), - ) - { - return Err(InvalidArgumentException { - message: format!( - "Package \"{}\" is installed but not a direct dependent of the root package.", - pkg.get_name() - ), - code: 0, - } - .into()); - } - - if matched_package.is_none() { - let options = input.borrow().get_options(); - let mut hint = String::new(); - if input.borrow().get_option("locked")?.as_bool() == Some(true) { - hint.push_str(" in lock file"); - } - if let Some(working_dir) = options.get("working-dir").filter(|v| !v.is_null()) { - hint.push_str(&format!( - " in {}/composer.json", - working_dir.as_string().unwrap_or("") - )); - } - if PlatformRepository::is_platform_package(pf) - && input.borrow().get_option("platform")?.as_bool() != Some(true) - { - hint.push_str(", try using --platform (-p) to show platform packages"); - } - if input.borrow().get_option("all")?.as_bool() != Some(true) - && input.borrow().get_option("available")?.as_bool() != Some(true) - { - hint.push_str(", try using --available (-a) to show all available packages"); - } - - return Err(InvalidArgumentException { - message: format!("Package \"{}\" not found{}.", pf, hint), - code: 0, - } - .into()); - } - single_package = matched_package; - versions_map = vers; - } - - if let Some(ref package) = single_package { - // assert(isset($versions)); - - let mut exit_code: i64 = 0; - if input.borrow().get_option("tree")?.as_bool() == Some(true) { - let array_tree = - self.generate_package_tree(package.clone().into(), &installed_repo, &repos); - - if format == "json" { - let mut wrapper: IndexMap<String, PhpMixed> = IndexMap::new(); - wrapper.insert( - "installed".to_string(), - PhpMixed::List(vec![PhpMixed::Array(array_tree.into_iter().collect())]), - ); - self.get_io().write(&JsonFile::encode(&PhpMixed::Array( - wrapper.into_iter().collect(), - ))?); - } else { - self.display_package_tree(vec![array_tree]); - } - - return Ok(exit_code); - } - - let mut latest_package: Option<crate::package::PackageInterfaceHandle> = None; - if input.borrow().get_option("latest")?.as_bool() == Some(true) { - latest_package = self.find_latest_package( - package.clone().into(), - composer.as_ref().unwrap(), - &platform_repo, - input - .borrow() - .get_option("major-only")? - .as_bool() - .unwrap_or(false), - input - .borrow() - .get_option("minor-only")? - .as_bool() - .unwrap_or(false), - input - .borrow() - .get_option("patch-only")? - .as_bool() - .unwrap_or(false), - platform_req_filter.clone(), - )?; - } - if input.borrow().get_option("outdated")?.as_bool() == Some(true) - && input.borrow().get_option("strict")?.as_bool() == Some(true) - && latest_package.is_some() - && latest_package - .as_ref() - .unwrap() - .get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev) - != package - .get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev) - && (latest_package - .as_ref() - .unwrap() - .as_complete() - .is_none_or(|c| !c.is_abandoned())) - { - exit_code = 1; - } - if input.borrow().get_option("path")?.as_bool() == Some(true) { - self.get_io().write_no_newline(&package.get_name()); - let path = { - let composer_ref = composer.as_ref().unwrap(); - composer_ref - .borrow_partial() - .get_installation_manager() - .borrow_mut() - .get_install_path(package.clone().into()) - }; - if let Some(path) = path { - let real = realpath(&path).unwrap_or_default(); - let trimmed = real.split(['\r', '\n']).next().unwrap_or(""); - self.get_io().write(&format!(" {}", trimmed)); - } else { - self.get_io().write(" null"); - } - - return Ok(exit_code); - } - - if format == "json" { - self.print_package_info_as_json( - package.clone(), - &versions_map, - &mut *installed_repo.borrow_mut(), - latest_package, - )?; - } else { - self.print_package_info( - package.clone(), - &versions_map, - &mut *installed_repo.borrow_mut(), - latest_package, - )?; - } - - return Ok(exit_code); - } - - // show tree view if requested - if input.borrow().get_option("tree")?.as_bool() == Some(true) { - let root_requires = self.get_root_requires(); - let mut packages = installed_repo.get_packages()?; - packages.sort_by(|a, b| { - let sa: String = a.to_string(); - let sb: String = b.to_string(); - sa.cmp(&sb) - }); - let mut array_tree: Vec<IndexMap<String, PhpMixed>> = Vec::new(); - for package in packages.iter() { - if in_array_strict( - package.get_name(), - &root_requires - .iter() - .map(|s| PhpMixed::String(s.clone())) - .collect::<Vec<_>>(), - ) { - array_tree.push(self.generate_package_tree( - package.clone(), - &installed_repo, - &repos, - )); - } - } - - if format == "json" { - let mut wrapper: IndexMap<String, PhpMixed> = IndexMap::new(); - wrapper.insert( - "installed".to_string(), - PhpMixed::List( - array_tree - .into_iter() - .map(|m| PhpMixed::Array(m.into_iter().collect())) - .collect(), - ), - ); - self.get_io().write(&JsonFile::encode(&PhpMixed::Array( - wrapper.into_iter().collect(), - ))?); - } else { - self.display_package_tree(array_tree); - } - - return Ok(0); - } - - // list packages - let mut packages: IndexMap<String, IndexMap<String, PackageOrName>> = IndexMap::new(); - let mut package_filter_regex: Option<String> = None; - if let Some(ref pf) = package_filter { - let escaped = shirabe_php_shim::preg_quote(pf, None); - package_filter_regex = Some(format!("{{^{}$}}i", escaped.replace("\\*", ".*?"))); - } - - let mut package_list_filter: Option<Vec<String>> = None; - if input.borrow().get_option("direct")?.as_bool() == Some(true) { - package_list_filter = Some(self.get_root_requires()); - } - - if input.borrow().get_option("path")?.as_bool() == Some(true) && composer.is_none() { - self.get_io().write_error( - "No composer.json found in the current directory, disabling \"path\" option", - ); - input.borrow_mut().set_option("path", PhpMixed::Bool(false)); - } - - for repo in RepositoryUtils::flatten_repositories(repos, true) { - let r#type = if Self::same_repository(&repo, &platform_repo) { - "platform" - } else if locked_repo - .as_ref() - .is_some_and(|lr| Self::same_repository(&repo, lr)) - { - "locked" - } else if Self::same_repository(&repo, &installed_repo) - || installed_repo - .borrow() - .as_any() - .downcast_ref::<InstalledRepository>() - .is_some_and(|ir| ir.get_repositories().iter().any(|r| r.ptr_eq(&repo))) - { - "installed" - } else { - "available" - }; - let type_owned = r#type.to_string(); - if let Some(cr_rc) = repo.downcast_rc::<ComposerRepository>() { - let names = cr_rc - .borrow_mut() - .get_package_names(package_filter.as_deref())?; - for name in names { - packages - .entry(type_owned.clone()) - .or_default() - .insert(name.clone(), PackageOrName::Name(name)); - } - } else { - for package in repo.get_packages()? { - let existing = packages - .get(&type_owned) - .and_then(|m| m.get(&package.get_name())); - let need_replace = match existing { - None => true, - Some(PackageOrName::Name(_)) => true, - Some(PackageOrName::Pkg(existing)) => { - version_compare(&existing.get_version(), &package.get_version(), "<") - } - }; - if need_replace { - let mut p: crate::package::PackageInterfaceHandle = package.clone(); - while let Some(alias) = p.as_alias() { - p = alias.get_alias_of().into(); - } - let matches_filter = match &package_filter_regex { - None => true, - Some(r) => Preg::is_match(r, &p.get_name()), - }; - if matches_filter { - let matches_list = match &package_list_filter { - None => true, - Some(list) => in_array_strict( - p.get_name(), - &list - .iter() - .map(|s| PhpMixed::String(s.clone())) - .collect::<Vec<_>>(), - ), - }; - if matches_list { - packages - .entry(type_owned.clone()) - .or_default() - .insert(p.get_name(), PackageOrName::Pkg(p)); - } - } - } - } - if Self::same_repository(&repo, &platform_repo) { - for (name, p) in platform_repo.borrow().get_disabled_packages() { - packages - .entry(type_owned.clone()) - .or_default() - .insert(name.clone(), PackageOrName::Pkg(p.clone().into())); - } - } - } - } - - let show_all_types = input.borrow().get_option("all")?.as_bool() == Some(true); - let show_latest = input.borrow().get_option("latest")?.as_bool() == Some(true); - let show_major_only = input.borrow().get_option("major-only")?.as_bool() == Some(true); - let show_minor_only = input.borrow().get_option("minor-only")?.as_bool() == Some(true); - let show_patch_only = input.borrow().get_option("patch-only")?.as_bool() == Some(true); - let ignored_packages_regex = base_package::package_names_to_regexp( - &input - .borrow() - .get_option("ignore")? - .as_list() - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(strtolower)) - .collect::<Vec<_>>() - }) - .unwrap_or_default(), - "{^(?:%s)$}iD", - ); - let indent = if show_all_types { " " } else { "" }; - let mut latest_packages: IndexMap<String, crate::package::PackageInterfaceHandle> = - IndexMap::new(); - let mut exit_code: i64 = 0; - let mut view_data: IndexMap<String, Vec<IndexMap<String, PhpMixed>>> = IndexMap::new(); - let mut view_meta_data: IndexMap<String, ViewMetaData> = IndexMap::new(); - - let mut write_version = false; - let mut write_description = false; - - let type_order: Vec<(&str, bool)> = vec![ - ("platform", true), - ("locked", true), - ("available", false), - ("installed", true), - ]; - for (r#type, show_version) in type_order.iter() { - if let Some(type_packages) = packages.get_mut(*r#type) { - type_packages.sort_keys(); - - let mut name_length: usize = 0; - let mut version_length: usize = 0; - let mut latest_length: usize = 0; - let mut release_date_length: usize = 0; - - if show_latest && *show_version { - for package_or_name in type_packages.values() { - if let PackageOrName::Pkg(package) = package_or_name - && !Preg::is_match(&ignored_packages_regex, &package.get_pretty_name()) - { - let latest = self.find_latest_package( - package.clone(), - composer.as_ref().unwrap(), - &platform_repo, - show_major_only, - show_minor_only, - show_patch_only, - platform_req_filter.clone(), - )?; - if latest.is_none() { - continue; - } - - latest_packages.insert(package.get_pretty_name(), latest.unwrap()); - } - } - } - - let write_path = input.borrow().get_option("name-only")?.as_bool() != Some(true) - && input.borrow().get_option("path")?.as_bool() == Some(true); - write_version = input.borrow().get_option("name-only")?.as_bool() != Some(true) - && input.borrow().get_option("path")?.as_bool() != Some(true) - && *show_version; - let write_latest = write_version && show_latest; - write_description = input.borrow().get_option("name-only")?.as_bool() != Some(true) - && input.borrow().get_option("path")?.as_bool() != Some(true); - let write_release_date = write_latest - && (input.borrow().get_option("sort-by-age")?.as_bool() == Some(true) - || format == "json"); - - let mut has_outdated_packages = false; - - if input.borrow().get_option("sort-by-age")?.as_bool() == Some(true) { - type_packages.sort_by(|_ka, a, _kb, b| match (a, b) { - (PackageOrName::Pkg(a), PackageOrName::Pkg(b)) => { - a.get_release_date().cmp(&b.get_release_date()) - } - _ => std::cmp::Ordering::Equal, - }); - } - - let mut view_type: Vec<IndexMap<String, PhpMixed>> = Vec::new(); - for package_or_name in type_packages.values() { - let mut package_view_data: IndexMap<String, PhpMixed> = IndexMap::new(); - if let PackageOrName::Pkg(package) = package_or_name { - let latest_package = if show_latest - && latest_packages.contains_key(&package.get_pretty_name()) - { - latest_packages.get(&package.get_pretty_name()) - } else { - None - }; - - // Determine if Composer is checking outdated dependencies and if current package should trigger non-default exit code - let mut package_is_up_to_date = if let Some(latest) = latest_package { - latest.get_full_pretty_version( - true, - crate::package::DisplayMode::SourceRefIfDev, - ) == package.get_full_pretty_version( - true, - crate::package::DisplayMode::SourceRefIfDev, - ) && latest.as_complete().is_none_or(|c| !c.is_abandoned()) - } else { - false - }; - // When using --major-only, and no bigger version than current major is found then it is considered up to date - package_is_up_to_date = - package_is_up_to_date || (latest_package.is_none() && show_major_only); - let package_is_ignored = - Preg::is_match(&ignored_packages_regex, &package.get_pretty_name()); - if input.borrow().get_option("outdated")?.as_bool() == Some(true) - && (package_is_up_to_date || package_is_ignored) - { - continue; - } - - if input.borrow().get_option("outdated")?.as_bool() == Some(true) - || input.borrow().get_option("strict")?.as_bool() == Some(true) - { - has_outdated_packages = true; - } - - package_view_data.insert( - "name".to_string(), - PhpMixed::String(package.get_pretty_name()), - ); - package_view_data.insert( - "direct-dependency".to_string(), - PhpMixed::Bool(in_array_strict( - package.get_name(), - &self - .get_root_requires() - .into_iter() - .map(PhpMixed::String) - .collect::<Vec<_>>(), - )), - ); - if format != "json" - || input.borrow().get_option("name-only")?.as_bool() != Some(true) - { - package_view_data.insert( - "homepage".to_string(), - match package.as_complete() { - Some(c) => match c.get_homepage() { - Some(h) => PhpMixed::String(h), - None => PhpMixed::Null, - }, - None => PhpMixed::Null, - }, - ); - package_view_data.insert( - "source".to_string(), - match PackageInfo::get_view_source_url(package.clone()) { - Some(s) => PhpMixed::String(s), - None => PhpMixed::Null, - }, - ); - } - name_length = name_length.max(package.get_pretty_name().len()); - if write_version { - let mut version_str = package.get_full_pretty_version( - true, - crate::package::DisplayMode::SourceRefIfDev, - ); - if format == "text" { - version_str = version_str.trim_start_matches('v').to_string(); - } - version_length = version_length.max(version_str.len()); - package_view_data - .insert("version".to_string(), PhpMixed::String(version_str)); - } - if write_release_date { - if let Some(release_date) = package.get_release_date() { - let mut age = self - .get_relative_time(&release_date) - .replace(" ago", " old"); - if !age.contains(" old") { - age = format!("from {}", age); - } - release_date_length = release_date_length.max(age.len()); - package_view_data - .insert("release-age".to_string(), PhpMixed::String(age)); - package_view_data.insert( - "release-date".to_string(), - PhpMixed::String(release_date.format(DATE_ATOM).to_string()), - ); - } else { - package_view_data.insert( - "release-age".to_string(), - PhpMixed::String(String::new()), - ); - package_view_data.insert( - "release-date".to_string(), - PhpMixed::String(String::new()), - ); - } - } - if write_latest && let Some(latest) = latest_package { - let mut latest_version_str = latest.get_full_pretty_version( - true, - crate::package::DisplayMode::SourceRefIfDev, - ); - if format == "text" { - latest_version_str = - latest_version_str.trim_start_matches('v').to_string(); - } - let update_status = - Self::get_update_status(latest.clone(), package.clone())?; - latest_length = latest_length.max(latest_version_str.len()); - package_view_data - .insert("latest".to_string(), PhpMixed::String(latest_version_str)); - package_view_data.insert( - "latest-status".to_string(), - PhpMixed::String(update_status), - ); - - if let Some(rd) = latest.get_release_date() { - package_view_data.insert( - "latest-release-date".to_string(), - PhpMixed::String(rd.format(DATE_ATOM).to_string()), - ); - } else { - package_view_data.insert( - "latest-release-date".to_string(), - PhpMixed::String(String::new()), - ); - } - } else if write_latest { - package_view_data.insert( - "latest".to_string(), - PhpMixed::String("[none matched]".to_string()), - ); - package_view_data.insert( - "latest-status".to_string(), - PhpMixed::String("up-to-date".to_string()), - ); - latest_length = latest_length.max("[none matched]".len()); - } - if write_description && let Some(c) = package.as_complete() { - package_view_data.insert( - "description".to_string(), - match c.get_description() { - Some(d) => PhpMixed::String(d), - None => PhpMixed::Null, - }, - ); - } - if write_path { - let installation_manager = composer - .as_ref() - .unwrap() - .borrow_partial() - .get_installation_manager(); - let path: Option<String> = installation_manager - .borrow_mut() - .get_install_path(package.clone()); - if let Some(p) = path { - let r = realpath(&p).unwrap_or_default(); - let trimmed = r.split(['\r', '\n']).next().unwrap_or(""); - package_view_data.insert( - "path".to_string(), - PhpMixed::String(trimmed.to_string()), - ); - } else { - package_view_data.insert("path".to_string(), PhpMixed::Null); - } - } - - let mut package_is_abandoned: PhpMixed = PhpMixed::Bool(false); - if let Some(latest) = latest_package - && let Some(c) = latest.as_complete() - && c.is_abandoned() - { - let replacement_package_name = c.get_replacement_package(); - let replacement = if let Some(ref rp) = replacement_package_name { - format!("Use {} instead", rp) - } else { - "No replacement was suggested".to_string() - }; - let package_warning = format!( - "Package {} is abandoned, you should avoid using it. {}.", - package.get_pretty_name(), - replacement - ); - package_view_data - .insert("warning".to_string(), PhpMixed::String(package_warning)); - package_is_abandoned = match replacement_package_name { - Some(rp) => PhpMixed::String(rp), - None => PhpMixed::Bool(true), - }; - } - - package_view_data.insert("abandoned".to_string(), package_is_abandoned); - } else if let PackageOrName::Name(name) = package_or_name { - package_view_data - .insert("name".to_string(), PhpMixed::String(name.clone())); - name_length = name_length.max(name.len()); - } - view_type.push(package_view_data); - } - view_data.insert(r#type.to_string(), view_type); - view_meta_data.insert( - r#type.to_string(), - ViewMetaData { - name_length, - version_length, - latest_length, - release_date_length, - write_latest, - write_release_date, - }, - ); - if input.borrow().get_option("strict")?.as_bool() == Some(true) - && has_outdated_packages - { - exit_code = 1; - break; - } - } - } - - if format == "json" { - let mut json_map: IndexMap<String, PhpMixed> = IndexMap::new(); - for (k, v) in view_data.iter() { - json_map.insert( - k.clone(), - PhpMixed::List( - v.iter() - .map(|m| { - PhpMixed::Array( - m.iter().map(|(k, v)| (k.clone(), v.clone())).collect(), - ) - }) - .collect(), - ), - ); - } - let io = self.get_io(); - io.write(&JsonFile::encode(&PhpMixed::Array( - json_map.into_iter().collect(), - ))?); - } else { - if input.borrow().get_option("latest")?.as_bool() == Some(true) - && view_data.values().any(|v| !v.is_empty()) - { - let io = self.get_io(); - if !io.is_decorated() { - io.write_error("Legend:"); - io.write_error("! patch or minor release available - update recommended"); - io.write_error("~ major release available - update possible"); - if input.borrow().get_option("outdated")?.as_bool() != Some(true) { - io.write_error("= up to date version"); - } - } else { - io.write_error("<info>Color legend:</info>"); - io.write_error("- <highlight>patch or minor</highlight> release available - update recommended"); - io.write_error( - "- <comment>major</comment> release available - update possible", - ); - if input.borrow().get_option("outdated")?.as_bool() != Some(true) { - io.write_error("- <info>up to date</info> version"); - } - } - } - - let width = self.get_terminal_width(); - - for (r#type, packages) in view_data.iter() { - let meta = match view_meta_data.get(r#type) { - Some(m) => m.clone(), - None => continue, - }; - let name_length = meta.name_length; - let version_length = meta.version_length; - let mut latest_length = meta.latest_length; - let release_date_length = meta.release_date_length; - let write_latest = meta.write_latest; - let write_release_date = meta.write_release_date; - - let width_usize = width as usize; - let version_fits = name_length + version_length + 3 <= width_usize; - let latest_fits = name_length + version_length + latest_length + 3 <= width_usize; - let release_date_fits = - name_length + version_length + latest_length + release_date_length + 3 - <= width_usize; - let description_fits = - name_length + version_length + latest_length + release_date_length + 24 - <= width_usize; - - if latest_fits && !self.get_io().is_decorated() { - latest_length += 2; - } - - if show_all_types { - if r#type == "available" { - self.get_io() - .write(&format!("<comment>{}</comment>:", r#type)); - } else { - self.get_io().write(&format!("<info>{}</info>:", r#type)); - } - } - - if write_latest && input.borrow().get_option("direct")?.as_bool() != Some(true) { - let mut direct_deps: Vec<IndexMap<String, PhpMixed>> = Vec::new(); - let mut transitive_deps: Vec<IndexMap<String, PhpMixed>> = Vec::new(); - for pkg in packages.iter() { - let is_direct = pkg - .get("direct-dependency") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - if is_direct { - direct_deps.push(pkg.clone()); - } else { - transitive_deps.push(pkg.clone()); - } - } - - self.get_io().write_error(""); - self.get_io() - .write_error("<info>Direct dependencies required in composer.json:</>"); - if !direct_deps.is_empty() { - self.print_packages( - &direct_deps, - indent, - write_version && version_fits, - latest_fits, - write_description && description_fits, - width_usize, - version_length, - name_length, - latest_length, - write_release_date && release_date_fits, - release_date_length, - ); - } else { - self.get_io().write_error("Everything up to date"); - } - self.get_io().write_error(""); - self.get_io().write_error( - "<info>Transitive dependencies not required in composer.json:</>", - ); - if !transitive_deps.is_empty() { - self.print_packages( - &transitive_deps, - indent, - write_version && version_fits, - latest_fits, - write_description && description_fits, - width_usize, - version_length, - name_length, - latest_length, - write_release_date && release_date_fits, - release_date_length, - ); - } else { - self.get_io().write_error("Everything up to date"); - } - } else { - if write_latest && packages.is_empty() { - self.get_io() - .write_error("All your direct dependencies are up to date"); - } else { - self.print_packages( - packages, - indent, - write_version && version_fits, - write_latest && latest_fits, - write_description && description_fits, - width_usize, - version_length, - name_length, - latest_length, - write_release_date && release_date_fits, - release_date_length, - ); - } - } - - if show_all_types { - self.get_io().write(""); - } - } - } - - Ok(exit_code) - } - - fn initialize( - &self, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<()> { - base_command_initialize(self, input, output) - } - - fn complete( - &self, - input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, - suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, - ) -> anyhow::Result<()> { - crate::command::base_command::base_command_complete(self, input, suggestions) - } - - shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); -} - -impl BaseCommand for ShowCommand { - fn base_command_data(&self) -> &crate::command::BaseCommandData { - &self.base_command_data - } - - crate::delegate_base_command_trait_impls_to_inner!(base_command_data); -} -impl ShowCommand { /// PHP: protected function suggestPackageBasedOnMode(): \Closure pub(crate) fn suggest_package_based_on_mode(&self) -> crate::console::input::SuggestedValues { crate::console::input::SuggestedValues::Closure(Box::new(|this, input, suggestions| { @@ -2908,6 +1533,1379 @@ impl ShowCommand { } } +impl Command for ShowCommand { + fn configure(&self) -> anyhow::Result<()> { + self.set_name("show")?; + self.set_aliases(vec!["info".to_string()])?; + self.set_description("Shows information about packages"); + let opt_none = |name: &str, shortcut: Option<&str>, description: &str| { + InputOption::new( + name, + shortcut.map(|s| PhpMixed::String(s.to_string())), + Some(InputOption::VALUE_NONE), + description, + None, + ) + .unwrap() + .into() + }; + self.set_definition(&[ + InputArgument::new5( + "package", + Some(InputArgument::OPTIONAL), + "Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.", + None, + self.suggest_package_based_on_mode(), + ) + .unwrap() + .into(), + InputArgument::new( + "version", + Some(InputArgument::OPTIONAL), + "Version or version constraint to inspect", + None, + ) + .unwrap() + .into(), + opt_none("all", None, "List all packages"), + opt_none("locked", None, "List all locked packages"), + opt_none( + "installed", + Some("i"), + "List installed packages only (enabled by default, only present for BC).", + ), + opt_none("platform", Some("p"), "List platform packages only"), + opt_none("available", Some("a"), "List available packages only"), + opt_none("self", Some("s"), "Show the root package information"), + opt_none("name-only", Some("N"), "List package names only"), + opt_none("path", Some("P"), "Show package paths"), + opt_none("tree", Some("t"), "List the dependencies as a tree"), + opt_none("latest", Some("l"), "Show the latest version"), + opt_none( + "outdated", + Some("o"), + "Show the latest version but only for packages that are outdated", + ), + InputOption::new6( + "ignore", + None, + Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), + "Ignore specified package(s). Can contain wildcards (*). Use it with the --outdated option if you don't want to be informed about new versions of some packages.", + None, + self.suggest_installed_package(false, false), + ) + .unwrap() + .into(), + opt_none( + "major-only", + Some("M"), + "Show only packages that have major SemVer-compatible updates. Use with the --latest or --outdated option.", + ), + opt_none( + "minor-only", + Some("m"), + "Show only packages that have minor SemVer-compatible updates. Use with the --latest or --outdated option.", + ), + opt_none( + "patch-only", + None, + "Show only packages that have patch SemVer-compatible updates. Use with the --latest or --outdated option.", + ), + opt_none( + "sort-by-age", + Some("A"), + "Displays the installed version's age, and sorts packages oldest first. Use with the --latest or --outdated option.", + ), + opt_none( + "direct", + Some("D"), + "Shows only packages that are directly required by the root package", + ), + opt_none( + "strict", + None, + "Return a non-zero exit code when there are outdated packages", + ), + InputOption::new6( + "format", + Some(PhpMixed::String("f".to_string())), + Some(InputOption::VALUE_REQUIRED), + "Format of the output: text or json", + Some(PhpMixed::String("text".to_string())), + SuggestedValues::List(vec!["json".to_string(), "text".to_string()]), + ) + .unwrap() + .into(), + opt_none("no-dev", None, "Disables search in require-dev packages."), + 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(), + opt_none( + "ignore-platform-reqs", + None, + "Ignore all platform requirements (php & ext- packages). Use with the --outdated option", + ), + ]); + 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(()) + } + + fn execute( + &self, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + *self.version_parser.borrow_mut() = VersionParser::new(); + if input.borrow().get_option("tree")?.as_bool() == Some(true) { + self.init_styles(output.clone()); + } + + let composer = self.try_composer(None, None); + + if input.borrow().get_option("installed")?.as_bool() == Some(true) + && input.borrow().get_option("self")?.as_bool() != Some(true) + { + self.get_io().write_error("<warning>You are using the deprecated option \"installed\". Only installed packages are shown by default now. The --all option can be used to show all packages.</warning>"); + } + + if input.borrow().get_option("outdated")?.as_bool() == Some(true) { + input + .borrow_mut() + .set_option("latest", PhpMixed::Bool(true)); + } else if input + .borrow() + .get_option("ignore")? + .as_list() + .map_or(0, |l| l.len()) + > 0 + { + self.get_io().write_error("<warning>You are using the option \"ignore\" for action other than \"outdated\", it will be ignored.</warning>"); + } + + if input.borrow().get_option("direct")?.as_bool() == Some(true) + && (input.borrow().get_option("all")?.as_bool() == Some(true) + || input.borrow().get_option("available")?.as_bool() == Some(true) + || input.borrow().get_option("platform")?.as_bool() == Some(true)) + { + self.get_io().write_error("The --direct (-D) option is not usable in combination with --all, --platform (-p) or --available (-a)"); + + return Ok(1); + } + + if input.borrow().get_option("tree")?.as_bool() == Some(true) + && (input.borrow().get_option("all")?.as_bool() == Some(true) + || input.borrow().get_option("available")?.as_bool() == Some(true)) + { + self.get_io().write_error("The --tree (-t) option is not usable in combination with --all or --available (-a)"); + + return Ok(1); + } + + let only_count: usize = [ + input.borrow().get_option("patch-only")?.as_bool() == Some(true), + input.borrow().get_option("minor-only")?.as_bool() == Some(true), + input.borrow().get_option("major-only")?.as_bool() == Some(true), + ] + .iter() + .filter(|b| **b) + .count(); + if only_count > 1 { + self.get_io().write_error( + "Only one of --major-only, --minor-only or --patch-only can be used at once", + ); + + return Ok(1); + } + + if input.borrow().get_option("tree")?.as_bool() == Some(true) + && input.borrow().get_option("latest")?.as_bool() == Some(true) + { + self.get_io().write_error( + "The --tree (-t) option is not usable in combination with --latest (-l)", + ); + + return Ok(1); + } + + if input.borrow().get_option("tree")?.as_bool() == Some(true) + && input.borrow().get_option("path")?.as_bool() == Some(true) + { + self.get_io().write_error( + "The --tree (-t) option is not usable in combination with --path (-P)", + ); + + return Ok(1); + } + + let format = input + .borrow() + .get_option("format")? + .as_string() + .unwrap_or("text") + .to_string(); + if !in_array_loose( + format.clone(), + &[ + PhpMixed::String("text".to_string()), + PhpMixed::String("json".to_string()), + ], + ) { + self.get_io().write_error(&format!( + "Unsupported format \"{}\". See help for supported formats.", + format + )); + + return Ok(1); + } + + let platform_req_filter = self.get_platform_requirement_filter(input.clone())?; + + // init repos + let mut platform_overrides: IndexMap<String, PhpMixed> = IndexMap::new(); + if let Some(ref composer) = composer { + let composer = crate::composer::composer_full(composer); + if let Some(p) = composer + .get_config() + .borrow() + .get("platform") + .as_array() + .cloned() + { + platform_overrides = p.into_iter().collect(); + } + } + let platform_repo = + PlatformRepositoryHandle::new(PlatformRepository::new(vec![], platform_overrides)?); + let mut locked_repo: Option<RepositoryInterfaceHandle> = None; + + // The single-package $package binding from PHP gets surfaced here. + let mut single_package: Option<crate::package::CompletePackageInterfaceHandle> = None; + let mut versions_map: IndexMap<String, String> = IndexMap::new(); + let installed_repo: RepositoryInterfaceHandle; + let repos: RepositoryInterfaceHandle; + + if input.borrow().get_option("self")?.as_bool() == Some(true) + && input.borrow().get_option("installed")?.as_bool() != Some(true) + && input.borrow().get_option("locked")?.as_bool() != Some(true) + { + let composer = self.require_composer(None, None)?; + let package = crate::package::RootPackageInterfaceHandle::dup( + composer.borrow_partial().get_package(), + ); + if input.borrow().get_option("name-only")?.as_bool() == Some(true) { + self.get_io().write(&package.get_name()); + + return Ok(0); + } + if input + .borrow() + .get_argument("package")? + .as_string() + .is_some() + { + return Err(InvalidArgumentException { + message: "You cannot use --self together with a package name".to_string(), + code: 0, + } + .into()); + } + installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ + RepositoryInterfaceHandle::new(RootPackageRepository::new(package.clone())), + ])); + repos = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ + RepositoryInterfaceHandle::new(RootPackageRepository::new(package.clone())), + ])); + single_package = Some(package.into()); + } else if input.borrow().get_option("platform")?.as_bool() == Some(true) { + installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ + platform_repo.clone().into(), + ])); + repos = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ + platform_repo.clone().into(), + ])); + } else if input.borrow().get_option("available")?.as_bool() == Some(true) { + let mut ir = InstalledRepository::new(vec![platform_repo.clone().into()]); + if let Some(ref composer) = composer { + let composer = crate::composer::composer_full(composer); + repos = RepositoryInterfaceHandle::new(CompositeRepository::new( + composer + .get_repository_manager() + .borrow() + .get_repositories() + .to_vec(), + )); + ir.add_repository( + composer + .get_repository_manager() + .borrow() + .get_local_repository(), + ); + installed_repo = RepositoryInterfaceHandle::new(ir); + } else { + let default_repos = + RepositoryFactory::default_repos_with_default_manager(self.get_io())?; + let names: Vec<String> = default_repos.keys().cloned().collect(); + repos = RepositoryInterfaceHandle::new(CompositeRepository::new( + default_repos.into_values().collect(), + )); + self.get_io().write_error(&format!( + "No composer.json found in the current directory, showing available packages from {}", + names.join(", ") + )); + installed_repo = RepositoryInterfaceHandle::new(ir); + } + } else if input.borrow().get_option("all")?.as_bool() == Some(true) && composer.is_some() { + let composer_ref = crate::composer::composer_full(composer.as_ref().unwrap()); + let local_repo = composer_ref + .get_repository_manager() + .borrow() + .get_local_repository(); + let locker_rc = composer_ref.get_locker().clone(); + let mut locker = locker_rc.borrow_mut(); + if locker.is_locked() { + let lr_handle: RepositoryInterfaceHandle = + locker.get_locked_repository(true)?.into(); + installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ + lr_handle.clone(), + local_repo, + platform_repo.clone().into(), + ])); + locked_repo = Some(lr_handle); + } else { + installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ + local_repo, + platform_repo.clone().into(), + ])); + } + let mut composite_input: Vec<RepositoryInterfaceHandle> = + vec![RepositoryInterfaceHandle::new(FilterRepository::new( + installed_repo.clone(), + { + let mut m = IndexMap::new(); + m.insert("canonical".to_string(), PhpMixed::Bool(false)); + m + }, + )?)]; + for r in composer_ref + .get_repository_manager() + .borrow() + .get_repositories() + { + composite_input.push(r.clone()); + } + repos = RepositoryInterfaceHandle::new(CompositeRepository::new(composite_input)); + } else if input.borrow().get_option("all")?.as_bool() == Some(true) { + let default_repos = + RepositoryFactory::default_repos_with_default_manager(self.get_io())?; + let names: Vec<String> = default_repos.keys().cloned().collect(); + self.get_io().write_error(&format!( + "No composer.json found in the current directory, showing available packages from {}", + names.join(", ") + )); + installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ + platform_repo.clone().into(), + ])); + let mut composite_input: Vec<RepositoryInterfaceHandle> = vec![installed_repo.clone()]; + for (_k, v) in default_repos.into_iter() { + composite_input.push(v); + } + repos = RepositoryInterfaceHandle::new(CompositeRepository::new(composite_input)); + } else if input.borrow().get_option("locked")?.as_bool() == Some(true) { + if composer.is_none() + || !crate::composer::composer_full(composer.as_ref().unwrap()) + .get_locker() + .borrow_mut() + .is_locked() + { + return Err(UnexpectedValueException { + message: "A valid composer.json and composer.lock files is required to run this command with --locked".to_string(), + code: 0, + } + .into()); + } + let composer_ref = crate::composer::composer_full(composer.as_ref().unwrap()); + let locker_rc = composer_ref.get_locker().clone(); + let mut locker = locker_rc.borrow_mut(); + let lr = locker.get_locked_repository( + input.borrow().get_option("no-dev")?.as_bool() != Some(true), + )?; + if input.borrow().get_option("self")?.as_bool() == Some(true) { + lr.add_package( + crate::package::RootPackageInterfaceHandle::dup(composer_ref.get_package()) + .into(), + )?; + } + let lr_handle: RepositoryInterfaceHandle = lr.into(); + locked_repo = Some(lr_handle.clone()); + let new_repo = + RepositoryInterfaceHandle::new(InstalledRepository::new(vec![lr_handle])); + installed_repo = new_repo.clone(); + repos = new_repo; + } else { + // --installed / default case + let composer_local_owned; + let _guard_from_existing; + let composer_local = match composer.as_ref() { + Some(c) => { + _guard_from_existing = crate::composer::composer_full(c); + &*_guard_from_existing + } + None => { + composer_local_owned = self.require_composer(None, None)?; + _guard_from_existing = crate::composer::composer_full(&composer_local_owned); + &*_guard_from_existing + } + }; + let root_pkg = composer_local.get_package(); + + let root_repo: RepositoryInterfaceHandle = + if input.borrow().get_option("self")?.as_bool() == Some(true) { + RepositoryInterfaceHandle::new(RootPackageRepository::new( + crate::package::RootPackageInterfaceHandle::dup( + composer_local.get_package(), + ), + )) + } else { + RepositoryInterfaceHandle::new(InstalledArrayRepository::new()?) + }; + if input.borrow().get_option("no-dev")?.as_bool() == Some(true) { + let local_packages = composer_local + .get_repository_manager() + .borrow() + .get_local_repository() + .get_packages()?; + let packages = RepositoryUtils::filter_required_packages( + &local_packages, + root_pkg.clone().into(), + false, + Vec::new(), + ); + let cloned: Vec<crate::package::PackageInterfaceHandle> = packages + .iter() + .map(crate::package::PackageInterfaceHandle::dup) + .collect(); + let new_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ + root_repo, + RepositoryInterfaceHandle::new(InstalledArrayRepository::new_with_packages( + cloned, + )?), + ])); + installed_repo = new_repo.clone(); + repos = new_repo; + } else { + let repository_manager = composer_local.get_repository_manager().clone(); + let repository_manager = repository_manager.borrow(); + let lr = repository_manager.get_local_repository(); + installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![ + root_repo.clone(), + lr.clone(), + ])); + repos = + RepositoryInterfaceHandle::new(InstalledRepository::new(vec![root_repo, lr])); + } + + if installed_repo.get_packages()?.is_empty() { + let has_non_platform_reqs = |reqs: &IndexMap<String, Link>| -> bool { + reqs.keys() + .any(|name| !PlatformRepository::is_platform_package(name)) + }; + + if has_non_platform_reqs(&root_pkg.get_requires()) + || has_non_platform_reqs(&root_pkg.get_dev_requires()) + { + // Borrow is local; release composer_local borrow first. + let _ = root_pkg; + self.get_io().write_error("<warning>No dependencies installed. Try running composer install or update.</warning>"); + } + } + } + + if let Some(ref composer) = composer { + let composer = crate::composer::composer_full(composer); + let mut command_event = CommandEvent::new6( + PluginEvents::COMMAND, + "show", + input.clone(), + output, + vec![], + IndexMap::new(), + ); + let command_event_name = command_event.get_name().to_string(); + composer + .get_event_dispatcher() + .borrow_mut() + .dispatch(Some(&command_event_name), Some(&mut command_event))?; + } + + if input.borrow().get_option("latest")?.as_bool() == Some(true) && composer.is_none() { + self.get_io().write_error( + "No composer.json found in the current directory, disabling \"latest\" option", + ); + input + .borrow_mut() + .set_option("latest", PhpMixed::Bool(false)); + } + + let package_filter: Option<String> = input + .borrow() + .get_argument("package")? + .as_string() + .map(|s| s.to_string()); + + // show single package or single version + if let Some(ref pkg) = single_package { + versions_map.insert(pkg.get_pretty_version(), pkg.get_version()); + } else if let Some(ref pf) = package_filter + && !pf.contains('*') + { + let (matched_package, vers) = self.get_package( + &installed_repo, + &repos, + pf, + input.borrow().get_argument("version")?, + )?; + + if let Some(ref pkg) = matched_package + && input.borrow().get_option("direct")?.as_bool() == Some(true) + && !in_array_strict( + pkg.get_name(), + &self + .get_root_requires() + .into_iter() + .map(PhpMixed::String) + .collect::<Vec<_>>(), + ) + { + return Err(InvalidArgumentException { + message: format!( + "Package \"{}\" is installed but not a direct dependent of the root package.", + pkg.get_name() + ), + code: 0, + } + .into()); + } + + if matched_package.is_none() { + let options = input.borrow().get_options(); + let mut hint = String::new(); + if input.borrow().get_option("locked")?.as_bool() == Some(true) { + hint.push_str(" in lock file"); + } + if let Some(working_dir) = options.get("working-dir").filter(|v| !v.is_null()) { + hint.push_str(&format!( + " in {}/composer.json", + working_dir.as_string().unwrap_or("") + )); + } + if PlatformRepository::is_platform_package(pf) + && input.borrow().get_option("platform")?.as_bool() != Some(true) + { + hint.push_str(", try using --platform (-p) to show platform packages"); + } + if input.borrow().get_option("all")?.as_bool() != Some(true) + && input.borrow().get_option("available")?.as_bool() != Some(true) + { + hint.push_str(", try using --available (-a) to show all available packages"); + } + + return Err(InvalidArgumentException { + message: format!("Package \"{}\" not found{}.", pf, hint), + code: 0, + } + .into()); + } + single_package = matched_package; + versions_map = vers; + } + + if let Some(ref package) = single_package { + // assert(isset($versions)); + + let mut exit_code: i64 = 0; + if input.borrow().get_option("tree")?.as_bool() == Some(true) { + let array_tree = + self.generate_package_tree(package.clone().into(), &installed_repo, &repos); + + if format == "json" { + let mut wrapper: IndexMap<String, PhpMixed> = IndexMap::new(); + wrapper.insert( + "installed".to_string(), + PhpMixed::List(vec![PhpMixed::Array(array_tree.into_iter().collect())]), + ); + self.get_io().write(&JsonFile::encode(&PhpMixed::Array( + wrapper.into_iter().collect(), + ))?); + } else { + self.display_package_tree(vec![array_tree]); + } + + return Ok(exit_code); + } + + let mut latest_package: Option<crate::package::PackageInterfaceHandle> = None; + if input.borrow().get_option("latest")?.as_bool() == Some(true) { + latest_package = self.find_latest_package( + package.clone().into(), + composer.as_ref().unwrap(), + &platform_repo, + input + .borrow() + .get_option("major-only")? + .as_bool() + .unwrap_or(false), + input + .borrow() + .get_option("minor-only")? + .as_bool() + .unwrap_or(false), + input + .borrow() + .get_option("patch-only")? + .as_bool() + .unwrap_or(false), + platform_req_filter.clone(), + )?; + } + if input.borrow().get_option("outdated")?.as_bool() == Some(true) + && input.borrow().get_option("strict")?.as_bool() == Some(true) + && latest_package.is_some() + && latest_package + .as_ref() + .unwrap() + .get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev) + != package + .get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev) + && (latest_package + .as_ref() + .unwrap() + .as_complete() + .is_none_or(|c| !c.is_abandoned())) + { + exit_code = 1; + } + if input.borrow().get_option("path")?.as_bool() == Some(true) { + self.get_io().write_no_newline(&package.get_name()); + let path = { + let composer_ref = composer.as_ref().unwrap(); + composer_ref + .borrow_partial() + .get_installation_manager() + .borrow_mut() + .get_install_path(package.clone().into()) + }; + if let Some(path) = path { + let real = realpath(&path).unwrap_or_default(); + let trimmed = real.split(['\r', '\n']).next().unwrap_or(""); + self.get_io().write(&format!(" {}", trimmed)); + } else { + self.get_io().write(" null"); + } + + return Ok(exit_code); + } + + if format == "json" { + self.print_package_info_as_json( + package.clone(), + &versions_map, + &mut *installed_repo.borrow_mut(), + latest_package, + )?; + } else { + self.print_package_info( + package.clone(), + &versions_map, + &mut *installed_repo.borrow_mut(), + latest_package, + )?; + } + + return Ok(exit_code); + } + + // show tree view if requested + if input.borrow().get_option("tree")?.as_bool() == Some(true) { + let root_requires = self.get_root_requires(); + let mut packages = installed_repo.get_packages()?; + packages.sort_by(|a, b| { + let sa: String = a.to_string(); + let sb: String = b.to_string(); + sa.cmp(&sb) + }); + let mut array_tree: Vec<IndexMap<String, PhpMixed>> = Vec::new(); + for package in packages.iter() { + if in_array_strict( + package.get_name(), + &root_requires + .iter() + .map(|s| PhpMixed::String(s.clone())) + .collect::<Vec<_>>(), + ) { + array_tree.push(self.generate_package_tree( + package.clone(), + &installed_repo, + &repos, + )); + } + } + + if format == "json" { + let mut wrapper: IndexMap<String, PhpMixed> = IndexMap::new(); + wrapper.insert( + "installed".to_string(), + PhpMixed::List( + array_tree + .into_iter() + .map(|m| PhpMixed::Array(m.into_iter().collect())) + .collect(), + ), + ); + self.get_io().write(&JsonFile::encode(&PhpMixed::Array( + wrapper.into_iter().collect(), + ))?); + } else { + self.display_package_tree(array_tree); + } + + return Ok(0); + } + + // list packages + let mut packages: IndexMap<String, IndexMap<String, PackageOrName>> = IndexMap::new(); + let mut package_filter_regex: Option<String> = None; + if let Some(ref pf) = package_filter { + let escaped = shirabe_php_shim::preg_quote(pf, None); + package_filter_regex = Some(format!("{{^{}$}}i", escaped.replace("\\*", ".*?"))); + } + + let mut package_list_filter: Option<Vec<String>> = None; + if input.borrow().get_option("direct")?.as_bool() == Some(true) { + package_list_filter = Some(self.get_root_requires()); + } + + if input.borrow().get_option("path")?.as_bool() == Some(true) && composer.is_none() { + self.get_io().write_error( + "No composer.json found in the current directory, disabling \"path\" option", + ); + input.borrow_mut().set_option("path", PhpMixed::Bool(false)); + } + + for repo in RepositoryUtils::flatten_repositories(repos, true) { + let r#type = if Self::same_repository(&repo, &platform_repo) { + "platform" + } else if locked_repo + .as_ref() + .is_some_and(|lr| Self::same_repository(&repo, lr)) + { + "locked" + } else if Self::same_repository(&repo, &installed_repo) + || installed_repo + .borrow() + .as_any() + .downcast_ref::<InstalledRepository>() + .is_some_and(|ir| ir.get_repositories().iter().any(|r| r.ptr_eq(&repo))) + { + "installed" + } else { + "available" + }; + let type_owned = r#type.to_string(); + if let Some(cr_rc) = repo.downcast_rc::<ComposerRepository>() { + let names = cr_rc + .borrow_mut() + .get_package_names(package_filter.as_deref())?; + for name in names { + packages + .entry(type_owned.clone()) + .or_default() + .insert(name.clone(), PackageOrName::Name(name)); + } + } else { + for package in repo.get_packages()? { + let existing = packages + .get(&type_owned) + .and_then(|m| m.get(&package.get_name())); + let need_replace = match existing { + None => true, + Some(PackageOrName::Name(_)) => true, + Some(PackageOrName::Pkg(existing)) => { + version_compare(&existing.get_version(), &package.get_version(), "<") + } + }; + if need_replace { + let mut p: crate::package::PackageInterfaceHandle = package.clone(); + while let Some(alias) = p.as_alias() { + p = alias.get_alias_of().into(); + } + let matches_filter = match &package_filter_regex { + None => true, + Some(r) => Preg::is_match(r, &p.get_name()), + }; + if matches_filter { + let matches_list = match &package_list_filter { + None => true, + Some(list) => in_array_strict( + p.get_name(), + &list + .iter() + .map(|s| PhpMixed::String(s.clone())) + .collect::<Vec<_>>(), + ), + }; + if matches_list { + packages + .entry(type_owned.clone()) + .or_default() + .insert(p.get_name(), PackageOrName::Pkg(p)); + } + } + } + } + if Self::same_repository(&repo, &platform_repo) { + for (name, p) in platform_repo.borrow().get_disabled_packages() { + packages + .entry(type_owned.clone()) + .or_default() + .insert(name.clone(), PackageOrName::Pkg(p.clone().into())); + } + } + } + } + + let show_all_types = input.borrow().get_option("all")?.as_bool() == Some(true); + let show_latest = input.borrow().get_option("latest")?.as_bool() == Some(true); + let show_major_only = input.borrow().get_option("major-only")?.as_bool() == Some(true); + let show_minor_only = input.borrow().get_option("minor-only")?.as_bool() == Some(true); + let show_patch_only = input.borrow().get_option("patch-only")?.as_bool() == Some(true); + let ignored_packages_regex = base_package::package_names_to_regexp( + &input + .borrow() + .get_option("ignore")? + .as_list() + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(strtolower)) + .collect::<Vec<_>>() + }) + .unwrap_or_default(), + "{^(?:%s)$}iD", + ); + let indent = if show_all_types { " " } else { "" }; + let mut latest_packages: IndexMap<String, crate::package::PackageInterfaceHandle> = + IndexMap::new(); + let mut exit_code: i64 = 0; + let mut view_data: IndexMap<String, Vec<IndexMap<String, PhpMixed>>> = IndexMap::new(); + let mut view_meta_data: IndexMap<String, ViewMetaData> = IndexMap::new(); + + let mut write_version = false; + let mut write_description = false; + + let type_order: Vec<(&str, bool)> = vec![ + ("platform", true), + ("locked", true), + ("available", false), + ("installed", true), + ]; + for (r#type, show_version) in type_order.iter() { + if let Some(type_packages) = packages.get_mut(*r#type) { + type_packages.sort_keys(); + + let mut name_length: usize = 0; + let mut version_length: usize = 0; + let mut latest_length: usize = 0; + let mut release_date_length: usize = 0; + + if show_latest && *show_version { + for package_or_name in type_packages.values() { + if let PackageOrName::Pkg(package) = package_or_name + && !Preg::is_match(&ignored_packages_regex, &package.get_pretty_name()) + { + let latest = self.find_latest_package( + package.clone(), + composer.as_ref().unwrap(), + &platform_repo, + show_major_only, + show_minor_only, + show_patch_only, + platform_req_filter.clone(), + )?; + if latest.is_none() { + continue; + } + + latest_packages.insert(package.get_pretty_name(), latest.unwrap()); + } + } + } + + let write_path = input.borrow().get_option("name-only")?.as_bool() != Some(true) + && input.borrow().get_option("path")?.as_bool() == Some(true); + write_version = input.borrow().get_option("name-only")?.as_bool() != Some(true) + && input.borrow().get_option("path")?.as_bool() != Some(true) + && *show_version; + let write_latest = write_version && show_latest; + write_description = input.borrow().get_option("name-only")?.as_bool() != Some(true) + && input.borrow().get_option("path")?.as_bool() != Some(true); + let write_release_date = write_latest + && (input.borrow().get_option("sort-by-age")?.as_bool() == Some(true) + || format == "json"); + + let mut has_outdated_packages = false; + + if input.borrow().get_option("sort-by-age")?.as_bool() == Some(true) { + type_packages.sort_by(|_ka, a, _kb, b| match (a, b) { + (PackageOrName::Pkg(a), PackageOrName::Pkg(b)) => { + a.get_release_date().cmp(&b.get_release_date()) + } + _ => std::cmp::Ordering::Equal, + }); + } + + let mut view_type: Vec<IndexMap<String, PhpMixed>> = Vec::new(); + for package_or_name in type_packages.values() { + let mut package_view_data: IndexMap<String, PhpMixed> = IndexMap::new(); + if let PackageOrName::Pkg(package) = package_or_name { + let latest_package = if show_latest + && latest_packages.contains_key(&package.get_pretty_name()) + { + latest_packages.get(&package.get_pretty_name()) + } else { + None + }; + + // Determine if Composer is checking outdated dependencies and if current package should trigger non-default exit code + let mut package_is_up_to_date = if let Some(latest) = latest_package { + latest.get_full_pretty_version( + true, + crate::package::DisplayMode::SourceRefIfDev, + ) == package.get_full_pretty_version( + true, + crate::package::DisplayMode::SourceRefIfDev, + ) && latest.as_complete().is_none_or(|c| !c.is_abandoned()) + } else { + false + }; + // When using --major-only, and no bigger version than current major is found then it is considered up to date + package_is_up_to_date = + package_is_up_to_date || (latest_package.is_none() && show_major_only); + let package_is_ignored = + Preg::is_match(&ignored_packages_regex, &package.get_pretty_name()); + if input.borrow().get_option("outdated")?.as_bool() == Some(true) + && (package_is_up_to_date || package_is_ignored) + { + continue; + } + + if input.borrow().get_option("outdated")?.as_bool() == Some(true) + || input.borrow().get_option("strict")?.as_bool() == Some(true) + { + has_outdated_packages = true; + } + + package_view_data.insert( + "name".to_string(), + PhpMixed::String(package.get_pretty_name()), + ); + package_view_data.insert( + "direct-dependency".to_string(), + PhpMixed::Bool(in_array_strict( + package.get_name(), + &self + .get_root_requires() + .into_iter() + .map(PhpMixed::String) + .collect::<Vec<_>>(), + )), + ); + if format != "json" + || input.borrow().get_option("name-only")?.as_bool() != Some(true) + { + package_view_data.insert( + "homepage".to_string(), + match package.as_complete() { + Some(c) => match c.get_homepage() { + Some(h) => PhpMixed::String(h), + None => PhpMixed::Null, + }, + None => PhpMixed::Null, + }, + ); + package_view_data.insert( + "source".to_string(), + match PackageInfo::get_view_source_url(package.clone()) { + Some(s) => PhpMixed::String(s), + None => PhpMixed::Null, + }, + ); + } + name_length = name_length.max(package.get_pretty_name().len()); + if write_version { + let mut version_str = package.get_full_pretty_version( + true, + crate::package::DisplayMode::SourceRefIfDev, + ); + if format == "text" { + version_str = version_str.trim_start_matches('v').to_string(); + } + version_length = version_length.max(version_str.len()); + package_view_data + .insert("version".to_string(), PhpMixed::String(version_str)); + } + if write_release_date { + if let Some(release_date) = package.get_release_date() { + let mut age = self + .get_relative_time(&release_date) + .replace(" ago", " old"); + if !age.contains(" old") { + age = format!("from {}", age); + } + release_date_length = release_date_length.max(age.len()); + package_view_data + .insert("release-age".to_string(), PhpMixed::String(age)); + package_view_data.insert( + "release-date".to_string(), + PhpMixed::String(release_date.format(DATE_ATOM).to_string()), + ); + } else { + package_view_data.insert( + "release-age".to_string(), + PhpMixed::String(String::new()), + ); + package_view_data.insert( + "release-date".to_string(), + PhpMixed::String(String::new()), + ); + } + } + if write_latest && let Some(latest) = latest_package { + let mut latest_version_str = latest.get_full_pretty_version( + true, + crate::package::DisplayMode::SourceRefIfDev, + ); + if format == "text" { + latest_version_str = + latest_version_str.trim_start_matches('v').to_string(); + } + let update_status = + Self::get_update_status(latest.clone(), package.clone())?; + latest_length = latest_length.max(latest_version_str.len()); + package_view_data + .insert("latest".to_string(), PhpMixed::String(latest_version_str)); + package_view_data.insert( + "latest-status".to_string(), + PhpMixed::String(update_status), + ); + + if let Some(rd) = latest.get_release_date() { + package_view_data.insert( + "latest-release-date".to_string(), + PhpMixed::String(rd.format(DATE_ATOM).to_string()), + ); + } else { + package_view_data.insert( + "latest-release-date".to_string(), + PhpMixed::String(String::new()), + ); + } + } else if write_latest { + package_view_data.insert( + "latest".to_string(), + PhpMixed::String("[none matched]".to_string()), + ); + package_view_data.insert( + "latest-status".to_string(), + PhpMixed::String("up-to-date".to_string()), + ); + latest_length = latest_length.max("[none matched]".len()); + } + if write_description && let Some(c) = package.as_complete() { + package_view_data.insert( + "description".to_string(), + match c.get_description() { + Some(d) => PhpMixed::String(d), + None => PhpMixed::Null, + }, + ); + } + if write_path { + let installation_manager = composer + .as_ref() + .unwrap() + .borrow_partial() + .get_installation_manager(); + let path: Option<String> = installation_manager + .borrow_mut() + .get_install_path(package.clone()); + if let Some(p) = path { + let r = realpath(&p).unwrap_or_default(); + let trimmed = r.split(['\r', '\n']).next().unwrap_or(""); + package_view_data.insert( + "path".to_string(), + PhpMixed::String(trimmed.to_string()), + ); + } else { + package_view_data.insert("path".to_string(), PhpMixed::Null); + } + } + + let mut package_is_abandoned: PhpMixed = PhpMixed::Bool(false); + if let Some(latest) = latest_package + && let Some(c) = latest.as_complete() + && c.is_abandoned() + { + let replacement_package_name = c.get_replacement_package(); + let replacement = if let Some(ref rp) = replacement_package_name { + format!("Use {} instead", rp) + } else { + "No replacement was suggested".to_string() + }; + let package_warning = format!( + "Package {} is abandoned, you should avoid using it. {}.", + package.get_pretty_name(), + replacement + ); + package_view_data + .insert("warning".to_string(), PhpMixed::String(package_warning)); + package_is_abandoned = match replacement_package_name { + Some(rp) => PhpMixed::String(rp), + None => PhpMixed::Bool(true), + }; + } + + package_view_data.insert("abandoned".to_string(), package_is_abandoned); + } else if let PackageOrName::Name(name) = package_or_name { + package_view_data + .insert("name".to_string(), PhpMixed::String(name.clone())); + name_length = name_length.max(name.len()); + } + view_type.push(package_view_data); + } + view_data.insert(r#type.to_string(), view_type); + view_meta_data.insert( + r#type.to_string(), + ViewMetaData { + name_length, + version_length, + latest_length, + release_date_length, + write_latest, + write_release_date, + }, + ); + if input.borrow().get_option("strict")?.as_bool() == Some(true) + && has_outdated_packages + { + exit_code = 1; + break; + } + } + } + + if format == "json" { + let mut json_map: IndexMap<String, PhpMixed> = IndexMap::new(); + for (k, v) in view_data.iter() { + json_map.insert( + k.clone(), + PhpMixed::List( + v.iter() + .map(|m| { + PhpMixed::Array( + m.iter().map(|(k, v)| (k.clone(), v.clone())).collect(), + ) + }) + .collect(), + ), + ); + } + let io = self.get_io(); + io.write(&JsonFile::encode(&PhpMixed::Array( + json_map.into_iter().collect(), + ))?); + } else { + if input.borrow().get_option("latest")?.as_bool() == Some(true) + && view_data.values().any(|v| !v.is_empty()) + { + let io = self.get_io(); + if !io.is_decorated() { + io.write_error("Legend:"); + io.write_error("! patch or minor release available - update recommended"); + io.write_error("~ major release available - update possible"); + if input.borrow().get_option("outdated")?.as_bool() != Some(true) { + io.write_error("= up to date version"); + } + } else { + io.write_error("<info>Color legend:</info>"); + io.write_error("- <highlight>patch or minor</highlight> release available - update recommended"); + io.write_error( + "- <comment>major</comment> release available - update possible", + ); + if input.borrow().get_option("outdated")?.as_bool() != Some(true) { + io.write_error("- <info>up to date</info> version"); + } + } + } + + let width = self.get_terminal_width(); + + for (r#type, packages) in view_data.iter() { + let meta = match view_meta_data.get(r#type) { + Some(m) => m.clone(), + None => continue, + }; + let name_length = meta.name_length; + let version_length = meta.version_length; + let mut latest_length = meta.latest_length; + let release_date_length = meta.release_date_length; + let write_latest = meta.write_latest; + let write_release_date = meta.write_release_date; + + let width_usize = width as usize; + let version_fits = name_length + version_length + 3 <= width_usize; + let latest_fits = name_length + version_length + latest_length + 3 <= width_usize; + let release_date_fits = + name_length + version_length + latest_length + release_date_length + 3 + <= width_usize; + let description_fits = + name_length + version_length + latest_length + release_date_length + 24 + <= width_usize; + + if latest_fits && !self.get_io().is_decorated() { + latest_length += 2; + } + + if show_all_types { + if r#type == "available" { + self.get_io() + .write(&format!("<comment>{}</comment>:", r#type)); + } else { + self.get_io().write(&format!("<info>{}</info>:", r#type)); + } + } + + if write_latest && input.borrow().get_option("direct")?.as_bool() != Some(true) { + let mut direct_deps: Vec<IndexMap<String, PhpMixed>> = Vec::new(); + let mut transitive_deps: Vec<IndexMap<String, PhpMixed>> = Vec::new(); + for pkg in packages.iter() { + let is_direct = pkg + .get("direct-dependency") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + if is_direct { + direct_deps.push(pkg.clone()); + } else { + transitive_deps.push(pkg.clone()); + } + } + + self.get_io().write_error(""); + self.get_io() + .write_error("<info>Direct dependencies required in composer.json:</>"); + if !direct_deps.is_empty() { + self.print_packages( + &direct_deps, + indent, + write_version && version_fits, + latest_fits, + write_description && description_fits, + width_usize, + version_length, + name_length, + latest_length, + write_release_date && release_date_fits, + release_date_length, + ); + } else { + self.get_io().write_error("Everything up to date"); + } + self.get_io().write_error(""); + self.get_io().write_error( + "<info>Transitive dependencies not required in composer.json:</>", + ); + if !transitive_deps.is_empty() { + self.print_packages( + &transitive_deps, + indent, + write_version && version_fits, + latest_fits, + write_description && description_fits, + width_usize, + version_length, + name_length, + latest_length, + write_release_date && release_date_fits, + release_date_length, + ); + } else { + self.get_io().write_error("Everything up to date"); + } + } else { + if write_latest && packages.is_empty() { + self.get_io() + .write_error("All your direct dependencies are up to date"); + } else { + self.print_packages( + packages, + indent, + write_version && version_fits, + write_latest && latest_fits, + write_description && description_fits, + width_usize, + version_length, + name_length, + latest_length, + write_release_date && release_date_fits, + release_date_length, + ); + } + } + + if show_all_types { + self.get_io().write(""); + } + } + } + + Ok(exit_code) + } + + fn initialize( + &self, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) + } + + fn complete( + &self, + input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, + suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, + ) -> anyhow::Result<()> { + crate::command::base_command::base_command_complete(self, input, suggestions) + } + + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} + +impl BaseCommand for ShowCommand { + fn base_command_data(&self) -> &crate::command::BaseCommandData { + &self.base_command_data + } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); +} + #[derive(Debug)] pub enum PackageOrName { Pkg(crate::package::PackageInterfaceHandle), diff --git a/crates/shirabe/src/command/update_command.rs b/crates/shirabe/src/command/update_command.rs index 1885e8eb..c928552a 100644 --- a/crates/shirabe/src/command/update_command.rs +++ b/crates/shirabe/src/command/update_command.rs @@ -62,6 +62,202 @@ impl UpdateCommand { .expect("UpdateCommand::configure uses static, valid metadata"); command } + + fn get_packages_interactively( + &self, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + composer: &PartialComposerHandle, + packages: Vec<String>, + ) -> anyhow::Result<Vec<String>> { + if !input.borrow().is_interactive() { + return Err(InvalidArgumentException { + message: "--interactive cannot be used in non-interactive terminals.".to_string(), + code: 0, + } + .into()); + } + + let composer_ref = crate::composer::composer_full(composer); + let platform_req_filter = self.get_platform_requirement_filter(input); + let stability_flags = composer_ref.get_package().get_stability_flags(); + let requires = array_merge_map( + composer_ref.get_package().get_requires(), + composer_ref.get_package().get_dev_requires(), + ); + + let filter: Option<String> = if !packages.is_empty() { + Some(base_package::package_names_to_regexp(&packages, "%s")) + } else { + None + }; + + io.write_error3( + "<info>Loading packages that can be updated...</info>", + true, + io_interface::NORMAL, + ); + let mut autocompleter_values: IndexMap<String, String> = IndexMap::new(); + let installed_packages: Vec<crate::package::PackageInterfaceHandle> = + if composer_ref.get_locker().borrow_mut().is_locked() { + let locked_repo = composer_ref + .get_locker() + .borrow_mut() + .get_locked_repository(true)?; + locked_repo.borrow_mut().get_canonical_packages()? + } else { + composer_ref + .get_repository_manager() + .borrow() + .get_local_repository() + .get_packages()? + }; + let mut version_selector = self.create_version_selector(composer)?; + for package in &installed_packages { + if let Some(filter) = &filter + && !Preg::is_match(filter, &package.get_name()) + { + continue; + } + let current_version = package.get_pretty_version(); + let constraint = requires + .get(&package.get_name()) + .map(|link| link.get_pretty_constraint()); + let stability = match stability_flags.get(&package.get_name()) { + Some(flag) => base_package::STABILITIES + .iter() + .find(|&(_, v)| v == flag) + .map(|(k, _)| k.to_string()) + .unwrap_or_default(), + None => composer_ref.get_package().get_minimum_stability(), + }; + let latest_version = version_selector.find_best_candidate( + &package.get_name(), + constraint, + &stability, + None, + 0, + None, + ShowWarnings::Always, + )?; + let _ = &platform_req_filter; + if let Some(latest) = latest_version + && (package.get_version() != latest.get_version() || latest.is_dev()) + { + autocompleter_values.insert( + package.get_name(), + format!( + "<comment>{}</comment> => <comment>{}</comment>", + current_version, + latest.get_pretty_version(), + ), + ); + } + } + if installed_packages.is_empty() { + for (req, _constraint) in &requires { + if PlatformRepository::is_platform_package(req) { + continue; + } + autocompleter_values.insert(req.to_string(), String::new()); + } + } + + if autocompleter_values.is_empty() { + return Err(RuntimeException { + message: "Could not find any package with new versions available".to_string(), + code: 0, + } + .into()); + } + + let select_result = io.select( + "Select packages: (Select more than one value separated by comma) ".to_string(), + PhpMixed::Array( + autocompleter_values + .iter() + .map(|(k, v)| (k.clone(), PhpMixed::String(v.clone()))) + .collect(), + ), + PhpMixed::Bool(false), + PhpMixed::Int(1), + "No package named \"%s\" is installed.".to_string(), + true, + )?; + let packages: Vec<String> = match select_result { + PhpMixed::List(l) => l + .into_iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect(), + _ => Vec::new(), + }; + + let mut table = Table::new(output); + table.set_headers(vec!["Selected packages".into()]); + for package in &packages { + table.add_row(PhpMixed::List(vec![PhpMixed::String(package.clone())]).into()); + } + table.render(); + + if io.ask_confirmation( + format!( + "Would you like to continue and update the above package{} [<comment>yes</comment>]? ", + if 1 == packages.len() { "" } else { "s" }, + ), + true, + ) { + return Ok(packages); + } + + Err(RuntimeException { + message: "Installation aborted.".to_string(), + code: 0, + } + .into()) + } + + fn create_version_selector( + &self, + composer: &PartialComposerHandle, + ) -> anyhow::Result<VersionSelector> { + let composer = crate::composer::composer_full(composer); + let root_aliases: Vec<crate::repository::RootAliasInput> = composer + .get_package() + .get_aliases() + .into_iter() + .map(|alias| crate::repository::RootAliasInput { + package: alias.get("package").cloned().unwrap_or_default(), + version: alias.get("version").cloned().unwrap_or_default(), + alias: alias.get("alias").cloned().unwrap_or_default(), + alias_normalized: alias.get("alias_normalized").cloned().unwrap_or_default(), + }) + .collect(); + let mut repository_set = RepositorySet::new( + &composer.get_package().get_minimum_stability(), + composer.get_package().get_stability_flags(), + root_aliases, + composer.get_package().get_references(), + IndexMap::new(), + IndexMap::new(), + ); + let repositories: Vec<crate::repository::RepositoryInterfaceHandle> = composer + .get_repository_manager() + .borrow() + .get_repositories() + .iter() + .filter(|repository| !repository.is::<PlatformRepository>()) + .cloned() + .collect(); + repository_set.add_repository(crate::repository::RepositoryInterfaceHandle::new( + CompositeRepository::new(repositories), + ))?; + + VersionSelector::new( + std::rc::Rc::new(std::cell::RefCell::new(repository_set)), + None, + ) + } } impl Command for UpdateCommand { @@ -591,201 +787,3 @@ impl BaseCommand for UpdateCommand { crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } - -impl UpdateCommand { - fn get_packages_interactively( - &self, - io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, - input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - composer: &PartialComposerHandle, - packages: Vec<String>, - ) -> anyhow::Result<Vec<String>> { - if !input.borrow().is_interactive() { - return Err(InvalidArgumentException { - message: "--interactive cannot be used in non-interactive terminals.".to_string(), - code: 0, - } - .into()); - } - - let composer_ref = crate::composer::composer_full(composer); - let platform_req_filter = self.get_platform_requirement_filter(input); - let stability_flags = composer_ref.get_package().get_stability_flags(); - let requires = array_merge_map( - composer_ref.get_package().get_requires(), - composer_ref.get_package().get_dev_requires(), - ); - - let filter: Option<String> = if !packages.is_empty() { - Some(base_package::package_names_to_regexp(&packages, "%s")) - } else { - None - }; - - io.write_error3( - "<info>Loading packages that can be updated...</info>", - true, - io_interface::NORMAL, - ); - let mut autocompleter_values: IndexMap<String, String> = IndexMap::new(); - let installed_packages: Vec<crate::package::PackageInterfaceHandle> = - if composer_ref.get_locker().borrow_mut().is_locked() { - let locked_repo = composer_ref - .get_locker() - .borrow_mut() - .get_locked_repository(true)?; - locked_repo.borrow_mut().get_canonical_packages()? - } else { - composer_ref - .get_repository_manager() - .borrow() - .get_local_repository() - .get_packages()? - }; - let mut version_selector = self.create_version_selector(composer)?; - for package in &installed_packages { - if let Some(filter) = &filter - && !Preg::is_match(filter, &package.get_name()) - { - continue; - } - let current_version = package.get_pretty_version(); - let constraint = requires - .get(&package.get_name()) - .map(|link| link.get_pretty_constraint()); - let stability = match stability_flags.get(&package.get_name()) { - Some(flag) => base_package::STABILITIES - .iter() - .find(|&(_, v)| v == flag) - .map(|(k, _)| k.to_string()) - .unwrap_or_default(), - None => composer_ref.get_package().get_minimum_stability(), - }; - let latest_version = version_selector.find_best_candidate( - &package.get_name(), - constraint, - &stability, - None, - 0, - None, - ShowWarnings::Always, - )?; - let _ = &platform_req_filter; - if let Some(latest) = latest_version - && (package.get_version() != latest.get_version() || latest.is_dev()) - { - autocompleter_values.insert( - package.get_name(), - format!( - "<comment>{}</comment> => <comment>{}</comment>", - current_version, - latest.get_pretty_version(), - ), - ); - } - } - if installed_packages.is_empty() { - for (req, _constraint) in &requires { - if PlatformRepository::is_platform_package(req) { - continue; - } - autocompleter_values.insert(req.to_string(), String::new()); - } - } - - if autocompleter_values.is_empty() { - return Err(RuntimeException { - message: "Could not find any package with new versions available".to_string(), - code: 0, - } - .into()); - } - - let select_result = io.select( - "Select packages: (Select more than one value separated by comma) ".to_string(), - PhpMixed::Array( - autocompleter_values - .iter() - .map(|(k, v)| (k.clone(), PhpMixed::String(v.clone()))) - .collect(), - ), - PhpMixed::Bool(false), - PhpMixed::Int(1), - "No package named \"%s\" is installed.".to_string(), - true, - )?; - let packages: Vec<String> = match select_result { - PhpMixed::List(l) => l - .into_iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect(), - _ => Vec::new(), - }; - - let mut table = Table::new(output); - table.set_headers(vec!["Selected packages".into()]); - for package in &packages { - table.add_row(PhpMixed::List(vec![PhpMixed::String(package.clone())]).into()); - } - table.render(); - - if io.ask_confirmation( - format!( - "Would you like to continue and update the above package{} [<comment>yes</comment>]? ", - if 1 == packages.len() { "" } else { "s" }, - ), - true, - ) { - return Ok(packages); - } - - Err(RuntimeException { - message: "Installation aborted.".to_string(), - code: 0, - } - .into()) - } - - fn create_version_selector( - &self, - composer: &PartialComposerHandle, - ) -> anyhow::Result<VersionSelector> { - let composer = crate::composer::composer_full(composer); - let root_aliases: Vec<crate::repository::RootAliasInput> = composer - .get_package() - .get_aliases() - .into_iter() - .map(|alias| crate::repository::RootAliasInput { - package: alias.get("package").cloned().unwrap_or_default(), - version: alias.get("version").cloned().unwrap_or_default(), - alias: alias.get("alias").cloned().unwrap_or_default(), - alias_normalized: alias.get("alias_normalized").cloned().unwrap_or_default(), - }) - .collect(); - let mut repository_set = RepositorySet::new( - &composer.get_package().get_minimum_stability(), - composer.get_package().get_stability_flags(), - root_aliases, - composer.get_package().get_references(), - IndexMap::new(), - IndexMap::new(), - ); - let repositories: Vec<crate::repository::RepositoryInterfaceHandle> = composer - .get_repository_manager() - .borrow() - .get_repositories() - .iter() - .filter(|repository| !repository.is::<PlatformRepository>()) - .cloned() - .collect(); - repository_set.add_repository(crate::repository::RepositoryInterfaceHandle::new( - CompositeRepository::new(repositories), - ))?; - - VersionSelector::new( - std::rc::Rc::new(std::cell::RefCell::new(repository_set)), - None, - ) - } -} diff --git a/crates/shirabe/src/command/validate_command.rs b/crates/shirabe/src/command/validate_command.rs index 0a89b71d..c663c868 100644 --- a/crates/shirabe/src/command/validate_command.rs +++ b/crates/shirabe/src/command/validate_command.rs @@ -41,6 +41,100 @@ impl ValidateCommand { .expect("ValidateCommand::configure uses static, valid metadata"); command } + + #[allow(clippy::too_many_arguments, reason = "to keep PHP signature")] + fn output_result( + &self, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + name: &str, + errors: &mut Vec<String>, + warnings: &mut Vec<String>, + check_publish: bool, + publish_errors: &mut Vec<String>, + check_lock: bool, + lock_errors: &mut Vec<String>, + print_schema_url: bool, + ) { + let mut do_print_schema_url = false; + + if !errors.is_empty() { + io.write_error(&format!( + "<error>{} is invalid, the following errors/warnings were found:</error>", + name + )); + } else if !publish_errors.is_empty() && check_publish { + io.write_error(&format!( + "<info>{} is valid for simple usage with Composer but has</info>", + name + )); + io.write_error( + "<info>strict errors that make it unable to be published as a package</info>", + ); + do_print_schema_url = print_schema_url; + } else if !warnings.is_empty() { + io.write_error(&format!( + "<info>{} is valid, but with a few warnings</info>", + name + )); + do_print_schema_url = print_schema_url; + } else if !lock_errors.is_empty() { + io.write(&format!( + "<info>{} is valid but your composer.lock has some {}</info>", + name, + if check_lock { "errors" } else { "warnings" } + )); + } else { + io.write(&format!("<info>{} is valid</info>", name)); + } + + if do_print_schema_url { + io.write_error("<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>"); + } + + if !errors.is_empty() { + *errors = errors.iter().map(|e| format!("- {}", e)).collect(); + errors.insert(0, "# General errors".to_string()); + } + if !warnings.is_empty() { + *warnings = warnings.iter().map(|w| format!("- {}", w)).collect(); + warnings.insert(0, "# General warnings".to_string()); + } + + let mut extra_warnings: Vec<String> = vec![]; + + if !publish_errors.is_empty() && check_publish { + *publish_errors = publish_errors.iter().map(|e| format!("- {}", e)).collect(); + publish_errors.insert(0, "# Publish errors".to_string()); + errors.append(publish_errors); + } + + if !lock_errors.is_empty() { + if check_lock { + lock_errors.insert(0, "# Lock file errors".to_string()); + errors.append(lock_errors); + } else { + lock_errors.insert(0, "# Lock file warnings".to_string()); + extra_warnings.append(lock_errors); + } + } + + let all_warnings: Vec<String> = warnings.iter().cloned().chain(extra_warnings).collect(); + + for msg in errors.iter() { + if msg.starts_with('#') { + io.write_error(&format!("<error>{}</error>", msg)); + } else { + io.write_error(msg); + } + } + for msg in &all_warnings { + if msg.starts_with('#') { + io.write_error(&format!("<warning>{}</warning>", msg)); + } else { + io.write_error(msg); + } + } + } } impl Command for ValidateCommand { @@ -328,99 +422,3 @@ impl BaseCommand for ValidateCommand { crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } - -impl ValidateCommand { - #[allow(clippy::too_many_arguments, reason = "to keep PHP signature")] - fn output_result( - &self, - io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, - name: &str, - errors: &mut Vec<String>, - warnings: &mut Vec<String>, - check_publish: bool, - publish_errors: &mut Vec<String>, - check_lock: bool, - lock_errors: &mut Vec<String>, - print_schema_url: bool, - ) { - let mut do_print_schema_url = false; - - if !errors.is_empty() { - io.write_error(&format!( - "<error>{} is invalid, the following errors/warnings were found:</error>", - name - )); - } else if !publish_errors.is_empty() && check_publish { - io.write_error(&format!( - "<info>{} is valid for simple usage with Composer but has</info>", - name - )); - io.write_error( - "<info>strict errors that make it unable to be published as a package</info>", - ); - do_print_schema_url = print_schema_url; - } else if !warnings.is_empty() { - io.write_error(&format!( - "<info>{} is valid, but with a few warnings</info>", - name - )); - do_print_schema_url = print_schema_url; - } else if !lock_errors.is_empty() { - io.write(&format!( - "<info>{} is valid but your composer.lock has some {}</info>", - name, - if check_lock { "errors" } else { "warnings" } - )); - } else { - io.write(&format!("<info>{} is valid</info>", name)); - } - - if do_print_schema_url { - io.write_error("<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>"); - } - - if !errors.is_empty() { - *errors = errors.iter().map(|e| format!("- {}", e)).collect(); - errors.insert(0, "# General errors".to_string()); - } - if !warnings.is_empty() { - *warnings = warnings.iter().map(|w| format!("- {}", w)).collect(); - warnings.insert(0, "# General warnings".to_string()); - } - - let mut extra_warnings: Vec<String> = vec![]; - - if !publish_errors.is_empty() && check_publish { - *publish_errors = publish_errors.iter().map(|e| format!("- {}", e)).collect(); - publish_errors.insert(0, "# Publish errors".to_string()); - errors.append(publish_errors); - } - - if !lock_errors.is_empty() { - if check_lock { - lock_errors.insert(0, "# Lock file errors".to_string()); - errors.append(lock_errors); - } else { - lock_errors.insert(0, "# Lock file warnings".to_string()); - extra_warnings.append(lock_errors); - } - } - - let all_warnings: Vec<String> = warnings.iter().cloned().chain(extra_warnings).collect(); - - for msg in errors.iter() { - if msg.starts_with('#') { - io.write_error(&format!("<error>{}</error>", msg)); - } else { - io.write_error(msg); - } - } - for msg in &all_warnings { - if msg.starts_with('#') { - io.write_error(&format!("<warning>{}</warning>", msg)); - } else { - io.write_error(msg); - } - } - } -} diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs index 4136a830..3ba6a324 100644 --- a/crates/shirabe/src/console/application.rs +++ b/crates/shirabe/src/console/application.rs @@ -2616,9 +2616,7 @@ impl ApplicationHandle { .borrow_mut() .get_composer(required, disable_plugins, disable_scripts) } -} -impl ApplicationHandle { /// Runs the current application (Symfony base; `parent::run`). pub fn base_run( &self, diff --git a/crates/shirabe/src/dependency_resolver/request.rs b/crates/shirabe/src/dependency_resolver/request.rs index 0a05f3e4..d78900c0 100644 --- a/crates/shirabe/src/dependency_resolver/request.rs +++ b/crates/shirabe/src/dependency_resolver/request.rs @@ -25,33 +25,7 @@ impl Request { pub const UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE: i64 = UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; pub const UPDATE_LISTED_WITH_TRANSITIVE_DEPS: i64 = UPDATE_LISTED_WITH_TRANSITIVE_DEPS; -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum UpdateAllowTransitiveDeps { - /// Corresponds to PHP false. - False, - /// \Composer\DependencyResolver\Request::UPDATE_ONLY_LISTED - UpdateOnlyListed, - /// \Composer\DependencyResolver\Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE - UpdateListedWithTransitiveDepsNoRootRequire, - /// \Composer\DependencyResolver\Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS - UpdateListedWithTransitiveDeps, -} - -#[derive(Debug)] -pub struct Request { - pub(crate) locked_repository: Option<LockArrayRepositoryHandle>, - pub(crate) requires: IndexMap<String, AnyConstraint>, - pub(crate) fixed_packages: IndexMap<String, BasePackageHandle>, - pub(crate) locked_packages: IndexMap<String, BasePackageHandle>, - pub(crate) fixed_locked_packages: IndexMap<String, BasePackageHandle>, - pub(crate) update_allow_list: Vec<String>, - pub(crate) update_allow_transitive_dependencies: UpdateAllowTransitiveDeps, - restrict_packages: Option<Vec<String>>, -} -impl Request { pub fn new(locked_repository: Option<LockArrayRepositoryHandle>) -> Self { Self { locked_repository, @@ -245,3 +219,27 @@ impl Request { self.restrict_packages.as_ref() } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UpdateAllowTransitiveDeps { + /// Corresponds to PHP false. + False, + /// \Composer\DependencyResolver\Request::UPDATE_ONLY_LISTED + UpdateOnlyListed, + /// \Composer\DependencyResolver\Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE + UpdateListedWithTransitiveDepsNoRootRequire, + /// \Composer\DependencyResolver\Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS + UpdateListedWithTransitiveDeps, +} + +#[derive(Debug)] +pub struct Request { + pub(crate) locked_repository: Option<LockArrayRepositoryHandle>, + pub(crate) requires: IndexMap<String, AnyConstraint>, + pub(crate) fixed_packages: IndexMap<String, BasePackageHandle>, + pub(crate) locked_packages: IndexMap<String, BasePackageHandle>, + pub(crate) fixed_locked_packages: IndexMap<String, BasePackageHandle>, + pub(crate) update_allow_list: Vec<String>, + pub(crate) update_allow_transitive_dependencies: UpdateAllowTransitiveDeps, + restrict_packages: Option<Vec<String>>, +} diff --git a/crates/shirabe/src/downloader/file_downloader.rs b/crates/shirabe/src/downloader/file_downloader.rs index d3b08b36..cdcf750e 100644 --- a/crates/shirabe/src/downloader/file_downloader.rs +++ b/crates/shirabe/src/downloader/file_downloader.rs @@ -150,6 +150,234 @@ impl FileDownloader { this } + + /// Shared body of `ChangeReportInterface::get_local_changes`. + /// + /// PHP's `getLocalChanges` calls `$this->download()` / `$this->install()`, which late-bind + /// to the concrete downloader class (e.g. `ArchiveDownloader::install` extracts the archive + /// instead of copying the dist file). The Rust port embeds the parent class as `inner`, so + /// delegating downloaders must pass themselves as `this` to preserve that dispatch. + pub(crate) fn base_get_local_changes( + &self, + this: &dyn DownloaderInterface, + package: PackageInterfaceHandle, + path: &str, + ) -> anyhow::Result<Option<String>> { + let prev_io = std::mem::replace( + &mut *self.io.borrow_mut(), + std::rc::Rc::new(std::cell::RefCell::new(NullIO::new())), + ); + self.io + .borrow() + .borrow_mut() + .load_configuration(&mut self.config.borrow_mut())?; + + let target_dir = Filesystem::trim_trailing_slash(path); + // PHP attaches an onRejected handler to capture the error and drives the promise via + // httpDownloader->wait() / process->wait(); the single-threaded sync bridge block_on's the + // download/install futures, so a rejection surfaces directly as the Err captured below. + let result: anyhow::Result<String> = (|| -> anyhow::Result<String> { + if is_dir(format!("{}_compare", target_dir)) { + self.filesystem + .borrow_mut() + .remove_directory(format!("{}_compare", target_dir))?; + } + + sync_executor::block_on(this.download( + package.clone(), + &format!("{}_compare", target_dir), + None, + false, + ))?; + sync_executor::block_on(this.install( + package.clone(), + &format!("{}_compare", target_dir), + false, + ))?; + + let mut comparer = Comparer::new(); + comparer.set_source(format!("{}_compare", target_dir)); + comparer.set_update(target_dir.clone()); + comparer.do_compare(); + let output = comparer.get_changed_as_string(true, false); + self.filesystem + .borrow_mut() + .remove_directory(format!("{}_compare", target_dir))?; + Ok(output) + })(); + + *self.io.borrow_mut() = prev_io; + + let (e, output) = match result { + Ok(output) => (None, output), + Err(err) => (Some(err), String::new()), + }; + + if let Some(err) = e { + if self.io.borrow().is_debug() { + return Err(err); + } + + return Ok(Some(format!( + "Failed to detect changes: [{}] {}", + get_class(&PhpMixed::Null), + err + ))); + } + + let output = trim(&output, None); + + Ok(if strlen(&output) > 0 { + Some(output) + } else { + None + }) + } + + /// Shared body of `DownloaderInterface::update`; see `base_get_local_changes` for why the + /// concrete downloader is threaded in as `this`. The appendix is computed by the caller + /// because `getInstallOperationAppendix` is protected and not part of `DownloaderInterface`. + pub(crate) async fn base_update( + &self, + this: &dyn DownloaderInterface, + install_operation_appendix: &str, + initial: PackageInterfaceHandle, + target: PackageInterfaceHandle, + path: &str, + ) -> anyhow::Result<Option<PhpMixed>> { + self.io.borrow().write_error(&format!( + " - {}{}", + UpdateOperation::format(initial.clone(), target.clone(), false), + install_operation_appendix + )); + + // PHP: return $this->remove($initial, $path, false)->then(fn () => $this->install($target, $path, false)); + let _ = this.remove(initial, path, false).await?; + this.install(target, path, false).await + } + + fn get_dist_path(&self, package: PackageInterfaceHandle, component: i64) -> String { + pathinfo( + parse_url( + &strtr(&package.get_dist_url().unwrap_or_default(), "\\", "/"), + PHP_URL_PATH, + ) + .as_string() + .unwrap_or(""), + component, + ) + } + + pub(crate) fn clear_last_cache_write(&self, package: PackageInterfaceHandle) { + let mut last_cache_writes = self.last_cache_writes.lock().unwrap(); + if let Some(cache) = &self.cache + && last_cache_writes.contains_key(&package.get_name()) + { + let key = last_cache_writes.get(&package.get_name()).unwrap().clone(); + cache.borrow_mut().remove(&key); + last_cache_writes.shift_remove(&package.get_name()); + } + } + + pub(crate) fn add_cleanup_path(&self, package: PackageInterfaceHandle, path: &str) { + self.additional_cleanup_paths + .borrow_mut() + .entry(package.get_name()) + .or_default() + .push(path.to_string()); + } + + pub(crate) fn remove_cleanup_path(&self, package: PackageInterfaceHandle, path: &str) { + if let Some(paths) = self + .additional_cleanup_paths + .borrow_mut() + .get_mut(&package.get_name()) + { + // PHP: array_search($path, ..., true) + let idx = paths.iter().position(|p| p == path); + if let Some(i) = idx { + paths.remove(i); + } + let _ = array_search; + } + } + + /// Gets file name for specific package + pub(crate) fn get_file_name(&self, package: PackageInterfaceHandle, _path: &str) -> String { + let extension = self.get_dist_path(package.clone(), PATHINFO_EXTENSION); + let extension = if extension.is_empty() { + package.get_dist_type().unwrap_or_default() + } else { + extension + }; + + rtrim( + &format!( + "{}/composer/tmp-{}.{}", + self.config + .borrow_mut() + .get("vendor-dir") + .as_string() + .unwrap_or(""), + hash( + "md5", + &format!("{}{}", package, spl_object_hash(&PhpMixed::Null)) + ), + extension + ), + Some("."), + ) + } + + /// Gets appendix message to add to the "- Upgrading x" string being output on update + fn get_install_operation_appendix( + &self, + _package: PackageInterfaceHandle, + _path: &str, + ) -> String { + String::new() + } + + /// For testing only: invoke the crate-private `get_file_name`. + pub fn __get_file_name(&self, package: PackageInterfaceHandle, path: &str) -> String { + self.get_file_name(package, path) + } + + /// For testing only: invoke the crate-private `process_url`. + pub fn __process_url( + &self, + package: PackageInterfaceHandle, + url: &str, + ) -> anyhow::Result<String> { + self.process_url(package, url) + } + + /// Process the download url + pub(crate) fn process_url( + &self, + package: PackageInterfaceHandle, + url: &str, + ) -> anyhow::Result<String> { + if !shirabe_php_shim::extension_loaded("openssl") && Some(0) == strpos(url, "https:") { + return Err(RuntimeException { + message: "You must enable the openssl extension to download files via https" + .to_string(), + code: 0, + } + .into()); + } + + let mut url = url.to_string(); + if package.get_dist_reference().is_some() { + url = UrlUtil::update_dist_reference( + &self.config.borrow(), + url, + &package.get_dist_reference().unwrap(), + ); + } + + Ok(url) + } } #[async_trait::async_trait(?Send)] @@ -604,238 +832,6 @@ impl ChangeReportInterface for FileDownloader { } } -impl FileDownloader { - /// Shared body of `ChangeReportInterface::get_local_changes`. - /// - /// PHP's `getLocalChanges` calls `$this->download()` / `$this->install()`, which late-bind - /// to the concrete downloader class (e.g. `ArchiveDownloader::install` extracts the archive - /// instead of copying the dist file). The Rust port embeds the parent class as `inner`, so - /// delegating downloaders must pass themselves as `this` to preserve that dispatch. - pub(crate) fn base_get_local_changes( - &self, - this: &dyn DownloaderInterface, - package: PackageInterfaceHandle, - path: &str, - ) -> anyhow::Result<Option<String>> { - let prev_io = std::mem::replace( - &mut *self.io.borrow_mut(), - std::rc::Rc::new(std::cell::RefCell::new(NullIO::new())), - ); - self.io - .borrow() - .borrow_mut() - .load_configuration(&mut self.config.borrow_mut())?; - - let target_dir = Filesystem::trim_trailing_slash(path); - // PHP attaches an onRejected handler to capture the error and drives the promise via - // httpDownloader->wait() / process->wait(); the single-threaded sync bridge block_on's the - // download/install futures, so a rejection surfaces directly as the Err captured below. - let result: anyhow::Result<String> = (|| -> anyhow::Result<String> { - if is_dir(format!("{}_compare", target_dir)) { - self.filesystem - .borrow_mut() - .remove_directory(format!("{}_compare", target_dir))?; - } - - sync_executor::block_on(this.download( - package.clone(), - &format!("{}_compare", target_dir), - None, - false, - ))?; - sync_executor::block_on(this.install( - package.clone(), - &format!("{}_compare", target_dir), - false, - ))?; - - let mut comparer = Comparer::new(); - comparer.set_source(format!("{}_compare", target_dir)); - comparer.set_update(target_dir.clone()); - comparer.do_compare(); - let output = comparer.get_changed_as_string(true, false); - self.filesystem - .borrow_mut() - .remove_directory(format!("{}_compare", target_dir))?; - Ok(output) - })(); - - *self.io.borrow_mut() = prev_io; - - let (e, output) = match result { - Ok(output) => (None, output), - Err(err) => (Some(err), String::new()), - }; - - if let Some(err) = e { - if self.io.borrow().is_debug() { - return Err(err); - } - - return Ok(Some(format!( - "Failed to detect changes: [{}] {}", - get_class(&PhpMixed::Null), - err - ))); - } - - let output = trim(&output, None); - - Ok(if strlen(&output) > 0 { - Some(output) - } else { - None - }) - } - - /// Shared body of `DownloaderInterface::update`; see `base_get_local_changes` for why the - /// concrete downloader is threaded in as `this`. The appendix is computed by the caller - /// because `getInstallOperationAppendix` is protected and not part of `DownloaderInterface`. - pub(crate) async fn base_update( - &self, - this: &dyn DownloaderInterface, - install_operation_appendix: &str, - initial: PackageInterfaceHandle, - target: PackageInterfaceHandle, - path: &str, - ) -> anyhow::Result<Option<PhpMixed>> { - self.io.borrow().write_error(&format!( - " - {}{}", - UpdateOperation::format(initial.clone(), target.clone(), false), - install_operation_appendix - )); - - // PHP: return $this->remove($initial, $path, false)->then(fn () => $this->install($target, $path, false)); - let _ = this.remove(initial, path, false).await?; - this.install(target, path, false).await - } -} - -impl FileDownloader { - fn get_dist_path(&self, package: PackageInterfaceHandle, component: i64) -> String { - pathinfo( - parse_url( - &strtr(&package.get_dist_url().unwrap_or_default(), "\\", "/"), - PHP_URL_PATH, - ) - .as_string() - .unwrap_or(""), - component, - ) - } - - pub(crate) fn clear_last_cache_write(&self, package: PackageInterfaceHandle) { - let mut last_cache_writes = self.last_cache_writes.lock().unwrap(); - if let Some(cache) = &self.cache - && last_cache_writes.contains_key(&package.get_name()) - { - let key = last_cache_writes.get(&package.get_name()).unwrap().clone(); - cache.borrow_mut().remove(&key); - last_cache_writes.shift_remove(&package.get_name()); - } - } - - pub(crate) fn add_cleanup_path(&self, package: PackageInterfaceHandle, path: &str) { - self.additional_cleanup_paths - .borrow_mut() - .entry(package.get_name()) - .or_default() - .push(path.to_string()); - } - - pub(crate) fn remove_cleanup_path(&self, package: PackageInterfaceHandle, path: &str) { - if let Some(paths) = self - .additional_cleanup_paths - .borrow_mut() - .get_mut(&package.get_name()) - { - // PHP: array_search($path, ..., true) - let idx = paths.iter().position(|p| p == path); - if let Some(i) = idx { - paths.remove(i); - } - let _ = array_search; - } - } - - /// Gets file name for specific package - pub(crate) fn get_file_name(&self, package: PackageInterfaceHandle, _path: &str) -> String { - let extension = self.get_dist_path(package.clone(), PATHINFO_EXTENSION); - let extension = if extension.is_empty() { - package.get_dist_type().unwrap_or_default() - } else { - extension - }; - - rtrim( - &format!( - "{}/composer/tmp-{}.{}", - self.config - .borrow_mut() - .get("vendor-dir") - .as_string() - .unwrap_or(""), - hash( - "md5", - &format!("{}{}", package, spl_object_hash(&PhpMixed::Null)) - ), - extension - ), - Some("."), - ) - } - - /// Gets appendix message to add to the "- Upgrading x" string being output on update - fn get_install_operation_appendix( - &self, - _package: PackageInterfaceHandle, - _path: &str, - ) -> String { - String::new() - } - - /// For testing only: invoke the crate-private `get_file_name`. - pub fn __get_file_name(&self, package: PackageInterfaceHandle, path: &str) -> String { - self.get_file_name(package, path) - } - - /// For testing only: invoke the crate-private `process_url`. - pub fn __process_url( - &self, - package: PackageInterfaceHandle, - url: &str, - ) -> anyhow::Result<String> { - self.process_url(package, url) - } - - /// Process the download url - pub(crate) fn process_url( - &self, - package: PackageInterfaceHandle, - url: &str, - ) -> anyhow::Result<String> { - if !shirabe_php_shim::extension_loaded("openssl") && Some(0) == strpos(url, "https:") { - return Err(RuntimeException { - message: "You must enable the openssl extension to download files via https" - .to_string(), - code: 0, - } - .into()); - } - - let mut url = url.to_string(); - if package.get_dist_reference().is_some() { - url = UrlUtil::update_dist_reference( - &self.config.borrow(), - url, - &package.get_dist_reference().unwrap(), - ); - } - - Ok(url) - } -} - #[derive(Debug, Clone)] struct UrlEntry { base: String, diff --git a/crates/shirabe/src/package/loader/array_loader.rs b/crates/shirabe/src/package/loader/array_loader.rs index f0399e89..0940183c 100644 --- a/crates/shirabe/src/package/loader/array_loader.rs +++ b/crates/shirabe/src/package/loader/array_loader.rs @@ -39,156 +39,7 @@ impl ArrayLoader { load_options, } } -} - -enum CompleteOrRootPackage { - Complete(CompletePackage), - Root(RootPackage), -} - -impl CompleteOrRootPackage { - fn package(&self) -> &Package { - match self { - Self::Complete(p) => &p.inner, - Self::Root(p) => &p.inner.inner, - } - } - - fn package_mut(&mut self) -> &mut Package { - match self { - Self::Complete(p) => &mut p.inner, - Self::Root(p) => &mut p.inner.inner, - } - } - - fn complete_mut(&mut self) -> &mut dyn CompletePackageInterface { - match self { - Self::Complete(p) => p, - Self::Root(p) => p, - } - } - - fn is_root(&self) -> bool { - matches!(self, Self::Root(_)) - } - - fn get_name(&self) -> &str { - self.package().get_name() - } - - fn get_pretty_version(&self) -> &str { - self.package().get_pretty_version() - } - - fn into_handle(self) -> PackageInterfaceHandle { - match self { - Self::Complete(p) => CompletePackageHandle::from_complete_package(p).into(), - Self::Root(p) => RootPackageHandle::from_root_package(p).into(), - } - } -} - -fn php_to_map(value: &PhpMixed) -> IndexMap<String, PhpMixed> { - match value { - PhpMixed::Array(m) => m.clone(), - _ => IndexMap::new(), - } -} - -fn php_to_string_vec(value: &PhpMixed) -> Vec<String> { - match value { - PhpMixed::List(l) => l.iter().map(strval).collect(), - PhpMixed::Array(m) => m.values().map(strval).collect(), - _ => Vec::new(), - } -} - -fn apply_link_setter(package: &mut Package, method: &str, links: IndexMap<String, Link>) { - if method == Link::TYPE_REQUIRE { - package.set_requires(links); - } else if method == Link::TYPE_DEV_REQUIRE { - package.set_dev_requires(links); - } else if method == Link::TYPE_CONFLICT { - package.set_conflicts(links); - } else if method == Link::TYPE_PROVIDE { - package.set_provides(links); - } else if method == Link::TYPE_REPLACE { - package.set_replaces(links); - } -} - -fn php_to_mirrors(value: &PhpMixed) -> Vec<Mirror> { - let entries: Vec<&PhpMixed> = match value { - PhpMixed::List(l) => l.iter().collect(), - PhpMixed::Array(m) => m.values().collect(), - _ => Vec::new(), - }; - entries - .into_iter() - .filter_map(|entry| match entry { - PhpMixed::Array(m) => Some(Mirror { - url: m - .get("url") - .and_then(|v| v.as_string()) - .unwrap_or("") - .to_string(), - preferred: m.get("preferred").is_some_and(|v| v.to_bool()), - }), - _ => None, - }) - .collect() -} - -impl LoaderInterface for ArrayLoader { - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn load( - &self, - mut config: IndexMap<String, PhpMixed>, - class: Option<String>, - ) -> anyhow::Result<PackageInterfaceHandle> { - let class = class.unwrap_or_else(|| "Composer\\Package\\CompletePackage".to_string()); - - if class != "Composer\\Package\\CompletePackage" - && class != "Composer\\Package\\RootPackage" - { - trigger_error( - "The $class arg is deprecated, please reach out to Composer maintainers ASAP if you still need this.", - E_USER_DEPRECATED, - ); - } - - let mut package = self.create_object(&config, &class)?; - - for (r#type, opts) in SUPPORTED_LINK_TYPES.iter() { - let entry = config.get(*r#type); - let entry_is_array = entry - .map(|v| matches!(v, PhpMixed::Array(_))) - .unwrap_or(false); - if entry.is_none() || !entry_is_array { - continue; - } - let links = self.parse_links( - package.get_name(), - package.get_pretty_version(), - opts.method, - match entry.unwrap() { - PhpMixed::Array(arr) => arr.clone(), - _ => IndexMap::new(), - }, - )?; - apply_link_setter(package.package_mut(), opts.method, links); - } - - let package = self.configure_object(package, &mut config)?; - - Ok(package) - } -} -impl ArrayLoader { #[tracing::instrument(skip_all)] pub fn load_packages( &self, @@ -938,3 +789,150 @@ impl ArrayLoader { Ok(None) } } + +enum CompleteOrRootPackage { + Complete(CompletePackage), + Root(RootPackage), +} + +impl CompleteOrRootPackage { + fn package(&self) -> &Package { + match self { + Self::Complete(p) => &p.inner, + Self::Root(p) => &p.inner.inner, + } + } + + fn package_mut(&mut self) -> &mut Package { + match self { + Self::Complete(p) => &mut p.inner, + Self::Root(p) => &mut p.inner.inner, + } + } + + fn complete_mut(&mut self) -> &mut dyn CompletePackageInterface { + match self { + Self::Complete(p) => p, + Self::Root(p) => p, + } + } + + fn is_root(&self) -> bool { + matches!(self, Self::Root(_)) + } + + fn get_name(&self) -> &str { + self.package().get_name() + } + + fn get_pretty_version(&self) -> &str { + self.package().get_pretty_version() + } + + fn into_handle(self) -> PackageInterfaceHandle { + match self { + Self::Complete(p) => CompletePackageHandle::from_complete_package(p).into(), + Self::Root(p) => RootPackageHandle::from_root_package(p).into(), + } + } +} + +fn php_to_map(value: &PhpMixed) -> IndexMap<String, PhpMixed> { + match value { + PhpMixed::Array(m) => m.clone(), + _ => IndexMap::new(), + } +} + +fn php_to_string_vec(value: &PhpMixed) -> Vec<String> { + match value { + PhpMixed::List(l) => l.iter().map(strval).collect(), + PhpMixed::Array(m) => m.values().map(strval).collect(), + _ => Vec::new(), + } +} + +fn apply_link_setter(package: &mut Package, method: &str, links: IndexMap<String, Link>) { + if method == Link::TYPE_REQUIRE { + package.set_requires(links); + } else if method == Link::TYPE_DEV_REQUIRE { + package.set_dev_requires(links); + } else if method == Link::TYPE_CONFLICT { + package.set_conflicts(links); + } else if method == Link::TYPE_PROVIDE { + package.set_provides(links); + } else if method == Link::TYPE_REPLACE { + package.set_replaces(links); + } +} + +fn php_to_mirrors(value: &PhpMixed) -> Vec<Mirror> { + let entries: Vec<&PhpMixed> = match value { + PhpMixed::List(l) => l.iter().collect(), + PhpMixed::Array(m) => m.values().collect(), + _ => Vec::new(), + }; + entries + .into_iter() + .filter_map(|entry| match entry { + PhpMixed::Array(m) => Some(Mirror { + url: m + .get("url") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(), + preferred: m.get("preferred").is_some_and(|v| v.to_bool()), + }), + _ => None, + }) + .collect() +} + +impl LoaderInterface for ArrayLoader { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn load( + &self, + mut config: IndexMap<String, PhpMixed>, + class: Option<String>, + ) -> anyhow::Result<PackageInterfaceHandle> { + let class = class.unwrap_or_else(|| "Composer\\Package\\CompletePackage".to_string()); + + if class != "Composer\\Package\\CompletePackage" + && class != "Composer\\Package\\RootPackage" + { + trigger_error( + "The $class arg is deprecated, please reach out to Composer maintainers ASAP if you still need this.", + E_USER_DEPRECATED, + ); + } + + let mut package = self.create_object(&config, &class)?; + + for (r#type, opts) in SUPPORTED_LINK_TYPES.iter() { + let entry = config.get(*r#type); + let entry_is_array = entry + .map(|v| matches!(v, PhpMixed::Array(_))) + .unwrap_or(false); + if entry.is_none() || !entry_is_array { + continue; + } + let links = self.parse_links( + package.get_name(), + package.get_pretty_version(), + opts.method, + match entry.unwrap() { + PhpMixed::Array(arr) => arr.clone(), + _ => IndexMap::new(), + }, + )?; + apply_link_setter(package.package_mut(), opts.method, links); + } + + let package = self.configure_object(package, &mut config)?; + + Ok(package) + } +} diff --git a/crates/shirabe/src/package/loader/validating_array_loader.rs b/crates/shirabe/src/package/loader/validating_array_loader.rs index 0be0ef38..a28df595 100644 --- a/crates/shirabe/src/package/loader/validating_array_loader.rs +++ b/crates/shirabe/src/package/loader/validating_array_loader.rs @@ -60,6 +60,282 @@ impl ValidatingArrayLoader { flags, } } + + pub fn get_warnings(&self) -> Vec<String> { + self.warnings.borrow().clone() + } + + pub fn get_errors(&self) -> Vec<String> { + self.errors.borrow().clone() + } + + pub fn has_package_naming_error(name: &str, is_link: bool) -> Option<String> { + if PlatformRepository::is_platform_package(name) { + return None; + } + + if !Preg::is_match( + php_regex!( + "{^[a-z0-9](?:[_.-]?[a-z0-9]++)*+/[a-z0-9](?:(?:[_.]|-{1,2})?[a-z0-9]++)*+$}iD" + ), + name, + ) { + return Some(format!( + "{} is invalid, it should have a vendor name, a forward slash, and a package name. The vendor and package name can be words separated by -, . or _. The complete name should match \"^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{{0,2}})[a-z0-9]+)*$\".", + name + )); + } + + let reserved_names = [ + "nul", "con", "prn", "aux", "com1", "com2", "com3", "com4", "com5", "com6", "com7", + "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", + ]; + let lower = strtolower(name); + let bits: Vec<&str> = lower.split('/').collect(); + if reserved_names.contains(&bits[0]) || reserved_names.contains(&bits[1]) { + return Some(format!( + "{} is reserved, package and vendor names can not match any of: {}.", + name, + reserved_names.join(", ") + )); + } + + if Preg::is_match(php_regex!("{\\.json$}"), name) { + return Some(format!( + "{} is invalid, package names can not end in .json, consider renaming it or perhaps using a -json suffix instead.", + name + )); + } + + if Preg::is_match(php_regex!("{[A-Z]}"), name) { + if is_link { + return Some(format!( + "{} is invalid, it should not contain uppercase characters. Please use {} instead.", + name, + strtolower(name) + )); + } + + let suggest_name = Preg::replace( + php_regex!("{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}"), + "\\1\\3-\\2\\4", + name, + ); + let suggest_name = strtolower(&suggest_name); + + return Some(format!( + "{} is invalid, it should not contain uppercase characters. We suggest using {} instead.", + name, suggest_name + )); + } + + None + } + + fn validate_regex(&self, property: &str, regex: &str, mandatory: bool) -> bool { + if !self.validate_string(property, mandatory) { + return false; + } + + let value = self.config.borrow()[property] + .as_string() + .unwrap_or("") + .to_string(); + if !Preg::is_match(format!("{{^{}$}}u", regex), &value) { + let message = format!( + "{} : invalid value ({}), must match {}", + property, value, regex + ); + if mandatory { + self.errors.borrow_mut().push(message); + } else { + self.warnings.borrow_mut().push(message); + } + self.config.borrow_mut().shift_remove(property); + + return false; + } + + true + } + + fn validate_string(&self, property: &str, mandatory: bool) -> bool { + if self.config.borrow().contains_key(property) + && !is_string(&self.config.borrow()[property]) + { + self.errors.borrow_mut().push(format!( + "{} : should be a string, {} given", + property, + get_debug_type(&self.config.borrow()[property]) + )); + self.config.borrow_mut().shift_remove(property); + + return false; + } + + let is_empty = !self.config.borrow().contains_key(property) + || trim( + self.config.borrow()[property].as_string().unwrap_or(""), + Some(" \t\n\r\0\u{0B}"), + ) + .is_empty(); + if is_empty { + if mandatory { + self.errors + .borrow_mut() + .push(format!("{} : must be present", property)); + } + self.config.borrow_mut().shift_remove(property); + + return false; + } + + true + } + + fn validate_array(&self, property: &str, mandatory: bool) -> bool { + if self.config.borrow().contains_key(property) && !is_array(&self.config.borrow()[property]) + { + self.errors.borrow_mut().push(format!( + "{} : should be an array, {} given", + property, + get_debug_type(&self.config.borrow()[property]) + )); + self.config.borrow_mut().shift_remove(property); + + return false; + } + + let is_empty = !self.config.borrow().contains_key(property) + || match &self.config.borrow()[property] { + PhpMixed::Array(m) => m.is_empty(), + PhpMixed::List(l) => l.is_empty(), + // is_array() above guarantees the value is Array or List here. + _ => unreachable!("validate_array: non-array value survived the is_array check"), + }; + if is_empty { + if mandatory { + self.errors.borrow_mut().push(format!( + "{} : must be present and contain at least one element", + property + )); + } + self.config.borrow_mut().shift_remove(property); + + return false; + } + + true + } + + fn validate_flat_array(&self, property: &str, regex: Option<&str>, mandatory: bool) -> bool { + if !self.validate_array(property, mandatory) { + return false; + } + + let mut pass = true; + let entries: Vec<(String, PhpMixed)> = self.config.borrow()[property] + .as_array() + .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) + .unwrap_or_default(); + for (key, value) in entries { + if !is_string(&value) && !is_numeric(&value) { + self.errors.borrow_mut().push(format!( + "{}.{} : must be a string or int, {} given", + property, + key, + get_debug_type(&value) + )); + if let Some(PhpMixed::Array(arr)) = self.config.borrow_mut().get_mut(property) { + arr.shift_remove(&key); + } + pass = false; + + continue; + } + + if let Some(regex_str) = regex { + let value_str = php_to_string(&value); + if !Preg::is_match(format!("{{^{}$}}u", regex_str), &value_str) { + self.warnings.borrow_mut().push(format!( + "{}.{} : invalid value ({}), must match {}", + property, key, value_str, regex_str + )); + if let Some(PhpMixed::Array(arr)) = self.config.borrow_mut().get_mut(property) { + arr.shift_remove(&key); + } + pass = false; + } + } + } + + pass + } + + fn validate_url(&self, property: &str, mandatory: bool) -> bool { + if !self.validate_string(property, mandatory) { + return false; + } + + let value = self.config.borrow()[property] + .as_string() + .unwrap_or("") + .to_string(); + if !self.filter_url(&value, &["http", "https"]) { + self.warnings.borrow_mut().push(format!( + "{} : invalid value ({}), must be an http/https URL", + property, value + )); + self.config.borrow_mut().shift_remove(property); + + return false; + } + + true + } + + fn filter_url(&self, value: &str, schemes: &[&str]) -> bool { + if value.is_empty() { + return true; + } + + let bits = parse_url_all(value); + let bits_map = match bits { + PhpMixed::Array(m) => m, + _ => return false, + }; + let scheme = bits_map + .get("scheme") + .and_then(|v| v.as_string()) + .unwrap_or(""); + let host = bits_map + .get("host") + .and_then(|v| v.as_string()) + .unwrap_or(""); + if scheme.is_empty() || host.is_empty() { + return false; + } + + if !schemes.contains(&scheme) { + return false; + } + + true + } + + fn is_empty_array(val: Option<&PhpMixed>) -> bool { + match val { + Some(v) => match v { + PhpMixed::Array(m) => m.is_empty(), + PhpMixed::Null => true, + PhpMixed::Bool(false) => true, + PhpMixed::String(s) => s.is_empty(), + PhpMixed::Int(0) => true, + _ => false, + }, + None => true, + } + } } impl LoaderInterface for ValidatingArrayLoader { @@ -1327,281 +1603,3 @@ impl LoaderInterface for ValidatingArrayLoader { Ok(package) } } - -impl ValidatingArrayLoader { - pub fn get_warnings(&self) -> Vec<String> { - self.warnings.borrow().clone() - } - - pub fn get_errors(&self) -> Vec<String> { - self.errors.borrow().clone() - } - - pub fn has_package_naming_error(name: &str, is_link: bool) -> Option<String> { - if PlatformRepository::is_platform_package(name) { - return None; - } - - if !Preg::is_match( - php_regex!( - "{^[a-z0-9](?:[_.-]?[a-z0-9]++)*+/[a-z0-9](?:(?:[_.]|-{1,2})?[a-z0-9]++)*+$}iD" - ), - name, - ) { - return Some(format!( - "{} is invalid, it should have a vendor name, a forward slash, and a package name. The vendor and package name can be words separated by -, . or _. The complete name should match \"^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{{0,2}})[a-z0-9]+)*$\".", - name - )); - } - - let reserved_names = [ - "nul", "con", "prn", "aux", "com1", "com2", "com3", "com4", "com5", "com6", "com7", - "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", - ]; - let lower = strtolower(name); - let bits: Vec<&str> = lower.split('/').collect(); - if reserved_names.contains(&bits[0]) || reserved_names.contains(&bits[1]) { - return Some(format!( - "{} is reserved, package and vendor names can not match any of: {}.", - name, - reserved_names.join(", ") - )); - } - - if Preg::is_match(php_regex!("{\\.json$}"), name) { - return Some(format!( - "{} is invalid, package names can not end in .json, consider renaming it or perhaps using a -json suffix instead.", - name - )); - } - - if Preg::is_match(php_regex!("{[A-Z]}"), name) { - if is_link { - return Some(format!( - "{} is invalid, it should not contain uppercase characters. Please use {} instead.", - name, - strtolower(name) - )); - } - - let suggest_name = Preg::replace( - php_regex!("{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}"), - "\\1\\3-\\2\\4", - name, - ); - let suggest_name = strtolower(&suggest_name); - - return Some(format!( - "{} is invalid, it should not contain uppercase characters. We suggest using {} instead.", - name, suggest_name - )); - } - - None - } - - fn validate_regex(&self, property: &str, regex: &str, mandatory: bool) -> bool { - if !self.validate_string(property, mandatory) { - return false; - } - - let value = self.config.borrow()[property] - .as_string() - .unwrap_or("") - .to_string(); - if !Preg::is_match(format!("{{^{}$}}u", regex), &value) { - let message = format!( - "{} : invalid value ({}), must match {}", - property, value, regex - ); - if mandatory { - self.errors.borrow_mut().push(message); - } else { - self.warnings.borrow_mut().push(message); - } - self.config.borrow_mut().shift_remove(property); - - return false; - } - - true - } - - fn validate_string(&self, property: &str, mandatory: bool) -> bool { - if self.config.borrow().contains_key(property) - && !is_string(&self.config.borrow()[property]) - { - self.errors.borrow_mut().push(format!( - "{} : should be a string, {} given", - property, - get_debug_type(&self.config.borrow()[property]) - )); - self.config.borrow_mut().shift_remove(property); - - return false; - } - - let is_empty = !self.config.borrow().contains_key(property) - || trim( - self.config.borrow()[property].as_string().unwrap_or(""), - Some(" \t\n\r\0\u{0B}"), - ) - .is_empty(); - if is_empty { - if mandatory { - self.errors - .borrow_mut() - .push(format!("{} : must be present", property)); - } - self.config.borrow_mut().shift_remove(property); - - return false; - } - - true - } - - fn validate_array(&self, property: &str, mandatory: bool) -> bool { - if self.config.borrow().contains_key(property) && !is_array(&self.config.borrow()[property]) - { - self.errors.borrow_mut().push(format!( - "{} : should be an array, {} given", - property, - get_debug_type(&self.config.borrow()[property]) - )); - self.config.borrow_mut().shift_remove(property); - - return false; - } - - let is_empty = !self.config.borrow().contains_key(property) - || match &self.config.borrow()[property] { - PhpMixed::Array(m) => m.is_empty(), - PhpMixed::List(l) => l.is_empty(), - // is_array() above guarantees the value is Array or List here. - _ => unreachable!("validate_array: non-array value survived the is_array check"), - }; - if is_empty { - if mandatory { - self.errors.borrow_mut().push(format!( - "{} : must be present and contain at least one element", - property - )); - } - self.config.borrow_mut().shift_remove(property); - - return false; - } - - true - } - - fn validate_flat_array(&self, property: &str, regex: Option<&str>, mandatory: bool) -> bool { - if !self.validate_array(property, mandatory) { - return false; - } - - let mut pass = true; - let entries: Vec<(String, PhpMixed)> = self.config.borrow()[property] - .as_array() - .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) - .unwrap_or_default(); - for (key, value) in entries { - if !is_string(&value) && !is_numeric(&value) { - self.errors.borrow_mut().push(format!( - "{}.{} : must be a string or int, {} given", - property, - key, - get_debug_type(&value) - )); - if let Some(PhpMixed::Array(arr)) = self.config.borrow_mut().get_mut(property) { - arr.shift_remove(&key); - } - pass = false; - - continue; - } - - if let Some(regex_str) = regex { - let value_str = php_to_string(&value); - if !Preg::is_match(format!("{{^{}$}}u", regex_str), &value_str) { - self.warnings.borrow_mut().push(format!( - "{}.{} : invalid value ({}), must match {}", - property, key, value_str, regex_str - )); - if let Some(PhpMixed::Array(arr)) = self.config.borrow_mut().get_mut(property) { - arr.shift_remove(&key); - } - pass = false; - } - } - } - - pass - } - - fn validate_url(&self, property: &str, mandatory: bool) -> bool { - if !self.validate_string(property, mandatory) { - return false; - } - - let value = self.config.borrow()[property] - .as_string() - .unwrap_or("") - .to_string(); - if !self.filter_url(&value, &["http", "https"]) { - self.warnings.borrow_mut().push(format!( - "{} : invalid value ({}), must be an http/https URL", - property, value - )); - self.config.borrow_mut().shift_remove(property); - - return false; - } - - true - } - - fn filter_url(&self, value: &str, schemes: &[&str]) -> bool { - if value.is_empty() { - return true; - } - - let bits = parse_url_all(value); - let bits_map = match bits { - PhpMixed::Array(m) => m, - _ => return false, - }; - let scheme = bits_map - .get("scheme") - .and_then(|v| v.as_string()) - .unwrap_or(""); - let host = bits_map - .get("host") - .and_then(|v| v.as_string()) - .unwrap_or(""); - if scheme.is_empty() || host.is_empty() { - return false; - } - - if !schemes.contains(&scheme) { - return false; - } - - true - } - - fn is_empty_array(val: Option<&PhpMixed>) -> bool { - match val { - Some(v) => match v { - PhpMixed::Array(m) => m.is_empty(), - PhpMixed::Null => true, - PhpMixed::Bool(false) => true, - PhpMixed::String(s) => s.is_empty(), - PhpMixed::Int(0) => true, - _ => false, - }, - None => true, - } - } -} |
