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
14 changes: 14 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ includes:
rules:
- Rector\Utils\PHPStan\Rule\RegisterRelatedPolyfillRectorRule
- Rector\Utils\PHPStan\Rule\NoResolveClassReflectionInForeachRule
- Rector\Utils\PHPStan\Rule\RegisterRectorRule

parameters:
level: 8
Expand Down Expand Up @@ -228,6 +229,19 @@ parameters:
- rules/Php81/Rector/FuncCall/NullToStrictStringFuncCallArgRector.php
- rules/Php80/Rector/NotIdentical/MbStrContainsRector.php

# not registered in a rector-src set on purpose: NullToStrictString is opt-in only (risky),
# the rest are registered in third-party sets (rector-laravel, drupal-rector) that cannot be
# installed here due to the rector/rector replace
-
identifier: rector.registerRector
paths:
- rules/Php81/Rector/FuncCall/NullToStrictStringFuncCallArgRector.php
- rules/Removing/Rector/Class_/RemoveTraitUseRector.php
- rules/Renaming/Rector/StaticCall/RenameStaticMethodRector.php
- rules/Transform/Rector/FuncCall/FuncCallToNewRector.php
- rules/Transform/Rector/FuncCall/FuncCallToStaticCallRector.php
- rules/Transform/Rector/String_/StringToClassConstantRector.php

# lack of generic array in nikic/php-parser
-
identifier: return.type
Expand Down
152 changes: 152 additions & 0 deletions utils/phpstan/src/Rule/RegisterRectorRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPStan\Rule;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Rector\Configuration\Deprecation\Contract\DeprecatedInterface;
use Rector\Contract\Rector\RectorInterface;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;

/**
* Every Rector rule must be registered in a set or level config, otherwise it can
* never run and becomes dead code. Registrations in sibling extension packages
* (rector-doctrine, rector-symfony, ...) count too. Deprecated rules are skipped,
* as they are intentionally unregistered.
*
* @implements Rule<InClassNode>
* @see \Rector\Utils\PHPStan\Tests\Rule\RegisterRectorRule\RegisterRectorRuleTest
*/
final class RegisterRectorRule implements Rule
{
private const string ERROR_MESSAGE = 'Rector rule "%s" is not registered in any set or level config, so it can never run. Register it in a set/level, or deprecate it if it is no longer wanted.';

/**
* @var string[]
*/
private readonly array $registrationDirectories;

/**
* @var array<string, true>|null
*/
private ?array $registeredShortClassNames = null;

/**
* @param string[]|null $registrationDirectories
*/
public function __construct(?array $registrationDirectories = null)
{
$root = __DIR__ . '/../../../../';

$this->registrationDirectories = $registrationDirectories ?? array_merge(
[$root . 'config', $root . 'src/Config'],
// sibling extension packages register rules in their own config sets
glob($root . 'vendor/rector/*/config') ?: []
);
}

public function getNodeType(): string
{
return InClassNode::class;
}

/**
* @param InClassNode $node
* @return list<IdentifierRuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
$classReflection = $node->getClassReflection();

if ($classReflection->isAbstract()) {
return [];
}

if (! $classReflection->implementsInterface(RectorInterface::class)) {
return [];
}

// test doubles are wired directly in their test, not through a set
if (str_starts_with($classReflection->getName(), 'Rector\\Tests\\')) {
return [];
}

// intentionally unregistered
if ($classReflection->implementsInterface(DeprecatedInterface::class)) {
return [];
}

$className = $classReflection->getName();
if ($this->isRegistered($className)) {
return [];
}

return [
RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $className))
->identifier('rector.registerRector')
->build(),
];
}

private function isRegistered(string $className): bool
{
$shortClassName = substr((string) strrchr($className, '\\'), 1);

// short class names are unique repo-wide (enforced by the "no duplicate short class names" check)
return isset($this->getRegisteredShortClassNames()[$shortClassName]);
}

/**
* Collect every "SomeRector::class" short name referenced in the scanned config directories once.
* The capture group stops at the preceding "\", so both "SomeRector::class" and a fully qualified
* "\Some\Ns\SomeRector::class" yield the same short name.
*
* @return array<string, true>
*/
private function getRegisteredShortClassNames(): array
{
if ($this->registeredShortClassNames !== null) {
return $this->registeredShortClassNames;
}

$registeredShortClassNames = [];
foreach ($this->registrationDirectories as $registrationDirectory) {
if (! is_dir($registrationDirectory)) {
continue;
}

$recursiveIterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($registrationDirectory, RecursiveDirectoryIterator::SKIP_DOTS)
);

/** @var SplFileInfo $fileInfo */
foreach ($recursiveIterator as $fileInfo) {
if ($fileInfo->getExtension() !== 'php') {
continue;
}

$fileContents = file_get_contents($fileInfo->getRealPath());
if ($fileContents === false) {
continue;
}

preg_match_all('#([A-Za-z_][A-Za-z0-9_]*)::class#', $fileContents, $matches);
foreach ($matches[1] as $shortClassName) {
$registeredShortClassNames[$shortClassName] = true;
}
}
}

$this->registeredShortClassNames = $registeredShortClassNames;

return $registeredShortClassNames;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPStan\Tests\Rule\RegisterRectorRule;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use Rector\Utils\PHPStan\Rule\RegisterRectorRule;

/**
* @extends RuleTestCase<RegisterRectorRule>
*/
final class RegisterRectorRuleTest extends RuleTestCase
{
public function testUnregistered(): void
{
$expectedErrorMessage = 'Rector rule "Rector\Utils\PHPStan\Tests\Rule\RegisterRectorRule\Source\UnregisteredRector" is not registered in any set or level config, so it can never run. Register it in a set/level, or deprecate it if it is no longer wanted.';

$this->analyse([__DIR__ . '/Source/UnregisteredRector.php'], [[$expectedErrorMessage, 12]]);
}

public function testUnregisteredConfigurable(): void
{
$expectedErrorMessage = 'Rector rule "Rector\Utils\PHPStan\Tests\Rule\RegisterRectorRule\Source\UnregisteredConfigurableRector" is not registered in any set or level config, so it can never run. Register it in a set/level, or deprecate it if it is no longer wanted.';

$this->analyse([__DIR__ . '/Source/UnregisteredConfigurableRector.php'], [[$expectedErrorMessage, 13]]);
}

public function testRegistered(): void
{
$this->analyse([__DIR__ . '/Source/RegisteredRector.php'], []);
}

public function testRegisteredInSiblingConfig(): void
{
$this->analyse([__DIR__ . '/Source/SiblingRegisteredRector.php'], []);
}

public function testSkipDeprecated(): void
{
$this->analyse([__DIR__ . '/Source/SkipDeprecatedRector.php'], []);
}

protected function getRule(): Rule
{
return new RegisterRectorRule([__DIR__ . '/config', __DIR__ . '/config-sibling']);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPStan\Tests\Rule\RegisterRectorRule\Source;

use PhpParser\Node;
use Rector\Rector\AbstractRector;
use Rector\RuleDoc\CodeSample\CodeSample;
use Rector\RuleDoc\RuleDefinition;

final class RegisteredRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Registered rule', [new CodeSample('before', 'after')]);
}

public function getNodeTypes(): array
{
return [Node::class];
}

public function refactor(Node $node): ?Node
{
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPStan\Tests\Rule\RegisterRectorRule\Source;

use PhpParser\Node;
use Rector\Rector\AbstractRector;
use Rector\RuleDoc\CodeSample\CodeSample;
use Rector\RuleDoc\RuleDefinition;

final class SiblingRegisteredRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Rule registered in a sibling package config', [new CodeSample('before', 'after')]);
}

public function getNodeTypes(): array
{
return [Node::class];
}

public function refactor(Node $node): ?Node
{
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPStan\Tests\Rule\RegisterRectorRule\Source;

use PhpParser\Node;
use Rector\Configuration\Deprecation\Contract\DeprecatedInterface;
use Rector\Rector\AbstractRector;
use Rector\RuleDoc\CodeSample\CodeSample;
use Rector\RuleDoc\RuleDefinition;

final class SkipDeprecatedRector extends AbstractRector implements DeprecatedInterface
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Deprecated rule', [new CodeSample('before', 'after')]);
}

public function getNodeTypes(): array
{
return [Node::class];
}

public function refactor(Node $node): ?Node
{
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPStan\Tests\Rule\RegisterRectorRule\Source;

use PhpParser\Node;
use Rector\Contract\Rector\ConfigurableRectorInterface;
use Rector\Rector\AbstractRector;
use Rector\RuleDoc\CodeSample\CodeSample;
use Rector\RuleDoc\RuleDefinition;

final class UnregisteredConfigurableRector extends AbstractRector implements ConfigurableRectorInterface
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Configurable rule', [new CodeSample('before', 'after')]);
}

public function getNodeTypes(): array
{
return [Node::class];
}

public function refactor(Node $node): ?Node
{
return null;
}

public function configure(array $configuration): void
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPStan\Tests\Rule\RegisterRectorRule\Source;

use PhpParser\Node;
use Rector\Rector\AbstractRector;
use Rector\RuleDoc\CodeSample\CodeSample;
use Rector\RuleDoc\RuleDefinition;

final class UnregisteredRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Unregistered rule', [new CodeSample('before', 'after')]);
}

public function getNodeTypes(): array
{
return [Node::class];
}

public function refactor(Node $node): ?Node
{
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

declare(strict_types=1);

use Rector\Utils\PHPStan\Tests\Rule\RegisterRectorRule\Source\SiblingRegisteredRector;

// mimics a sibling extension package config set
return [SiblingRegisteredRector::class];
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

declare(strict_types=1);

use Rector\Utils\PHPStan\Tests\Rule\RegisterRectorRule\Source\RegisteredRector;

// mimics a set/level config; the rule only checks the "::class" reference is present
return [RegisteredRector::class];
Loading