diff --git a/.github/workflows/code_analysis.yaml b/.github/workflows/code_analysis.yaml index e427c16a2..7926b339f 100644 --- a/.github/workflows/code_analysis.yaml +++ b/.github/workflows/code_analysis.yaml @@ -34,11 +34,11 @@ jobs: - name: 'Check Commented Code' - run: bin/swiss-knife check-commented-code src tests --ansi + run: bin/swiss-knife check-commented-code src tests - name: 'Check Active Classes' - run: vendor/bin/class-leak check bin src --ansi + run: vendor/bin/class-leak check bin src --skip-type="\Entropy\Console\Contract\CommandInterface" - name: 'Unusued check' diff --git a/bin/swiss-knife.php b/bin/swiss-knife.php index bc8cc9b1a..f12534588 100755 --- a/bin/swiss-knife.php +++ b/bin/swiss-knife.php @@ -2,9 +2,7 @@ declare(strict_types=1); -use Symfony\Component\Console\Application; -use Symfony\Component\Console\Input\ArgvInput; -use Symfony\Component\Console\Output\ConsoleOutput; +use Entropy\Console\ConsoleApplication; use Rector\SwissKnife\DependencyInjection\ContainerFactory; $scoperAutoloadFilepath = __DIR__ . '/../vendor/scoper-autoload.php'; @@ -32,7 +30,7 @@ $containerFactory = new ContainerFactory(); $container = $containerFactory->create(); -$application = $container->make(Application::class); +$consoleApplication = $container->make(ConsoleApplication::class); -$exitCode = $application->run(new ArgvInput(), new ConsoleOutput()); +$exitCode = $consoleApplication->run($argv); exit($exitCode); diff --git a/composer.json b/composer.json index 6effa6410..0077e5ef1 100644 --- a/composer.json +++ b/composer.json @@ -21,10 +21,10 @@ "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^12.5", + "symfony/config": "^6.4", "rector/jack": "^0.5.1", "rector/rector": "^2.3", "shipmonk/composer-dependency-analyser": "^1.8", - "symfony/config": "^6.4", "symfony/dependency-injection": "^6.4", "symplify/phpstan-extensions": "^12.0", "tomasvotruba/class-leak": "^2.1", diff --git a/phpstan.neon b/phpstan.neon index e3e6eb43f..55574fd35 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -33,7 +33,12 @@ parameters: identifier: argument.type path: src/Testing/MockWire.php - # optional command, depends on present of classes + # magic contract - - identifier: argument.unresolvableType - path: src/Command/GenerateSymfonyConfigBuildersCommand.php + identifier: public.method.unused + message: '#Public method "Rector\\SwissKnife\\(.*?)Command\:\:run\(\)" is never used#' + + # depends on symfony extenison class presence + - + message: '#Parameter \#1 \$objectOrClass of class ReflectionClass constructor contains unresolvable type#' + path: src/Command/GenerateSymfonyConfigBuildersCommand.php diff --git a/rector.php b/rector.php index ec6fe6492..2870b9d88 100644 --- a/rector.php +++ b/rector.php @@ -5,18 +5,20 @@ use Rector\Config\RectorConfig; return RectorConfig::configure() - ->withPaths([__DIR__ . '/src', __DIR__ . '/tests']) + ->withPaths([__DIR__ . '/bin', __DIR__ . '/src', __DIR__ . '/tests']) ->withPhpSets() + ->withRootFiles() ->withPreparedSets( - codeQuality: true, deadCode: true, + codeQuality: true, + codingStyle: true, typeDeclarations: true, typeDeclarationDocblocks: true, privatization: true, - earlyReturn: true, - codingStyle: true, + naming: true, instanceOf: true, - naming: true + earlyReturn: true, + rectorPreset: true, ) ->withImportNames(removeUnusedImports: true) ->withSkip(['*/scoper.php', '*/Source/*', '*/Fixture/*']); diff --git a/src/Command/AliceYamlFixturesToPhpCommand.php b/src/Command/AliceYamlFixturesToPhpCommand.php index 4349a8a1b..5197d042a 100644 --- a/src/Command/AliceYamlFixturesToPhpCommand.php +++ b/src/Command/AliceYamlFixturesToPhpCommand.php @@ -4,45 +4,32 @@ namespace Rector\SwissKnife\Command; +use Entropy\Console\Contract\CommandInterface; +use Entropy\Console\Enum\ExitCode; use Nette\Utils\FileSystem; use PhpParser\BuilderHelpers; use PhpParser\Node\Stmt\Return_; use PhpParser\PrettyPrinter\Standard; use Rector\SwissKnife\Finder\FilesFinder; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Yaml\Yaml; /** * @see https://github.com/nelmio/alice/blob/v2.3.0/doc/complete-reference.md#php */ -final class AliceYamlFixturesToPhpCommand extends Command +final readonly class AliceYamlFixturesToPhpCommand implements CommandInterface { public function __construct( - private readonly SymfonyStyle $symfonyStyle, + private SymfonyStyle $symfonyStyle, ) { - parent::__construct(); } - protected function configure(): void - { - $this->setName('alice-yaml-fixtures-to-php'); - - $this->addArgument( - 'sources', - InputArgument::REQUIRED | InputArgument::IS_ARRAY, - 'One or more paths to check' - ); - - $this->setDescription('Converts Alice YAML fixtures to PHP format, so Rector and PHPStan can understand it'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int + /** + * @param string[] $sources One or more paths to check + * @return ExitCode::* + */ + public function run(array $sources): int { - $sources = (array) $input->getArgument('sources'); $yamlFileInfos = FilesFinder::findYamlFiles($sources); $standard = new Standard(); @@ -75,7 +62,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int sprintf('Successfully converted %d Alice YAML fixtures to PHP', count($yamlFileInfos)) ); - return self::SUCCESS; + return ExitCode::SUCCESS; + } + + public function getName(): string + { + return 'alice-yaml-fixtures-to-php'; + } + + public function getDescription(): string + { + return 'Converts Alice YAML fixtures to PHP format, so Rector and PHPStan can understand it'; } /** diff --git a/src/Command/CheckCommentedCodeCommand.php b/src/Command/CheckCommentedCodeCommand.php index 6cdfc8b6e..166951fa6 100644 --- a/src/Command/CheckCommentedCodeCommand.php +++ b/src/Command/CheckCommentedCodeCommand.php @@ -4,64 +4,36 @@ namespace Rector\SwissKnife\Command; +use Entropy\Console\Contract\CommandInterface; +use Entropy\Console\Enum\ExitCode; use Rector\SwissKnife\Comments\CommentedCodeAnalyzer; use Rector\SwissKnife\Finder\PhpFilesFinder; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class CheckCommentedCodeCommand extends Command +final readonly class CheckCommentedCodeCommand implements CommandInterface { private const int DEFAULT_LINE_LIMIT = 5; public function __construct( - private readonly CommentedCodeAnalyzer $commentedCodeAnalyzer, - private readonly SymfonyStyle $symfonyStyle, + private CommentedCodeAnalyzer $commentedCodeAnalyzer, + private SymfonyStyle $symfonyStyle, ) { - parent::__construct(); } - protected function configure(): void + /** + * @param string[] $sources One or more paths to check + * @param string[] $skipFiles File paths to skip + * @param int $lineLimit Maximum number of comment lines in a row allowed + * + * @return ExitCode::* + */ + public function run(array $sources, array $skipFiles = [], int $lineLimit = self::DEFAULT_LINE_LIMIT): int { - $this->setName('check-commented-code'); - - $this->addArgument( - 'sources', - InputArgument::REQUIRED | InputArgument::IS_ARRAY, - 'One or more paths to check' - ); - $this->addOption( - 'skip-file', - null, - InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Skip file path' - ); - $this->setDescription('Checks code for commented snippets'); - - $this->addOption( - 'line-limit', - null, - InputOption::VALUE_REQUIRED | InputOption::VALUE_OPTIONAL, - 'Amount of allowed comment lines in a row', - self::DEFAULT_LINE_LIMIT - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $sources = (array) $input->getArgument('sources'); - $skipFiles = (array) $input->getOption('skip-file'); - $phpFileInfos = PhpFilesFinder::find($sources, $skipFiles); $message = sprintf('Analysing %d *.php files', count($phpFileInfos)); $this->symfonyStyle->note($message); - $lineLimit = (int) $input->getOption('line-limit'); - $commentedLinesByFilePaths = []; foreach ($phpFileInfos as $phpFileInfo) { $commentedLines = $this->commentedCodeAnalyzer->process($phpFileInfo->getRealPath(), $lineLimit); @@ -75,7 +47,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($commentedLinesByFilePaths === []) { $this->symfonyStyle->success('No commented code found'); - return self::SUCCESS; + return ExitCode::SUCCESS; } foreach ($commentedLinesByFilePaths as $filePath => $commentedLines) { @@ -86,6 +58,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $this->symfonyStyle->error('Errors found'); - return self::FAILURE; + + return ExitCode::ERROR; + } + + public function getName(): string + { + return 'check-commented-code'; + } + + public function getDescription(): string + { + return 'Checks code for commented snippets'; } } diff --git a/src/Command/CheckConflictsCommand.php b/src/Command/CheckConflictsCommand.php index 866a90da3..75121b749 100644 --- a/src/Command/CheckConflictsCommand.php +++ b/src/Command/CheckConflictsCommand.php @@ -4,37 +4,28 @@ namespace Rector\SwissKnife\Command; +use Entropy\Console\Contract\CommandInterface; +use Entropy\Console\Enum\ExitCode; use Rector\SwissKnife\Finder\FilesFinder; use Rector\SwissKnife\Git\ConflictResolver; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class CheckConflictsCommand extends Command +final readonly class CheckConflictsCommand implements CommandInterface { public function __construct( - private readonly ConflictResolver $conflictResolver, - private readonly SymfonyStyle $symfonyStyle, + private ConflictResolver $conflictResolver, + private SymfonyStyle $symfonyStyle, ) { - parent::__construct(); } - protected function configure(): void + /** + * @param string[] $sources One or more path to project + * @return ExitCode::* + */ + public function run(array $sources): int { - $this->setName('check-conflicts'); - - $this->setDescription('Check files for missed git conflicts'); - $this->addArgument('sources', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'Path to project'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - /** @var string[] $sources */ - $sources = (array) $input->getArgument('sources'); - $fileInfos = FilesFinder::find($sources); + $filePaths = []; foreach ($fileInfos as $fileInfo) { $filePaths[] = $fileInfo->getRealPath(); @@ -45,7 +36,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $message = sprintf('No conflicts found in %d files', count($fileInfos)); $this->symfonyStyle->success($message); - return self::SUCCESS; + return ExitCode::SUCCESS; } foreach ($conflictsCountByFilePath as $file => $conflictCount) { @@ -53,6 +44,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->symfonyStyle->error($message); } - return self::FAILURE; + return ExitCode::ERROR; + } + + public function getName(): string + { + return 'check-conflicts'; + } + + public function getDescription(): string + { + return 'Check files for missed git conflicts'; } } diff --git a/src/Command/DumpEditorconfigCommand.php b/src/Command/DumpEditorconfigCommand.php index 7ff3938e1..3919429ec 100644 --- a/src/Command/DumpEditorconfigCommand.php +++ b/src/Command/DumpEditorconfigCommand.php @@ -4,37 +4,41 @@ namespace Rector\SwissKnife\Command; +use Entropy\Console\Contract\CommandInterface; +use Entropy\Console\Enum\ExitCode; use Nette\Utils\FileSystem; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class DumpEditorconfigCommand extends Command +final readonly class DumpEditorconfigCommand implements CommandInterface { public function __construct( - private readonly SymfonyStyle $symfonyStyle, + private SymfonyStyle $symfonyStyle, ) { - parent::__construct(); } - protected function configure(): void + public function getName(): string { - $this->setName('dump-editorconfig'); - $this->setDescription('Dump .editorconfig file to project root'); + return 'dump-editorconfig'; } - protected function execute(InputInterface $input, OutputInterface $output): int + public function getDescription(): string + { + return 'Dump .editorconfig file to project root'; + } + + public function run(): int { $projectEditorconfigFilePath = getcwd() . '/.editorconfig'; if (file_exists($projectEditorconfigFilePath)) { $this->symfonyStyle->error('.editorconfig file already exists'); - return self::FAILURE; + + return ExitCode::ERROR; } FileSystem::copy(__DIR__ . '/../../templates/.editorconfig', $projectEditorconfigFilePath); + $this->symfonyStyle->success('.editorconfig file was created'); - return self::SUCCESS; + return ExitCode::SUCCESS; } } diff --git a/src/Command/FinalizeClassesCommand.php b/src/Command/FinalizeClassesCommand.php index 858f17ec4..207872273 100644 --- a/src/Command/FinalizeClassesCommand.php +++ b/src/Command/FinalizeClassesCommand.php @@ -4,6 +4,8 @@ namespace Rector\SwissKnife\Command; +use Entropy\Console\Contract\CommandInterface; +use Entropy\Console\Enum\ExitCode; use Nette\Utils\FileSystem; use Nette\Utils\Strings; use Rector\SwissKnife\Analyzer\NeedsFinalizeAnalyzer; @@ -13,14 +15,9 @@ use Rector\SwissKnife\MockedClassResolver; use Rector\SwissKnife\ParentClassResolver; use Rector\SwissKnife\PhpParser\CachedPhpParser; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class FinalizeClassesCommand extends Command +final readonly class FinalizeClassesCommand implements CommandInterface { /** * @see https://regex101.com/r/Q5Nfbo/1 @@ -28,66 +25,35 @@ final class FinalizeClassesCommand extends Command private const string NEWLINE_CLASS_START_REGEX = '#^(readonly )?class\s#m'; public function __construct( - private readonly SymfonyStyle $symfonyStyle, - private readonly ParentClassResolver $parentClassResolver, - private readonly EntityClassResolver $entityClassResolver, - private readonly CachedPhpParser $cachedPhpParser, - private readonly MockedClassResolver $mockedClassResolver, + private SymfonyStyle $symfonyStyle, + private ParentClassResolver $parentClassResolver, + private EntityClassResolver $entityClassResolver, + private CachedPhpParser $cachedPhpParser, + private MockedClassResolver $mockedClassResolver, ) { - parent::__construct(); - } - - protected function configure(): void - { - $this->setName('finalize-classes'); - $this->setAliases(['finalise', 'finalise-classes']); - - $this->setDescription('Finalize classes without children'); - - $this->addArgument('paths', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Directories to finalize'); - - $this->addOption( - 'skip-mocked', - null, - InputOption::VALUE_NONE, - 'Skip mocked classes as well (use only if unable to run bypass-finals package)' - ); - - $this->addOption( - 'skip-file', - null, - InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, - 'Skip file or files by path' - ); - - $this->addOption( - 'dry-run', - null, - InputOption::VALUE_NONE, - 'Do no change anything, only list classes about to be finalized. If there are classes to finalize, it will exit with code 1. Useful for CI.' - ); - - $this->addOption('no-progress', null, InputOption::VALUE_NONE, 'Do not show progress bar, only results'); } /** - * @return self::FAILURE|self::SUCCESS + * @param string[] $paths Directories to finalize + * @param bool $dryRun Do no change anything, only list classes about to be finalized. If there are classes to finalize, it will exit with code 1. Useful for CI. + * @param bool $skipMocked Skip mocked classes as well (use only if unable to run bypass-finals package) + * @param string[] $skipFiles Skip file or files by path + * @param bool $noProgress Do not show progress bar, only results */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - $paths = (array) $input->getArgument('paths'); - $isDryRun = (bool) $input->getOption('dry-run'); - $areMockedSkipped = (bool) $input->getOption('skip-mocked'); - + public function run( + array $paths, + bool $dryRun = false, + bool $skipMocked = false, + array $skipFiles = [], + bool $noProgress = false + ): int { $this->symfonyStyle->title('1. Detecting parent and entity classes'); - $skippedFiles = $input->getOption('skip-file'); - $phpFileInfos = PhpFilesFinder::find($paths, $skippedFiles); + $phpFileInfos = PhpFilesFinder::find($paths, $skipFiles); - $noProgress = (bool) $input->getOption('no-progress'); if (! $noProgress) { // double to count for both parent and entity resolver - $stepRatio = $areMockedSkipped ? 3 : 2; + $stepRatio = $skipMocked ? 3 : 2; $this->symfonyStyle->progressStart($stepRatio * count($phpFileInfos)); } @@ -103,7 +69,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $parentClassNames = $this->parentClassResolver->resolve($phpFileInfos, $progressClosure); $entityClassNames = $this->entityClassResolver->resolve($paths, $progressClosure); - $mockedClassNames = $areMockedSkipped ? $this->mockedClassResolver->resolve($paths, $progressClosure) : []; + $mockedClassNames = $skipMocked ? $this->mockedClassResolver->resolve($paths, $progressClosure) : []; if (! $noProgress) { $this->symfonyStyle->progressFinish(); @@ -115,7 +81,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int count($entityClassNames) )); - if ($areMockedSkipped) { + if ($skipMocked) { $this->symfonyStyle->writeln(sprintf('Also %d mocked classes', count($mockedClassNames))); } @@ -142,14 +108,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $finalizedFilePaths[] = PathHelper::relativeToCwd($phpFileInfo->getRealPath()); - if ($isDryRun === false) { + if ($dryRun === false) { FileSystem::write($phpFileInfo->getRealPath(), $finalizedContents, null); } } if ($finalizedFilePaths === []) { $this->symfonyStyle->success('Nothing to finalize'); - return self::SUCCESS; + return ExitCode::SUCCESS; } $this->symfonyStyle->listing($finalizedFilePaths); @@ -158,18 +124,28 @@ protected function execute(InputInterface $input, OutputInterface $output): int $pluralClassText = $countFinalizedClasses === 1 ? 'class' : 'classes'; // to make it fail in CI - if ($isDryRun) { + if ($dryRun) { $this->symfonyStyle->error(sprintf( '%d %s can be finalized', $countFinalizedClasses, $pluralClassText, )); - return self::FAILURE; + return ExitCode::ERROR; } $this->symfonyStyle->success(sprintf('%d %s finalized', $countFinalizedClasses, $pluralClassText)); - return self::SUCCESS; + return ExitCode::SUCCESS; + } + + public function getName(): string + { + return 'finalize-classes'; + } + + public function getDescription(): string + { + return 'Finalize classes without children'; } } diff --git a/src/Command/FindMultiClassesCommand.php b/src/Command/FindMultiClassesCommand.php index bab77c1e8..3a607b65a 100644 --- a/src/Command/FindMultiClassesCommand.php +++ b/src/Command/FindMultiClassesCommand.php @@ -4,59 +4,36 @@ namespace Rector\SwissKnife\Command; +use Entropy\Console\Contract\CommandInterface; +use Entropy\Console\Enum\ExitCode; use Rector\SwissKnife\FileSystem\PathHelper; use Rector\SwissKnife\Finder\MultipleClassInOneFileFinder; use Rector\SwissKnife\Finder\PhpFilesFinder; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class FindMultiClassesCommand extends Command +final readonly class FindMultiClassesCommand implements CommandInterface { public function __construct( - private readonly MultipleClassInOneFileFinder $multipleClassInOneFileFinder, - private readonly SymfonyStyle $symfonyStyle, + private MultipleClassInOneFileFinder $multipleClassInOneFileFinder, + private SymfonyStyle $symfonyStyle, ) { - parent::__construct(); } - protected function configure(): void + /** + * @param string[] $sources Path to source to analyse + * @param string[] $excludePaths Paths to exclude + * + * @return ExitCode::* + */ + public function run(array $sources, array $excludePaths): int { - $this->setName('find-multi-classes'); + $phpFileInfos = PhpFilesFinder::find($sources, $excludePaths); - $this->setDescription('Find multiple classes in one file'); - - $this->addArgument( - 'sources', - InputArgument::REQUIRED | InputArgument::IS_ARRAY, - 'Path to source to analyse' - ); - - $this->addOption( - 'exclude-path', - null, - InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, - 'Path to exclude' - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - /** @var string[] $source */ - $source = $input->getArgument('sources'); - - $excludedPaths = (array) $input->getOption('exclude-path'); - - $phpFileInfos = PhpFilesFinder::find($source, $excludedPaths); - - $multipleClassesByFile = $this->multipleClassInOneFileFinder->findInDirectories($source, $excludedPaths); + $multipleClassesByFile = $this->multipleClassInOneFileFinder->findInDirectories($sources, $excludePaths); if ($multipleClassesByFile === []) { $this->symfonyStyle->success(sprintf('No file with 2+ classes found in %d files', count($phpFileInfos))); - return self::SUCCESS; + return ExitCode::SUCCESS; } foreach ($multipleClassesByFile as $filePath => $classes) { @@ -68,6 +45,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->symfonyStyle->listing($classes); } - return self::FAILURE; + return ExitCode::ERROR; + } + + public function getName(): string + { + return 'find-multi-classes'; + } + + public function getDescription(): string + { + return 'Find multiple classes in one file'; } } diff --git a/src/Command/GenerateSymfonyConfigBuildersCommand.php b/src/Command/GenerateSymfonyConfigBuildersCommand.php index 4e6ddd8c0..66ebc0303 100644 --- a/src/Command/GenerateSymfonyConfigBuildersCommand.php +++ b/src/Command/GenerateSymfonyConfigBuildersCommand.php @@ -1,14 +1,13 @@ setName('generate-symfony-config-builders'); + return 'generate-symfony-config-builders'; + } - $this->setDescription( - 'Generate Symfony config classes to /var/cache/Symfony directory, see https://symfony.com/blog/new-in-symfony-5-3-config-builder-classes' - ); + public function getDescription(): string + { + return 'Generate Symfony config classes to /var/cache/Symfony directory, see https://symfony.com/blog/new-in-symfony-5-3-config-builder-classes'; } - protected function execute(InputInterface $input, OutputInterface $output): int + public function run(InputInterface $input, OutputInterface $output): int { // make sure the classes exist if (! class_exists(ConfigBuilderGenerator::class) || ! class_exists(ContainerBuilder::class)) { @@ -57,7 +56,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'This command requires symfony/config and symfony/dependency-injection 5.3+ to run. Update your dependencies or install them first.' ); - return self::FAILURE; + return ExitCode::ERROR; } $configBuilderGenerator = new ConfigBuilderGenerator(getcwd() . '/var/cache'); @@ -82,7 +81,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->symfonyStyle->success('Done'); - return self::SUCCESS; + return ExitCode::SUCCESS; } /** diff --git a/src/Command/NamespaceToPSR4Command.php b/src/Command/NamespaceToPSR4Command.php index fc050774a..b36ed03cd 100644 --- a/src/Command/NamespaceToPSR4Command.php +++ b/src/Command/NamespaceToPSR4Command.php @@ -4,52 +4,30 @@ namespace Rector\SwissKnife\Command; +use Entropy\Console\Contract\CommandInterface; +use Entropy\Console\Enum\ExitCode; use Nette\Utils\FileSystem; use Nette\Utils\Strings; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; -final class NamespaceToPSR4Command extends Command +final readonly class NamespaceToPSR4Command implements CommandInterface { public function __construct( - private readonly SymfonyStyle $symfonyStyle, + private SymfonyStyle $symfonyStyle, ) { - parent::__construct(); - } - - protected function configure(): void - { - $this->setName('namespace-to-psr-4'); - - $this->setDescription('Change namespace in your PHP files to match PSR-4 root'); - - $this->addArgument( - 'path', - InputArgument::REQUIRED, - 'Single directory path to ensure namespace matches, e.g. "tests"' - ); - - $this->addOption( - 'namespace-root', - null, - InputOption::VALUE_REQUIRED, - 'Namespace root for files in provided path, e.g. "App\\Tests"' - ); } /** - * @return self::* + * @param string $path Single directory path to ensure namespace matches, e.g. "tests" + * @param string $namespaceRoot Namespace root for files in provided path, e.g. "App\\Tests" + * + * @return ExitCode::* */ - protected function execute(InputInterface $input, OutputInterface $output): int + public function run(string $path, string $namespaceRoot): int { - $path = (string) $input->getArgument('path'); - $namespaceRoot = rtrim((string) $input->getOption('namespace-root'), '\\'); + $namespaceRoot = rtrim($namespaceRoot, '\\'); $namespaceRoot = str_replace('\\\\', '\\', $namespaceRoot); $fileInfos = $this->findFilesInPath($path); @@ -93,7 +71,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->symfonyStyle->success(sprintf('Fixed %d files', $changedFilesCount)); } - return self::SUCCESS; + return ExitCode::SUCCESS; + } + + public function getName(): string + { + return 'namespace-to-psr-4'; + } + + public function getDescription(): string + { + return 'Change namespace in your PHP files to match PSR-4 root'; } /** diff --git a/src/Command/PrettyJsonCommand.php b/src/Command/PrettyJsonCommand.php index 59b335ad0..9273af135 100644 --- a/src/Command/PrettyJsonCommand.php +++ b/src/Command/PrettyJsonCommand.php @@ -4,56 +4,40 @@ namespace Rector\SwissKnife\Command; +use Entropy\Console\Contract\CommandInterface; +use Entropy\Console\Enum\ExitCode; use Nette\Utils\FileSystem; use Nette\Utils\Json; use Rector\SwissKnife\FileSystem\JsonAnalyzer; use Rector\SwissKnife\Finder\FilesFinder; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class PrettyJsonCommand extends Command +final readonly class PrettyJsonCommand implements CommandInterface { public function __construct( - private readonly SymfonyStyle $symfonyStyle, - private readonly JsonAnalyzer $jsonAnalyzer, + private SymfonyStyle $symfonyStyle, + private JsonAnalyzer $jsonAnalyzer, ) { - parent::__construct(); } - protected function configure(): void + /** + * @param string[] $sources JSON file or directory with JSON files to prettify + * @param bool $dryRun Dry run - no changes will be made + * + * @return ExitCode::* + */ + public function run(array $sources, bool $dryRun = false): int { - $this->setName('pretty-json'); - - $this->setDescription('Turns JSON files from 1-line to pretty print format'); - - $this->addArgument( - 'sources', - InputArgument::REQUIRED | InputArgument::IS_ARRAY, - 'JSON file or directory with JSON files to prettify' - ); - - $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Dry run - no changes will be made'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $sources = (array) $input->getArgument('sources'); $jsonFileInfos = FilesFinder::findJsonFiles($sources); if ($jsonFileInfos === []) { $this->symfonyStyle->error('No *.json files found'); - return self::FAILURE; + return ExitCode::ERROR; } $message = sprintf('Analysing %d *.json files', count($jsonFileInfos)); $this->symfonyStyle->note($message); - $isDryRun = (bool) $input->getOption('dry-run'); - $printedFilePaths = []; // convert file infos from uggly json to pretty json @@ -70,7 +54,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $printedFilePaths[] = $jsonFileInfo->getRelativePathname(); // nothing will be changed - if ($isDryRun) { + if ($dryRun) { continue; } @@ -82,12 +66,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int '%d file%s %s', count($printedFilePaths), count($printedFilePaths) === 1 ? '' : 's', - $isDryRun ? 'would be changed' : 'changed' + $dryRun ? 'would be changed' : 'changed' ); $this->symfonyStyle->success($successMessage); $this->symfonyStyle->listing($printedFilePaths); - return self::SUCCESS; + return ExitCode::SUCCESS; + } + + public function getName(): string + { + return 'pretty-json'; + } + + public function getDescription(): string + { + return 'Turns JSON files from 1-line to pretty print format'; } } diff --git a/src/Command/PrivatizeConstantsCommand.php b/src/Command/PrivatizeConstantsCommand.php index 5449b4b53..2afb7d8c6 100644 --- a/src/Command/PrivatizeConstantsCommand.php +++ b/src/Command/PrivatizeConstantsCommand.php @@ -4,6 +4,8 @@ namespace Rector\SwissKnife\Command; +use Entropy\Console\Contract\CommandInterface; +use Entropy\Console\Enum\ExitCode; use Nette\Utils\FileSystem; use Nette\Utils\Strings; use Rector\SwissKnife\Contract\ClassConstantFetchInterface; @@ -15,62 +17,43 @@ use Rector\SwissKnife\ValueObject\ClassConstantFetch\CurrentClassConstantFetch; use Rector\SwissKnife\ValueObject\VisibilityChangeStats; use Rector\SwissKnife\YAML\YamlConfigConstantExtractor; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Finder\SplFileInfo; -final class PrivatizeConstantsCommand extends Command +final readonly class PrivatizeConstantsCommand implements CommandInterface { public function __construct( - private readonly SymfonyStyle $symfonyStyle, - private readonly ClassConstantFetchFinder $classConstantFetchFinder, - private readonly ClassConstFinder $classConstFinder, - private readonly TwigTemplateConstantExtractor $twigTemplateConstantExtractor, - private readonly YamlConfigConstantExtractor $yamlConfigConstantExtractor + private SymfonyStyle $symfonyStyle, + private ClassConstantFetchFinder $classConstantFetchFinder, + private ClassConstFinder $classConstFinder, + private TwigTemplateConstantExtractor $twigTemplateConstantExtractor, + private YamlConfigConstantExtractor $yamlConfigConstantExtractor ) { - parent::__construct(); } - protected function configure(): void + public function getName(): string { - $this->setName('privatize-constants'); - - $this->addArgument( - 'sources', - InputArgument::REQUIRED | InputArgument::IS_ARRAY, - 'One or more paths to check, include tests directory as well' - ); - - $this->addOption( - 'exclude-path', - null, - InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, - 'Path to exclude' - ); - - $this->addOption('debug', null, InputOption::VALUE_NONE, 'Debug output'); + return 'privatize-constants'; + } - $this->setDescription('Make class constants private if not used outside in PHP, Twig and YAML files'); + public function getDescription(): string + { + return 'Make class constants private if not used outside in PHP, Twig and YAML files'; } /** - * @return Command::* + * @param string[] $sources One or more paths to check, include tests directory as well + * @param string[] $excludedPaths Paths to exclude + * @param bool $isDebug Debug output + * @return ExitCode::* */ - protected function execute(InputInterface $input, OutputInterface $output): int + public function run(array $sources, array $excludedPaths = [], bool $isDebug = false): int { - $sources = (array) $input->getArgument('sources'); - $excludedPaths = (array) $input->getOption('exclude-path'); - $isDebug = (bool) $input->getOption('debug'); - $phpFileInfos = PhpFilesFinder::find($sources, $excludedPaths); if ($phpFileInfos === []) { $this->symfonyStyle->warning('No PHP files found in provided paths'); - return self::SUCCESS; + return ExitCode::SUCCESS; } $this->symfonyStyle->title('Finding class const fetches...'); @@ -107,7 +90,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (! $visibilityChangeStats->hasAnyChange()) { $this->symfonyStyle->warning('No constants were privatized'); - return self::SUCCESS; + + return ExitCode::SUCCESS; } $this->symfonyStyle->newLine(2); @@ -116,7 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int sprintf('Totally %d constants were made private', $visibilityChangeStats->getPrivateCount()) ); - return self::SUCCESS; + return ExitCode::SUCCESS; } /** diff --git a/src/Command/SearchRegexCommand.php b/src/Command/SearchRegexCommand.php index f2a3ee18d..f31017867 100644 --- a/src/Command/SearchRegexCommand.php +++ b/src/Command/SearchRegexCommand.php @@ -4,44 +4,31 @@ namespace Rector\SwissKnife\Command; +use Entropy\Console\Contract\CommandInterface; +use Entropy\Console\Enum\ExitCode; use Nette\Utils\Strings; use Rector\SwissKnife\Finder\PhpFilesFinder; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Webmozart\Assert\Assert; -final class SearchRegexCommand extends Command +final readonly class SearchRegexCommand implements CommandInterface { public function __construct( - private readonly SymfonyStyle $symfonyStyle, + private SymfonyStyle $symfonyStyle, ) { - parent::__construct(); } - protected function configure(): void + /** + * @param string $regex Code snippet to look in PHP files in the whole codebase + * @param string $projectDirectory Project directory + * + * @return ExitCode::* + */ + public function run(string $regex, ?string $projectDirectory = null): int { - $this->setName('search-regex'); - - $this->addArgument( - 'regex', - InputArgument::REQUIRED, - 'Code snippet to look in PHP files in the whole codebase' - ); - - $this->addOption('project-directory', null, InputOption::VALUE_REQUIRED, 'Project directory', getcwd()); - - $this->setDescription('Search for regex in PHP files of the whole codebase'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $regex = (string) $input->getArgument('regex'); - - $projectDirectory = (string) $input->getOption('project-directory'); + if ($projectDirectory === null) { + $projectDirectory = getcwd(); + } Assert::directory($projectDirectory); @@ -82,6 +69,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->symfonyStyle->newLine(2); $this->symfonyStyle->success(sprintf('Found %d cases in %d files', $foundCasesCount, count($markedFiles))); - return self::SUCCESS; + return ExitCode::SUCCESS; + } + + public function getName(): string + { + return 'search-regex'; + } + + public function getDescription(): string + { + return 'Search for regex in PHP files of the whole codebase'; } } diff --git a/src/Command/SplitSymfonyConfigToPerPackageCommand.php b/src/Command/SplitSymfonyConfigToPerPackageCommand.php index aadb2a105..ac8da390d 100644 --- a/src/Command/SplitSymfonyConfigToPerPackageCommand.php +++ b/src/Command/SplitSymfonyConfigToPerPackageCommand.php @@ -4,6 +4,8 @@ namespace Rector\SwissKnife\Command; +use Entropy\Console\Contract\CommandInterface; +use Entropy\Console\Enum\ExitCode; use Nette\Utils\FileSystem; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Scalar\String_; @@ -16,43 +18,28 @@ use Rector\SwissKnife\PhpParser\NodeFactory\SplitConfigClosureFactory; use Rector\SwissKnife\PhpParser\NodeVisitor\AddImportConfigMethodCallNodeVisitor; use Rector\SwissKnife\PhpParser\NodeVisitor\ExtractSymfonyExtensionCallNodeVisitor; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Webmozart\Assert\Assert; -final class SplitSymfonyConfigToPerPackageCommand extends Command +final readonly class SplitSymfonyConfigToPerPackageCommand implements CommandInterface { - private readonly Standard $printerStandard; + private Standard $printerStandard; public function __construct( - private readonly SymfonyStyle $symfonyStyle, - private readonly SplitConfigClosureFactory $splitConfigClosureFactory, + private SymfonyStyle $symfonyStyle, + private SplitConfigClosureFactory $splitConfigClosureFactory, ) { $this->printerStandard = new Standard(); - - parent::__construct(); } - protected function configure(): void - { - $this->setName('split-config-per-package'); - $this->setDescription( - 'Split Symfony configs that contains many extension() calls to /packages directory with config per package' - ); - - $this->addArgument('config-path', InputArgument::REQUIRED, 'Path to the config file'); - $this->addOption('output-dir', null, InputOption::VALUE_REQUIRED, 'Directory to save the split config files'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int + /** + * @param string $configPath Path to the config file + * @param string $outputDir Directory to save the split config files + * + * @return ExitCode::* + */ + public function run(string $configPath, string $outputDir): int { - $configPath = $input->getArgument('config-path'); - $outputDir = $input->getOption('output-dir'); - Assert::fileExists($configPath); Assert::notEmpty($outputDir); @@ -63,7 +50,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($symfonyExtensionMethodCalls === []) { $this->symfonyStyle->warning('No extension() method calls found'); - return self::SUCCESS; + return ExitCode::SUCCESS; } foreach ($symfonyExtensionMethodCalls as $symfonyExtensionMethodCall) { @@ -93,6 +80,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + public function getName(): string + { + return 'split-config-per-package'; + } + + public function getDescription(): string + { + return 'Split Symfony configs that contains many extension() calls to /packages directory with config per package'; + } + /** * @return Stmt[] */ diff --git a/src/Command/SpotLazyTraitsCommand.php b/src/Command/SpotLazyTraitsCommand.php index 31c3e950c..b404fccbe 100644 --- a/src/Command/SpotLazyTraitsCommand.php +++ b/src/Command/SpotLazyTraitsCommand.php @@ -4,51 +4,33 @@ namespace Rector\SwissKnife\Command; +use Entropy\Console\Contract\CommandInterface; +use Entropy\Console\Enum\ExitCode; use Rector\SwissKnife\Traits\TraitSpotter; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class SpotLazyTraitsCommand extends Command +final readonly class SpotLazyTraitsCommand implements CommandInterface { public function __construct( - private readonly SymfonyStyle $symfonyStyle, - private readonly TraitSpotter $traitSpotter, + private SymfonyStyle $symfonyStyle, + private TraitSpotter $traitSpotter, ) { - parent::__construct(); } - protected function configure(): void + /** + * @param string[] $sources Paths to scan for traits + * @param int $maxUsed Maximum number of times a trait is used to be considered lazy + * @return ExitCode::* + */ + public function run(array $sources, int $maxUsed = 2): int { - $this->setName('spot-lazy-traits'); - - $this->addArgument( - 'sources', - InputArgument::REQUIRED | InputArgument::IS_ARRAY, - 'One or more paths to check' - ); - - $this->addOption('max-used', null, InputOption::VALUE_REQUIRED, 'Maximum count the trait is used', 2); - - $this->setDescription( - 'Spot traits that are use only once, to potentially inline them and make code more robust and readable' - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $sources = (array) $input->getArgument('sources'); - $maxUsedCount = (int) $input->getOption('max-used'); - $this->symfonyStyle->title('Looking for trait definitions'); $traitSpottingResult = $this->traitSpotter->analyse($sources); if ($traitSpottingResult->getTraitCount() === 0) { $this->symfonyStyle->success('No traits were found in your project, nothing to worry about'); - return self::SUCCESS; + + return ExitCode::SUCCESS; } $this->symfonyStyle->writeln( @@ -62,9 +44,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->symfonyStyle->newLine(); - $this->symfonyStyle->title(sprintf('Looking for traits used less than %d-times', $maxUsedCount)); + $this->symfonyStyle->title(sprintf('Looking for traits used less than %d-times', $maxUsed)); - $leastUsedTraitsMetadatas = $traitSpottingResult->getTraitMaximumUsedTimes($maxUsedCount); + $leastUsedTraitsMetadatas = $traitSpottingResult->getTraitMaximumUsedTimes($maxUsed); foreach ($leastUsedTraitsMetadatas as $leastUsedTraitMetadata) { $this->symfonyStyle->writeln(sprintf( @@ -84,6 +66,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int PHP_EOL )); - return self::SUCCESS; + return ExitCode::SUCCESS; + } + + public function getName(): string + { + return 'spot-lazy-traits'; + } + + public function getDescription(): string + { + return 'Spot traits that are use only once, to potentially inline them and make code more robust and readable'; } } diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index 32c3da2a4..b7088c67b 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -7,8 +7,6 @@ use Entropy\Container\Container; use PhpParser\Parser; use PhpParser\ParserFactory; -use Symfony\Component\Console\Application; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Style\SymfonyStyle; @@ -24,20 +22,6 @@ public function create(): Container $container->autodiscover(__DIR__ . '/../Command'); - // console - $container->service(Application::class, function (Container $container): Application { - $application = new Application('Rector Swiss Knife'); - - $commands = $container->findByContract(Command::class); - $application->addCommands($commands); - - // remove basic command to make output clear - $this->hideDefaultCommands($application); - - return $application; - }); - - // parser $container->service(Parser::class, static function (): Parser { $phpParserFactory = new ParserFactory(); return $phpParserFactory->createForNewestSupportedVersion(); @@ -50,14 +34,4 @@ public function create(): Container return $container; } - - public function hideDefaultCommands(Application $application): void - { - $application->get('list') - ->setHidden(true); - $application->get('completion') - ->setHidden(true); - $application->get('help') - ->setHidden(true); - } } diff --git a/src/RobotLoader/PhpClassLoader.php b/src/RobotLoader/PhpClassLoader.php index 9b91c0f73..13d965391 100644 --- a/src/RobotLoader/PhpClassLoader.php +++ b/src/RobotLoader/PhpClassLoader.php @@ -1,5 +1,7 @@ setName('detect-unit-tests'); - - $this->setDescription('Get list of tests in specific directory, that are considered "unit"'); - - $this->addArgument( - 'sources', - InputArgument::REQUIRED | InputArgument::IS_ARRAY, - 'Path to directory with tests' - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $sources = (array) $input->getArgument('sources'); Assert::allString($sources); $unitTestCasesClassesToFilePaths = $this->unitTestFilePathsFinder->findInDirectories($sources); if ($unitTestCasesClassesToFilePaths === []) { $this->symfonyStyle->note('No unit tests found in provided paths'); - return self::SUCCESS; + + return ExitCode::SUCCESS; } $filesPHPUnitXmlContents = $this->phpunitXmlPrinter->printFiles($unitTestCasesClassesToFilePaths); @@ -63,6 +50,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->symfonyStyle->success($successMessage); - return self::SUCCESS; + return ExitCode::SUCCESS; + } + + public function getName(): string + { + return 'detect-unit-tests'; + } + + public function getDescription(): string + { + return 'Get list of tests in specific directory, that are considered "unit"'; } }