diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-16 15:42:13 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-16 15:42:13 +0900 |
| commit | eec7dc3646324d9c9064f91d7369deacf31342b6 (patch) | |
| tree | c771b61a487fb0d82efbf8b2f295571bfbfe888c /crates/shirabe | |
| parent | 65f83576e4f3b901f15add9a957ce5ccac2b4280 (diff) | |
| download | php-shirabe-eec7dc3646324d9c9064f91d7369deacf31342b6.tar.gz php-shirabe-eec7dc3646324d9c9064f91d7369deacf31342b6.tar.zst php-shirabe-eec7dc3646324d9c9064f91d7369deacf31342b6.zip | |
feat(port): port PackageDiscoveryTrait.php
Diffstat (limited to 'crates/shirabe')
| -rw-r--r-- | crates/shirabe/src/command/package_discovery_trait.rs | 918 |
1 files changed, 918 insertions, 0 deletions
diff --git a/crates/shirabe/src/command/package_discovery_trait.rs b/crates/shirabe/src/command/package_discovery_trait.rs index 85e8e2a..df55c5c 100644 --- a/crates/shirabe/src/command/package_discovery_trait.rs +++ b/crates/shirabe/src/command/package_discovery_trait.rs @@ -1 +1,919 @@ //! ref: composer/src/Composer/Command/PackageDiscoveryTrait.php + +use std::any::Any; + +use anyhow::Result; +use indexmap::IndexMap; +use shirabe_external_packages::composer::pcre::preg::Preg; +use shirabe_external_packages::symfony::component::console::input::input_interface::InputInterface; +use shirabe_external_packages::symfony::component::console::output::output_interface::OutputInterface; +use shirabe_php_shim::{ + array_keys, array_slice, array_unshift, array_values, asort, count, explode, file_get_contents, + implode, in_array, is_array, is_file, is_numeric, is_string, json_decode, levenshtein, + sprintf, strlen, strpos, trim, InvalidArgumentException, LogicException, PhpMixed, PHP_EOL, +}; + +use crate::composer::Composer; +use crate::factory::Factory; +use crate::filter::platform_requirement_filter::ignore_all_platform_requirement_filter::IgnoreAllPlatformRequirementFilter; +use crate::filter::platform_requirement_filter::platform_requirement_filter_factory::PlatformRequirementFilterFactory; +use crate::io::io_interface::IOInterface; +use crate::package::base_package::BasePackage; +use crate::package::complete_package_interface::CompletePackageInterface; +use crate::package::package_interface::PackageInterface; +use crate::package::version::version_parser::VersionParser; +use crate::package::version::version_selector::VersionSelector; +use crate::repository::composite_repository::CompositeRepository; +use crate::repository::platform_repository::PlatformRepository; +use crate::repository::repository_factory::RepositoryFactory; +use crate::repository::repository_interface::SearchResult; +use crate::repository::repository_set::RepositorySet; +use crate::util::filesystem::Filesystem; + +/// @internal +pub trait PackageDiscoveryTrait { + // PHP: private $repos; private $repositorySets; + // TODO(phase-b): trait fields require an associated state struct in Rust; expose via accessors + fn get_repos_mut(&mut self) -> &mut Option<CompositeRepository>; + fn get_repository_sets_mut(&mut self) -> &mut IndexMap<String, RepositorySet>; + + // PHP: trait dependencies (provided by BaseCommand) + fn get_io(&self) -> &dyn IOInterface; + fn try_composer(&self) -> Option<Composer>; + fn require_composer(&self, disable_plugins: Option<bool>, disable_scripts: Option<bool>) + -> Composer; + fn get_platform_requirement_filter( + &self, + input: &dyn InputInterface, + ) -> Box<dyn crate::filter::platform_requirement_filter::platform_requirement_filter_interface::PlatformRequirementFilterInterface>; + + fn normalize_requirements( + &self, + requires: Vec<String>, + ) -> Vec<IndexMap<String, String>>; + + fn get_repos(&mut self) -> &CompositeRepository { + if self.get_repos_mut().is_none() { + // PHP: array_merge([new PlatformRepository], RepositoryFactory::defaultReposWithDefaultManager($this->getIO())) + let mut repos: Vec<Box<dyn crate::repository::repository_interface::RepositoryInterface>> = vec![ + // TODO(phase-b): PlatformRepository::new() signature + Box::new(todo!("PlatformRepository::new()") as PlatformRepository), + ]; + let io_owned: Box<dyn IOInterface> = todo!("clone self.get_io() into a Box"); + for repo in + RepositoryFactory::default_repos_with_default_manager(io_owned) + { + repos.push(repo); + } + *self.get_repos_mut() = Some(CompositeRepository::new(repos)); + } + + self.get_repos_mut().as_ref().unwrap() + } + + /// @param key-of<BasePackage::STABILITIES>|null $minimumStability + fn get_repository_set( + &mut self, + input: &dyn InputInterface, + minimum_stability: Option<&str>, + ) -> &RepositorySet { + let key = minimum_stability.unwrap_or("default").to_string(); + + if !self.get_repository_sets_mut().contains_key(&key) { + let stability = minimum_stability + .map(|s| s.to_string()) + .unwrap_or_else(|| self.get_minimum_stability(input)); + let mut repository_set = RepositorySet::new( + &stability, + IndexMap::new(), + vec![], + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + ); + // TODO(phase-b): self.get_repos() returns reference; add_repository takes ownership + let repos = todo!("self.get_repos() owned/cloned for add_repository"); + let _ = repository_set.add_repository(repos); + self.get_repository_sets_mut() + .insert(key.clone(), repository_set); + } + + self.get_repository_sets_mut().get(&key).unwrap() + } + + /// @return key-of<BasePackage::STABILITIES> + fn get_minimum_stability(&self, input: &dyn InputInterface) -> String { + if input.has_option("stability") { + // @phpstan-ignore-line as InitCommand does have this option but not all classes using this trait do + return VersionParser::normalize_stability( + &input + .get_option("stability") + .as_string() + .map(|s| s.to_string()) + .unwrap_or_else(|| "stable".to_string()), + ); + } + + // @phpstan-ignore-next-line as RequireCommand does not have the option above so this code is reachable there + let file = Factory::get_composer_file(); + if is_file(&file) && Filesystem::is_readable(&file) { + let contents = file_get_contents(&file).unwrap_or_default(); + let composer = json_decode(&contents, true).unwrap_or(PhpMixed::Null); + if is_array(&composer) { + if let Some(arr) = composer.as_array() { + if let Some(ms) = arr.get("minimum-stability") { + if let Some(s) = ms.as_string() { + return VersionParser::normalize_stability(s); + } + } + } + } + } + + "stable".to_string() + } + + /// @param array<string> $requires + /// + /// @return array<string> + /// @throws \Exception + fn determine_requirements( + &mut self, + input: &dyn InputInterface, + _output: &dyn OutputInterface, + mut requires: Vec<String>, + platform_repo: Option<&PlatformRepository>, + preferred_stability: &str, + use_best_version_constraint: bool, + fixed: bool, + ) -> Result<Vec<String>> { + if requires.len() > 0 { + let requires_norm = self.normalize_requirements(requires.clone()); + let mut result: Vec<String> = vec![]; + let io = self.get_io(); + + for mut requirement in requires_norm { + if requirement.contains_key("version") + && Preg::is_match( + r"{^\d+(\.\d+)?$}", + requirement.get("version").map(|s| s.as_str()).unwrap_or(""), + ) + { + io.write_error( + PhpMixed::String(format!( + "<warning>The \"{}\" constraint for \"{}\" appears too strict and will likely not match what you want. See https://getcomposer.org/constraints</warning>", + requirement.get("version").map(|s| s.as_str()).unwrap_or(""), + requirement.get("name").map(|s| s.as_str()).unwrap_or(""), + )), + true, + IOInterface::NORMAL, + ); + } + + if !requirement.contains_key("version") { + // determine the best version automatically + let (name, version) = self.find_best_version_and_name_for_package( + self.get_io(), + input, + requirement.get("name").map(|s| s.as_str()).unwrap_or(""), + platform_repo, + preferred_stability, + fixed, + )?; + + // replace package name from packagist.org + requirement.insert("name".to_string(), name); + + if use_best_version_constraint { + requirement.insert("version".to_string(), version.clone()); + io.write_error( + PhpMixed::String(sprintf( + "Using version <info>%s</info> for <info>%s</info>", + &[ + PhpMixed::String(version), + PhpMixed::String( + requirement + .get("name") + .cloned() + .unwrap_or_default(), + ), + ], + )), + true, + IOInterface::NORMAL, + ); + } else { + requirement.insert("version".to_string(), "guess".to_string()); + } + } + + result.push(format!( + "{} {}", + requirement.get("name").map(|s| s.as_str()).unwrap_or(""), + requirement.get("version").map(|s| s.as_str()).unwrap_or(""), + )); + } + + return Ok(result); + } + + let version_parser = VersionParser::new(); + + // Collect existing packages + let composer = self.try_composer(); + let mut installed_repo: Option<_> = None; + if let Some(c) = &composer { + installed_repo = Some(c.get_repository_manager().get_local_repository()); + } + let mut existing_packages: Vec<String> = vec![]; + if let Some(repo) = &installed_repo { + for package in repo.get_packages() { + existing_packages.push(package.get_name().to_string()); + } + } + // PHP: unset($composer, $installedRepo); + drop(composer); + drop(installed_repo); + + let io = self.get_io(); + loop { + let package_input = io.ask("Search for a package: ".to_string(), PhpMixed::Null); + let mut package = match package_input.as_string() { + Some(s) => s.to_string(), + None => break, + }; + let mut matches = self.get_repos().search(package.clone(), 0, None); + + if count(&PhpMixed::List( + matches + .iter() + .map(|_| Box::new(PhpMixed::Null)) + .collect(), + )) > 0 + { + // Remove existing packages from search results. + matches.retain(|found_package| { + !in_array( + PhpMixed::String(found_package.name.clone()), + &PhpMixed::List( + existing_packages + .iter() + .map(|s| Box::new(PhpMixed::String(s.clone()))) + .collect(), + ), + true, + ) + }); + // PHP: $matches = array_values($matches); — already a Vec in Rust + let mut exact_match = false; + for r#match in &matches { + if r#match.name == package { + exact_match = true; + break; + } + } + + // no match, prompt which to pick + if !exact_match { + let providers = self.get_repos().get_providers(package.clone()); + if count(&PhpMixed::List( + providers + .iter() + .map(|_| Box::new(PhpMixed::Null)) + .collect(), + )) > 0 + { + // PHP: array_unshift($matches, ['name' => $package, 'description' => '']); + matches.insert( + 0, + SearchResult { + name: package.clone(), + description: Some(String::new()), + abandoned: None, + url: None, + }, + ); + } + + let mut choices: Vec<String> = vec![]; + for (position, found_package) in matches.iter().enumerate() { + let mut abandoned = String::new(); + if let Some(ai) = &found_package.abandoned { + let replacement = match ai { + crate::repository::repository_interface::AbandonedInfo::Replacement(r) => { + sprintf( + "Use %s instead", + &[PhpMixed::String(r.clone())], + ) + } + crate::repository::repository_interface::AbandonedInfo::Abandoned => { + "No replacement was suggested".to_string() + } + }; + abandoned = sprintf( + "<warning>Abandoned. %s.</warning>", + &[PhpMixed::String(replacement)], + ); + } + + choices.push(sprintf( + " <info>%5s</info> %s %s", + &[ + PhpMixed::String(format!("[{}]", position)), + PhpMixed::String(found_package.name.clone()), + PhpMixed::String(abandoned), + ], + )); + } + + io.write_error( + PhpMixed::List(vec![ + Box::new(PhpMixed::String(String::new())), + Box::new(PhpMixed::String(sprintf( + "Found <info>%s</info> packages matching <info>%s</info>", + &[ + PhpMixed::Int(matches.len() as i64), + PhpMixed::String(package.clone()), + ], + ))), + Box::new(PhpMixed::String(String::new())), + ]), + true, + IOInterface::NORMAL, + ); + + io.write_error( + PhpMixed::List( + choices + .iter() + .map(|s| Box::new(PhpMixed::String(s.clone()))) + .collect(), + ), + true, + IOInterface::NORMAL, + ); + io.write_error( + PhpMixed::String(String::new()), + true, + IOInterface::NORMAL, + ); + + let matches_clone = matches.clone(); + let version_parser_clone = version_parser.clone(); + let validator: Box<dyn Fn(PhpMixed) -> PhpMixed> = Box::new( + move |selection_mixed: PhpMixed| -> PhpMixed { + let selection = selection_mixed.as_string().unwrap_or("").to_string(); + if "" == selection { + return PhpMixed::Bool(false); + } + + if is_numeric(&PhpMixed::String(selection.clone())) { + let idx: usize = selection.parse().unwrap_or(0); + if let Some(p) = matches_clone.get(idx) { + return PhpMixed::String(p.name.clone()); + } + } + + if let Some(m) = Preg::is_match_strict_groups( + r"{^\s*(?P<name>[\S/]+)(?:\s+(?P<version>\S+))?\s*$}", + &selection, + ) { + if let Some(v) = m.get("version") { + // parsing `acme/example ~2.3` + // validate version constraint + // TODO(phase-b): parse_constraints returns Result + let _ = version_parser_clone.parse_constraints(v); + + return PhpMixed::String(format!( + "{} {}", + m.get("name").cloned().unwrap_or_default(), + v, + )); + } + + // parsing `acme/example` + return PhpMixed::String( + m.get("name").cloned().unwrap_or_default(), + ); + } + + // TODO(phase-b): throw new \Exception('Not a valid selection'); + panic!("Not a valid selection"); + }, + ); + + package = io + .ask_and_validate( + "Enter package # to add, or the complete package name if it is not listed: ".to_string(), + validator, + Some(3), + PhpMixed::String(String::new()), + ) + .as_string() + .map(|s| s.to_string()) + .unwrap_or_default(); + } + + // no constraint yet, determine the best version automatically + if !package.is_empty() && strpos(&package, " ").is_none() { + let validator: Box<dyn Fn(PhpMixed) -> PhpMixed> = Box::new( + |input_mixed: PhpMixed| -> PhpMixed { + let input = trim(input_mixed.as_string().unwrap_or(""), None); + if strlen(&input) > 0 { + PhpMixed::String(input) + } else { + PhpMixed::Bool(false) + } + }, + ); + + let constraint_mixed = io.ask_and_validate( + "Enter the version constraint to require (or leave blank to use the latest version): ".to_string(), + validator, + Some(3), + PhpMixed::String(String::new()), + ); + + let constraint: String = match &constraint_mixed { + PhpMixed::Bool(false) => { + let (_name, c) = self.find_best_version_and_name_for_package( + self.get_io(), + input, + &package, + platform_repo, + preferred_stability, + fixed, + )?; + + io.write_error( + PhpMixed::String(sprintf( + "Using version <info>%s</info> for <info>%s</info>", + &[ + PhpMixed::String(c.clone()), + PhpMixed::String(package.clone()), + ], + )), + true, + IOInterface::NORMAL, + ); + + c + } + PhpMixed::String(s) => s.clone(), + _ => String::new(), + }; + + package = format!("{} {}", package, constraint); + } + + if !package.is_empty() { + requires.push(package.clone()); + existing_packages.push(explode(" ", &package)[0].clone()); + } + } + } + + Ok(requires) + } + + /// Given a package name, this determines the best version to use in the require key. + /// + /// This returns a version with the ~ operator prefixed when possible. + /// + /// @throws \InvalidArgumentException + /// @return array{string, string} name version + fn find_best_version_and_name_for_package( + &mut self, + io: &dyn IOInterface, + input: &dyn InputInterface, + name: &str, + platform_repo: Option<&PlatformRepository>, + preferred_stability: &str, + fixed: bool, + ) -> Result<(String, String)> { + // handle ignore-platform-reqs flag if present + let platform_requirement_filter = if input.has_option("ignore-platform-reqs") + && input.has_option("ignore-platform-req") + { + self.get_platform_requirement_filter(input) + } else { + PlatformRequirementFilterFactory::ignore_nothing() + }; + + // find the latest version allowed in this repo set + let repo_set = self.get_repository_set(input, None); + let version_selector = VersionSelector::new_with_platform_repo(repo_set, platform_repo); + let effective_minimum_stability = self.get_minimum_stability(input); + + let package = version_selector.find_best_candidate( + name, + None, + preferred_stability, + &*platform_requirement_filter, + // TODO(phase-b): extra optional arguments (0, $this->getIO()) + ); + + if package.is_none() { + // platform packages can not be found in the pool in versions other than the local platform's has + // so if platform reqs are ignored we just take the user's word for it + if platform_requirement_filter.is_ignored(name) { + return Ok((name.to_string(), "*".to_string())); + } + + // Check if it is a virtual package provided by others + let providers = repo_set.get_providers(name); + if count(&PhpMixed::List( + providers + .iter() + .map(|_| Box::new(PhpMixed::Null)) + .collect(), + )) > 0 + { + let mut constraint = "*".to_string(); + if input.is_interactive() { + let providers_count = providers.len(); + let name_owned = name.to_string(); + let validator: Box<dyn Fn(PhpMixed) -> PhpMixed> = Box::new( + move |value_mixed: PhpMixed| -> PhpMixed { + let value = value_mixed.as_string().unwrap_or("").to_string(); + let parser = VersionParser::new(); + // TODO(phase-b): parse_constraints returns Result + let _ = parser.parse_constraints(&value); + + PhpMixed::String(value) + }, + ); + constraint = self + .get_io() + .ask_and_validate( + format!( + "Package \"<info>{}</info>\" does not exist but is provided by {} packages. Which version constraint would you like to use? [<info>*</info>] ", + name_owned, providers_count, + ), + validator, + Some(3), + PhpMixed::String("*".to_string()), + ) + .as_string() + .map(|s| s.to_string()) + .unwrap_or_default(); + } + + return Ok((name.to_string(), constraint)); + } + + // Check whether the package requirements were the problem + let is_ignore_all = (platform_requirement_filter.as_ref().as_any() as &dyn Any) + .downcast_ref::<IgnoreAllPlatformRequirementFilter>() + .is_some(); + if !is_ignore_all { + let candidate = version_selector.find_best_candidate( + name, + None, + preferred_stability, + &*PlatformRequirementFilterFactory::ignore_all(), + ); + if let Some(candidate) = candidate { + return Err(InvalidArgumentException { + message: sprintf( + &format!( + "Package %s has requirements incompatible with your PHP version, PHP extensions and Composer version{}", + self.get_platform_exception_details(&*candidate, platform_repo), + ), + &[PhpMixed::String(name.to_string())], + ), + code: 0, + } + .into()); + } + } + // Check whether the minimum stability was the problem but the package exists + let package_at_unacceptable = version_selector.find_best_candidate_with_flags( + name, + None, + preferred_stability, + &*platform_requirement_filter, + RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES, + ); + if let Some(package) = package_at_unacceptable { + // we must first verify if a valid package would be found in a lower priority repository + let all_repos_package = version_selector.find_best_candidate_with_flags( + name, + None, + preferred_stability, + &*platform_requirement_filter, + RepositorySet::ALLOW_SHADOWED_REPOSITORIES, + ); + if let Some(all_repos_package) = all_repos_package { + return Err(InvalidArgumentException { + message: format!( + "Package {} exists in {} and {} which has a higher repository priority. The packages from the higher priority repository do not match your minimum-stability and are therefore not installable. That repository is canonical so the lower priority repo's packages are not installable. See https://getcomposer.org/repoprio for details and assistance.", + name, + all_repos_package.get_repository().unwrap().get_repo_name(), + package.get_repository().unwrap().get_repo_name(), + ), + code: 0, + } + .into()); + } + + return Err(InvalidArgumentException { + message: sprintf( + "Could not find a version of package %s matching your minimum-stability (%s). Require it with an explicit version constraint allowing its desired stability.", + &[ + PhpMixed::String(name.to_string()), + PhpMixed::String(effective_minimum_stability.clone()), + ], + ), + code: 0, + } + .into()); + } + // Check whether the PHP version was the problem for all versions + if !is_ignore_all { + let candidate = version_selector.find_best_candidate_with_flags( + name, + None, + preferred_stability, + &*PlatformRequirementFilterFactory::ignore_all(), + RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES, + ); + if let Some(candidate) = candidate { + let mut additional = String::new(); + let no_match = version_selector.find_best_candidate( + name, + None, + preferred_stability, + &*PlatformRequirementFilterFactory::ignore_all(), + ); + if no_match.is_none() { + additional = format!( + "{}{}Additionally, the package was only found with a stability of \"{}\" while your minimum stability is \"{}\".", + PHP_EOL, + PHP_EOL, + candidate.get_stability(), + effective_minimum_stability, + ); + } + + return Err(InvalidArgumentException { + message: sprintf( + &format!( + "Could not find package %s in any version matching your PHP version, PHP extensions and Composer version{}%s", + self.get_platform_exception_details(&*candidate, platform_repo), + ), + &[ + PhpMixed::String(name.to_string()), + PhpMixed::String(additional), + ], + ), + code: 0, + } + .into()); + } + } + + // Check for similar names/typos + let similar = self.find_similar(name)?; + if count(&PhpMixed::List( + similar + .iter() + .map(|_| Box::new(PhpMixed::Null)) + .collect(), + )) > 0 + { + if in_array( + PhpMixed::String(name.to_string()), + &PhpMixed::List( + similar + .iter() + .map(|s| Box::new(PhpMixed::String(s.clone()))) + .collect(), + ), + true, + ) { + return Err(InvalidArgumentException { + message: sprintf( + "Could not find package %s. It was however found via repository search, which indicates a consistency issue with the repository.", + &[PhpMixed::String(name.to_string())], + ), + code: 0, + } + .into()); + } + + if input.is_interactive() { + let result_mixed = io.select( + format!( + "<error>Could not find package {}.</error>\nPick one of these or leave empty to abort:", + name, + ), + similar + .iter() + .map(|s| (s.clone(), s.clone())) + .collect(), + false, + 1, + "No package named \"%s\" is installed.".to_string(), + false, + ); + if let Some(idx_str) = result_mixed.as_string() { + if let Ok(idx) = idx_str.parse::<usize>() { + if let Some(selected) = similar.get(idx) { + return self.find_best_version_and_name_for_package( + io, + input, + selected, + platform_repo, + preferred_stability, + fixed, + ); + } + } + } + } + + return Err(InvalidArgumentException { + message: sprintf( + &format!( + "Could not find package %s.\n\nDid you mean {}?\n %s", + if similar.len() > 1 { "one of these" } else { "this" }, + ), + &[ + PhpMixed::String(name.to_string()), + PhpMixed::String(implode("\n ", &similar)), + ], + ), + code: 0, + } + .into()); + } + + return Err(InvalidArgumentException { + message: sprintf( + "Could not find a matching version of package %s. Check the package spelling, your version constraint and that the package is available in a stability which matches your minimum-stability (%s).", + &[ + PhpMixed::String(name.to_string()), + PhpMixed::String(effective_minimum_stability), + ], + ), + code: 0, + } + .into()); + } + + let package = package.unwrap(); + Ok(( + package.get_pretty_name().to_string(), + if fixed { + package.get_pretty_version().to_string() + } else { + version_selector.find_recommended_require_version(&*package) + }, + )) + } + + /// @return array<string> + fn find_similar(&mut self, package: &str) -> Result<Vec<String>> { + // TODO(phase-b): use anyhow::Result<Result<T, E>> to model PHP try/catch + let results: Vec<SearchResult> = match (|| -> Result<Vec<SearchResult>> { + if self.get_repos_mut().is_none() { + return Err(LogicException { + message: "findSimilar was called before $this->repos was initialized".to_string(), + code: 0, + } + .into()); + } + Ok(self + .get_repos_mut() + .as_mut() + .unwrap() + .search(package.to_string(), 0, None)) + })() { + Ok(r) => r, + Err(e) => { + // PHP: if ($e instanceof \LogicException) throw $e; + // TODO(phase-b): downcast to LogicException + let is_logic: bool = todo!("e instanceof LogicException"); + if is_logic { + return Err(e); + } + + // ignore search errors + return Ok(vec![]); + } + }; + let mut similar_packages: IndexMap<String, i64> = IndexMap::new(); + + let installed_repo = self + .require_composer(None, None) + .get_repository_manager() + .get_local_repository(); + + for result in &results { + // TODO(phase-b): installed_repo.find_package signature mismatch with FindPackageConstraint + if installed_repo + .find_package( + result.name.clone(), + crate::repository::repository_interface::FindPackageConstraint::String( + "*".to_string(), + ), + ) + .is_some() + { + // Ignore installed package + continue; + } + similar_packages.insert(result.name.clone(), levenshtein(package, &result.name)); + } + asort(&mut similar_packages); + + Ok(array_keys(&array_slice(&similar_packages, 0, Some(5)))) + } + + fn get_platform_exception_details( + &self, + candidate: &dyn PackageInterface, + platform_repo: Option<&PlatformRepository>, + ) -> String { + let mut details: Vec<String> = vec![]; + let platform_repo = match platform_repo { + None => return String::new(), + Some(p) => p, + }; + + for link in candidate.get_requires().values() { + if !PlatformRepository::is_platform_package(link.get_target()) { + continue; + } + let platform_pkg = platform_repo.find_package( + link.get_target().to_string(), + crate::repository::repository_interface::FindPackageConstraint::String( + "*".to_string(), + ), + ); + let platform_pkg = match platform_pkg { + None => { + if platform_repo.is_platform_package_disabled(link.get_target()) { + details.push(format!( + "{} {} requires {} {} but it is disabled by your platform config. Enable it again with \"composer config platform.{} --unset\".", + candidate.get_pretty_name(), + candidate.get_pretty_version(), + link.get_target(), + link.get_pretty_constraint().unwrap_or(""), + link.get_target(), + )); + } else { + details.push(format!( + "{} {} requires {} {} but it is not present.", + candidate.get_pretty_name(), + candidate.get_pretty_version(), + link.get_target(), + link.get_pretty_constraint().unwrap_or(""), + )); + } + continue; + } + Some(p) => p, + }; + if !link.get_constraint().matches( + &shirabe_semver::constraint::constraint::Constraint::new( + "==", + platform_pkg.get_version(), + ), + ) { + let mut platform_pkg_version = platform_pkg.get_pretty_version().to_string(); + let platform_extra = platform_pkg.get_extra(); + let has_config_platform = platform_extra.contains_key("config.platform"); + let is_complete = (platform_pkg.as_any() as &dyn Any) + .downcast_ref::<dyn CompletePackageInterface>() + .is_some(); + if has_config_platform && is_complete { + // TODO(phase-b): platform_pkg.get_description() via CompletePackageInterface + platform_pkg_version = + format!("{} ({})", platform_pkg_version, todo!("platform_pkg.get_description()")); + } + details.push(format!( + "{} {} requires {} {} which does not match your installed version {}.", + candidate.get_pretty_name(), + candidate.get_pretty_version(), + link.get_target(), + link.get_pretty_constraint().unwrap_or(""), + platform_pkg_version, + )); + } + } + + if count(&PhpMixed::List( + details + .iter() + .map(|_| Box::new(PhpMixed::Null)) + .collect(), + )) == 0 + { + return String::new(); + } + + format!(":{} - {}", PHP_EOL, implode(&format!("{} - ", PHP_EOL), &details)) + } +} |
