diff --git a/ecs.php b/ecs.php index b100e7a..bc1550f 100644 --- a/ecs.php +++ b/ecs.php @@ -2,9 +2,11 @@ declare(strict_types=1); +use Symplify\CodingStandard\Fixer\LineLength\LineLengthFixer; use Symplify\EasyCodingStandard\Config\ECSConfig; return ECSConfig::configure() ->withPreparedSets(psr12: true, common: true, symplify: true) + ->withRules([LineLengthFixer::class]) ->withPaths([__DIR__ . '/src', __DIR__ . '/tests']) ->withRootFiles(); diff --git a/src/Composer/ComposerOutdatedResponseProvider.php b/src/Composer/ComposerOutdatedResponseProvider.php index 69fd563..37d9ceb 100644 --- a/src/Composer/ComposerOutdatedResponseProvider.php +++ b/src/Composer/ComposerOutdatedResponseProvider.php @@ -4,6 +4,7 @@ namespace Rector\Jack\Composer; +use Nette\Utils\DateTime; use Nette\Utils\FileSystem; use Symfony\Component\Process\Process; @@ -11,10 +12,12 @@ final class ComposerOutdatedResponseProvider { public function provide(): string { - // load from cache, temporarily - @todo cache on json hash + week timeout - $outdatedFilename = __DIR__ . '/../../dumped-outdated.json'; - if (is_file($outdatedFilename)) { - return FileSystem::read($outdatedFilename); + $composerOutdatedFilePath = $this->resolveComposerOutdatedFilePath(); + + // let's use cache + if ($this->shouldLoadCacheFile($composerOutdatedFilePath)) { + /** @var string $composerOutdatedFilePath */ + return FileSystem::read($composerOutdatedFilePath); } $composerOutdatedProcess = Process::fromShellCommandline( @@ -23,9 +26,60 @@ public function provide(): string ); $composerOutdatedProcess->mustRun(); + $processResult = $composerOutdatedProcess->getOutput(); - FileSystem::write($outdatedFilename, $processResult); + if (is_string($composerOutdatedFilePath)) { + FileSystem::write($composerOutdatedFilePath, $processResult); + } + return $processResult; } + + private function resolveProjectComposerHash(): ?string + { + if (file_exists(getcwd() . '/composer.lock')) { + return sha1(getcwd() . '/composer.lock'); + } + + if (file_exists(getcwd() . '/composer.json')) { + return getcwd() . '/composer.json'; + } + + return null; + } + + private function resolveComposerOutdatedFilePath(): ?string + { + $projectComposerHash = $this->resolveProjectComposerHash(); + if ($projectComposerHash) { + // load from cache, temporarily - @todo cache on json hash + week timeout + return sys_get_temp_dir() . '/jack/composer-outdated-' . $projectComposerHash . '.json'; + } + + return null; + } + + private function isFileYoungerThanWeek(string $filePath): bool + { + $fileTime = filemtime($filePath); + if ($fileTime === false) { + return false; + } + + return (time() - $fileTime) < DateTime::WEEK; + } + + private function shouldLoadCacheFile(?string $cacheFilePath): bool + { + if (! is_string($cacheFilePath)) { + return false; + } + + if (! file_exists($cacheFilePath)) { + return false; + } + + return $this->isFileYoungerThanWeek($cacheFilePath); + } } diff --git a/src/Composer/NextVersionResolver.php b/src/Composer/NextVersionResolver.php index 92330ff..7ac37e3 100644 --- a/src/Composer/NextVersionResolver.php +++ b/src/Composer/NextVersionResolver.php @@ -11,14 +11,14 @@ /** * @see \Rector\Jack\Tests\Composer\NextVersionResolver\NextVersionResolverTest */ -final class NextVersionResolver +final readonly class NextVersionResolver { private const MAJOR = 'major'; private const MINOR = 'minor'; public function __construct( - private readonly VersionParser $versionParser + private VersionParser $versionParser ) { } diff --git a/src/Console/Command/CleanListCommand.php b/src/Console/Command/CleanListCommand.php new file mode 100644 index 0000000..40a8b50 --- /dev/null +++ b/src/Console/Command/CleanListCommand.php @@ -0,0 +1,67 @@ +getApplication(), Application::class); + + $output->writeln($this->getApplication()->getName()); + $output->writeln(''); + $output->writeln('Available commands:'); + + $applicationDescription = new ApplicationDescription($this->getApplication()); + $this->describeCommands($applicationDescription, $output); + + return self::SUCCESS; + } + + /** + * @param non-empty-array $commands + */ + private function resolveCommandNameColumnWidth(array $commands): int + { + $commandNameLengths = []; + foreach ($commands as $command) { + $commandNameLengths[] = strlen((string) $command->getName()); + } + + return max($commandNameLengths) + 4; + } + + private function describeCommands(ApplicationDescription $applicationDescription, OutputInterface $output): void + { + if ($applicationDescription->getCommands() === []) { + return; + } + + $commands = $applicationDescription->getCommands(); + $commandNameColumnWidth = $this->resolveCommandNameColumnWidth($commands); + + foreach ($commands as $command) { + $spacingWidth = $commandNameColumnWidth - strlen((string) $command->getName()); + + $output->writeln(sprintf( + ' %s%s%s', + $command->getName(), + str_repeat(' ', $spacingWidth), + $command->getDescription() + )); + } + } +} diff --git a/src/Console/JackConsoleApplication.php b/src/Console/JackConsoleApplication.php new file mode 100644 index 0000000..5830d35 --- /dev/null +++ b/src/Console/JackConsoleApplication.php @@ -0,0 +1,26 @@ +singleton(Application::class, function (Container $container): Application { - $application = new Application('Rector Jack'); + $jackConsoleApplication = new JackConsoleApplication('Rector Jack'); $commandClasses = $this->findCommandClasses(); // register commands foreach ($commandClasses as $commandClass) { $command = $container->make($commandClass); - $application->add($command); + $jackConsoleApplication->add($command); } // remove basic command to make output clear - $this->hideDefaultCommands($application); + $this->hideDefaultCommands($jackConsoleApplication); - return $application; + return $jackConsoleApplication; }); $container->singleton( diff --git a/src/OutdatedComposerFactory.php b/src/OutdatedComposerFactory.php index 9e866a7..83e302b 100644 --- a/src/OutdatedComposerFactory.php +++ b/src/OutdatedComposerFactory.php @@ -7,6 +7,9 @@ use Rector\Jack\Mapper\OutdatedPackageMapper; use Rector\Jack\ValueObject\OutdatedComposer; +/** + * @see \Rector\Jack\Tests\OutdatedComposerFactory\OutdatedComposerFactoryTest + */ final readonly class OutdatedComposerFactory { public function __construct( diff --git a/src/ValueObject/OutdatedComposer.php b/src/ValueObject/OutdatedComposer.php index 62b8baf..eddd859 100644 --- a/src/ValueObject/OutdatedComposer.php +++ b/src/ValueObject/OutdatedComposer.php @@ -66,11 +66,7 @@ public function getPackagesShuffled(bool $onlyDev = false): array { // adds random effect, not to always update by A-Z, as would force too narrow pattern // this is also more fun :) - if ($onlyDev) { - $outdatedPackages = $this->getDevPackages(); - } else { - $outdatedPackages = $this->outdatedPackages; - } + $outdatedPackages = $onlyDev ? $this->getDevPackages() : $this->outdatedPackages; shuffle($outdatedPackages); diff --git a/tests/OutdatedComposerFactory/Fixture/some-composer.json b/tests/OutdatedComposerFactory/Fixture/some-composer.json new file mode 100644 index 0000000..96aa4e1 --- /dev/null +++ b/tests/OutdatedComposerFactory/Fixture/some-composer.json @@ -0,0 +1,6 @@ +{ + "name": "some/project", + "require": { + "symfony/console": "^3.5" + } +} diff --git a/tests/OutdatedComposerFactory/OutdatedComposerFactoryTest.php b/tests/OutdatedComposerFactory/OutdatedComposerFactoryTest.php new file mode 100644 index 0000000..52f933f --- /dev/null +++ b/tests/OutdatedComposerFactory/OutdatedComposerFactoryTest.php @@ -0,0 +1,39 @@ +make(OutdatedComposerFactory::class); + + $outdatedComposer = $outdatedComposerFactory->createOutdatedComposer([ + [ + 'name' => 'symfony/console', + 'direct-dependency' => true, + 'homepage' => 'https://symfony.com', + 'source' => 'https://github.com/symfony/console/tree/v6.4.20', + 'version' => 'v6.4.20', + 'release-age' => '1 month old', + 'release-date' => '2025-03-03T17:16:38+00:00', + 'latest' => 'v7.2.6', + 'latest-status' => 'update-possible', + 'latest-release-date' => '2025-04-07T19:09:28+00:00', + 'description' => 'Eases the creation of beautiful and testable command line interfaces', + 'abandoned' => false, + ], + ], __DIR__ . '/Fixture/some-composer.json'); + + $this->assertCount(1, $outdatedComposer->getProdPackages()); + $this->assertContainsOnlyInstancesOf(OutdatedPackage::class, $outdatedComposer->getProdPackages()); + + $this->assertCount(0, $outdatedComposer->getDevPackages()); + } +}