From efd426f8bb7b6759253d41a9000c3809f63034a8 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jun 2022 15:05:34 +0200 Subject: [PATCH] Extract some common logic for filtering away dev requirements into a RepositoryUtils --- src/Composer/Command/AuditCommand.php | 30 +----- src/Composer/Command/LicensesCommand.php | 51 +--------- src/Composer/Command/ShowCommand.php | 28 +----- src/Composer/Repository/RepositorySet.php | 2 + src/Composer/Repository/RepositoryUtils.php | 52 +++++++++++ src/Composer/Util/PackageSorter.php | 16 ++++ .../Test/Repository/RepositoryUtilsTest.php | 92 +++++++++++++++++++ 7 files changed, 171 insertions(+), 100 deletions(-) create mode 100644 src/Composer/Repository/RepositoryUtils.php create mode 100644 tests/Composer/Test/Repository/RepositoryUtilsTest.php diff --git a/src/Composer/Command/AuditCommand.php b/src/Composer/Command/AuditCommand.php index a5d0f5d6f..839c05006 100644 --- a/src/Composer/Command/AuditCommand.php +++ b/src/Composer/Command/AuditCommand.php @@ -4,11 +4,11 @@ namespace Composer\Command; use Composer\Composer; use Composer\Repository\RepositorySet; +use Composer\Repository\RepositoryUtils; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Composer\Package\PackageInterface; use Composer\Repository\InstalledRepository; -use Composer\Repository\RepositoryInterface; use Composer\Advisory\Auditor; use Composer\Console\Input\InputOption; @@ -73,35 +73,9 @@ EOT $installedRepo = new InstalledRepository(array($composer->getRepositoryManager()->getLocalRepository())); if ($input->getOption('no-dev')) { - return $this->filterRequiredPackages($installedRepo, $rootPkg); + return RepositoryUtils::filterRequiredPackages($installedRepo->getPackages(), $rootPkg); } return $installedRepo->getPackages(); } - - /** - * Find package requires and child requires. - * Effectively filters out dev dependencies. - * - * @param PackageInterface[] $bucket - * @return PackageInterface[] - */ - private function filterRequiredPackages(RepositoryInterface $repo, PackageInterface $package, array $bucket = array()): array - { - $requires = $package->getRequires(); - - foreach ($repo->getPackages() as $candidate) { - foreach ($candidate->getNames() as $name) { - if (isset($requires[$name])) { - if (!in_array($candidate, $bucket, true)) { - $bucket[] = $candidate; - $bucket = $this->filterRequiredPackages($repo, $candidate, $bucket); - } - break; - } - } - } - - return $bucket; - } } diff --git a/src/Composer/Command/LicensesCommand.php b/src/Composer/Command/LicensesCommand.php index 9d34824a1..13f332e8e 100644 --- a/src/Composer/Command/LicensesCommand.php +++ b/src/Composer/Command/LicensesCommand.php @@ -19,7 +19,9 @@ use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Package\PackageInterface; use Composer\Repository\RepositoryInterface; +use Composer\Repository\RepositoryUtils; use Composer\Util\PackageInfo; +use Composer\Util\PackageSorter; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; @@ -65,12 +67,12 @@ EOT $repo = $composer->getRepositoryManager()->getLocalRepository(); if ($input->getOption('no-dev')) { - $packages = $this->filterRequiredPackages($repo, $root); + $packages = RepositoryUtils::filterRequiredPackages($repo->getPackages(), $root); } else { - $packages = $this->appendPackages($repo->getPackages(), array()); + $packages = $repo->getPackages(); } - ksort($packages); + $packages = PackageSorter::sortPackagesAlphabetically($packages); $io = $this->getIO(); switch ($format = $input->getOption('format')) { @@ -153,47 +155,4 @@ EOT return 0; } - - /** - * Find package requires and child requires - * - * @param array $bucket - * @return array - */ - private function filterRequiredPackages(RepositoryInterface $repo, PackageInterface $package, array $bucket = array()): array - { - $requires = array_keys($package->getRequires()); - - $packageListNames = array_keys($bucket); - $packages = array_filter( - $repo->getPackages(), - static function ($package) use ($requires, $packageListNames): bool { - return in_array($package->getName(), $requires) && !in_array($package->getName(), $packageListNames); - } - ); - - $bucket = $this->appendPackages($packages, $bucket); - - foreach ($packages as $package) { - $bucket = $this->filterRequiredPackages($repo, $package, $bucket); - } - - return $bucket; - } - - /** - * Adds packages to the package list - * - * @param PackageInterface[] $packages the list of packages to add - * @param array $bucket the list to add packages to - * @return array - */ - public function appendPackages(array $packages, array $bucket): array - { - foreach ($packages as $package) { - $bucket[$package->getName()] = $package; - } - - return $bucket; - } } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 6866dbc7c..68ea43661 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -36,6 +36,7 @@ use Composer\Repository\RepositoryFactory; use Composer\Repository\InstalledRepository; use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositorySet; +use Composer\Repository\RepositoryUtils; use Composer\Repository\RootPackageRepository; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Semver; @@ -248,7 +249,7 @@ EOT $repos = $installedRepo = new InstalledRepository(array($composer->getRepositoryManager()->getLocalRepository())); if ($input->getOption('no-dev')) { - $packages = $this->filterRequiredPackages($installedRepo, $rootPkg); + $packages = RepositoryUtils::filterRequiredPackages($installedRepo->getPackages(), $rootPkg); $repos = $installedRepo = new InstalledRepository(array(new InstalledArrayRepository(array_map(static function ($pkg): PackageInterface { return clone $pkg; }, $packages)))); @@ -1423,29 +1424,4 @@ EOT return $this->repositorySet; } - - /** - * Find package requires and child requires - * - * @param array $bucket - * @return array - */ - private function filterRequiredPackages(RepositoryInterface $repo, PackageInterface $package, array $bucket = array()): array - { - $requires = $package->getRequires(); - - foreach ($repo->getPackages() as $candidate) { - foreach ($candidate->getNames() as $name) { - if (isset($requires[$name])) { - if (!in_array($candidate, $bucket, true)) { - $bucket[] = $candidate; - $bucket = $this->filterRequiredPackages($repo, $candidate, $bucket); - } - break; - } - } - } - - return $bucket; - } } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index c9183ce84..3f408ad0d 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -33,6 +33,8 @@ use Composer\Semver\Constraint\MatchAllConstraint; /** * @author Nils Adermann + * + * @see RepositoryUtils for ways to work with single repos */ class RepositorySet { diff --git a/src/Composer/Repository/RepositoryUtils.php b/src/Composer/Repository/RepositoryUtils.php new file mode 100644 index 000000000..e62c9dfdf --- /dev/null +++ b/src/Composer/Repository/RepositoryUtils.php @@ -0,0 +1,52 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +use Composer\Package\PackageInterface; + +/** + * @author Jordi Boggiano + * + * @see RepositorySet for ways to work with sets of repos + */ +class RepositoryUtils +{ + /** + * Find all of $packages which are required by $requirer, either directly or transitively + * + * Require-dev is ignored + * + * @template T of PackageInterface + * @param array $packages + * @param array $bucket Do not pass this in, only used to avoid recursion with circular deps + * @return list + */ + public static function filterRequiredPackages(array $packages, PackageInterface $requirer, array $bucket = array()): array + { + $requires = $requirer->getRequires(); + + foreach ($packages as $candidate) { + foreach ($candidate->getNames() as $name) { + if (isset($requires[$name])) { + if (!in_array($candidate, $bucket, true)) { + $bucket[] = $candidate; + $bucket = self::filterRequiredPackages($packages, $candidate, $bucket); + } + break; + } + } + } + + return $bucket; + } +} diff --git a/src/Composer/Util/PackageSorter.php b/src/Composer/Util/PackageSorter.php index 8032f732b..38b606e6a 100644 --- a/src/Composer/Util/PackageSorter.php +++ b/src/Composer/Util/PackageSorter.php @@ -17,6 +17,22 @@ use Composer\Package\RootPackageInterface; class PackageSorter { + /** + * Sorts packages by name + * + * @template T of PackageInterface + * @param array $packages + * @return array + */ + public static function sortPackagesAlphabetically(array $packages): array + { + usort($packages, static function (PackageInterface $a, PackageInterface $b) { + return $a->getName() <=> $b->getName(); + }); + + return $packages; + } + /** * Sorts packages by dependency weight * diff --git a/tests/Composer/Test/Repository/RepositoryUtilsTest.php b/tests/Composer/Test/Repository/RepositoryUtilsTest.php new file mode 100644 index 000000000..035320650 --- /dev/null +++ b/tests/Composer/Test/Repository/RepositoryUtilsTest.php @@ -0,0 +1,92 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Repository; + +use Composer\Package\PackageInterface; +use Composer\Repository\RepositoryUtils; +use Composer\Test\TestCase; +use Generator; + +class RepositoryUtilsTest extends TestCase +{ + /** + * @dataProvider provideFilterRequireTests + * @param PackageInterface[] $pkgs + * @param PackageInterface $requirer + * @param string[] $expected + */ + public function testFilterRequiredPackages(array $pkgs, PackageInterface $requirer, array $expected): void + { + $expected = array_map(static function (string $name) use ($pkgs): PackageInterface { + return $pkgs[$name]; + }, $expected); + + self::assertSame($expected, RepositoryUtils::filterRequiredPackages($pkgs, $requirer)); + } + + /** + * @return array + */ + private function getPackages(): array + { + $packageA = $this->getPackage('required/a'); + $packageB = $this->getPackage('required/b'); + $this->configureLinks($packageB, ['require' => ['required/c' => '*']]); + $packageC = $this->getPackage('required/c'); + $packageCAlias = $this->getAliasPackage($packageC, '2.0.0'); + + $packageCircular = $this->getPackage('required/circular'); + $this->configureLinks($packageCircular, ['require' => ['required/circular-b' => '*']]); + $packageCircularB = $this->getPackage('required/circular-b'); + $this->configureLinks($packageCircularB, ['require' => ['required/circular' => '*']]); + + return [ + $this->getPackage('dummy/pkg'), + $this->getPackage('dummy/pkg2', '2.0.0'), + 'a' => $packageA, + 'b' => $packageB, + 'c' => $packageC, + 'c-alias' => $packageCAlias, + 'circular' => $packageCircular, + 'circular-b' => $packageCircularB, + ]; + } + + public function provideFilterRequireTests(): Generator + { + $pkgs = $this->getPackages(); + + $requirer = $this->getPackage('requirer/pkg'); + yield 'no require' => [$pkgs, $requirer, []]; + + $requirer = $this->getPackage('requirer/pkg'); + $this->configureLinks($requirer, ['require-dev' => ['required/a' => '*']]); + yield 'require-dev has no effect' => [$pkgs, $requirer, []]; + + $requirer = $this->getPackage('requirer/pkg'); + $this->configureLinks($requirer, ['require' => ['required/a' => '*']]); + yield 'simple require' => [$pkgs, $requirer, ['a']]; + + $requirer = $this->getPackage('requirer/pkg'); + $this->configureLinks($requirer, ['require' => ['required/a' => 'dev-lala']]); + yield 'require constraint is irrelevant' => [$pkgs, $requirer, ['a']]; + + $requirer = $this->getPackage('requirer/pkg'); + $this->configureLinks($requirer, ['require' => ['required/b' => '*']]); + yield 'require transitive deps and aliases are included' => [$pkgs, $requirer, ['b', 'c', 'c-alias']]; + + $requirer = $this->getPackage('requirer/pkg'); + $this->configureLinks($requirer, ['require' => ['required/circular' => '*']]); + yield 'circular deps are no problem' => [$pkgs, $requirer, ['circular', 'circular-b']]; + } +}