Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions src/Console/Command/ValidateConfigCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

declare(strict_types=1);

namespace Rector\Console\Command;

use Nette\Utils\Json;
use Rector\ChangesReporting\Output\ConsoleOutputFormatter;
use Rector\ChangesReporting\Output\JsonOutputFormatter;
use Rector\Configuration\Option;
use Rector\Console\ExitCode;
use Rector\Reporting\DeprecatedRulesReporter;
use Rector\Reporting\MissConfigurationReporter;
use Rector\Skipper\SkipCriteriaResolver\SkippedClassResolver;
use Rector\ValueObject\Configuration;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
* @see \Rector\Tests\Console\Command\ValidateConfigCommandTest
*/
final class ValidateConfigCommand extends Command
{
public function __construct(
private readonly SymfonyStyle $symfonyStyle,
private readonly DeprecatedRulesReporter $deprecatedRulesReporter,
private readonly MissConfigurationReporter $missConfigurationReporter,
private readonly SkippedClassResolver $skippedClassResolver,
) {
parent::__construct();
}

protected function configure(): void
{
$this->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);
}
}
2 changes: 2 additions & 0 deletions src/DependencyInjection/LazyContainerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
44 changes: 36 additions & 8 deletions src/Reporting/DeprecatedRulesReporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -129,7 +154,10 @@ public function reportDeprecatedRectorUnsupportedMethods(): void
$rector::class,
FileNode::class
));
++$reportedCount;
}
}

return $reportedCount;
}
}
12 changes: 8 additions & 4 deletions src/Reporting/MissConfigurationReporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -61,7 +61,7 @@ public function reportSkippedNeverRegisteredRules(): void
);

if ($neverRegisteredSkippedRules === []) {
return;
return 0;
}

$this->symfonyStyle->warning(sprintf(
Expand All @@ -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(
Expand All @@ -91,6 +93,8 @@ public function reportSkippedNonRectorClasses(): void
count($skippedNonRectorClasses) > 1 ? 'they can' : 'it can',
RectorInterface::class
));

return count($skippedNonRectorClasses);
}

/**
Expand Down
11 changes: 11 additions & 0 deletions tests/Console/Command/Source/DeprecatedFixtureRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\Console\Command\Source;

use Rector\Configuration\Deprecation\Contract\DeprecatedInterface;

final class DeprecatedFixtureRule implements DeprecatedInterface
{
}
Loading
Loading