From 6d1b36be3b9923e267de455de1e0d07adf54a7bc Mon Sep 17 00:00:00 2001 From: PrinsFrank <25006490+PrinsFrank@users.noreply.github.com> Date: Fri, 16 Dec 2022 16:48:24 +0100 Subject: [PATCH] Check missing-from-lock-file required packages when running install and fail when there are any (#11195) --- phpstan/baseline.neon | 10 ----- src/Composer/Command/ValidateCommand.php | 30 +------------ src/Composer/Installer.php | 7 +++ src/Composer/Package/Locker.php | 43 +++++++++++++++++++ ...test => install-from-incomplete-lock.test} | 12 +++++- ...rements-do-not-affect-locked-versions.test | 12 +++++- 6 files changed, 71 insertions(+), 43 deletions(-) rename tests/Composer/Test/Fixtures/installer/{install-from-empty-lock.test => install-from-incomplete-lock.test} (52%) diff --git a/phpstan/baseline.neon b/phpstan/baseline.neon index 7fc49702e..b7b5d0da2 100644 --- a/phpstan/baseline.neon +++ b/phpstan/baseline.neon @@ -1020,21 +1020,11 @@ parameters: count: 1 path: ../src/Composer/Command/UpdateCommand.php - - - message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" - count: 1 - path: ../src/Composer/Command/ValidateCommand.php - - message: "#^Only booleans are allowed in an elseif condition, array\\ given\\.$#" count: 3 path: ../src/Composer/Command/ValidateCommand.php - - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" - count: 1 - path: ../src/Composer/Command/ValidateCommand.php - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" count: 5 diff --git a/src/Composer/Command/ValidateCommand.php b/src/Composer/Command/ValidateCommand.php index 42f7f4fbf..92ff158af 100644 --- a/src/Composer/Command/ValidateCommand.php +++ b/src/Composer/Command/ValidateCommand.php @@ -100,35 +100,7 @@ EOT } if ($locker->isLocked()) { - $missingRequirements = false; - $sets = [ - ['repo' => $locker->getLockedRepository(false), 'method' => 'getRequires', 'description' => 'Required'], - ['repo' => $locker->getLockedRepository(true), 'method' => 'getDevRequires', 'description' => 'Required (in require-dev)'], - ]; - foreach ($sets as $set) { - $installedRepo = new InstalledRepository([$set['repo']]); - - foreach (call_user_func([$composer->getPackage(), $set['method']]) as $link) { - if (PlatformRepository::isPlatformPackage($link->getTarget())) { - continue; - } - if (!$installedRepo->findPackagesWithReplacersAndProviders($link->getTarget(), $link->getConstraint())) { - if ($results = $installedRepo->findPackagesWithReplacersAndProviders($link->getTarget())) { - $provider = reset($results); - $lockErrors[] = '- ' . $set['description'].' package "' . $link->getTarget() . '" is in the lock file as "'.$provider->getPrettyVersion().'" but that does not satisfy your constraint "'.$link->getPrettyConstraint().'".'; - } else { - $lockErrors[] = '- ' . $set['description'].' package "' . $link->getTarget() . '" is not present in the lock file.'; - } - $missingRequirements = true; - } - } - } - - if ($missingRequirements) { - $lockErrors[] = 'This usually happens when composer files are incorrectly merged or the composer.json file is manually edited.'; - $lockErrors[] = 'Read more about correctly resolving merge conflicts https://getcomposer.org/doc/articles/resolving-merge-conflicts.md'; - $lockErrors[] = 'and prefer using the "require" command over editing the composer.json file directly https://getcomposer.org/doc/03-cli.md#require'; - } + $lockErrors = array_merge($lockErrors, $locker->getMissingRequirementInfo($composer->getPackage(), true)); } $this->outputResult($io, $file, $errors, $warnings, $checkPublish, $publishErrors, $checkLock, $lockErrors, true); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index ee12fab89..57890e3cd 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -711,6 +711,13 @@ class Installer $this->io->writeError('Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. It is recommended that you run `composer update` or `composer update `.', true, IOInterface::QUIET); } + $missingRequirementInfo = $this->locker->getMissingRequirementInfo($this->package, $this->devMode); + if ($missingRequirementInfo !== []) { + $this->io->writeError($missingRequirementInfo); + + return self::ERROR_LOCK_FILE_INVALID; + } + foreach ($lockedRepository->getPackages() as $package) { $request->fixLockedPackage($package); } diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 4a7cb14c8..34a0ed306 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -15,7 +15,9 @@ namespace Composer\Package; use Composer\Json\JsonFile; use Composer\Installer\InstallationManager; use Composer\Pcre\Preg; +use Composer\Repository\InstalledRepository; use Composer\Repository\LockArrayRepository; +use Composer\Repository\PlatformRepository; use Composer\Util\ProcessExecutor; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Loader\ArrayLoader; @@ -496,4 +498,45 @@ class Locker return $datetime ? $datetime->format(DATE_RFC3339) : null; } + + /** + * @return array + */ + public function getMissingRequirementInfo(RootPackageInterface $package, bool $includeDev): array + { + $missingRequirementInfo = []; + $missingRequirements = false; + $sets = [['repo' => $this->getLockedRepository(false), 'method' => 'getRequires', 'description' => 'Required']]; + if ($includeDev === true) { + $sets[] = ['repo' => $this->getLockedRepository(true), 'method' => 'getDevRequires', 'description' => 'Required (in require-dev)']; + } + + foreach ($sets as $set) { + $installedRepo = new InstalledRepository([$set['repo']]); + + foreach (call_user_func([$package, $set['method']]) as $link) { + if (PlatformRepository::isPlatformPackage($link->getTarget())) { + continue; + } + if ($installedRepo->findPackagesWithReplacersAndProviders($link->getTarget(), $link->getConstraint()) === []) { + $results = $installedRepo->findPackagesWithReplacersAndProviders($link->getTarget()); + if ($results !== []) { + $provider = reset($results); + $missingRequirementInfo[] = '- ' . $set['description'].' package "' . $link->getTarget() . '" is in the lock file as "'.$provider->getPrettyVersion().'" but that does not satisfy your constraint "'.$link->getPrettyConstraint().'".'; + } else { + $missingRequirementInfo[] = '- ' . $set['description'].' package "' . $link->getTarget() . '" is not present in the lock file.'; + } + $missingRequirements = true; + } + } + } + + if ($missingRequirements) { + $missingRequirementInfo[] = 'This usually happens when composer files are incorrectly merged or the composer.json file is manually edited.'; + $missingRequirementInfo[] = 'Read more about correctly resolving merge conflicts https://getcomposer.org/doc/articles/resolving-merge-conflicts.md'; + $missingRequirementInfo[] = 'and prefer using the "require" command over editing the composer.json file directly https://getcomposer.org/doc/03-cli.md#require'; + } + + return $missingRequirementInfo; + } } diff --git a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test b/tests/Composer/Test/Fixtures/installer/install-from-incomplete-lock.test similarity index 52% rename from tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test rename to tests/Composer/Test/Fixtures/installer/install-from-incomplete-lock.test index 0bba90cff..b794832b8 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-incomplete-lock.test @@ -1,5 +1,5 @@ --TEST-- -Requirements from the composer file are not installed if the lock file is present +Requirements from the composer file are not installed if the lock file is present, but fails on missing requirements --COMPOSER-- { "repositories": [ @@ -30,5 +30,13 @@ Requirements from the composer file are not installed if the lock file is presen } --RUN-- install +--EXPECT-OUTPUT-- +Installing dependencies from lock file (including require-dev) +Verifying lock file contents can be installed on current platform. +- Required package "newly-required/pkg" is not present in the lock file. +This usually happens when composer files are incorrectly merged or the composer.json file is manually edited. +Read more about correctly resolving merge conflicts https://getcomposer.org/doc/articles/resolving-merge-conflicts.md +and prefer using the "require" command over editing the composer.json file directly https://getcomposer.org/doc/03-cli.md#require +--EXPECT-EXIT-CODE-- +4 --EXPECT-- -Installing required/pkg (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test index 202767c1f..23cf41532 100644 --- a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test +++ b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test @@ -1,5 +1,5 @@ --TEST-- -The locked version will not get overwritten by an install +The locked version will not get overwritten by an install but fails on invalid packages --COMPOSER-- { "repositories": [ @@ -37,5 +37,13 @@ The locked version will not get overwritten by an install ] --RUN-- install +--EXPECT-OUTPUT-- +Installing dependencies from lock file (including require-dev) +Verifying lock file contents can be installed on current platform. +- Required package "foo/bar" is in the lock file as "1.0.0" but that does not satisfy your constraint "2.0.0". +This usually happens when composer files are incorrectly merged or the composer.json file is manually edited. +Read more about correctly resolving merge conflicts https://getcomposer.org/doc/articles/resolving-merge-conflicts.md +and prefer using the "require" command over editing the composer.json file directly https://getcomposer.org/doc/03-cli.md#require +--EXPECT-EXIT-CODE-- +4 --EXPECT-- -Upgrading foo/baz (1.0.0 => 2.0.0)