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 /crates/shirabe/src/command/require_command.rs | |
| 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>
Diffstat (limited to 'crates/shirabe/src/command/require_command.rs')
| -rw-r--r-- | crates/shirabe/src/command/require_command.rs | 1164 |
1 files changed, 581 insertions, 583 deletions
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); +} |
