//! ref: composer/src/Composer/Repository/InstalledRepository.php use indexmap::IndexMap; use shirabe_php_shim::LogicException; use shirabe_semver::constraint::Constraint; use shirabe_semver::constraint::ConstraintInterface; use shirabe_semver::constraint::MatchAllConstraint; use crate::package::BasePackage; use crate::package::Link; use crate::package::PackageInterface; use crate::package::RootPackageInterface; use crate::package::version::VersionParser; use crate::repository::CompositeRepository; use crate::repository::InstalledRepositoryInterface; use crate::repository::LockArrayRepository; use crate::repository::PlatformRepository; use crate::repository::RootPackageRepository; use crate::repository::{ FindPackageConstraint, LoadPackagesResult, ProviderInfo, RepositoryInterface, SearchResult, }; pub enum NeedleInput { Single(String), Multiple(Vec), } pub struct DependentsEntry( pub Box, pub Link, pub Option>, ); #[derive(Debug)] pub struct InstalledRepository { inner: CompositeRepository, } impl InstalledRepository { pub fn new(repositories: Vec>) -> Self { let mut this = Self { inner: CompositeRepository::new(vec![]), }; for repo in repositories { // TODO(phase-b): add_repository validates the inner repo type and may return Err; // ignoring the error during Phase B since callers do not handle it. let _ = this.add_repository(repo); } this } pub fn find_packages_with_replacers_and_providers( &self, name: &str, constraint: Option, ) -> Vec> { let name = name.to_lowercase(); let constraint: Option> = match constraint { None => None, Some(FindPackageConstraint::Constraint(c)) => Some(c), Some(FindPackageConstraint::String(s)) => { let version_parser = VersionParser::new(); Some(version_parser.parse_constraints(&s).unwrap()) } }; let mut matches = vec![]; for repo in self.inner.get_repositories() { 'candidates: for candidate in repo.get_packages() { if name == candidate.get_name() { if constraint.is_none() || constraint .as_ref() .unwrap() .matches(&Constraint::new("==", candidate.get_version())) { matches.push(candidate); } continue; } let provides = candidate.get_provides(); let replaces = candidate.get_replaces(); let mut provides_and_replaces: Vec<&Link> = vec![]; for link in provides.values() { provides_and_replaces.push(link); } for link in replaces.values() { provides_and_replaces.push(link); } for link in provides_and_replaces { if name == link.get_target() && (constraint.is_none() || constraint.as_ref().unwrap().matches(link.get_constraint())) { matches.push(candidate); continue 'candidates; } } } } matches } pub fn get_dependents( &self, needle: NeedleInput, constraint: Option>, invert: bool, recurse: bool, packages_found: Option>, ) -> Vec { let mut needles: Vec = match needle { NeedleInput::Single(s) => vec![s.to_lowercase()], NeedleInput::Multiple(v) => v.into_iter().map(|s| s.to_lowercase()).collect(), }; let mut results: Vec = vec![]; let mut packages_found = packages_found.unwrap_or_else(|| needles.clone()); let mut root_package: Option> = None; for package in self.inner.get_packages() { if package.as_root_package_interface().is_some() { root_package = Some(package); break; } } for package in self.inner.get_packages() { let mut links: IndexMap = package.get_requires(); let mut packages_in_tree = packages_found.clone(); if !invert { for (k, v) in package.get_replaces() { links.entry(k).or_insert(v); } let needles_snapshot = needles.clone(); for link in package.get_replaces().values() { for needle in &needles_snapshot { if link.get_source() == needle.as_str() { if constraint.is_none() || link .get_constraint() .matches(constraint.as_ref().unwrap().as_ref()) { if packages_in_tree.contains(&link.get_target().to_string()) { results.push(DependentsEntry( package.clone_box(), link.clone(), None, )); continue; } packages_in_tree.push(link.get_target().to_string()); let dependents = if recurse { self.get_dependents( NeedleInput::Single(link.get_target().to_string()), None, false, true, Some(packages_in_tree.clone()), ) } else { vec![] }; results.push(DependentsEntry( package.clone_box(), link.clone(), Some(dependents), )); needles.push(link.get_target().to_string()); } } } } } if package.as_root_package_interface().is_some() { for (k, v) in package.get_dev_requires() { links.entry(k).or_insert(v); } } for link in links.values() { for needle in &needles { if link.get_target() == needle.as_str() { let matches_constraint = constraint.as_ref().map_or(true, |c| { link.get_constraint().matches(c.as_ref()) == !invert }); if constraint.is_none() || matches_constraint { if packages_in_tree.contains(&link.get_source().to_string()) { results.push(DependentsEntry( package.clone_box(), link.clone(), None, )); continue; } packages_in_tree.push(link.get_source().to_string()); let dependents = if recurse { self.get_dependents( NeedleInput::Single(link.get_source().to_string()), None, false, true, Some(packages_in_tree.clone()), ) } else { vec![] }; results.push(DependentsEntry( package.clone_box(), link.clone(), Some(dependents), )); } } } } if invert && needles.contains(&package.get_name().to_string()) { for link in package.get_conflicts().values() { for pkg in self.find_packages(link.get_target(), None) { let version = Constraint::new("=", pkg.get_version()); if link.get_constraint().matches(&version) == invert { results.push(DependentsEntry(package.clone_box(), link.clone(), None)); } } } } for link in package.get_conflicts().values() { if needles.contains(&link.get_target().to_string()) { for pkg in self.find_packages(link.get_target(), None) { let version = Constraint::new("=", pkg.get_version()); if link.get_constraint().matches(&version) == invert { results.push(DependentsEntry(package.clone_box(), link.clone(), None)); } } } } if invert && constraint.is_some() && needles.contains(&package.get_name().to_string()) && constraint .as_ref() .unwrap() .matches(&Constraint::new("=", package.get_version())) { 'requires: for link in package.get_requires().values() { if PlatformRepository::is_platform_package(link.get_target()) { if self .find_package( link.get_target(), FindPackageConstraint::Constraint( link.get_constraint().clone_box(), ), ) .is_some() { continue; } let platform_pkg = self.find_package( link.get_target(), FindPackageConstraint::String("*".to_string()), ); let description = platform_pkg .as_ref() .map(|p| format!("but {} is installed", p.get_pretty_version())) .unwrap_or_else(|| "but it is missing".to_string()); results.push(DependentsEntry( package.clone_box(), Link::new( package.get_name().to_string(), link.get_target().to_string(), Box::new(MatchAllConstraint::new()), Some(Link::TYPE_REQUIRE.to_string()), Some(format!( "{} {}", link.get_pretty_constraint().unwrap_or_default(), description )), ), None, )); continue; } for pkg in self.get_packages() { if !pkg.get_names(true).contains(&link.get_target().to_string()) { continue; } let mut version: Box = Box::new(Constraint::new("=", pkg.get_version())); if link.get_target() != pkg.get_name() { let mut replaces_and_provides: IndexMap = pkg.get_replaces(); for (k, v) in pkg.get_provides() { replaces_and_provides.entry(k).or_insert(v); } for prov in replaces_and_provides.values() { if link.get_target() == prov.get_target() { version = prov.get_constraint().clone_box(); break; } } } if !link.get_constraint().matches(version.as_ref()) { if let Some(root_pkg) = root_package.as_ref() { let mut root_reqs: IndexMap = root_pkg.get_requires(); for (k, v) in root_pkg.get_dev_requires() { root_reqs.entry(k).or_insert(v); } for root_req in root_reqs.values() { if pkg .get_names(true) .contains(&root_req.get_target().to_string()) && !root_req.get_constraint().matches(link.get_constraint()) { results.push(DependentsEntry( package.clone_box(), link.clone(), None, )); results.push(DependentsEntry( root_pkg.clone_box(), root_req.clone(), None, )); continue 'requires; } } results.push(DependentsEntry( package.clone_box(), link.clone(), None, )); results.push(DependentsEntry( root_pkg.clone_box(), Link::new( root_pkg.get_name().to_string(), link.get_target().to_string(), Box::new(MatchAllConstraint::new()), Some(Link::TYPE_DOES_NOT_REQUIRE.to_string()), Some(format!( "but {} is installed", pkg.get_pretty_version() )), ), None, )); } else { results.push(DependentsEntry( package.clone_box(), link.clone(), None, )); } } continue 'requires; } } } } // ksort($results) - no-op for a numerically-indexed Vec results } pub fn add_repository( &mut self, repository: Box, ) -> anyhow::Result<()> { // TODO(phase-b): cannot Any::is::; replace with a // dedicated downcast/marker method on RepositoryInterface. if repository.as_any().is::() || repository.as_any().is::() || repository.as_any().is::() { self.inner.add_repository(repository); return Ok(()); } Err(anyhow::anyhow!(LogicException { message: format!( "An InstalledRepository can not contain a repository of type {} ({})", std::any::type_name_of_val(&*repository), repository.get_repo_name(), ), code: 0, })) } } impl shirabe_php_shim::Countable for InstalledRepository { fn count(&self) -> i64 { self.inner.count() } } impl RepositoryInterface for InstalledRepository { fn get_repo_name(&self) -> String { let names: Vec = self .inner .get_repositories() .iter() .map(|repo| repo.get_repo_name()) .collect(); format!("installed repo ({})", names.join(", ")) } fn has_package(&self, package: &dyn PackageInterface) -> bool { self.inner.has_package(package) } fn find_package( &self, name: &str, constraint: FindPackageConstraint, ) -> Option> { self.inner.find_package(name, constraint) } fn find_packages( &self, name: &str, constraint: Option, ) -> Vec> { self.inner.find_packages(name, constraint) } fn get_packages(&self) -> Vec> { self.inner.get_packages() } fn load_packages( &self, package_name_map: IndexMap>>, acceptable_stabilities: IndexMap, stability_flags: IndexMap, already_loaded: IndexMap>>, ) -> LoadPackagesResult { self.inner.load_packages( package_name_map, acceptable_stabilities, stability_flags, already_loaded, ) } fn search(&self, query: String, mode: i64, r#type: Option) -> Vec { self.inner.search(query, mode, r#type) } fn get_providers(&self, package_name: String) -> IndexMap { self.inner.get_providers(package_name) } fn as_any(&self) -> &dyn std::any::Any { self } }