Skip to content

Commit 384af18

Browse files
committed
[Console] Add validate-config command for config hygiene checks
1 parent 6d22545 commit 384af18

6 files changed

Lines changed: 224 additions & 12 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Console\Command;
6+
7+
use Rector\Console\ExitCode;
8+
use Rector\Reporting\DeprecatedRulesReporter;
9+
use Rector\Reporting\MissConfigurationReporter;
10+
use Rector\Skipper\SkipCriteriaResolver\SkippedClassResolver;
11+
use Rector\ValueObject\Configuration;
12+
use Symfony\Component\Console\Command\Command;
13+
use Symfony\Component\Console\Input\InputInterface;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
use Symfony\Component\Console\Style\SymfonyStyle;
16+
17+
/**
18+
* @see \Rector\Tests\Console\Command\ValidateConfigCommandTest
19+
*/
20+
final class ValidateConfigCommand extends Command
21+
{
22+
public function __construct(
23+
private readonly SymfonyStyle $symfonyStyle,
24+
private readonly DeprecatedRulesReporter $deprecatedRulesReporter,
25+
private readonly MissConfigurationReporter $missConfigurationReporter,
26+
private readonly SkippedClassResolver $skippedClassResolver,
27+
) {
28+
parent::__construct();
29+
}
30+
31+
protected function configure(): void
32+
{
33+
$this->setName('validate-config');
34+
$this->setDescription('Report config hygiene issues without processing any files');
35+
}
36+
37+
protected function execute(InputInterface $input, OutputInterface $output): int
38+
{
39+
$issueCount = 0;
40+
41+
$issueCount += $this->deprecatedRulesReporter->reportDeprecatedRules();
42+
$issueCount += $this->deprecatedRulesReporter->reportDeprecatedSkippedRules();
43+
$issueCount += $this->deprecatedRulesReporter->reportDeprecatedCacheMetaExtensions();
44+
$issueCount += $this->deprecatedRulesReporter->reportDeprecatedPhpSetsMethods();
45+
$issueCount += $this->deprecatedRulesReporter->reportDeprecatedAttributesSetsArgs();
46+
$issueCount += $this->deprecatedRulesReporter->reportDeprecatedComposerBasedArgs();
47+
$issueCount += $this->deprecatedRulesReporter->reportDeprecatedRectorUnsupportedMethods();
48+
49+
$issueCount += $this->missConfigurationReporter->reportSkippedNeverRegisteredRules();
50+
$issueCount += $this->missConfigurationReporter->reportSkippedNonRectorClasses();
51+
52+
$issueCount += $this->reportDeprecatedSkippedClasses();
53+
$issueCount += $this->reportSetAndRulesDuplicatedRegistrations();
54+
55+
if ($issueCount === 0) {
56+
$this->symfonyStyle->success('Config is valid, no issues found');
57+
return ExitCode::SUCCESS;
58+
}
59+
60+
$this->symfonyStyle->error(sprintf(
61+
'%d config %s found, see the warnings above',
62+
$issueCount,
63+
$issueCount === 1 ? 'issue' : 'issues'
64+
));
65+
66+
return ExitCode::FAILURE;
67+
}
68+
69+
private function reportDeprecatedSkippedClasses(): int
70+
{
71+
$deprecatedSkippedClasses = $this->skippedClassResolver->resolveDeprecatedSkippedClasses();
72+
if ($deprecatedSkippedClasses === []) {
73+
return 0;
74+
}
75+
76+
$this->symfonyStyle->warning(sprintf(
77+
'These rules are skipped, but are deprecated. Most likely you do not need to skip them anymore, remove them: %s%s',
78+
"\n\n",
79+
'* ' . implode("\n* ", $deprecatedSkippedClasses) . "\n"
80+
));
81+
82+
return count($deprecatedSkippedClasses);
83+
}
84+
85+
private function reportSetAndRulesDuplicatedRegistrations(): int
86+
{
87+
$setAndRulesDuplicatedRegistrations = new Configuration()
88+
->getBothSetAndRulesDuplicatedRegistrations();
89+
if ($setAndRulesDuplicatedRegistrations === []) {
90+
return 0;
91+
}
92+
93+
$this->symfonyStyle->warning(sprintf(
94+
'These rules are registered in both sets and "withRules()". Remove them from "withRules()" to avoid duplications: %s* %s',
95+
"\n\n",
96+
implode(' * ', $setAndRulesDuplicatedRegistrations) . "\n"
97+
));
98+
99+
return count($setAndRulesDuplicatedRegistrations);
100+
}
101+
}

‎src/DependencyInjection/LazyContainerFactory.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
use Rector\Console\Command\ListRulesCommand;
3838
use Rector\Console\Command\ProcessCommand;
3939
use Rector\Console\Command\SetupCICommand;
40+
use Rector\Console\Command\ValidateConfigCommand;
4041
use Rector\Console\Command\WorkerCommand;
4142
use Rector\Console\ConsoleApplication;
4243
use Rector\Console\Style\SymfonyStyleFactory;
@@ -158,6 +159,7 @@ private function registerConsole(RectorConfig $rectorConfig): void
158159

159160
$rectorConfig->singleton(ProcessCommand::class);
160161
$rectorConfig->singleton(WorkerCommand::class);
162+
$rectorConfig->singleton(ValidateConfigCommand::class);
161163
$rectorConfig->singleton(SetupCICommand::class);
162164
$rectorConfig->singleton(ListRulesCommand::class);
163165
$rectorConfig->singleton(CustomRuleCommand::class);

‎src/Reporting/DeprecatedRulesReporter.php‎

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ public function __construct(
2323
) {
2424
}
2525

26-
public function reportDeprecatedRules(): void
26+
public function reportDeprecatedRules(): int
2727
{
2828
/** @var string[] $registeredRectorRules */
2929
$registeredRectorRules = SimpleParameterProvider::provideArrayParameter(Option::REGISTERED_RECTOR_RULES);
3030

31+
$reportedCount = 0;
3132
foreach ($registeredRectorRules as $registeredRectorRule) {
3233
if (! is_a($registeredRectorRule, DeprecatedInterface::class, true)) {
3334
continue;
@@ -39,88 +40,112 @@ public function reportDeprecatedRules(): void
3940
$registeredRectorRule
4041
)
4142
);
43+
++$reportedCount;
4244
}
45+
46+
return $reportedCount;
4347
}
4448

45-
public function reportDeprecatedSkippedRules(): void
49+
public function reportDeprecatedSkippedRules(): int
4650
{
4751
/** @var string[] $skippedRectorRules */
4852
$skippedRectorRules = SimpleParameterProvider::provideArrayParameter(Option::SKIPPED_RECTOR_RULES);
4953

54+
$reportedCount = 0;
5055
foreach ($skippedRectorRules as $skippedRectorRule) {
5156
if (! is_a($skippedRectorRule, DeprecatedInterface::class, true)) {
5257
continue;
5358
}
5459

5560
$this->symfonyStyle->warning(sprintf('Skipped rule "%s" is deprecated', $skippedRectorRule));
61+
++$reportedCount;
5662
}
63+
64+
return $reportedCount;
5765
}
5866

59-
public function reportDeprecatedCacheMetaExtensions(): void
67+
public function reportDeprecatedCacheMetaExtensions(): int
6068
{
6169
/** @var string[] $cacheMetaExtensions */
6270
$cacheMetaExtensions = SimpleParameterProvider::provideArrayParameter(Option::CACHE_META_EXTENSIONS);
6371

72+
$reportedCount = 0;
6473
foreach ($cacheMetaExtensions as $cacheMetumExtension) {
6574
$this->symfonyStyle->warning(sprintf(
6675
'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.',
6776
$cacheMetumExtension
6877
));
78+
++$reportedCount;
6979
}
80+
81+
return $reportedCount;
7082
}
7183

72-
public function reportDeprecatedPhpSetsMethods(): void
84+
public function reportDeprecatedPhpSetsMethods(): int
7385
{
7486
/** @var string[] $deprecatedPhpSetsMethods */
7587
$deprecatedPhpSetsMethods = SimpleParameterProvider::provideArrayParameter(
7688
Option::DEPRECATED_PHP_SETS_METHODS
7789
);
7890

91+
$reportedCount = 0;
7992
foreach (array_unique($deprecatedPhpSetsMethods) as $deprecatedPhpSetsMethod) {
8093
$this->symfonyStyle->warning(sprintf(
8194
'The "->%s()" method is deprecated and no longer applied. Use "->withPhpLevel()" instead, to raise PHP level one rule at a time.',
8295
$deprecatedPhpSetsMethod
8396
));
97+
++$reportedCount;
8498
}
99+
100+
return $reportedCount;
85101
}
86102

87-
public function reportDeprecatedAttributesSetsArgs(): void
103+
public function reportDeprecatedAttributesSetsArgs(): int
88104
{
89105
/** @var string[] $deprecatedAttributesSetsArgs */
90106
$deprecatedAttributesSetsArgs = SimpleParameterProvider::provideArrayParameter(
91107
Option::DEPRECATED_ATTRIBUTES_SETS_ARGS
92108
);
93109

110+
$reportedCount = 0;
94111
foreach (array_unique($deprecatedAttributesSetsArgs) as $deprecatedAttributesSetsArg) {
95112
$this->symfonyStyle->warning(sprintf(
96113
'The "->withAttributesSets(%s: true)" argument is deprecated and no longer applied. It is already included in the "symfony: true" argument, use it instead.',
97114
$deprecatedAttributesSetsArg
98115
));
116+
++$reportedCount;
99117
}
118+
119+
return $reportedCount;
100120
}
101121

102-
public function reportDeprecatedComposerBasedArgs(): void
122+
public function reportDeprecatedComposerBasedArgs(): int
103123
{
104124
/** @var string[] $deprecatedComposerBasedArgs */
105125
$deprecatedComposerBasedArgs = SimpleParameterProvider::provideArrayParameter(
106126
Option::DEPRECATED_COMPOSER_BASED_ARGS
107127
);
108128

129+
$reportedCount = 0;
109130
foreach (array_unique($deprecatedComposerBasedArgs) as $deprecatedComposerBasedArg) {
110131
$this->symfonyStyle->warning(sprintf(
111132
'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.',
112133
$deprecatedComposerBasedArg
113134
));
135+
++$reportedCount;
114136
}
137+
138+
return $reportedCount;
115139
}
116140

117-
public function reportDeprecatedRectorUnsupportedMethods(): void
141+
public function reportDeprecatedRectorUnsupportedMethods(): int
118142
{
119143
// to be added in related PR
120144
if (! class_exists(FileNode::class)) {
121-
return;
145+
return 0;
122146
}
123147

148+
$reportedCount = 0;
124149
foreach ($this->rectors as $rector) {
125150
$beforeTraverseMethodReflection = new ReflectionMethod($rector, 'beforeTraverse');
126151
if ($beforeTraverseMethodReflection->getDeclaringClass()->getName() === $rector::class) {
@@ -129,7 +154,10 @@ public function reportDeprecatedRectorUnsupportedMethods(): void
129154
$rector::class,
130155
FileNode::class
131156
));
157+
++$reportedCount;
132158
}
133159
}
160+
161+
return $reportedCount;
134162
}
135163
}

‎src/Reporting/MissConfigurationReporter.php‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function reportUnusedSkips(ProcessResult $processResult): void
4747
$this->symfonyStyle->listing($spacedUnusedSkips);
4848
}
4949

50-
public function reportSkippedNeverRegisteredRules(): void
50+
public function reportSkippedNeverRegisteredRules(): int
5151
{
5252
$registeredRules = SimpleParameterProvider::provideArrayParameter(Option::REGISTERED_RECTOR_RULES);
5353
$skippedRules = SimpleParameterProvider::provideArrayParameter(Option::SKIPPED_RECTOR_RULES);
@@ -61,7 +61,7 @@ public function reportSkippedNeverRegisteredRules(): void
6161
);
6262

6363
if ($neverRegisteredSkippedRules === []) {
64-
return;
64+
return 0;
6565
}
6666

6767
$this->symfonyStyle->warning(sprintf(
@@ -71,16 +71,18 @@ public function reportSkippedNeverRegisteredRules(): void
7171
));
7272

7373
$this->symfonyStyle->listing($neverRegisteredSkippedRules);
74+
75+
return count($neverRegisteredSkippedRules);
7476
}
7577

76-
public function reportSkippedNonRectorClasses(): void
78+
public function reportSkippedNonRectorClasses(): int
7779
{
7880
$skippedNonRectorClasses = SimpleParameterProvider::provideArrayParameter(
7981
Option::SKIPPED_NON_RECTOR_CLASSES
8082
);
8183

8284
if ($skippedNonRectorClasses === []) {
83-
return;
85+
return 0;
8486
}
8587

8688
$this->symfonyStyle->warning(sprintf(
@@ -91,6 +93,8 @@ public function reportSkippedNonRectorClasses(): void
9193
count($skippedNonRectorClasses) > 1 ? 'they can' : 'it can',
9294
RectorInterface::class
9395
));
96+
97+
return count($skippedNonRectorClasses);
9498
}
9599

96100
/**
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\Console\Command\Source;
6+
7+
use Rector\Configuration\Deprecation\Contract\DeprecatedInterface;
8+
9+
final class DeprecatedFixtureRule implements DeprecatedInterface
10+
{
11+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\Console\Command;
6+
7+
use Rector\Configuration\Option;
8+
use Rector\Configuration\Parameter\SimpleParameterProvider;
9+
use Rector\Configuration\VendorMissAnalyseGuard;
10+
use Rector\Console\Command\ValidateConfigCommand;
11+
use Rector\Console\ExitCode;
12+
use Rector\Php85\Rector\FuncCall\OrdSingleByteRector;
13+
use Rector\Reporting\DeprecatedRulesReporter;
14+
use Rector\Reporting\MissConfigurationReporter;
15+
use Rector\Reporting\UnusedSkipResolver;
16+
use Rector\Skipper\SkipCriteriaResolver\SkippedClassResolver;
17+
use Rector\Testing\PHPUnit\AbstractLazyTestCase;
18+
use Rector\Tests\Console\Command\Source\DeprecatedFixtureRule;
19+
use Symfony\Component\Console\Input\ArrayInput;
20+
use Symfony\Component\Console\Output\BufferedOutput;
21+
use Symfony\Component\Console\Style\SymfonyStyle;
22+
use Symfony\Component\Console\Tester\CommandTester;
23+
24+
final class ValidateConfigCommandTest extends AbstractLazyTestCase
25+
{
26+
private CommandTester $commandTester;
27+
28+
protected function setUp(): void
29+
{
30+
parent::setUp();
31+
32+
$symfonyStyle = new SymfonyStyle(new ArrayInput([]), new BufferedOutput());
33+
34+
$validateConfigCommand = new ValidateConfigCommand(
35+
$symfonyStyle,
36+
new DeprecatedRulesReporter($symfonyStyle, []),
37+
new MissConfigurationReporter(
38+
$symfonyStyle,
39+
new VendorMissAnalyseGuard(),
40+
$this->make(UnusedSkipResolver::class),
41+
),
42+
$this->make(SkippedClassResolver::class),
43+
);
44+
45+
$this->commandTester = new CommandTester($validateConfigCommand);
46+
}
47+
48+
protected function tearDown(): void
49+
{
50+
SimpleParameterProvider::setParameter(Option::REGISTERED_RECTOR_RULES, []);
51+
}
52+
53+
public function testFailsOnDeprecatedRegisteredRule(): void
54+
{
55+
SimpleParameterProvider::setParameter(Option::REGISTERED_RECTOR_RULES, [DeprecatedFixtureRule::class]);
56+
57+
$this->assertSame(ExitCode::FAILURE, $this->commandTester->execute([]));
58+
}
59+
60+
public function testSucceedsOnCleanConfig(): void
61+
{
62+
SimpleParameterProvider::setParameter(Option::REGISTERED_RECTOR_RULES, [OrdSingleByteRector::class]);
63+
64+
$this->assertSame(ExitCode::SUCCESS, $this->commandTester->execute([]));
65+
}
66+
}

0 commit comments

Comments
 (0)