//! ref: composer/src/Composer/Command/BaseDependencyCommand.php use indexmap::IndexMap; use shirabe_external_packages::symfony::console::formatter::output_formatter::OutputFormatter; use shirabe_external_packages::symfony::console::formatter::output_formatter_style::OutputFormatterStyle; use shirabe_external_packages::symfony::console::input::input_interface::InputInterface; use shirabe_external_packages::symfony::console::output::output_interface::OutputInterface; use shirabe_php_shim::{InvalidArgumentException, UnexpectedValueException}; use shirabe_semver::constraint::bound::Bound; use shirabe_semver::constraint::constraint_interface::ConstraintInterface; use crate::command::base_command::{BaseCommand, BaseCommandData, HasBaseCommandData}; use crate::package::complete_package_interface::CompletePackageInterface; use crate::package::link::Link; use crate::package::package::Package; use crate::package::root_package::RootPackage; use crate::package::version::version_parser::VersionParser; use crate::repository::composite_repository::CompositeRepository; use crate::repository::installed_array_repository::InstalledArrayRepository; use crate::repository::installed_repository::{DependentsEntry, InstalledRepository, NeedleInput}; use crate::repository::platform_repository::PlatformRepository; use crate::repository::repository_factory::RepositoryFactory; use crate::repository::repository_interface::{FindPackageConstraint, RepositoryInterface}; use crate::repository::root_package_repository::RootPackageRepository; use crate::util::package_info::PackageInfo; pub trait BaseDependencyCommand: BaseCommand { const ARGUMENT_PACKAGE: &'static str = "package"; const ARGUMENT_CONSTRAINT: &'static str = "version"; const OPTION_RECURSIVE: &'static str = "recursive"; const OPTION_TREE: &'static str = "tree"; fn colors(&self) -> &[String]; fn colors_mut(&mut self) -> &mut [String]; // 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)?; // 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(); 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)?)); repos.push(Box::new(PlatformRepository::new( vec![], locker.get_platform_overrides(), )?)); } else { let local_repo = composer.get_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_interface::OUTPUT_NORMAL, ); return Ok(1); } repos.push(Box::new(local_repo)); let platform_overrides = composer .get_config() .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.clone(), 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.clone(), 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()), Some(&mut composer.get_repository_manager()), )?); if let Some(r#match) = default_repos.find_package( needle.clone(), FindPackageConstraint::String(text_constraint.clone()), ) { installed_repo.add_repository(Box::new(InstalledArrayRepository::new(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( 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 = if has_constraint { let version_parser = VersionParser::new(); Some(version_parser.parse_constraints(&text_constraint)?) } 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; } self.render_table(table, 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() { let style = OutputFormatterStyle::new(color.clone()); output.get_formatter().set_style(color, style); } } fn print_tree(&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(&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); } }