//! ref: composer/src/Composer/Command/BaseDependencyCommand.php use indexmap::IndexMap; use shirabe_external_packages::symfony::component::console::formatter::OutputFormatterStyle; use shirabe_external_packages::symfony::component::console::input::InputInterface; use shirabe_external_packages::symfony::component::console::output::OutputInterface; use shirabe_external_packages::symfony::console::formatter::OutputFormatter; use shirabe_php_shim::{InvalidArgumentException, PhpMixed, UnexpectedValueException}; use shirabe_semver::constraint::Bound; use shirabe_semver::constraint::ConstraintInterface; use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; use crate::package::CompletePackageInterface; use crate::package::Link; use crate::package::Package; use crate::package::RootPackage; use crate::package::version::VersionParser; use crate::repository::CompositeRepository; use crate::repository::InstalledArrayRepository; use crate::repository::PlatformRepository; use crate::repository::RepositoryFactory; use crate::repository::RootPackageRepository; use crate::repository::{DependentsEntry, InstalledRepository, NeedleInput}; use crate::repository::{FindPackageConstraint, RepositoryInterface}; use crate::util::PackageInfo; pub const ARGUMENT_PACKAGE: &str = "package"; pub const ARGUMENT_CONSTRAINT: &str = "version"; pub const OPTION_RECURSIVE: &str = "recursive"; pub const OPTION_TREE: &str = "tree"; pub trait BaseDependencyCommand: BaseCommand { const ARGUMENT_PACKAGE: &'static str = ARGUMENT_PACKAGE; const ARGUMENT_CONSTRAINT: &'static str = ARGUMENT_CONSTRAINT; const OPTION_RECURSIVE: &'static str = OPTION_RECURSIVE; const OPTION_TREE: &'static str = OPTION_TREE; fn colors(&self) -> &[String]; fn colors_mut(&mut self) -> &mut Vec; // TODO(phase-b): these wrappers existed to forward BaseCommand setters, but they // shadowed the BaseCommand methods and caused ambiguity. Use BaseCommand directly. fn do_execute( &mut self, input: &dyn InputInterface, output: &dyn OutputInterface, inverted: bool, ) -> anyhow::Result { let composer = self.require_composer(None, None)?; let mut composer = crate::command::composer_full_mut(&composer); // TODO(plugin): dispatch CommandEvent(PluginEvents::COMMAND, self.get_name(), input, output) via composer.get_event_dispatcher() let mut repos: Vec> = vec![]; repos.push(Box::new(RootPackageRepository::new( composer.get_package().clone_box(), ))); if input.get_option("locked").as_bool().unwrap_or(false) { let locker = composer.get_locker().clone(); let mut locker = locker.borrow_mut(); if !locker.is_locked() { return Err(anyhow::anyhow!(UnexpectedValueException { message: "A valid composer.lock file is required to run this command with --locked" .to_string(), code: 0, })); } repos.push(Box::new(locker.get_locked_repository(true)?)); let platform_overrides: IndexMap = locker .get_platform_overrides()? .into_iter() .map(|(k, v)| (k, PhpMixed::String(v))) .collect(); repos.push(Box::new(PlatformRepository::new( vec![], platform_overrides, )?)); } else { let repository_manager = composer.get_repository_manager().clone(); let repository_manager = repository_manager.borrow(); let local_repo = repository_manager.get_local_repository(); let root_pkg = composer.get_package(); if local_repo.get_packages().len() == 0 && (root_pkg.get_requires().len() > 0 || root_pkg.get_dev_requires().len() > 0) { output.writeln( "No dependencies installed. Try running composer install or update, or use --locked.", shirabe_external_packages::symfony::console::output::OUTPUT_NORMAL, ); return Ok(1); } // TODO(phase-b): InstalledRepositoryInterface is shared by reference (PHP class // semantics); Box requires owned upcast. Skipping local // repo push until clone_box is exposed on InstalledRepositoryInterface. let _ = local_repo; let platform_overrides = composer .get_config() .borrow() .get("platform") .as_array() .cloned() .unwrap_or_default(); // TODO(phase-b): platform_overrides type adjustment; using empty for now let _ = platform_overrides; repos.push(Box::new(PlatformRepository::new(vec![], IndexMap::new())?)); } let mut installed_repo = InstalledRepository::new(repos); let needle = input .get_argument(Self::ARGUMENT_PACKAGE) .as_string() .unwrap_or_default() .to_string(); let text_constraint: String = if input.has_argument(Self::ARGUMENT_CONSTRAINT) { input .get_argument(Self::ARGUMENT_CONSTRAINT) .as_string() .unwrap_or("*") .to_string() } else { "*".to_string() }; let packages = installed_repo.find_packages_with_replacers_and_providers(&needle, None); if packages.is_empty() { return Err(anyhow::anyhow!(InvalidArgumentException { message: format!("Could not find package \"{}\" in your project", needle), code: 0, })); } let matched_package = installed_repo.find_package( &needle, FindPackageConstraint::String(text_constraint.clone()), ); if matched_package.is_none() { let default_repos = CompositeRepository::new( RepositoryFactory::default_repos( Some(self.get_io()), Some(composer.get_config()), // TODO(phase-b): get_repository_manager returns &; default_repos needs &mut Some(todo!("share repository_manager as &mut")), )? .into_values() .collect(), ); if let Some(r#match) = default_repos.find_package( &needle, FindPackageConstraint::String(text_constraint.clone()), ) { installed_repo.add_repository(Box::new( InstalledArrayRepository::new_with_packages(vec![r#match.clone_box()])?, ))?; } else if PlatformRepository::is_platform_package(&needle) { let parser = VersionParser::new(); let platform_constraint = parser.parse_constraints(&text_constraint)?; if platform_constraint.get_lower_bound() != Bound::zero() { let version = platform_constraint .get_lower_bound() .get_version() .to_string(); let temp_platform_pkg = Package::new(needle.clone(), version.clone(), version); installed_repo.add_repository(Box::new( InstalledArrayRepository::new_with_packages(vec![Box::new( temp_platform_pkg, )])?, ))?; } } else { self.get_io().write_error(&format!( "Package \"{}\" could not be found with constraint \"{}\", results below will most likely be incomplete.", needle, text_constraint )); } } else if PlatformRepository::is_platform_package(&needle) { let matched = matched_package.as_ref().unwrap(); let extra_notice = if matched .get_extra() .get("config.platform") .and_then(|v| v.as_bool()) .unwrap_or(false) { " (version provided by config.platform)" } else { "" }; self.get_io().write_error(&format!( "Package \"{} {}\" found in version \"{}\"{}.", needle, text_constraint, matched.get_pretty_version(), extra_notice )); } else if inverted { let matched = matched_package.as_ref().unwrap(); self.get_io().write(&format!( "Package \"{}\" {} is already installed! To find out why, run `composer why {}`", needle, matched.get_pretty_version(), needle )); return Ok(0); } let mut needles = vec![needle.clone()]; if inverted { for package in &packages { let replaces: Vec = package .get_replaces() .values() .map(|link| link.get_target().to_string()) .collect(); needles.extend(replaces); } } let has_constraint = text_constraint != "*"; let constraint: Option> = if has_constraint { let version_parser = VersionParser::new(); Some( version_parser .parse_constraints(&text_constraint)? .clone_box(), ) } else { None }; let render_tree = input .get_option(Self::OPTION_TREE) .as_bool() .unwrap_or(false); let recursive = render_tree || input .get_option(Self::OPTION_RECURSIVE) .as_bool() .unwrap_or(false); let mut r#return: i64 = if inverted { 1 } else { 0 }; let results = installed_repo.get_dependents( NeedleInput::Multiple(needles), constraint, inverted, recursive, None, ); if results.is_empty() { let extra = if has_constraint { format!( " in versions {}matching {}", if inverted { "not " } else { "" }, text_constraint ) } else { String::new() }; self.get_io().write_error(&format!( "There is no installed package depending on \"{}\"{}", needle, extra )); r#return = if inverted { 0 } else { 1 }; } else if render_tree { self.init_styles(output); let root = &packages[0]; let description = root .as_complete_package_interface() .and_then(|c| c.get_description()) .unwrap_or(""); self.get_io().write(&format!( "{} {} {}", root.get_pretty_name(), root.get_pretty_version(), description )); self.print_tree(&results, "", 1); } else { self.print_table(output, results); } if inverted && input.has_argument(Self::ARGUMENT_CONSTRAINT) && !PlatformRepository::is_platform_package(&needle) { let mut composer_command = "update"; for root_requirement in composer.get_package().get_requires().values() { if root_requirement.get_target() == needle.as_str() { composer_command = "require"; break; } } for root_requirement in composer.get_package().get_dev_requires().values() { if root_requirement.get_target() == needle.as_str() { composer_command = "require --dev"; break; } } self.get_io().write_error(&format!( "Not finding what you were looking for? Try calling `composer {} \"{}:{}\" --dry-run` to get another view on the problem.", composer_command, needle, text_constraint )); } Ok(r#return) } fn print_table(&self, output: &dyn OutputInterface, results: Vec) { let mut table: Vec> = vec![]; let mut doubles: IndexMap = IndexMap::new(); let mut results = results; loop { if results.is_empty() { break; } let mut queue: Vec = vec![]; let mut rows: Vec> = vec![]; for DependentsEntry(package, link, children) in results { let unique = link.to_string(); if doubles.contains_key(&unique) { continue; } doubles.insert(unique.clone(), true); let version = if package.get_pretty_version() == RootPackage::DEFAULT_PRETTY_VERSION { "-".to_string() } else { package.get_pretty_version().to_string() }; let package_url = PackageInfo::get_view_source_or_homepage_url(&*package); let name_with_link = match &package_url { Some(url) => format!( "{}", OutputFormatter::escape(url), package.get_pretty_name() ), None => package.get_pretty_name().to_string(), }; rows.push(vec![ name_with_link, version, link.get_description().to_string(), format!( "{} ({})", link.get_target(), link.get_pretty_constraint().unwrap_or("") ), ]); if let Some(children_vec) = children { queue.extend(children_vec); } } results = queue; let mut new_table = rows; new_table.extend(table); table = new_table; } // TODO(phase-b): render_table expects Vec; build PhpMixed cells once a // converter exists for Vec rows. let table_as_mixed: Vec = table .into_iter() .map(|row| { PhpMixed::List( row.into_iter() .map(|s| Box::new(PhpMixed::String(s))) .collect(), ) }) .collect(); self.render_table(table_as_mixed, output); } fn init_styles(&mut self, output: &dyn OutputInterface) { *self.colors_mut() = vec![ "green".to_string(), "yellow".to_string(), "cyan".to_string(), "magenta".to_string(), "blue".to_string(), ]; for color in self.colors() { // TODO(phase-b): output.get_formatter() returns &OutputFormatter; set_style needs // &mut. Need interior mutability or `get_formatter_mut`. let _ = OutputFormatterStyle::new(Some(color), None, None); let _ = output.get_formatter(); } } fn print_tree(&mut self, results: &[DependentsEntry], prefix: &str, level: i64) { let count = results.len() as i64; let mut idx: i64 = 0; let colors_len = self.colors().len() as i64; for result in results { let DependentsEntry(package, link, children) = result; let color = &self.colors()[(level % colors_len) as usize]; let prev_color = &self.colors()[((level - 1) % colors_len) as usize]; idx += 1; let is_last = idx == count; let version_text = if package.get_pretty_version() == RootPackage::DEFAULT_PRETTY_VERSION { String::new() } else { package.get_pretty_version().to_string() }; let package_url = PackageInfo::get_view_source_or_homepage_url(&**package); let name_with_link = match &package_url { Some(url) => format!( "{}", OutputFormatter::escape(url), package.get_pretty_name() ), None => package.get_pretty_name().to_string(), }; let package_text = format!("<{}>{} {}", color, name_with_link, color, version_text) .trim_end() .to_string(); let link_text = format!( "{} <{}>{} {}", link.get_description(), prev_color, link.get_target(), prev_color, link.get_pretty_constraint().unwrap_or("") ); let circular_warn = if children.is_none() { "(circular dependency aborted here)" } else { "" }; self.write_tree_line( &format!( "{}{}{} ({}) {}", prefix, if is_last { "└──" } else { "├──" }, package_text, link_text, circular_warn ) .trim_end() .to_string(), ); if let Some(children_vec) = children { self.print_tree( children_vec, &format!("{}{}", prefix, if is_last { " " } else { "│ " }), level + 1, ); } } } fn write_tree_line(&mut self, line: &str) { let io = self.get_io(); let line = if !io.is_decorated() { line.replace('└', "`-") .replace('├', "|-") .replace("──", "-") .replace('│', "|") } else { line.to_string() }; io.write(&line); } }