Refresh abandoned state from the repositories in audit
The abandoned flag in composer.lock and installed.json is only written when a package gets updated, so composer audit never reported packages that were abandoned after that. The audit command now loads the audited versions from the repositories, first repository with the package name wins, and syncs the abandoned state and replacement from there. This is skipped with --abandoned=ignore and unreachable repositories are skipped with --ignore-unreachable like for advisories.
This commit is contained in:
1 parent
12931dd03f
commit
d4a3091c46
2 files changed
+80
No files matched your search
@@ -13,6 +13,7 @@
|
||||
namespace Composer\Command;
|
||||
|
||||
use Composer\Composer;
|
||||
use Composer\Downloader\TransportException;
|
||||
use Composer\FilterList\FilterListProvider\FilterListProviderSet;
|
||||
use Composer\Policy\ListPolicyConfig;
|
||||
use Composer\Policy\PolicyConfig;
|
||||
@@ -20,10 +21,14 @@ use Composer\Repository\RepositorySet;
|
||||
use Composer\Repository\RepositoryUtils;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Composer\Package\AliasPackage;
|
||||
use Composer\Package\BasePackage;
|
||||
use Composer\Package\CompletePackageInterface;
|
||||
use Composer\Package\PackageInterface;
|
||||
use Composer\Repository\InstalledRepository;
|
||||
use Composer\Advisory\Auditor;
|
||||
use Composer\Console\Input\InputOption;
|
||||
use Composer\Semver\Constraint\Constraint;
|
||||
|
||||
class AuditCommand extends BaseCommand
|
||||
{
|
||||
@@ -96,6 +101,10 @@ EOT
|
||||
$policyConfig = $policyConfig->withIgnoreUnreachable('audit');
|
||||
}
|
||||
|
||||
if ($policyConfig->abandoned->audit !== ListPolicyConfig::AUDIT_IGNORE) {
|
||||
$this->refreshAbandonedState($composer, $packages, $policyConfig->ignoreUnreachable->audit);
|
||||
}
|
||||
|
||||
$filterListProviderSet = $policyConfig->enabled ? FilterListProviderSet::create($policyConfig, $composer->getRepositoryManager()->getRepositories(), $composer->getLoop()->getHttpDownloader()) : null;
|
||||
|
||||
return min(255, $auditor->audit(
|
||||
@@ -133,4 +142,54 @@ EOT
|
||||
|
||||
return $installedRepo->getPackages();
|
||||
}
|
||||
|
||||
/**
|
||||
* The lock file and installed.json keep the abandoned state a package had when it was last updated,
|
||||
* so it is refreshed from the repositories in case the package was abandoned or unabandoned since
|
||||
*
|
||||
* @param PackageInterface[] $packages
|
||||
*/
|
||||
private function refreshAbandonedState(Composer $composer, array $packages, bool $ignoreUnreachable): void
|
||||
{
|
||||
$packagesByName = [];
|
||||
$packageNameMap = [];
|
||||
$stabilityFlags = [];
|
||||
foreach ($packages as $package) {
|
||||
if (!$package instanceof CompletePackageInterface || $package instanceof AliasPackage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$packagesByName[$package->getName()] = $package;
|
||||
$packageNameMap[$package->getName()] = new Constraint('==', $package->getVersion());
|
||||
// per package stability so stable versions do not need to load the ~dev metadata
|
||||
$stabilityFlags[$package->getName()] = BasePackage::STABILITIES[$package->getStability()];
|
||||
}
|
||||
|
||||
foreach ($composer->getRepositoryManager()->getRepositories() as $repository) {
|
||||
if ($packageNameMap === []) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $repository->loadPackages($packageNameMap, [], $stabilityFlags);
|
||||
} catch (TransportException $e) {
|
||||
if (!$ignoreUnreachable) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($result['packages'] as $repoPackage) {
|
||||
if ($repoPackage instanceof CompletePackageInterface && !$repoPackage instanceof AliasPackage) {
|
||||
$packagesByName[$repoPackage->getName()]->setAbandoned($repoPackage->isAbandoned() ? ($repoPackage->getReplacementPackage() ?? true) : false);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($result['namesFound'] as $name) {
|
||||
// like RepositorySet::findPackages, do not query lower priority repositories for names already found
|
||||
unset($packageNameMap[$name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,6 +146,27 @@ class AuditCommandTest extends TestCase
|
||||
self::assertSame(Auditor::STATUS_OK, $exitCode);
|
||||
}
|
||||
|
||||
public function testAuditReportsPackageAbandonedAfterItWasLocked(): void
|
||||
{
|
||||
$this->initTempComposer([
|
||||
'repositories' => [
|
||||
'packages' => [
|
||||
'type' => 'package',
|
||||
'package' => [
|
||||
['name' => 'vendor/abandoned', 'version' => '1.0.0', 'abandoned' => 'vendor/replacement'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->createComposerLockWithPackages([['name' => 'vendor/abandoned', 'version' => '1.0.0']]);
|
||||
|
||||
$appTester = $this->getApplicationTester();
|
||||
$exitCode = $appTester->run(['command' => 'audit', '--format' => 'plain']);
|
||||
|
||||
self::assertStringContainsString('vendor/abandoned is abandoned. Use vendor/replacement instead.', $appTester->getDisplay(true));
|
||||
self::assertSame(Auditor::STATUS_FAILED, $exitCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{name: string, version: string}> $packages
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user