From 9f52b7a5d1197d5e7265df979f1a5ec34526d548 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 20 Jun 2026 17:40:11 +0200 Subject: [PATCH 1/3] add code sample --- ...actorDuplicatedNodeInstanceCheckRector.php | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/rules/DeadCode/Rector/Class_/RemoveRefactorDuplicatedNodeInstanceCheckRector.php b/rules/DeadCode/Rector/Class_/RemoveRefactorDuplicatedNodeInstanceCheckRector.php index 1aebff9b4a1..4308015a630 100644 --- a/rules/DeadCode/Rector/Class_/RemoveRefactorDuplicatedNodeInstanceCheckRector.php +++ b/rules/DeadCode/Rector/Class_/RemoveRefactorDuplicatedNodeInstanceCheckRector.php @@ -19,6 +19,7 @@ use Rector\PHPStan\ScopeFetcher; use Rector\Rector\AbstractRector; use Rector\StaticTypeMapper\ValueObject\Type\ShortenedObjectType; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; use Webmozart\Assert\Assert; @@ -37,7 +38,46 @@ public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( 'Remove refactor() method of Rector rule double check of $classMethod instance, if already defined in @param type', - [] + [ + new CodeSample( + <<<'CODE_SAMPLE' +final class SomeRector extends AbstractRector +{ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node) + { + if (! $node instanceof ClassMethod) { + return null; + } + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +final class SomeRector extends AbstractRector +{ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node) + { + } +} +CODE_SAMPLE + ), + ] ); } From 78efc30c6609cdefdf5de3a2a99a5871b281ef3b Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 20 Jun 2026 17:43:22 +0200 Subject: [PATCH 2/3] [ci] add rule validation to check code samples have at least one diff --- .github/workflows/code_analysis.yaml | 4 ++ scripts/validate-rule-definitions.php | 62 +++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 scripts/validate-rule-definitions.php diff --git a/.github/workflows/code_analysis.yaml b/.github/workflows/code_analysis.yaml index 1670042a6bc..84de1b31a50 100644 --- a/.github/workflows/code_analysis.yaml +++ b/.github/workflows/code_analysis.yaml @@ -36,6 +36,10 @@ jobs: name: 'Avoid duplicate short class names' run: php scripts/unique-rector-short-class-name.php + - + name: 'Validate rule definitions' + run: php scripts/validate-rule-definitions.php + - name: 'Help and Version' run: diff --git a/scripts/validate-rule-definitions.php b/scripts/validate-rule-definitions.php new file mode 100644 index 00000000000..b1b7037afe1 --- /dev/null +++ b/scripts/validate-rule-definitions.php @@ -0,0 +1,62 @@ +find([ + __DIR__ . '/../rules', + __DIR__ . '/../vendor/rector/rector-doctrine', + __DIR__ . '/../vendor/rector/rector-phpunit', + __DIR__ . '/../vendor/rector/rector-symfony', + __DIR__ . '/../vendor/rector/rector-downgrade-php', +]); + +$errorMessages = []; + +foreach ($rectorClasses as $rectorClass) { + $reflectionClass = new ReflectionClass($rectorClass); + + // rule definition does not depend on constructor dependencies, skip them + $rector = $reflectionClass->newInstanceWithoutConstructor(); + if (! $rector instanceof DocumentedRuleInterface) { + continue; + } + + $ruleDefinition = $rector->getRuleDefinition(); + + if (trim($ruleDefinition->getDescription()) === '') { + $errorMessages[] = sprintf('Rule "%s" is missing a clear description', $rectorClass); + } + + $codeSamples = $ruleDefinition->getCodeSamples(); + if ($codeSamples === []) { + $errorMessages[] = sprintf('Rule "%s" is missing at least one code sample', $rectorClass); + continue; + } + + foreach ($codeSamples as $codeSample) { + if (trim($codeSample->getBadCode()) === '' || trim($codeSample->getGoodCode()) === '') { + $errorMessages[] = sprintf('Rule "%s" has an empty code sample, fill before/after code', $rectorClass); + break; + } + } +} + +if ($errorMessages !== []) { + echo sprintf("Found %d rule definition error(s):\n\n", count($errorMessages)); + foreach ($errorMessages as $errorMessage) { + echo sprintf("- %s\n", $errorMessage); + } + + exit(ExitCode::FAILURE); +} + +echo sprintf("All %d Rector rule definitions are valid!\n", count($rectorClasses)); + +exit(ExitCode::SUCCESS); From 97b4233b357d9dc4ff96333359da86968d896a3f Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 20 Jun 2026 17:45:57 +0200 Subject: [PATCH 3/3] perfect --- scripts/validate-rule-definitions.php | 30 ++++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/scripts/validate-rule-definitions.php b/scripts/validate-rule-definitions.php index b1b7037afe1..6a1c4b1f42c 100644 --- a/scripts/validate-rule-definitions.php +++ b/scripts/validate-rule-definitions.php @@ -4,18 +4,23 @@ use Rector\Console\ExitCode; use Rector\Scripts\Finder\RectorClassFinder; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Style\SymfonyStyle; use Symplify\RuleDocGenerator\Contract\DocumentedRuleInterface; require __DIR__ . '/../vendor/autoload.php'; +$symfonyStyle = new SymfonyStyle(new ArrayInput([]), new ConsoleOutput()); + +// core rules + all installed rector extensions (rector-doctrine, rector-symfony, rector-phpunit, ...) +$ruleDirectories = array_merge( + [__DIR__ . '/../rules'], + glob(__DIR__ . '/../vendor/rector/rector-*', GLOB_ONLYDIR) ?: [] +); + $rectorClassFinder = new RectorClassFinder(); -$rectorClasses = $rectorClassFinder->find([ - __DIR__ . '/../rules', - __DIR__ . '/../vendor/rector/rector-doctrine', - __DIR__ . '/../vendor/rector/rector-phpunit', - __DIR__ . '/../vendor/rector/rector-symfony', - __DIR__ . '/../vendor/rector/rector-downgrade-php', -]); +$rectorClasses = $rectorClassFinder->find($ruleDirectories); $errorMessages = []; @@ -49,14 +54,15 @@ } if ($errorMessages !== []) { - echo sprintf("Found %d rule definition error(s):\n\n", count($errorMessages)); - foreach ($errorMessages as $errorMessage) { - echo sprintf("- %s\n", $errorMessage); - } + $symfonyStyle->listing($errorMessages); + $symfonyStyle->error(sprintf('Found %d rule definition error(s), see above', count($errorMessages))); exit(ExitCode::FAILURE); } -echo sprintf("All %d Rector rule definitions are valid!\n", count($rectorClasses)); +$symfonyStyle->writeln('Scanned paths:'); +$symfonyStyle->listing(array_map(static fn (string $directory): string => realpath($directory), $ruleDirectories)); + +$symfonyStyle->success(sprintf('All %d Rector rule definitions are valid!', count($rectorClasses))); exit(ExitCode::SUCCESS);