From f749a47804cd296a3059cd3f8079c62dbaa5fdc0 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 7 Aug 2026 07:26:48 +0900 Subject: 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) --- crates/shirabe/src/command/show_command.rs | 5146 ++++++++++++++-------------- 1 file changed, 2572 insertions(+), 2574 deletions(-) (limited to 'crates/shirabe/src/command/show_command.rs') 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,2831 +81,2829 @@ 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>, - output: std::rc::Rc>, - ) -> anyhow::Result { - *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("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."); - } - - 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("You are using the option \"ignore\" for action other than \"outdated\", it will be ignored."); - } - 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); - } + /// 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| { + if input.get_option("available")?.to_bool() || input.get_option("all")?.to_bool() { + return this.suggest_available_package_incl_platform().call( + this, + input, + suggestions, + ); + } - 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)"); + if input.get_option("platform")?.to_bool() { + return this + .suggest_platform_package() + .call(this, input, suggestions); + } - return Ok(1); - } + this.suggest_installed_package(false, false) + .call(this, input, suggestions) + })) + } - 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", - ); + #[allow(clippy::too_many_arguments, reason = "to keep PHP signature")] + fn print_packages( + &self, + packages: &[IndexMap], + indent: &str, + write_version: bool, + write_latest: bool, + write_description: bool, + width: usize, + version_length: usize, + name_length: usize, + latest_length: usize, + write_release_date: bool, + release_date_length: usize, + ) { + let io = self.get_io(); + let pad_name = write_version || write_latest || write_release_date || write_description; + let pad_version = write_latest || write_release_date || write_description; + let pad_latest = write_description || write_release_date; + let pad_release_date = write_description; + for package in packages.iter() { + let link = package + .get("source") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .or_else(|| { + package + .get("homepage") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + }) + .unwrap_or_default(); + let name = package + .get("name") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + if !link.is_empty() { + let pad = if pad_name && name_length > name.len() { + name_length - name.len() + } else { + 0 + }; + io.write_no_newline(&format!( + "{}{}{}", + indent, + OutputFormatter::escape(&link).expect("OutputFormatter::escape failed"), + name, + " ".repeat(pad) + )); + } else { + let width_pad = if pad_name { name_length } else { 0 }; + io.write_no_newline(&format!("{}{:{:", + style, + latest_version, + style, + width = width_pad + )); + if write_release_date + && let Some(age) = package.get("release-age").and_then(|v| v.as_string()) + { + let width_pad = if pad_release_date { + release_date_length + } else { + 0 + }; + io.write_no_newline(&format!(" {: remaining as usize { + description = format!( + "{}...", + description + .chars() + .take((remaining as usize).saturating_sub(3)) + .collect::() + ); + } + } else { + // Fallback when mbstring is not available: do a conservative byte-based cut. + // Ensure cut length is non-negative and leave room for the ellipsis. + let cut = (remaining - 3).max(0) as usize; + if description.len() > cut { + description = format!("{}...", &description[..cut]); + } + } - return Ok(1); + io.write_no_newline(&format!(" {}", description)); + } + if package.contains_key("path") { + let path_str = match package.get("path") { + Some(PhpMixed::String(s)) => s.clone(), + _ => "null".to_string(), + }; + io.write_no_newline(&format!(" {}", path_str)); + } + io.write(""); + if let Some(warning) = package.get("warning").and_then(|v| v.as_string()) { + io.write(&format!("{}", warning)); + } } + } - 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); - } + pub(crate) fn get_root_requires(&self) -> Vec { + let composer_rc = self.try_composer(None, None); + let composer_rc = match composer_rc { + None => return vec![], + Some(c) => c, + }; + let composer = crate::composer::composer_full(&composer_rc); - 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 - )); + let root_package = composer.get_package(); - return Ok(1); + let mut combined: IndexMap = IndexMap::new(); + for (k, v) in root_package.get_requires().iter() { + combined.insert(k.clone(), v.clone()); } - - let platform_req_filter = self.get_platform_requirement_filter(input.clone())?; - - // init repos - let mut platform_overrides: IndexMap = 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(); - } + for (k, v) in root_package.get_dev_requires().iter() { + combined.insert(k.clone(), v.clone()); } - let platform_repo = - PlatformRepositoryHandle::new(PlatformRepository::new(vec![], platform_overrides)?); - let mut locked_repo: Option = None; - - // The single-package $package binding from PHP gets surfaced here. - let mut single_package: Option = None; - let mut versions_map: IndexMap = 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 = 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 = - 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() + combined.keys().map(|k| strtolower(k)).collect() + } + + pub(crate) fn get_version_style( + &self, + latest_package: PackageInterfaceHandle, + package: PackageInterfaceHandle, + ) -> anyhow::Result { + Ok( + Self::update_status_to_version_style(&Self::get_update_status( + latest_package, + package, + )?) + .to_string(), + ) + } + + /// finds a package by name and version if provided + pub(crate) fn get_package( + &self, + installed_repo: &RepositoryInterfaceHandle, + repos: &RepositoryInterfaceHandle, + name: &str, + version: PhpMixed, + ) -> anyhow::Result<( + Option, + IndexMap, + )> { + let name = strtolower(name); + let constraint: Option = match &version { + PhpMixed::String(s) => Some(self.version_parser.borrow().parse_constraints(s)?), + PhpMixed::Null => None, + _ => None, // already a ConstraintInterface + }; + + let policy = DefaultPolicy::new(false, false, None); + let mut repository_set = RepositorySet::new( + "dev", + IndexMap::new(), + Vec::new(), + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + ); + repository_set.allow_installed_repositories(true); + repository_set.add_repository(repos.clone())?; + + let mut matched_package: Option = None; + let mut versions: IndexMap = IndexMap::new(); + let mut pool = if PlatformRepository::is_platform_package(&name) { + repository_set.create_pool_with_all_packages()? + } else { + repository_set.create_pool_for_package(&name, None)? + }; + let matches = pool.what_provides(&name, constraint.as_ref()); + let mut literals: Vec = Vec::new(); + for package in matches.iter() { + // avoid showing the 9999999-dev alias if the default branch has no branch-alias set + let mut p: crate::package::PackageInterfaceHandle = package.clone(); + if let Some(alias) = p.as_alias() + && p.get_version() == VersionParser::DEFAULT_BRANCH_ALIAS { - composite_input.push(r.clone()); + p = alias.get_alias_of().into(); } - 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 = 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 = vec![installed_repo.clone()]; - for (_k, v) in default_repos.into_iter() { - composite_input.push(v); + + // select an exact match if it is in the installed repo and no specific version was required + if version.is_null() && installed_repo.has_package(p.clone())? { + matched_package = Some(p.clone()); } - 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(), + + versions.insert(p.get_pretty_version(), p.get_version()); + literals.push(p.get_id()); + } + + // select preferred package according to policy rules + if matched_package.is_none() && !literals.is_empty() { + let preferred = policy.select_preferred_packages(&pool, literals.clone(), None); + matched_package = Some(pool.literal_to_package(preferred[0])); + } + + if let Some(ref mp) = matched_package + && mp.as_complete().is_none() + { + return Err(LogicException { + message: format!( + "ShowCommand::getPackage can only work with CompletePackageInterface, but got {}", + shirabe_php_shim::get_class(&PhpMixed::Null) + ), code: 0, } .into()); + } + + let matched_package = matched_package.and_then(|mp| mp.as_complete()); + Ok((matched_package, versions)) + } + + /// Prints package info. + pub(crate) fn print_package_info( + &self, + package: CompletePackageInterfaceHandle, + versions: &IndexMap, + installed_repo: &mut dyn RepositoryInterface, + latest_package: Option, + ) -> anyhow::Result<()> { + self.print_meta(package.clone(), versions, installed_repo, latest_package)?; + self.print_links(package.clone(), Link::TYPE_REQUIRE, None); + self.print_links( + package.clone(), + Link::TYPE_DEV_REQUIRE, + Some("requires (dev)"), + ); + + if !package.get_suggests().is_empty() { + self.get_io().write("\nsuggests"); + for (suggested, reason) in package.get_suggests().iter() { + self.get_io() + .write(&format!("{} {}", suggested, reason)); } - 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 + } + + self.print_links(package.clone(), Link::TYPE_PROVIDE, None); + self.print_links(package.clone(), Link::TYPE_CONFLICT, None); + self.print_links(package, Link::TYPE_REPLACE, None); + Ok(()) + } + + /// Prints package metadata. + pub(crate) fn print_meta( + &self, + package: CompletePackageInterfaceHandle, + versions: &IndexMap, + installed_repo: &mut dyn RepositoryInterface, + latest_package: Option, + ) -> anyhow::Result<()> { + let is_installed_package = !PlatformRepository::is_platform_package(&package.get_name()) + && installed_repo.has_package(package.clone().into())?; + + self.get_io().write(&format!( + "name : {}", + package.get_pretty_name() + )); + self.get_io().write(&format!( + "descrip. : {}", + package.get_description().unwrap_or_default() + )); + let keywords = package.get_keywords(); + self.get_io() + .write(&format!("keywords : {}", keywords.join(", "))); + self.print_versions(package.clone(), versions, installed_repo)?; + if is_installed_package && let Some(rd) = package.get_release_date() { + let rel = self.get_relative_time(&rd); + self.get_io().write(&format!( + "released : {}, {}", + rd.format(date_format_to_strftime("Y-m-d")), + rel + )); + } + let latest: PackageInterfaceHandle = if let Some(latest) = latest_package { + let style = self.get_version_style(latest.clone(), package.clone().into())?; + let released_time = match latest.get_release_date() { + None => String::new(), + Some(rd) => { + let rel = self.get_relative_time(&rd); + format!( + " released {}, {}", + rd.format(date_format_to_strftime("Y-m-d")), + rel + ) } }; - let root_pkg = composer_local.get_package(); + self.get_io().write(&format!( + "latest : <{}>{}{}", + style, + latest.get_pretty_version(), + style, + released_time + )); + latest + } else { + package.clone().into() + }; + self.get_io() + .write(&format!("type : {}", package.get_type())); + self.print_licenses(package.clone()); + self.get_io().write(&format!( + "homepage : {}", + package.get_homepage().unwrap_or_default() + )); + self.get_io().write(&format!( + "source : [{}] {} {}", + package.get_source_type().unwrap_or_default(), + package.get_source_url().unwrap_or_default(), + package.get_source_reference().unwrap_or_default() + )); + self.get_io().write(&format!( + "dist : [{}] {} {}", + package.get_dist_type().unwrap_or_default(), + package.get_dist_url().unwrap_or_default(), + package.get_dist_reference().unwrap_or_default() + )); + if is_installed_package { + let path: Option = self.require_composer(None, None).ok().and_then(|c| { + let installation_manager = c.borrow_partial().get_installation_manager(); + + installation_manager + .borrow_mut() + .get_install_path(package.clone().into()) + }); + if let Some(p) = path { + self.get_io().write(&format!( + "path : {}", + realpath(&p).unwrap_or_default() + )); + } else { + self.get_io().write("path : null"); + } + } + self.get_io().write(&format!( + "names : {}", + package.get_names(true).join(", ") + )); + + if let Some(c) = latest.as_complete() + && c.is_abandoned() + { + let replacement = match c.get_replacement_package() { + Some(rp) => format!(" The author suggests using the {} package instead.", rp), + None => String::new(), + }; - 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 = 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])); + self.get_io().write_error(&format!( + "Attention: This package is abandoned and no longer maintained.{}", + replacement + )); + } + + let support = package.get_support(); + if !support.is_empty() { + self.get_io().write("\nsupport"); + for (r#type, value) in support.iter() { + self.get_io() + .write(&format!("{} : {}", r#type, value)); } + } - if installed_repo.get_packages()?.is_empty() { - let has_non_platform_reqs = |reqs: &IndexMap| -> bool { - reqs.keys() - .any(|name| !PlatformRepository::is_platform_package(name)) - }; + let autoload_config = package.get_autoload(); + if !autoload_config.is_empty() { + self.get_io().write("\nautoload"); + for (r#type, autoloads) in autoload_config.iter() { + self.get_io() + .write(&format!("{}", r#type)); - if has_non_platform_reqs(&root_pkg.get_requires()) - || has_non_platform_reqs(&root_pkg.get_dev_requires()) + if r#type == "psr-0" || r#type == "psr-4" { + if let PhpMixed::Array(map) = autoloads { + for (name, path) in map.iter() { + let path_str = match path { + PhpMixed::List(l) => l + .iter() + .filter_map(|p| p.as_string().map(|s| s.to_string())) + .collect::>() + .join(", "), + PhpMixed::String(s) if !s.is_empty() => s.clone(), + _ => ".".to_string(), + }; + let name_disp = if name.is_empty() { "*" } else { name }; + self.get_io() + .write(&format!("{} => {}", name_disp, path_str)); + } + } + } else if r#type == "classmap" + && let PhpMixed::List(l) = autoloads { - // Borrow is local; release composer_local borrow first. - let _ = root_pkg; - self.get_io().write_error("No dependencies installed. Try running composer install or update."); + let joined: Vec = l + .iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect(); + self.get_io().write(&joined.join(", ")); } } + let include_paths = package.get_include_paths(); + if !include_paths.is_empty() { + self.get_io().write("include-path"); + self.get_io().write(&include_paths.join(", ")); + } } - 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))?; - } + Ok(()) + } - 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)); + /// Prints all available versions of this package and highlights the installed one if any. + pub(crate) fn print_versions( + &self, + package: CompletePackageInterfaceHandle, + versions: &IndexMap, + installed_repo: &mut dyn RepositoryInterface, + ) -> anyhow::Result<()> { + let mut versions_keys: Vec = versions.keys().cloned().collect(); + versions_keys = Semver::rsort(versions_keys)?; + + // highlight installed version + let installed_packages = installed_repo.find_packages(&package.get_name(), None)?; + if !installed_packages.is_empty() { + for installed_package in installed_packages.iter() { + let installed_version = installed_package.get_pretty_version(); + let key_map: IndexMap = versions_keys + .iter() + .map(|v| (v.clone(), v.clone())) + .collect(); + if let Some(found) = array_search(&installed_version, &key_map) + && let Some(idx) = versions_keys.iter().position(|v| v == &found) + { + versions_keys[idx] = format!("* {}", installed_version); + } + } } - let package_filter: Option = input - .borrow() - .get_argument("package")? - .as_string() - .map(|s| s.to_string()); + let versions_str = versions_keys.join(", "); - // 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")?, - )?; + self.get_io() + .write(&format!("versions : {}", versions_str)); - 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::>(), - ) - { - return Err(InvalidArgumentException { - message: format!( - "Package \"{}\" is installed but not a direct dependent of the root package.", - pkg.get_name() - ), - code: 0, - } - .into()); - } + Ok(()) + } - 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"); - } + /// print link objects + pub(crate) fn print_links( + &self, + package: CompletePackageInterfaceHandle, + link_type: &str, + title: Option<&str>, + ) { + let title = title.unwrap_or(link_type); + let io = self.get_io(); + let links = package.get_links_for_type(link_type); + if !links.is_empty() { + io.write(&format!("\n{}", title)); - return Err(InvalidArgumentException { - message: format!("Package \"{}\" not found{}.", pf, hint), - code: 0, - } - .into()); + for link in links.iter() { + io.write(&format!( + "{} {}", + link.1.get_target(), + link.1.get_pretty_constraint(), + )); } - single_package = matched_package; - versions_map = vers; } + } - if let Some(ref package) = single_package { - // assert(isset($versions)); + /// Prints the licenses of a package with metadata + pub(crate) fn print_licenses(&self, package: CompletePackageInterfaceHandle) { + let spdx_licenses = SpdxLicenses::new(); - 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); + let licenses = package.get_license(); + let io = self.get_io(); - if format == "json" { - let mut wrapper: IndexMap = 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]); + for license_id in licenses.iter() { + let license = spdx_licenses.get_license_by_identifier(license_id); + + let out = match license { + None => license_id.clone(), + Some(license) => { + if license.is_osi_approved { + format!( + "{} ({}) (OSI approved) {}", + license.name, license_id, license.url + ) + } else { + format!("{} ({}) {}", license.name, license_id, license.url) + } } + }; - return Ok(exit_code); - } + io.write(&format!("license : {}", out)); + } + } - let mut latest_package: Option = 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"); - } + /// Prints package info in JSON format. + pub(crate) fn print_package_info_as_json( + &self, + package: CompletePackageInterfaceHandle, + versions: &IndexMap, + installed_repo: &mut dyn RepositoryInterface, + latest_package: Option, + ) -> anyhow::Result<()> { + let mut json: IndexMap = IndexMap::new(); + json.insert( + "name".to_string(), + PhpMixed::String(package.get_pretty_name()), + ); + json.insert( + "description".to_string(), + PhpMixed::String(package.get_description().unwrap_or_default()), + ); + let keywords: Vec = package + .get_keywords() + .into_iter() + .map(PhpMixed::String) + .collect(); + json.insert("keywords".to_string(), PhpMixed::List(keywords)); + json.insert("type".to_string(), PhpMixed::String(package.get_type())); + json.insert( + "homepage".to_string(), + match package.get_homepage() { + Some(h) => PhpMixed::String(h), + None => PhpMixed::Null, + }, + ); + json.insert( + "names".to_string(), + PhpMixed::List( + package + .get_names(true) + .into_iter() + .map(PhpMixed::String) + .collect(), + ), + ); - return Ok(exit_code); - } + json = Self::append_versions(json, versions); + json = Self::append_licenses(json, package.clone()); - 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, - )?; - } + let latest: PackageInterfaceHandle = if let Some(latest) = latest_package { + json.insert( + "latest".to_string(), + PhpMixed::String(latest.get_pretty_version()), + ); + latest + } else { + package.clone().into() + }; - return Ok(exit_code); + if package.get_source_type().is_some() { + let mut src: IndexMap = IndexMap::new(); + src.insert( + "type".to_string(), + PhpMixed::String(package.get_source_type().unwrap_or_default()), + ); + src.insert( + "url".to_string(), + PhpMixed::String(package.get_source_url().unwrap_or_default()), + ); + src.insert( + "reference".to_string(), + PhpMixed::String(package.get_source_reference().unwrap_or_default()), + ); + json.insert( + "source".to_string(), + PhpMixed::Array(src.into_iter().collect()), + ); } - // 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> = Vec::new(); - for package in packages.iter() { - if in_array_strict( - package.get_name(), - &root_requires - .iter() - .map(|s| PhpMixed::String(s.clone())) - .collect::>(), - ) { - array_tree.push(self.generate_package_tree( - package.clone(), - &installed_repo, - &repos, - )); + if package.get_dist_type().is_some() { + let mut dst: IndexMap = IndexMap::new(); + dst.insert( + "type".to_string(), + PhpMixed::String(package.get_dist_type().unwrap_or_default()), + ); + dst.insert( + "url".to_string(), + PhpMixed::String(package.get_dist_url().unwrap_or_default()), + ); + dst.insert( + "reference".to_string(), + PhpMixed::String(package.get_dist_reference().unwrap_or_default()), + ); + json.insert( + "dist".to_string(), + PhpMixed::Array(dst.into_iter().collect()), + ); + } + + if !PlatformRepository::is_platform_package(&package.get_name()) + && installed_repo.has_package(package.clone().into())? + { + let composer = self.require_composer(None, None)?; + let installation_manager = composer.borrow_partial().get_installation_manager(); + let path: Option = installation_manager + .borrow_mut() + .get_install_path(package.clone().into()); + match path { + Some(p) => { + if let Some(r) = realpath(&p) { + json.insert("path".to_string(), PhpMixed::String(r)); + } + } + None => { + json.insert("path".to_string(), PhpMixed::Null); } } - if format == "json" { - let mut wrapper: IndexMap = IndexMap::new(); - wrapper.insert( - "installed".to_string(), - PhpMixed::List( - array_tree - .into_iter() - .map(|m| PhpMixed::Array(m.into_iter().collect())) - .collect(), - ), + if let Some(rd) = package.get_release_date() { + json.insert( + "released".to_string(), + PhpMixed::String(rd.format(DATE_ATOM).to_string()), ); - 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> = IndexMap::new(); - let mut package_filter_regex: Option = 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> = None; - if input.borrow().get_option("direct")?.as_bool() == Some(true) { - package_list_filter = Some(self.get_root_requires()); + if let Some(c) = latest.as_complete() + && c.is_abandoned() + { + json.insert( + "replacement".to_string(), + match c.get_replacement_package() { + Some(rp) => PhpMixed::String(rp), + None => PhpMixed::Null, + }, + ); } - 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", + if !package.get_suggests().is_empty() { + let mut s: IndexMap = IndexMap::new(); + for (k, v) in package.get_suggests().iter() { + s.insert(k.clone(), PhpMixed::String(v.clone())); + } + json.insert( + "suggests".to_string(), + PhpMixed::Array(s.into_iter().collect()), ); - 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::() - .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::() { - 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::>(), - ), - }; - 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())); - } - } + if !package.get_support().is_empty() { + let mut s: IndexMap = IndexMap::new(); + for (k, v) in package.get_support().iter() { + s.insert(k.clone(), PhpMixed::String(v.clone())); } + json.insert( + "support".to_string(), + PhpMixed::Array(s.into_iter().collect()), + ); } - 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::>() - }) - .unwrap_or_default(), - "{^(?:%s)$}iD", - ); - let indent = if show_all_types { " " } else { "" }; - let mut latest_packages: IndexMap = - IndexMap::new(); - let mut exit_code: i64 = 0; - let mut view_data: IndexMap>> = IndexMap::new(); - let mut view_meta_data: IndexMap = 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; + json = Self::append_autoload(json, package.clone()); - 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; - } + if !package.get_include_paths().is_empty() { + json.insert( + "include_path".to_string(), + PhpMixed::List( + package + .get_include_paths() + .into_iter() + .map(PhpMixed::String) + .collect(), + ), + ); + } - latest_packages.insert(package.get_pretty_name(), latest.unwrap()); - } - } - } + json = Self::append_links(json, package); - 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"); + self.get_io().write(&JsonFile::encode(&PhpMixed::Array( + json.into_iter().collect(), + ))?); + Ok(()) + } - let mut has_outdated_packages = false; + fn append_versions( + mut json: IndexMap, + versions: &IndexMap, + ) -> IndexMap { + let mut versions_pairs: Vec<(String, String)> = versions + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + // uasort($versions, 'version_compare'); + versions_pairs.sort_by(|a, b| { + if version_compare(&a.1, &b.1, "<") { + std::cmp::Ordering::Less + } else if version_compare(&a.1, &b.1, ">") { + std::cmp::Ordering::Greater + } else { + std::cmp::Ordering::Equal + } + }); + versions_pairs.reverse(); + let keys: Vec = versions_pairs + .into_iter() + .map(|(k, _)| PhpMixed::String(k)) + .collect(); + json.insert("versions".to_string(), PhpMixed::List(keys)); - 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, - }); - } + json + } - let mut view_type: Vec> = Vec::new(); - for package_or_name in type_packages.values() { - let mut package_view_data: IndexMap = 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 - }; + fn append_licenses( + mut json: IndexMap, + package: CompletePackageInterfaceHandle, + ) -> IndexMap { + let licenses = package.get_license(); + if !licenses.is_empty() { + let spdx_licenses = SpdxLicenses::new(); - // 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; + let mapped: Vec = licenses + .into_iter() + .map(|license_id| { + let license = spdx_licenses.get_license_by_identifier(&license_id); + match license { + None => PhpMixed::String(license_id), + Some(l) => { + // The 'osi' key holds the license id string, not the OSI-approved flag. + let mut m: IndexMap = IndexMap::new(); + m.insert("name".to_string(), PhpMixed::String(l.name)); + m.insert("osi".to_string(), PhpMixed::String(license_id)); + m.insert("url".to_string(), PhpMixed::String(l.url)); + PhpMixed::Array(m.into_iter().collect()) } + } + }) + .collect(); + json.insert("licenses".to_string(), PhpMixed::List(mapped)); + } - if input.borrow().get_option("outdated")?.as_bool() == Some(true) - || input.borrow().get_option("strict")?.as_bool() == Some(true) - { - has_outdated_packages = true; - } + json + } - 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::>(), - )), - ); - 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), - ); + fn append_autoload( + mut json: IndexMap, + package: CompletePackageInterfaceHandle, + ) -> IndexMap { + let autoload_config = package.get_autoload(); + if !autoload_config.is_empty() { + let mut autoload: IndexMap = IndexMap::new(); - 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 = 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); - } - } + for (r#type, autoloads) in autoload_config.iter() { + if r#type == "psr-0" || r#type == "psr-4" { + let mut psr: IndexMap = IndexMap::new(); - 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), + if let PhpMixed::Array(map) = autoloads { + for (name, path) in map.iter() { + let mut path_val = path.clone(); + let is_empty_path = match &path_val { + PhpMixed::String(s) => s.is_empty(), + PhpMixed::Null => true, + _ => false, }; - } + if is_empty_path { + path_val = PhpMixed::String(".".to_string()); + } - 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()); + let key = if name.is_empty() { + "*".to_string() + } else { + name.clone() + }; + psr.insert(key, path_val); + } } - 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; + + autoload.insert(r#type.clone(), PhpMixed::Array(psr.into_iter().collect())); + } else if r#type == "classmap" { + autoload.insert("classmap".to_string(), autoloads.clone()); } } + + json.insert( + "autoload".to_string(), + PhpMixed::Array(autoload.into_iter().collect()), + ); } - if format == "json" { - let mut json_map: IndexMap = 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(), - ), + json + } + + fn append_links( + mut json: IndexMap, + package: CompletePackageInterfaceHandle, + ) -> IndexMap { + for link_type in Link::types().iter() { + json = Self::append_link(json, package.clone(), link_type); + } + + json + } + + fn append_link( + mut json: IndexMap, + package: CompletePackageInterfaceHandle, + link_type: &str, + ) -> IndexMap { + let links = package.get_links_for_type(link_type); + + if !links.is_empty() { + let mut m: IndexMap = IndexMap::new(); + for link in links.iter() { + m.insert( + link.1.get_target().to_string(), + PhpMixed::String(link.1.get_pretty_constraint().to_string()), ); } - 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"); + json.insert( + link_type.to_string(), + PhpMixed::Array(m.into_iter().collect()), + ); + } + + json + } + + /// Init styles for tree + pub(crate) fn init_styles(&self, output: std::rc::Rc>) { + *self.colors.borrow_mut() = vec![ + "green".to_string(), + "yellow".to_string(), + "cyan".to_string(), + "magenta".to_string(), + "blue".to_string(), + ]; + + for color in self.colors.borrow().iter() { + let style = OutputFormatterStyle::new(Some(color.as_str()), None, vec![]); + output + .borrow() + .get_formatter() + .borrow_mut() + .set_style(color, Box::new(style)); + } + } + + /// Display the tree + pub(crate) fn display_package_tree(&self, array_tree: Vec>) { + for package in array_tree.iter() { + let name = package + .get("name") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + self.get_io() + .write_no_newline(&format!("{}", name)); + let version = package + .get("version") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + self.get_io().write_no_newline(&format!(" {}", version)); + if let Some(description) = package.get("description").and_then(|v| v.as_string()) { + let trimmed = description.split(['\r', '\n']).next().unwrap_or(""); + self.get_io().write(&format!(" {}", trimmed)); + } else { + // output newline + self.get_io().write(""); + } + + if let Some(requires) = package.get("requires").and_then(|v| v.as_list()).cloned() { + let mut tree_bar = "├".to_string(); + let mut j = 0_usize; + let total = requires.len(); + for require_mixed in requires.iter() { + let require = match require_mixed.as_array() { + Some(a) => a, + None => continue, + }; + let require_name = require + .get("name") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + j += 1; + if j == total { + tree_bar = "└".to_string(); } - } else { - io.write_error("Color legend:"); - io.write_error("- patch or minor release available - update recommended"); - io.write_error( - "- major release available - update possible", + let level: usize = 1; + let color = self.colors.borrow().get(level).cloned().unwrap_or_default(); + let info = format!( + "{}──<{}>{} {}", + tree_bar, + color, + require_name, + color, + require + .get("version") + .and_then(|v| v.as_string()) + .unwrap_or("") + ); + self.write_tree_line(&info); + + tree_bar = tree_bar.replace('└', " "); + let packages_in_tree: Vec = vec![ + PhpMixed::String(name.clone()), + PhpMixed::String(require_name.clone()), + ]; + + self.display_tree( + &PhpMixed::Array( + require + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + ), + &packages_in_tree, + &tree_bar, + level + 1, ); - if input.borrow().get_option("outdated")?.as_bool() != Some(true) { - io.write_error("- up to date version"); - } } } + } + } + + /// Generate the package tree + pub(crate) fn generate_package_tree( + &self, + package: PackageInterfaceHandle, + installed_repo: &RepositoryInterfaceHandle, + remote_repos: &RepositoryInterfaceHandle, + ) -> IndexMap { + let requires = { + let mut r: IndexMap = package.get_requires(); + r.sort_keys(); + r + }; + let mut children: Vec = Vec::new(); + for (require_name, require) in requires.iter() { + let packages_in_tree: Vec = vec![ + PhpMixed::String(package.get_name().to_string()), + PhpMixed::String(require_name.clone()), + ]; + + let mut tree_child_desc: IndexMap = IndexMap::new(); + tree_child_desc.insert("name".to_string(), PhpMixed::String(require_name.clone())); + tree_child_desc.insert( + "version".to_string(), + PhpMixed::String(require.get_pretty_constraint().to_string()), + ); + + let deep_children = self + .add_tree( + require_name, + require, + installed_repo, + remote_repos, + &packages_in_tree, + ) + .unwrap_or_default(); + + if !deep_children.is_empty() { + tree_child_desc.insert( + "requires".to_string(), + PhpMixed::List( + deep_children + .into_iter() + .map(|m| PhpMixed::Array(m.into_iter().collect())) + .collect(), + ), + ); + } - let width = self.get_terminal_width(); + children.push(PhpMixed::Array(tree_child_desc.into_iter().collect())); + } + let mut tree: IndexMap = IndexMap::new(); + tree.insert( + "name".to_string(), + PhpMixed::String(package.get_pretty_name()), + ); + tree.insert( + "version".to_string(), + PhpMixed::String(package.get_pretty_version()), + ); + tree.insert( + "description".to_string(), + match package.as_complete() { + Some(c) => match c.get_description() { + Some(d) => PhpMixed::String(d), + None => PhpMixed::Null, + }, + None => PhpMixed::String(String::new()), + }, + ); - 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; + if !children.is_empty() { + tree.insert("requires".to_string(), PhpMixed::List(children)); + } - 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; + tree + } - if latest_fits && !self.get_io().is_decorated() { - latest_length += 2; - } + /// Display a package tree + pub(crate) fn display_tree( + &self, + package: &PhpMixed, + packages_in_tree: &[PhpMixed], + previous_tree_bar: &str, + level: usize, + ) { + let previous_tree_bar = previous_tree_bar.replace('├', "│"); + let arr = match package.as_array() { + Some(a) => a, + None => return, + }; + let requires = match arr.get("requires").and_then(|v| v.as_list()).cloned() { + Some(l) => l, + None => return, + }; + let mut tree_bar = format!("{} ├", previous_tree_bar); + let mut i = 0_usize; + let total = requires.len(); + for require_mixed in requires.iter() { + let mut current_tree = packages_in_tree.to_vec(); + i += 1; + if i == total { + tree_bar = format!("{} └", previous_tree_bar); + } + let color_ident = level % self.colors.borrow().len(); + let color = self + .colors + .borrow() + .get(color_ident) + .cloned() + .unwrap_or_default(); - if show_all_types { - if r#type == "available" { - self.get_io() - .write(&format!("{}:", r#type)); - } else { - self.get_io().write(&format!("{}:", r#type)); - } - } + let require = match require_mixed.as_array() { + Some(a) => a, + None => continue, + }; + let require_name = require + .get("name") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + let require_version = require + .get("version") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); - if write_latest && input.borrow().get_option("direct")?.as_bool() != Some(true) { - let mut direct_deps: Vec> = Vec::new(); - let mut transitive_deps: Vec> = 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()); - } - } + let circular_warn = if in_array_strict(require_name.clone(), ¤t_tree) { + "(circular dependency aborted here)" + } else { + "" + }; + let info = format!( + "{}──<{}>{} {} {}", + tree_bar, color, require_name, color, require_version, circular_warn + ) + .trim_end() + .to_string(); + self.write_tree_line(&info); - self.get_io().write_error(""); - self.get_io() - .write_error("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( - "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, + tree_bar = tree_bar.replace('└', " "); + + current_tree.push(PhpMixed::String(require_name.clone())); + self.display_tree(require_mixed, ¤t_tree, &tree_bar, level + 1); + } + } + + /// Display a package tree + pub(crate) fn add_tree( + &self, + name: &str, + link: &Link, + installed_repo: &RepositoryInterfaceHandle, + remote_repos: &RepositoryInterfaceHandle, + packages_in_tree: &[PhpMixed], + ) -> anyhow::Result>> { + let mut children: Vec> = Vec::new(); + let version_arg: PhpMixed = if link.get_pretty_constraint() == "self.version" { + // pass the ConstraintInterface object — signal via Null in this scalar shape + PhpMixed::Null + } else { + PhpMixed::String(link.get_pretty_constraint().to_string()) + }; + let (package, _) = self.get_package(installed_repo, remote_repos, name, version_arg)?; + if let Some(package) = package { + let mut requires = package.get_requires(); + requires.sort_keys(); + for (require_name, require) in requires.iter() { + let mut current_tree = packages_in_tree.to_vec(); + + let mut tree_child_desc: IndexMap = IndexMap::new(); + tree_child_desc.insert("name".to_string(), PhpMixed::String(require_name.clone())); + tree_child_desc.insert( + "version".to_string(), + PhpMixed::String(require.get_pretty_constraint().to_string()), + ); + + if !in_array_strict(require_name.clone(), ¤t_tree) { + current_tree.push(PhpMixed::String(require_name.clone())); + let deep_children = self.add_tree( + require_name, + require, + installed_repo, + remote_repos, + ¤t_tree, + )?; + if !deep_children.is_empty() { + tree_child_desc.insert( + "requires".to_string(), + PhpMixed::List( + deep_children + .into_iter() + .map(|m| PhpMixed::Array(m.into_iter().collect())) + .collect(), + ), ); } } - if show_all_types { - self.get_io().write(""); - } + children.push(tree_child_desc); } } - Ok(exit_code) + Ok(children) } - fn initialize( - &self, - input: std::rc::Rc>, - output: std::rc::Rc>, - ) -> anyhow::Result<()> { - base_command_initialize(self, input, output) + fn update_status_to_version_style(update_status: &str) -> &'static str { + // 'up-to-date' is printed green + // 'semver-safe-update' is printed red + // 'update-possible' is printed yellow + match update_status { + "up-to-date" => "info", + "semver-safe-update" => "highlight", + "update-possible" => "comment", + _ => "comment", + } } - 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) - } + fn get_update_status( + latest_package: PackageInterfaceHandle, + package: PackageInterfaceHandle, + ) -> anyhow::Result { + if latest_package.get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev) + == package.get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev) + { + return Ok("up-to-date".to_string()); + } - shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); -} + let mut constraint = package.get_version(); + if !constraint.starts_with("dev-") { + constraint = format!("^{}", constraint); + } + if !latest_package.get_version().is_empty() + && Semver::satisfies(latest_package.get_version(), constraint)? + { + // it needs an immediate semver-compliant upgrade + return Ok("semver-safe-update".to_string()); + } -impl BaseCommand for ShowCommand { - fn base_command_data(&self) -> &crate::command::BaseCommandData { - &self.base_command_data + // it needs an upgrade but has potential BC breaks so is not urgent + Ok("update-possible".to_string()) } - 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| { - if input.get_option("available")?.to_bool() || input.get_option("all")?.to_bool() { - return this.suggest_available_package_incl_platform().call( - this, - input, - suggestions, - ); - } - - if input.get_option("platform")?.to_bool() { - return this - .suggest_platform_package() - .call(this, input, suggestions); - } + fn write_tree_line(&self, line: &str) { + let io = self.get_io(); + let mut line = line.to_string(); + if !io.is_decorated() { + line = line + .replace('└', "`-") + .replace('├', "|-") + .replace("──", "-") + .replace('│', "|"); + } - this.suggest_installed_package(false, false) - .call(this, input, suggestions) - })) + io.write(&line); } + /// Given a package, this finds the latest package matching it #[allow(clippy::too_many_arguments, reason = "to keep PHP signature")] - fn print_packages( + fn find_latest_package( &self, - packages: &[IndexMap], - indent: &str, - write_version: bool, - write_latest: bool, - write_description: bool, - width: usize, - version_length: usize, - name_length: usize, - latest_length: usize, - write_release_date: bool, - release_date_length: usize, - ) { - let io = self.get_io(); - let pad_name = write_version || write_latest || write_release_date || write_description; - let pad_version = write_latest || write_release_date || write_description; - let pad_latest = write_description || write_release_date; - let pad_release_date = write_description; - for package in packages.iter() { - let link = package - .get("source") - .and_then(|v| v.as_string()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) - .or_else(|| { - package - .get("homepage") - .and_then(|v| v.as_string()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) - }) - .unwrap_or_default(); - let name = package - .get("name") - .and_then(|v| v.as_string()) - .unwrap_or("") - .to_string(); - if !link.is_empty() { - let pad = if pad_name && name_length > name.len() { - name_length - name.len() - } else { - 0 - }; - io.write_no_newline(&format!( - "{}{}{}", - indent, - OutputFormatter::escape(&link).expect("OutputFormatter::escape failed"), - name, - " ".repeat(pad) - )); - } else { - let width_pad = if pad_name { name_length } else { 0 }; - io.write_no_newline(&format!("{}{:{:", - style, - latest_version, - style, - width = width_pad - )); - if write_release_date - && let Some(age) = package.get("release-age").and_then(|v| v.as_string()) - { - let width_pad = if pad_release_date { - release_date_length - } else { - 0 - }; - io.write_no_newline(&format!(" {:, + ) -> anyhow::Result> { + // find the latest version allowed in this repo set + let name = package.get_name(); + let repo_set = self.get_repository_set(composer)?; + let composer_ref = crate::composer::composer_full(composer); + let mut version_selector = + VersionSelector::new(repo_set, Some(&mut *platform_repo.borrow_mut()))?; + let mut stability = composer_ref.get_package().get_minimum_stability(); + let flags = composer_ref.get_package().get_stability_flags(); + if let Some(flag_value) = flags.get(&name) { + let key_map: IndexMap = base_package::STABILITIES + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let needle = flag_value.to_string(); + if let Some(found_key) = array_search(&needle, &key_map) { + stability = found_key; } - if let Some(description) = package.get("description").and_then(|v| v.as_string()) - && write_description - { - let mut description = description - .split(['\r', '\n']) - .next() - .unwrap_or("") - .to_string(); + } - // Compute remaining width available for the description. - let mut remaining = (width as i64) - - (name_length as i64) - - (version_length as i64) - - (release_date_length as i64) - - 4; - if write_latest { - remaining -= latest_length as i64; - } + let mut best_stability = stability; + if composer_ref.get_package().get_prefer_stable() { + best_stability = package.get_stability(); + } - // If nothing fits, clear the description. - if remaining <= 0 { - description = String::new(); - } else if extension_loaded("mbstring") { - // Use mb_strwidth/mb_strimwidth to measure and trim by display width - // (CJK characters count as width 2). mb_strimwidth counts the trim - // marker ('...') in the width parameter, so pass $remaining directly. - if description.chars().count() > remaining as usize { - description = format!( - "{}...", - description - .chars() - .take((remaining as usize).saturating_sub(3)) - .collect::() - ); - } - } else { - // Fallback when mbstring is not available: do a conservative byte-based cut. - // Ensure cut length is non-negative and leave room for the ellipsis. - let cut = (remaining - 3).max(0) as usize; - if description.len() > cut { - description = format!("{}...", &description[..cut]); - } - } + let mut target_version: Option = None; + if package.get_version().starts_with("dev-") { + target_version = Some(package.get_version()); + + // dev-x branches are considered to be on the latest major version always, do not look up for a new commit as that is deemed a minor upgrade (albeit risky) + if major_only { + return Ok(None); + } + } - io.write_no_newline(&format!(" {}", description)); + if target_version.is_none() { + let mut groups: IndexMap = IndexMap::new(); + if major_only + && Preg::is_match3( + php_regex!(r"{^(?P(?:0\.)+)?(?P\d+)\.}"), + &package.get_version(), + Some(&mut groups), + ) + { + let zero_major = groups + .get(&CaptureKey::ByName("zero_major".to_string())) + .cloned() + .unwrap_or_default(); + let first_meaningful = groups + .get(&CaptureKey::ByName("first_meaningful".to_string())) + .cloned() + .unwrap_or_default() + .parse::() + .unwrap_or(0); + target_version = Some(format!( + ">={}{},<9999999-dev", + zero_major, + first_meaningful + 1 + )); } - if package.contains_key("path") { - let path_str = match package.get("path") { - Some(PhpMixed::String(s)) => s.clone(), - _ => "null".to_string(), + + if minor_only { + target_version = Some(format!("^{}", package.get_version())); + } + + if patch_only { + let trimmed_version = + Preg::replace(php_regex!(r"{(\.0)+$}D"), "", &package.get_version()); + let parts_needed = if trimmed_version.starts_with('0') { + 4 + } else { + 3 }; - io.write_no_newline(&format!(" {}", path_str)); + let mut trimmed_version = trimmed_version; + while trimmed_version.chars().filter(|&c| c == '.').count() + 1 < parts_needed { + trimmed_version.push_str(".0"); + } + target_version = Some(format!("~{}", trimmed_version)); } - io.write(""); - if let Some(warning) = package.get("warning").and_then(|v| v.as_string()) { - io.write(&format!("{}", warning)); + } + + let show_warnings = if self.get_io().is_verbose() { + ShowWarnings::Always + } else { + let package_version = package.get_version(); + ShowWarnings::Predicate(Box::new( + move |candidate: &PackageInterfaceHandle| -> bool { + if candidate.get_version().starts_with("dev-") + || package_version.starts_with("dev-") + { + return false; + } + + version_compare(&candidate.get_version(), &package_version, "<=") + }, + )) + }; + let mut candidate = version_selector.find_best_candidate( + &name, + target_version.as_deref(), + &best_stability, + Some(platform_req_filter), + 0, + Some(self.get_io().clone()), + show_warnings, + )?; + while let Some(ref c) = candidate { + if let Some(alias) = c.as_alias() { + candidate = Some(alias.get_alias_of().into()); + } else { + break; } } + + Ok(candidate) } - pub(crate) fn get_root_requires(&self) -> Vec { - let composer_rc = self.try_composer(None, None); - let composer_rc = match composer_rc { - None => return vec![], - Some(c) => c, - }; - let composer = crate::composer::composer_full(&composer_rc); + fn get_repository_set( + &self, + composer: &PartialComposerHandle, + ) -> anyhow::Result>> { + let composer = crate::composer::composer_full(composer); + if self.repository_set.borrow().is_none() { + let mut rs = RepositorySet::new( + &composer.get_package().get_minimum_stability(), + composer.get_package().get_stability_flags(), + Vec::new(), + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + ); + rs.add_repository(RepositoryInterfaceHandle::new(CompositeRepository::new( + composer + .get_repository_manager() + .borrow() + .get_repositories() + .to_vec(), + )))?; + *self.repository_set.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new(rs))); + } - let root_package = composer.get_package(); + Ok(self.repository_set.borrow().as_ref().unwrap().clone()) + } - let mut combined: IndexMap = IndexMap::new(); - for (k, v) in root_package.get_requires().iter() { - combined.insert(k.clone(), v.clone()); + fn get_relative_time(&self, release_date: &chrono::DateTime) -> String { + if release_date + .format(date_format_to_strftime("Ymd")) + .to_string() + == date("Ymd", None) + { + return "today".to_string(); } - for (k, v) in root_package.get_dev_requires().iter() { - combined.insert(k.clone(), v.clone()); + + let diff = chrono::Utc::now().signed_duration_since(*release_date); + let days = diff.num_days(); + if days < 7 { + return "this week".to_string(); } - combined.keys().map(|k| strtolower(k)).collect() + + if days < 14 { + return "last week".to_string(); + } + + let months = days / 30; + if months < 1 && days < 31 { + return format!("{} weeks ago", days / 7); + } + + let years = days / 365; + if years < 1 { + return format!("{} month{} ago", months, if months > 1 { "s" } else { "" }); + } + + format!("{} year{} ago", years, if years > 1 { "s" } else { "" }) } - pub(crate) fn get_version_style( - &self, - latest_package: PackageInterfaceHandle, - package: PackageInterfaceHandle, - ) -> anyhow::Result { - Ok( - Self::update_status_to_version_style(&Self::get_update_status( - latest_package, - package, - )?) - .to_string(), - ) + fn same_repository(a: &T, b: &U) -> bool + where + T: Into + Clone, + U: Into + Clone, + { + let a = a.clone().into(); + let b = b.clone().into(); + Self::same_repository_handle(&a, &b) + } + + fn same_repository_handle( + a: &RepositoryInterfaceHandle, + b: &RepositoryInterfaceHandle, + ) -> bool { + a.ptr_eq(b) + } +} + +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(()) } - /// finds a package by name and version if provided - pub(crate) fn get_package( + fn execute( &self, - installed_repo: &RepositoryInterfaceHandle, - repos: &RepositoryInterfaceHandle, - name: &str, - version: PhpMixed, - ) -> anyhow::Result<( - Option, - IndexMap, - )> { - let name = strtolower(name); - let constraint: Option = match &version { - PhpMixed::String(s) => Some(self.version_parser.borrow().parse_constraints(s)?), - PhpMixed::Null => None, - _ => None, // already a ConstraintInterface - }; - - let policy = DefaultPolicy::new(false, false, None); - let mut repository_set = RepositorySet::new( - "dev", - IndexMap::new(), - Vec::new(), - IndexMap::new(), - IndexMap::new(), - IndexMap::new(), - ); - repository_set.allow_installed_repositories(true); - repository_set.add_repository(repos.clone())?; - - let mut matched_package: Option = None; - let mut versions: IndexMap = IndexMap::new(); - let mut pool = if PlatformRepository::is_platform_package(&name) { - repository_set.create_pool_with_all_packages()? - } else { - repository_set.create_pool_for_package(&name, None)? - }; - let matches = pool.what_provides(&name, constraint.as_ref()); - let mut literals: Vec = Vec::new(); - for package in matches.iter() { - // avoid showing the 9999999-dev alias if the default branch has no branch-alias set - let mut p: crate::package::PackageInterfaceHandle = package.clone(); - if let Some(alias) = p.as_alias() - && p.get_version() == VersionParser::DEFAULT_BRANCH_ALIAS - { - p = alias.get_alias_of().into(); - } - - // select an exact match if it is in the installed repo and no specific version was required - if version.is_null() && installed_repo.has_package(p.clone())? { - matched_package = Some(p.clone()); - } - - versions.insert(p.get_pretty_version(), p.get_version()); - literals.push(p.get_id()); + input: std::rc::Rc>, + output: std::rc::Rc>, + ) -> anyhow::Result { + *self.version_parser.borrow_mut() = VersionParser::new(); + if input.borrow().get_option("tree")?.as_bool() == Some(true) { + self.init_styles(output.clone()); } - // select preferred package according to policy rules - if matched_package.is_none() && !literals.is_empty() { - let preferred = policy.select_preferred_packages(&pool, literals.clone(), None); - matched_package = Some(pool.literal_to_package(preferred[0])); - } + let composer = self.try_composer(None, None); - if let Some(ref mp) = matched_package - && mp.as_complete().is_none() + if input.borrow().get_option("installed")?.as_bool() == Some(true) + && input.borrow().get_option("self")?.as_bool() != Some(true) { - return Err(LogicException { - message: format!( - "ShowCommand::getPackage can only work with CompletePackageInterface, but got {}", - shirabe_php_shim::get_class(&PhpMixed::Null) - ), - code: 0, - } - .into()); + self.get_io().write_error("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."); } - let matched_package = matched_package.and_then(|mp| mp.as_complete()); - Ok((matched_package, versions)) - } - - /// Prints package info. - pub(crate) fn print_package_info( - &self, - package: CompletePackageInterfaceHandle, - versions: &IndexMap, - installed_repo: &mut dyn RepositoryInterface, - latest_package: Option, - ) -> anyhow::Result<()> { - self.print_meta(package.clone(), versions, installed_repo, latest_package)?; - self.print_links(package.clone(), Link::TYPE_REQUIRE, None); - self.print_links( - package.clone(), - Link::TYPE_DEV_REQUIRE, - Some("requires (dev)"), - ); - - if !package.get_suggests().is_empty() { - self.get_io().write("\nsuggests"); - for (suggested, reason) in package.get_suggests().iter() { - self.get_io() - .write(&format!("{} {}", suggested, reason)); - } + 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("You are using the option \"ignore\" for action other than \"outdated\", it will be ignored."); } - self.print_links(package.clone(), Link::TYPE_PROVIDE, None); - self.print_links(package.clone(), Link::TYPE_CONFLICT, None); - self.print_links(package, Link::TYPE_REPLACE, None); - Ok(()) - } - - /// Prints package metadata. - pub(crate) fn print_meta( - &self, - package: CompletePackageInterfaceHandle, - versions: &IndexMap, - installed_repo: &mut dyn RepositoryInterface, - latest_package: Option, - ) -> anyhow::Result<()> { - let is_installed_package = !PlatformRepository::is_platform_package(&package.get_name()) - && installed_repo.has_package(package.clone().into())?; + 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)"); - self.get_io().write(&format!( - "name : {}", - package.get_pretty_name() - )); - self.get_io().write(&format!( - "descrip. : {}", - package.get_description().unwrap_or_default() - )); - let keywords = package.get_keywords(); - self.get_io() - .write(&format!("keywords : {}", keywords.join(", "))); - self.print_versions(package.clone(), versions, installed_repo)?; - if is_installed_package && let Some(rd) = package.get_release_date() { - let rel = self.get_relative_time(&rd); - self.get_io().write(&format!( - "released : {}, {}", - rd.format(date_format_to_strftime("Y-m-d")), - rel - )); + return Ok(1); } - let latest: PackageInterfaceHandle = if let Some(latest) = latest_package { - let style = self.get_version_style(latest.clone(), package.clone().into())?; - let released_time = match latest.get_release_date() { - None => String::new(), - Some(rd) => { - let rel = self.get_relative_time(&rd); - format!( - " released {}, {}", - rd.format(date_format_to_strftime("Y-m-d")), - rel - ) - } - }; - self.get_io().write(&format!( - "latest : <{}>{}{}", - style, - latest.get_pretty_version(), - style, - released_time - )); - latest - } else { - package.clone().into() - }; - self.get_io() - .write(&format!("type : {}", package.get_type())); - self.print_licenses(package.clone()); - self.get_io().write(&format!( - "homepage : {}", - package.get_homepage().unwrap_or_default() - )); - self.get_io().write(&format!( - "source : [{}] {} {}", - package.get_source_type().unwrap_or_default(), - package.get_source_url().unwrap_or_default(), - package.get_source_reference().unwrap_or_default() - )); - self.get_io().write(&format!( - "dist : [{}] {} {}", - package.get_dist_type().unwrap_or_default(), - package.get_dist_url().unwrap_or_default(), - package.get_dist_reference().unwrap_or_default() - )); - if is_installed_package { - let path: Option = self.require_composer(None, None).ok().and_then(|c| { - let installation_manager = c.borrow_partial().get_installation_manager(); - installation_manager - .borrow_mut() - .get_install_path(package.clone().into()) - }); - if let Some(p) = path { - self.get_io().write(&format!( - "path : {}", - realpath(&p).unwrap_or_default() - )); - } else { - self.get_io().write("path : null"); - } + 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); } - self.get_io().write(&format!( - "names : {}", - package.get_names(true).join(", ") - )); - if let Some(c) = latest.as_complete() - && c.is_abandoned() + 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) { - let replacement = match c.get_replacement_package() { - Some(rp) => format!(" The author suggests using the {} package instead.", rp), - None => String::new(), - }; + 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!( - "Attention: This package is abandoned and no longer maintained.{}", - replacement - )); + "Unsupported format \"{}\". See help for supported formats.", + format + )); + + return Ok(1); } - let support = package.get_support(); - if !support.is_empty() { - self.get_io().write("\nsupport"); - for (r#type, value) in support.iter() { - self.get_io() - .write(&format!("{} : {}", r#type, value)); + let platform_req_filter = self.get_platform_requirement_filter(input.clone())?; + + // init repos + let mut platform_overrides: IndexMap = 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 = None; - let autoload_config = package.get_autoload(); - if !autoload_config.is_empty() { - self.get_io().write("\nautoload"); - for (r#type, autoloads) in autoload_config.iter() { - self.get_io() - .write(&format!("{}", r#type)); + // The single-package $package binding from PHP gets surfaced here. + let mut single_package: Option = None; + let mut versions_map: IndexMap = IndexMap::new(); + let installed_repo: RepositoryInterfaceHandle; + let repos: RepositoryInterfaceHandle; - if r#type == "psr-0" || r#type == "psr-4" { - if let PhpMixed::Array(map) = autoloads { - for (name, path) in map.iter() { - let path_str = match path { - PhpMixed::List(l) => l - .iter() - .filter_map(|p| p.as_string().map(|s| s.to_string())) - .collect::>() - .join(", "), - PhpMixed::String(s) if !s.is_empty() => s.clone(), - _ => ".".to_string(), - }; - let name_disp = if name.is_empty() { "*" } else { name }; - self.get_io() - .write(&format!("{} => {}", name_disp, path_str)); - } - } - } else if r#type == "classmap" - && let PhpMixed::List(l) = autoloads - { - let joined: Vec = l - .iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect(); - self.get_io().write(&joined.join(", ")); + 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()); } - let include_paths = package.get_include_paths(); - if !include_paths.is_empty() { - self.get_io().write("include-path"); - self.get_io().write(&include_paths.join(", ")); + 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 = 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 = + 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 = 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 = vec![installed_repo.clone()]; + for (_k, v) in default_repos.into_iter() { + composite_input.push(v); } - } - - Ok(()) - } - - /// Prints all available versions of this package and highlights the installed one if any. - pub(crate) fn print_versions( - &self, - package: CompletePackageInterfaceHandle, - versions: &IndexMap, - installed_repo: &mut dyn RepositoryInterface, - ) -> anyhow::Result<()> { - let mut versions_keys: Vec = versions.keys().cloned().collect(); - versions_keys = Semver::rsort(versions_keys)?; - - // highlight installed version - let installed_packages = installed_repo.find_packages(&package.get_name(), None)?; - if !installed_packages.is_empty() { - for installed_package in installed_packages.iter() { - let installed_version = installed_package.get_pretty_version(); - let key_map: IndexMap = versions_keys - .iter() - .map(|v| (v.clone(), v.clone())) - .collect(); - if let Some(found) = array_search(&installed_version, &key_map) - && let Some(idx) = versions_keys.iter().position(|v| v == &found) - { - versions_keys[idx] = format!("* {}", installed_version); + 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 versions_str = versions_keys.join(", "); - - self.get_io() - .write(&format!("versions : {}", versions_str)); - - Ok(()) - } - - /// print link objects - pub(crate) fn print_links( - &self, - package: CompletePackageInterfaceHandle, - link_type: &str, - title: Option<&str>, - ) { - let title = title.unwrap_or(link_type); - let io = self.get_io(); - let links = package.get_links_for_type(link_type); - if !links.is_empty() { - io.write(&format!("\n{}", title)); - - for link in links.iter() { - io.write(&format!( - "{} {}", - link.1.get_target(), - link.1.get_pretty_constraint(), - )); + 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(), + )?; } - } - } - - /// Prints the licenses of a package with metadata - pub(crate) fn print_licenses(&self, package: CompletePackageInterfaceHandle) { - let spdx_licenses = SpdxLicenses::new(); - - let licenses = package.get_license(); - let io = self.get_io(); - - for license_id in licenses.iter() { - let license = spdx_licenses.get_license_by_identifier(license_id); - - let out = match license { - None => license_id.clone(), - Some(license) => { - if license.is_osi_approved { - format!( - "{} ({}) (OSI approved) {}", - license.name, license_id, license.url - ) - } else { - format!("{} ({}) {}", license.name, license_id, license.url) - } + 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(); - io.write(&format!("license : {}", out)); - } - } - - /// Prints package info in JSON format. - pub(crate) fn print_package_info_as_json( - &self, - package: CompletePackageInterfaceHandle, - versions: &IndexMap, - installed_repo: &mut dyn RepositoryInterface, - latest_package: Option, - ) -> anyhow::Result<()> { - let mut json: IndexMap = IndexMap::new(); - json.insert( - "name".to_string(), - PhpMixed::String(package.get_pretty_name()), - ); - json.insert( - "description".to_string(), - PhpMixed::String(package.get_description().unwrap_or_default()), - ); - let keywords: Vec = package - .get_keywords() - .into_iter() - .map(PhpMixed::String) - .collect(); - json.insert("keywords".to_string(), PhpMixed::List(keywords)); - json.insert("type".to_string(), PhpMixed::String(package.get_type())); - json.insert( - "homepage".to_string(), - match package.get_homepage() { - Some(h) => PhpMixed::String(h), - None => PhpMixed::Null, - }, - ); - json.insert( - "names".to_string(), - PhpMixed::List( - package - .get_names(true) - .into_iter() - .map(PhpMixed::String) - .collect(), - ), - ); - - json = Self::append_versions(json, versions); - json = Self::append_licenses(json, package.clone()); - - let latest: PackageInterfaceHandle = if let Some(latest) = latest_package { - json.insert( - "latest".to_string(), - PhpMixed::String(latest.get_pretty_version()), - ); - latest - } else { - package.clone().into() - }; - - if package.get_source_type().is_some() { - let mut src: IndexMap = IndexMap::new(); - src.insert( - "type".to_string(), - PhpMixed::String(package.get_source_type().unwrap_or_default()), - ); - src.insert( - "url".to_string(), - PhpMixed::String(package.get_source_url().unwrap_or_default()), - ); - src.insert( - "reference".to_string(), - PhpMixed::String(package.get_source_reference().unwrap_or_default()), - ); - json.insert( - "source".to_string(), - PhpMixed::Array(src.into_iter().collect()), - ); - } + 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 = 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 package.get_dist_type().is_some() { - let mut dst: IndexMap = IndexMap::new(); - dst.insert( - "type".to_string(), - PhpMixed::String(package.get_dist_type().unwrap_or_default()), - ); - dst.insert( - "url".to_string(), - PhpMixed::String(package.get_dist_url().unwrap_or_default()), - ); - dst.insert( - "reference".to_string(), - PhpMixed::String(package.get_dist_reference().unwrap_or_default()), - ); - json.insert( - "dist".to_string(), - PhpMixed::Array(dst.into_iter().collect()), - ); - } + if installed_repo.get_packages()?.is_empty() { + let has_non_platform_reqs = |reqs: &IndexMap| -> bool { + reqs.keys() + .any(|name| !PlatformRepository::is_platform_package(name)) + }; - if !PlatformRepository::is_platform_package(&package.get_name()) - && installed_repo.has_package(package.clone().into())? - { - let composer = self.require_composer(None, None)?; - let installation_manager = composer.borrow_partial().get_installation_manager(); - let path: Option = installation_manager - .borrow_mut() - .get_install_path(package.clone().into()); - match path { - Some(p) => { - if let Some(r) = realpath(&p) { - json.insert("path".to_string(), PhpMixed::String(r)); - } - } - None => { - json.insert("path".to_string(), PhpMixed::Null); + 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("No dependencies installed. Try running composer install or update."); } } - - if let Some(rd) = package.get_release_date() { - json.insert( - "released".to_string(), - PhpMixed::String(rd.format(DATE_ATOM).to_string()), - ); - } } - if let Some(c) = latest.as_complete() - && c.is_abandoned() - { - json.insert( - "replacement".to_string(), - match c.get_replacement_package() { - Some(rp) => PhpMixed::String(rp), - None => PhpMixed::Null, - }, + 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 !package.get_suggests().is_empty() { - let mut s: IndexMap = IndexMap::new(); - for (k, v) in package.get_suggests().iter() { - s.insert(k.clone(), PhpMixed::String(v.clone())); - } - json.insert( - "suggests".to_string(), - PhpMixed::Array(s.into_iter().collect()), + 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)); } - if !package.get_support().is_empty() { - let mut s: IndexMap = IndexMap::new(); - for (k, v) in package.get_support().iter() { - s.insert(k.clone(), PhpMixed::String(v.clone())); - } - json.insert( - "support".to_string(), - PhpMixed::Array(s.into_iter().collect()), - ); - } + let package_filter: Option = input + .borrow() + .get_argument("package")? + .as_string() + .map(|s| s.to_string()); - json = Self::append_autoload(json, package.clone()); + // 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 !package.get_include_paths().is_empty() { - json.insert( - "include_path".to_string(), - PhpMixed::List( - package - .get_include_paths() + 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(), - ), - ); - } - - json = Self::append_links(json, package); - - self.get_io().write(&JsonFile::encode(&PhpMixed::Array( - json.into_iter().collect(), - ))?); - Ok(()) - } - - fn append_versions( - mut json: IndexMap, - versions: &IndexMap, - ) -> IndexMap { - let mut versions_pairs: Vec<(String, String)> = versions - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - // uasort($versions, 'version_compare'); - versions_pairs.sort_by(|a, b| { - if version_compare(&a.1, &b.1, "<") { - std::cmp::Ordering::Less - } else if version_compare(&a.1, &b.1, ">") { - std::cmp::Ordering::Greater - } else { - std::cmp::Ordering::Equal - } - }); - versions_pairs.reverse(); - let keys: Vec = versions_pairs - .into_iter() - .map(|(k, _)| PhpMixed::String(k)) - .collect(); - json.insert("versions".to_string(), PhpMixed::List(keys)); - - json - } - - fn append_licenses( - mut json: IndexMap, - package: CompletePackageInterfaceHandle, - ) -> IndexMap { - let licenses = package.get_license(); - if !licenses.is_empty() { - let spdx_licenses = SpdxLicenses::new(); - - let mapped: Vec = licenses - .into_iter() - .map(|license_id| { - let license = spdx_licenses.get_license_by_identifier(&license_id); - match license { - None => PhpMixed::String(license_id), - Some(l) => { - // The 'osi' key holds the license id string, not the OSI-approved flag. - let mut m: IndexMap = IndexMap::new(); - m.insert("name".to_string(), PhpMixed::String(l.name)); - m.insert("osi".to_string(), PhpMixed::String(license_id)); - m.insert("url".to_string(), PhpMixed::String(l.url)); - PhpMixed::Array(m.into_iter().collect()) - } - } - }) - .collect(); - json.insert("licenses".to_string(), PhpMixed::List(mapped)); - } - - json - } - - fn append_autoload( - mut json: IndexMap, - package: CompletePackageInterfaceHandle, - ) -> IndexMap { - let autoload_config = package.get_autoload(); - if !autoload_config.is_empty() { - let mut autoload: IndexMap = IndexMap::new(); - - for (r#type, autoloads) in autoload_config.iter() { - if r#type == "psr-0" || r#type == "psr-4" { - let mut psr: IndexMap = IndexMap::new(); - - if let PhpMixed::Array(map) = autoloads { - for (name, path) in map.iter() { - let mut path_val = path.clone(); - let is_empty_path = match &path_val { - PhpMixed::String(s) => s.is_empty(), - PhpMixed::Null => true, - _ => false, - }; - if is_empty_path { - path_val = PhpMixed::String(".".to_string()); + .collect::>(), + ) + { + return Err(InvalidArgumentException { + message: format!( + "Package \"{}\" is installed but not a direct dependent of the root package.", + pkg.get_name() + ), + code: 0, } + .into()); + } - let key = if name.is_empty() { - "*".to_string() - } else { - name.clone() - }; - psr.insert(key, path_val); - } - } + 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"); + } - autoload.insert(r#type.clone(), PhpMixed::Array(psr.into_iter().collect())); - } else if r#type == "classmap" { - autoload.insert("classmap".to_string(), autoloads.clone()); + return Err(InvalidArgumentException { + message: format!("Package \"{}\" not found{}.", pf, hint), + code: 0, } + .into()); } - - json.insert( - "autoload".to_string(), - PhpMixed::Array(autoload.into_iter().collect()), - ); + single_package = matched_package; + versions_map = vers; } - json - } - - fn append_links( - mut json: IndexMap, - package: CompletePackageInterfaceHandle, - ) -> IndexMap { - for link_type in Link::types().iter() { - json = Self::append_link(json, package.clone(), link_type); - } + if let Some(ref package) = single_package { + // assert(isset($versions)); - json - } + 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); - fn append_link( - mut json: IndexMap, - package: CompletePackageInterfaceHandle, - link_type: &str, - ) -> IndexMap { - let links = package.get_links_for_type(link_type); + if format == "json" { + let mut wrapper: IndexMap = 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]); + } - if !links.is_empty() { - let mut m: IndexMap = IndexMap::new(); - for link in links.iter() { - m.insert( - link.1.get_target().to_string(), - PhpMixed::String(link.1.get_pretty_constraint().to_string()), - ); + return Ok(exit_code); } - json.insert( - link_type.to_string(), - PhpMixed::Array(m.into_iter().collect()), - ); - } - - json - } - - /// Init styles for tree - pub(crate) fn init_styles(&self, output: std::rc::Rc>) { - *self.colors.borrow_mut() = vec![ - "green".to_string(), - "yellow".to_string(), - "cyan".to_string(), - "magenta".to_string(), - "blue".to_string(), - ]; - - for color in self.colors.borrow().iter() { - let style = OutputFormatterStyle::new(Some(color.as_str()), None, vec![]); - output - .borrow() - .get_formatter() - .borrow_mut() - .set_style(color, Box::new(style)); - } - } - /// Display the tree - pub(crate) fn display_package_tree(&self, array_tree: Vec>) { - for package in array_tree.iter() { - let name = package - .get("name") - .and_then(|v| v.as_string()) - .unwrap_or("") - .to_string(); - self.get_io() - .write_no_newline(&format!("{}", name)); - let version = package - .get("version") - .and_then(|v| v.as_string()) - .unwrap_or("") - .to_string(); - self.get_io().write_no_newline(&format!(" {}", version)); - if let Some(description) = package.get("description").and_then(|v| v.as_string()) { - let trimmed = description.split(['\r', '\n']).next().unwrap_or(""); - self.get_io().write(&format!(" {}", trimmed)); - } else { - // output newline - self.get_io().write(""); + let mut latest_package: Option = 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 let Some(requires) = package.get("requires").and_then(|v| v.as_list()).cloned() { - let mut tree_bar = "├".to_string(); - let mut j = 0_usize; - let total = requires.len(); - for require_mixed in requires.iter() { - let require = match require_mixed.as_array() { - Some(a) => a, - None => continue, - }; - let require_name = require - .get("name") - .and_then(|v| v.as_string()) - .unwrap_or("") - .to_string(); - j += 1; - if j == total { - tree_bar = "└".to_string(); - } - let level: usize = 1; - let color = self.colors.borrow().get(level).cloned().unwrap_or_default(); - let info = format!( - "{}──<{}>{} {}", - tree_bar, - color, - require_name, - color, - require - .get("version") - .and_then(|v| v.as_string()) - .unwrap_or("") - ); - self.write_tree_line(&info); - - tree_bar = tree_bar.replace('└', " "); - let packages_in_tree: Vec = vec![ - PhpMixed::String(name.clone()), - PhpMixed::String(require_name.clone()), - ]; - - self.display_tree( - &PhpMixed::Array( - require - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - ), - &packages_in_tree, - &tree_bar, - level + 1, - ); + 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); } - } - } - /// Generate the package tree - pub(crate) fn generate_package_tree( - &self, - package: PackageInterfaceHandle, - installed_repo: &RepositoryInterfaceHandle, - remote_repos: &RepositoryInterfaceHandle, - ) -> IndexMap { - let requires = { - let mut r: IndexMap = package.get_requires(); - r.sort_keys(); - r - }; - let mut children: Vec = Vec::new(); - for (require_name, require) in requires.iter() { - let packages_in_tree: Vec = vec![ - PhpMixed::String(package.get_name().to_string()), - PhpMixed::String(require_name.clone()), - ]; + 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, + )?; + } - let mut tree_child_desc: IndexMap = IndexMap::new(); - tree_child_desc.insert("name".to_string(), PhpMixed::String(require_name.clone())); - tree_child_desc.insert( - "version".to_string(), - PhpMixed::String(require.get_pretty_constraint().to_string()), - ); + return Ok(exit_code); + } - let deep_children = self - .add_tree( - require_name, - require, - installed_repo, - remote_repos, - &packages_in_tree, - ) - .unwrap_or_default(); + // 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> = Vec::new(); + for package in packages.iter() { + if in_array_strict( + package.get_name(), + &root_requires + .iter() + .map(|s| PhpMixed::String(s.clone())) + .collect::>(), + ) { + array_tree.push(self.generate_package_tree( + package.clone(), + &installed_repo, + &repos, + )); + } + } - if !deep_children.is_empty() { - tree_child_desc.insert( - "requires".to_string(), + if format == "json" { + let mut wrapper: IndexMap = IndexMap::new(); + wrapper.insert( + "installed".to_string(), PhpMixed::List( - deep_children + 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); } - children.push(PhpMixed::Array(tree_child_desc.into_iter().collect())); + return Ok(0); } - let mut tree: IndexMap = IndexMap::new(); - tree.insert( - "name".to_string(), - PhpMixed::String(package.get_pretty_name()), - ); - tree.insert( - "version".to_string(), - PhpMixed::String(package.get_pretty_version()), - ); - tree.insert( - "description".to_string(), - match package.as_complete() { - Some(c) => match c.get_description() { - Some(d) => PhpMixed::String(d), - None => PhpMixed::Null, - }, - None => PhpMixed::String(String::new()), - }, - ); - if !children.is_empty() { - tree.insert("requires".to_string(), PhpMixed::List(children)); + // list packages + let mut packages: IndexMap> = IndexMap::new(); + let mut package_filter_regex: Option = 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> = 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::() + .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::() { + 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::>(), + ), + }; + 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())); + } + } + } } - tree - } + 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::>() + }) + .unwrap_or_default(), + "{^(?:%s)$}iD", + ); + let indent = if show_all_types { " " } else { "" }; + let mut latest_packages: IndexMap = + IndexMap::new(); + let mut exit_code: i64 = 0; + let mut view_data: IndexMap>> = IndexMap::new(); + let mut view_meta_data: IndexMap = 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; - /// Display a package tree - pub(crate) fn display_tree( - &self, - package: &PhpMixed, - packages_in_tree: &[PhpMixed], - previous_tree_bar: &str, - level: usize, - ) { - let previous_tree_bar = previous_tree_bar.replace('├', "│"); - let arr = match package.as_array() { - Some(a) => a, - None => return, - }; - let requires = match arr.get("requires").and_then(|v| v.as_list()).cloned() { - Some(l) => l, - None => return, - }; - let mut tree_bar = format!("{} ├", previous_tree_bar); - let mut i = 0_usize; - let total = requires.len(); - for require_mixed in requires.iter() { - let mut current_tree = packages_in_tree.to_vec(); - i += 1; - if i == total { - tree_bar = format!("{} └", previous_tree_bar); - } - let color_ident = level % self.colors.borrow().len(); - let color = self - .colors - .borrow() - .get(color_ident) - .cloned() - .unwrap_or_default(); + 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 require = match require_mixed.as_array() { - Some(a) => a, - None => continue, - }; - let require_name = require - .get("name") - .and_then(|v| v.as_string()) - .unwrap_or("") - .to_string(); - let require_version = require - .get("version") - .and_then(|v| v.as_string()) - .unwrap_or("") - .to_string(); + let mut view_type: Vec> = Vec::new(); + for package_or_name in type_packages.values() { + let mut package_view_data: IndexMap = 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 + }; - let circular_warn = if in_array_strict(require_name.clone(), ¤t_tree) { - "(circular dependency aborted here)" - } else { - "" - }; - let info = format!( - "{}──<{}>{} {} {}", - tree_bar, color, require_name, color, require_version, circular_warn - ) - .trim_end() - .to_string(); - self.write_tree_line(&info); + // 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; + } - tree_bar = tree_bar.replace('└', " "); + if input.borrow().get_option("outdated")?.as_bool() == Some(true) + || input.borrow().get_option("strict")?.as_bool() == Some(true) + { + has_outdated_packages = true; + } - current_tree.push(PhpMixed::String(require_name.clone())); - self.display_tree(require_mixed, ¤t_tree, &tree_bar, level + 1); - } - } + 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::>(), + )), + ); + 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), + ); - /// Display a package tree - pub(crate) fn add_tree( - &self, - name: &str, - link: &Link, - installed_repo: &RepositoryInterfaceHandle, - remote_repos: &RepositoryInterfaceHandle, - packages_in_tree: &[PhpMixed], - ) -> anyhow::Result>> { - let mut children: Vec> = Vec::new(); - let version_arg: PhpMixed = if link.get_pretty_constraint() == "self.version" { - // pass the ConstraintInterface object — signal via Null in this scalar shape - PhpMixed::Null - } else { - PhpMixed::String(link.get_pretty_constraint().to_string()) - }; - let (package, _) = self.get_package(installed_repo, remote_repos, name, version_arg)?; - if let Some(package) = package { - let mut requires = package.get_requires(); - requires.sort_keys(); - for (require_name, require) in requires.iter() { - let mut current_tree = packages_in_tree.to_vec(); + 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 = 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 tree_child_desc: IndexMap = IndexMap::new(); - tree_child_desc.insert("name".to_string(), PhpMixed::String(require_name.clone())); - tree_child_desc.insert( - "version".to_string(), - PhpMixed::String(require.get_pretty_constraint().to_string()), - ); + 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), + }; + } - if !in_array_strict(require_name.clone(), ¤t_tree) { - current_tree.push(PhpMixed::String(require_name.clone())); - let deep_children = self.add_tree( - require_name, - require, - installed_repo, - remote_repos, - ¤t_tree, - )?; - if !deep_children.is_empty() { - tree_child_desc.insert( - "requires".to_string(), - PhpMixed::List( - deep_children - .into_iter() - .map(|m| PhpMixed::Array(m.into_iter().collect())) - .collect(), - ), - ); + 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; } - - children.push(tree_child_desc); - } - } - - Ok(children) - } - - fn update_status_to_version_style(update_status: &str) -> &'static str { - // 'up-to-date' is printed green - // 'semver-safe-update' is printed red - // 'update-possible' is printed yellow - match update_status { - "up-to-date" => "info", - "semver-safe-update" => "highlight", - "update-possible" => "comment", - _ => "comment", - } - } - - fn get_update_status( - latest_package: PackageInterfaceHandle, - package: PackageInterfaceHandle, - ) -> anyhow::Result { - if latest_package.get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev) - == package.get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev) - { - return Ok("up-to-date".to_string()); - } - - let mut constraint = package.get_version(); - if !constraint.starts_with("dev-") { - constraint = format!("^{}", constraint); - } - if !latest_package.get_version().is_empty() - && Semver::satisfies(latest_package.get_version(), constraint)? - { - // it needs an immediate semver-compliant upgrade - return Ok("semver-safe-update".to_string()); - } - - // it needs an upgrade but has potential BC breaks so is not urgent - Ok("update-possible".to_string()) - } - - fn write_tree_line(&self, line: &str) { - let io = self.get_io(); - let mut line = line.to_string(); - if !io.is_decorated() { - line = line - .replace('└', "`-") - .replace('├', "|-") - .replace("──", "-") - .replace('│', "|"); - } - - io.write(&line); - } - - /// Given a package, this finds the latest package matching it - #[allow(clippy::too_many_arguments, reason = "to keep PHP signature")] - fn find_latest_package( - &self, - package: PackageInterfaceHandle, - composer: &PartialComposerHandle, - platform_repo: &PlatformRepositoryHandle, - major_only: bool, - minor_only: bool, - patch_only: bool, - platform_req_filter: std::rc::Rc, - ) -> anyhow::Result> { - // find the latest version allowed in this repo set - let name = package.get_name(); - let repo_set = self.get_repository_set(composer)?; - let composer_ref = crate::composer::composer_full(composer); - let mut version_selector = - VersionSelector::new(repo_set, Some(&mut *platform_repo.borrow_mut()))?; - let mut stability = composer_ref.get_package().get_minimum_stability(); - let flags = composer_ref.get_package().get_stability_flags(); - if let Some(flag_value) = flags.get(&name) { - let key_map: IndexMap = base_package::STABILITIES - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - let needle = flag_value.to_string(); - if let Some(found_key) = array_search(&needle, &key_map) { - stability = found_key; } } - let mut best_stability = stability; - if composer_ref.get_package().get_prefer_stable() { - best_stability = package.get_stability(); - } - - let mut target_version: Option = None; - if package.get_version().starts_with("dev-") { - target_version = Some(package.get_version()); - - // dev-x branches are considered to be on the latest major version always, do not look up for a new commit as that is deemed a minor upgrade (albeit risky) - if major_only { - return Ok(None); + if format == "json" { + let mut json_map: IndexMap = 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(), + ), + ); } - } - - if target_version.is_none() { - let mut groups: IndexMap = IndexMap::new(); - if major_only - && Preg::is_match3( - php_regex!(r"{^(?P(?:0\.)+)?(?P\d+)\.}"), - &package.get_version(), - Some(&mut groups), - ) + 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 zero_major = groups - .get(&CaptureKey::ByName("zero_major".to_string())) - .cloned() - .unwrap_or_default(); - let first_meaningful = groups - .get(&CaptureKey::ByName("first_meaningful".to_string())) - .cloned() - .unwrap_or_default() - .parse::() - .unwrap_or(0); - target_version = Some(format!( - ">={}{},<9999999-dev", - zero_major, - first_meaningful + 1 - )); + 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("Color 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"); + } + } } - if minor_only { - target_version = Some(format!("^{}", package.get_version())); - } + let width = self.get_terminal_width(); - if patch_only { - let trimmed_version = - Preg::replace(php_regex!(r"{(\.0)+$}D"), "", &package.get_version()); - let parts_needed = if trimmed_version.starts_with('0') { - 4 - } else { - 3 + for (r#type, packages) in view_data.iter() { + let meta = match view_meta_data.get(r#type) { + Some(m) => m.clone(), + None => continue, }; - let mut trimmed_version = trimmed_version; - while trimmed_version.chars().filter(|&c| c == '.').count() + 1 < parts_needed { - trimmed_version.push_str(".0"); + 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; } - target_version = Some(format!("~{}", trimmed_version)); - } - } - let show_warnings = if self.get_io().is_verbose() { - ShowWarnings::Always - } else { - let package_version = package.get_version(); - ShowWarnings::Predicate(Box::new( - move |candidate: &PackageInterfaceHandle| -> bool { - if candidate.get_version().starts_with("dev-") - || package_version.starts_with("dev-") - { - return false; + if show_all_types { + if r#type == "available" { + self.get_io() + .write(&format!("{}:", r#type)); + } else { + self.get_io().write(&format!("{}:", r#type)); } + } - version_compare(&candidate.get_version(), &package_version, "<=") - }, - )) - }; - let mut candidate = version_selector.find_best_candidate( - &name, - target_version.as_deref(), - &best_stability, - Some(platform_req_filter), - 0, - Some(self.get_io().clone()), - show_warnings, - )?; - while let Some(ref c) = candidate { - if let Some(alias) = c.as_alias() { - candidate = Some(alias.get_alias_of().into()); - } else { - break; + if write_latest && input.borrow().get_option("direct")?.as_bool() != Some(true) { + let mut direct_deps: Vec> = Vec::new(); + let mut transitive_deps: Vec> = 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("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( + "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(candidate) + Ok(exit_code) } - fn get_repository_set( + fn initialize( &self, - composer: &PartialComposerHandle, - ) -> anyhow::Result>> { - let composer = crate::composer::composer_full(composer); - if self.repository_set.borrow().is_none() { - let mut rs = RepositorySet::new( - &composer.get_package().get_minimum_stability(), - composer.get_package().get_stability_flags(), - Vec::new(), - IndexMap::new(), - IndexMap::new(), - IndexMap::new(), - ); - rs.add_repository(RepositoryInterfaceHandle::new(CompositeRepository::new( - composer - .get_repository_manager() - .borrow() - .get_repositories() - .to_vec(), - )))?; - *self.repository_set.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new(rs))); - } - - Ok(self.repository_set.borrow().as_ref().unwrap().clone()) + input: std::rc::Rc>, + output: std::rc::Rc>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn get_relative_time(&self, release_date: &chrono::DateTime) -> String { - if release_date - .format(date_format_to_strftime("Ymd")) - .to_string() - == date("Ymd", None) - { - return "today".to_string(); - } - - let diff = chrono::Utc::now().signed_duration_since(*release_date); - let days = diff.num_days(); - if days < 7 { - return "this week".to_string(); - } - - if days < 14 { - return "last week".to_string(); - } - - let months = days / 30; - if months < 1 && days < 31 { - return format!("{} weeks ago", days / 7); - } - - let years = days / 365; - if years < 1 { - return format!("{} month{} ago", months, if months > 1 { "s" } else { "" }); - } - - format!("{} year{} ago", years, if years > 1 { "s" } else { "" }) + 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) } - fn same_repository(a: &T, b: &U) -> bool - where - T: Into + Clone, - U: Into + Clone, - { - let a = a.clone().into(); - let b = b.clone().into(); - Self::same_repository_handle(&a, &b) - } + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} - fn same_repository_handle( - a: &RepositoryInterfaceHandle, - b: &RepositoryInterfaceHandle, - ) -> bool { - a.ptr_eq(b) +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)] -- cgit v1.3.1