diff --git a/src/Console/Command/ValidateConfigCommand.php b/src/Console/Command/ValidateConfigCommand.php new file mode 100644 index 00000000000..7e193f92ae0 --- /dev/null +++ b/src/Console/Command/ValidateConfigCommand.php @@ -0,0 +1,128 @@ +setName('validate-config'); + $this->setDescription('Report config hygiene issues without processing any files'); + $this->addOption( + Option::OUTPUT_FORMAT, + null, + InputOption::VALUE_REQUIRED, + sprintf('Output format: "%s" or "%s"', ConsoleOutputFormatter::NAME, JsonOutputFormatter::NAME), + ConsoleOutputFormatter::NAME + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $isJsonOutput = $input->getOption(Option::OUTPUT_FORMAT) === JsonOutputFormatter::NAME; + + // silence the human-readable warnings, so only the JSON payload lands on stdout + if ($isJsonOutput) { + $this->symfonyStyle->setVerbosity(OutputInterface::VERBOSITY_QUIET); + } + + $issueCount = 0; + + $issueCount += $this->deprecatedRulesReporter->reportDeprecatedRules(); + $issueCount += $this->deprecatedRulesReporter->reportDeprecatedSkippedRules(); + $issueCount += $this->deprecatedRulesReporter->reportDeprecatedCacheMetaExtensions(); + $issueCount += $this->deprecatedRulesReporter->reportDeprecatedPhpSetsMethods(); + $issueCount += $this->deprecatedRulesReporter->reportDeprecatedAttributesSetsArgs(); + $issueCount += $this->deprecatedRulesReporter->reportDeprecatedComposerBasedArgs(); + $issueCount += $this->deprecatedRulesReporter->reportDeprecatedRectorUnsupportedMethods(); + + $issueCount += $this->missConfigurationReporter->reportSkippedNeverRegisteredRules(); + $issueCount += $this->missConfigurationReporter->reportSkippedNonRectorClasses(); + + $issueCount += $this->reportDeprecatedSkippedClasses(); + $issueCount += $this->reportSetAndRulesDuplicatedRegistrations(); + + if ($isJsonOutput) { + echo Json::encode([ + 'valid' => $issueCount === 0, + 'issue_count' => $issueCount, + ], pretty: true) . PHP_EOL; + return $issueCount === 0 ? ExitCode::SUCCESS : ExitCode::FAILURE; + } + + if ($issueCount === 0) { + $this->symfonyStyle->success('Config is valid, no issues found'); + return ExitCode::SUCCESS; + } + + $this->symfonyStyle->error(sprintf( + '%d config %s found, see the warnings above', + $issueCount, + $issueCount === 1 ? 'issue' : 'issues' + )); + + return ExitCode::FAILURE; + } + + private function reportDeprecatedSkippedClasses(): int + { + $deprecatedSkippedClasses = $this->skippedClassResolver->resolveDeprecatedSkippedClasses(); + if ($deprecatedSkippedClasses === []) { + return 0; + } + + $this->symfonyStyle->warning(sprintf( + 'These rules are skipped, but are deprecated. Most likely you do not need to skip them anymore, remove them: %s%s', + "\n\n", + '* ' . implode("\n* ", $deprecatedSkippedClasses) . "\n" + )); + + return count($deprecatedSkippedClasses); + } + + private function reportSetAndRulesDuplicatedRegistrations(): int + { + $setAndRulesDuplicatedRegistrations = new Configuration() + ->getBothSetAndRulesDuplicatedRegistrations(); + if ($setAndRulesDuplicatedRegistrations === []) { + return 0; + } + + $this->symfonyStyle->warning(sprintf( + 'These rules are registered in both sets and "withRules()". Remove them from "withRules()" to avoid duplications: %s* %s', + "\n\n", + implode(' * ', $setAndRulesDuplicatedRegistrations) . "\n" + )); + + return count($setAndRulesDuplicatedRegistrations); + } +} diff --git a/src/DependencyInjection/LazyContainerFactory.php b/src/DependencyInjection/LazyContainerFactory.php index 7c60bb2a9ae..33d6146a522 100644 --- a/src/DependencyInjection/LazyContainerFactory.php +++ b/src/DependencyInjection/LazyContainerFactory.php @@ -37,6 +37,7 @@ use Rector\Console\Command\ListRulesCommand; use Rector\Console\Command\ProcessCommand; use Rector\Console\Command\SetupCICommand; +use Rector\Console\Command\ValidateConfigCommand; use Rector\Console\Command\WorkerCommand; use Rector\Console\ConsoleApplication; use Rector\Console\Style\SymfonyStyleFactory; @@ -158,6 +159,7 @@ private function registerConsole(RectorConfig $rectorConfig): void $rectorConfig->singleton(ProcessCommand::class); $rectorConfig->singleton(WorkerCommand::class); + $rectorConfig->singleton(ValidateConfigCommand::class); $rectorConfig->singleton(SetupCICommand::class); $rectorConfig->singleton(ListRulesCommand::class); $rectorConfig->singleton(CustomRuleCommand::class); diff --git a/src/Reporting/DeprecatedRulesReporter.php b/src/Reporting/DeprecatedRulesReporter.php index 97dc68d929d..b0c579a77b2 100644 --- a/src/Reporting/DeprecatedRulesReporter.php +++ b/src/Reporting/DeprecatedRulesReporter.php @@ -23,11 +23,12 @@ public function __construct( ) { } - public function reportDeprecatedRules(): void + public function reportDeprecatedRules(): int { /** @var string[] $registeredRectorRules */ $registeredRectorRules = SimpleParameterProvider::provideArrayParameter(Option::REGISTERED_RECTOR_RULES); + $reportedCount = 0; foreach ($registeredRectorRules as $registeredRectorRule) { if (! is_a($registeredRectorRule, DeprecatedInterface::class, true)) { continue; @@ -39,88 +40,112 @@ public function reportDeprecatedRules(): void $registeredRectorRule ) ); + ++$reportedCount; } + + return $reportedCount; } - public function reportDeprecatedSkippedRules(): void + public function reportDeprecatedSkippedRules(): int { /** @var string[] $skippedRectorRules */ $skippedRectorRules = SimpleParameterProvider::provideArrayParameter(Option::SKIPPED_RECTOR_RULES); + $reportedCount = 0; foreach ($skippedRectorRules as $skippedRectorRule) { if (! is_a($skippedRectorRule, DeprecatedInterface::class, true)) { continue; } $this->symfonyStyle->warning(sprintf('Skipped rule "%s" is deprecated', $skippedRectorRule)); + ++$reportedCount; } + + return $reportedCount; } - public function reportDeprecatedCacheMetaExtensions(): void + public function reportDeprecatedCacheMetaExtensions(): int { /** @var string[] $cacheMetaExtensions */ $cacheMetaExtensions = SimpleParameterProvider::provideArrayParameter(Option::CACHE_META_EXTENSIONS); + $reportedCount = 0; foreach ($cacheMetaExtensions as $cacheMetumExtension) { $this->symfonyStyle->warning(sprintf( 'Cache meta extension "%s" is deprecated and no longer applied. It is a niche mechanism, let Rector handle cache on its own. If custom invalidation is needed, handle it in CI in a more generic way, e.g. by clearing the cache directory.', $cacheMetumExtension )); + ++$reportedCount; } + + return $reportedCount; } - public function reportDeprecatedPhpSetsMethods(): void + public function reportDeprecatedPhpSetsMethods(): int { /** @var string[] $deprecatedPhpSetsMethods */ $deprecatedPhpSetsMethods = SimpleParameterProvider::provideArrayParameter( Option::DEPRECATED_PHP_SETS_METHODS ); + $reportedCount = 0; foreach (array_unique($deprecatedPhpSetsMethods) as $deprecatedPhpSetsMethod) { $this->symfonyStyle->warning(sprintf( 'The "->%s()" method is deprecated and no longer applied. Use "->withPhpLevel()" instead, to raise PHP level one rule at a time.', $deprecatedPhpSetsMethod )); + ++$reportedCount; } + + return $reportedCount; } - public function reportDeprecatedAttributesSetsArgs(): void + public function reportDeprecatedAttributesSetsArgs(): int { /** @var string[] $deprecatedAttributesSetsArgs */ $deprecatedAttributesSetsArgs = SimpleParameterProvider::provideArrayParameter( Option::DEPRECATED_ATTRIBUTES_SETS_ARGS ); + $reportedCount = 0; foreach (array_unique($deprecatedAttributesSetsArgs) as $deprecatedAttributesSetsArg) { $this->symfonyStyle->warning(sprintf( 'The "->withAttributesSets(%s: true)" argument is deprecated and no longer applied. It is already included in the "symfony: true" argument, use it instead.', $deprecatedAttributesSetsArg )); + ++$reportedCount; } + + return $reportedCount; } - public function reportDeprecatedComposerBasedArgs(): void + public function reportDeprecatedComposerBasedArgs(): int { /** @var string[] $deprecatedComposerBasedArgs */ $deprecatedComposerBasedArgs = SimpleParameterProvider::provideArrayParameter( Option::DEPRECATED_COMPOSER_BASED_ARGS ); + $reportedCount = 0; foreach (array_unique($deprecatedComposerBasedArgs) as $deprecatedComposerBasedArg) { $this->symfonyStyle->warning(sprintf( 'The "->withComposerBased(%s: true)" argument is deprecated and no longer applied. It only added named args to 2 methods of a single package, register the rule directly if needed.', $deprecatedComposerBasedArg )); + ++$reportedCount; } + + return $reportedCount; } - public function reportDeprecatedRectorUnsupportedMethods(): void + public function reportDeprecatedRectorUnsupportedMethods(): int { // to be added in related PR if (! class_exists(FileNode::class)) { - return; + return 0; } + $reportedCount = 0; foreach ($this->rectors as $rector) { $beforeTraverseMethodReflection = new ReflectionMethod($rector, 'beforeTraverse'); if ($beforeTraverseMethodReflection->getDeclaringClass()->getName() === $rector::class) { @@ -129,7 +154,10 @@ public function reportDeprecatedRectorUnsupportedMethods(): void $rector::class, FileNode::class )); + ++$reportedCount; } } + + return $reportedCount; } } diff --git a/src/Reporting/MissConfigurationReporter.php b/src/Reporting/MissConfigurationReporter.php index 4a3b91f433a..3a137588b65 100644 --- a/src/Reporting/MissConfigurationReporter.php +++ b/src/Reporting/MissConfigurationReporter.php @@ -47,7 +47,7 @@ public function reportUnusedSkips(ProcessResult $processResult): void $this->symfonyStyle->listing($spacedUnusedSkips); } - public function reportSkippedNeverRegisteredRules(): void + public function reportSkippedNeverRegisteredRules(): int { $registeredRules = SimpleParameterProvider::provideArrayParameter(Option::REGISTERED_RECTOR_RULES); $skippedRules = SimpleParameterProvider::provideArrayParameter(Option::SKIPPED_RECTOR_RULES); @@ -61,7 +61,7 @@ public function reportSkippedNeverRegisteredRules(): void ); if ($neverRegisteredSkippedRules === []) { - return; + return 0; } $this->symfonyStyle->warning(sprintf( @@ -71,16 +71,18 @@ public function reportSkippedNeverRegisteredRules(): void )); $this->symfonyStyle->listing($neverRegisteredSkippedRules); + + return count($neverRegisteredSkippedRules); } - public function reportSkippedNonRectorClasses(): void + public function reportSkippedNonRectorClasses(): int { $skippedNonRectorClasses = SimpleParameterProvider::provideArrayParameter( Option::SKIPPED_NON_RECTOR_CLASSES ); if ($skippedNonRectorClasses === []) { - return; + return 0; } $this->symfonyStyle->warning(sprintf( @@ -91,6 +93,8 @@ public function reportSkippedNonRectorClasses(): void count($skippedNonRectorClasses) > 1 ? 'they can' : 'it can', RectorInterface::class )); + + return count($skippedNonRectorClasses); } /** diff --git a/tests/Console/Command/Source/DeprecatedFixtureRule.php b/tests/Console/Command/Source/DeprecatedFixtureRule.php new file mode 100644 index 00000000000..4029fbc22f4 --- /dev/null +++ b/tests/Console/Command/Source/DeprecatedFixtureRule.php @@ -0,0 +1,11 @@ +make(UnusedSkipResolver::class), + ), + $this->make(SkippedClassResolver::class), + ); + + $this->commandTester = new CommandTester($validateConfigCommand); + } + + protected function tearDown(): void + { + SimpleParameterProvider::setParameter(Option::REGISTERED_RECTOR_RULES, []); + } + + public function testFailsOnDeprecatedRegisteredRule(): void + { + SimpleParameterProvider::setParameter(Option::REGISTERED_RECTOR_RULES, [DeprecatedFixtureRule::class]); + + $this->assertSame(ExitCode::FAILURE, $this->commandTester->execute([])); + } + + public function testSucceedsOnCleanConfig(): void + { + SimpleParameterProvider::setParameter(Option::REGISTERED_RECTOR_RULES, [OrdSingleByteRector::class]); + + $this->assertSame(ExitCode::SUCCESS, $this->commandTester->execute([])); + } + + public function testJsonOutputOnCleanConfig(): void + { + SimpleParameterProvider::setParameter(Option::REGISTERED_RECTOR_RULES, [OrdSingleByteRector::class]); + + ob_start(); + $exitCode = $this->commandTester->execute([ + '--output-format' => 'json', + ]); + $json = (string) ob_get_clean(); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + $this->assertSame([ + 'valid' => true, + 'issue_count' => 0, + ], Json::decode($json, forceArrays: true)); + } + + public function testJsonOutputOnDeprecatedRegisteredRule(): void + { + SimpleParameterProvider::setParameter(Option::REGISTERED_RECTOR_RULES, [DeprecatedFixtureRule::class]); + + ob_start(); + $exitCode = $this->commandTester->execute([ + '--output-format' => 'json', + ]); + $json = (string) ob_get_clean(); + + $this->assertSame(ExitCode::FAILURE, $exitCode); + $this->assertSame([ + 'valid' => false, + 'issue_count' => 1, + ], Json::decode($json, forceArrays: true)); + } +}