Last month, we merged a total 188 pull-requests to Rector code. We could not afford such a high rate without having a robust CI setup we trust. Dozens of custom PHPStan rules help us on every commit.
Today we'll share with you 4 of them. You can use them in your code to save time and let robots work for you.
Following PHPStan rules saves us from pushing bugs to GitHub. They also save us precious time in code-reviews commenting obvious errors - that are easy to miss in huge pull-requests. They allow us not to think about code details but about business logic or architecture design.
Rector is using php-parser and it's nodes, e.g. PhpParser\Node\Expr\Variable
.
A Rector rule that should change $something
would look like this:
use Symfony\Component\DependencyInjection\Variable;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use PhpParser\Node;
use Rector\Rector\AbstractRector;
final class ChangeSomethignRector extends AbstractRector
{
public function getNodeTypes() : array
{
return [Variable::class];
}
/**
* @param Variable $node
*/
public function refactor(Node $node) : ?Node
{
// ...
}
// ...
}
Now look at the code again, and try to find the bug.
Found it!
-use Symfony\Component\DependencyInjection\Variable;
+use PhpParser\Node\Expr\Variable;
There is more than 2 option actually:
PHPStorm has no idea which class you want to use. When we code, we don't have time or thought about which imports are right. 99 % it's the first one. Of course, PHPStorm can learn from our manual choices and prefer the most selected one. But that means every contributor has to teach their PHPStorm, to avoid these bugs. That's non-sense.
Instead, we added known miss-types that appeared in pull-requests to PHPStan checks. Not only the wrong type but neatly with a suggestion for the right node class.
# phpstan.neon
services:
-
class: Symplify\PHPStanRules\Rules\PreferredClassRule
tags: [phpstan.rules.rule]
arguments:
oldToPreferredClasses:
'PHPUnit\TextUI\Configuration\Variable': 'PhpParser\Node\Expr\Variable'
'Symfony\Component\DependencyInjection\Variable': 'PhpParser\Node\Expr\Variable'
'phpDocumentor\Reflection\Types\Expression': 'PhpParser\Node\Stmt\Expression'
'phpDocumentor\Reflection\DocBlock\Tags\Param': 'PhpParser\Node\Param'
'phpDocumentor\Reflection\DocBlock\Tags\Return_': 'PhpParser\Node\Stmt\Return_'
'SebastianBergmann\Type\MixedType': 'PHPStan\Type\MixedType'
'Hoa\Protocol\Node\Node': 'PhpParser\Node'
The PreferredClassRule works in 100 % of our cases. We never had to use the left type in our code.
When we work with Rector, we either create new rules or fix bug in existing rule. To fix a bug, we add failing test fixture, then we switch to the rule class and fixed the bug. Add a test fixture, fix it... Add a test fixture, fix it...
That means constant jumping between rule and its tests. To save cognitive overload, we add a @see
annotation above every rule class:
This way we avoid a long search for the correct class and just jump right to it with one click.
Does every new rule has this annotation? No need to bother each other during code-reviews, because PHPStan has our back:
# phpstan.neon
services:
-
class: Symplify\PHPStanRules\Rules\SeeAnnotationToTestRule
tags: [phpstan.rules.rule]
arguments:
requiredSeeTypes:
- Rector\Rector\AbstractRector
An unintended side effect of the SeeAnnotationToTestRule is that every Rector rule is tested.
Do you want to add this PHPStan rule yourself but don't like hundreds of PHPStan errors in your CI?
Use this Rector rule to complete @see
annotations for you.
Rector test cases can provide configuration in 3 different methods:
getRectorClass()
methodgetRectorsWithConfiguration()
methodconfig.php
with services in provideConfigFileInfo()
methodAt least that was the idea. In reality, it was possible to mix these methods:
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class SomeRectorTest extends AbstractRectorTestCase
{
public function getRectorClass()
{
return SomeRector::class;
}
// WTF? why is this here?
// it only duplicates the previous method
public function getRectorsWithConfiguration(): array
{
return [
SomeRector::class => [],
];
}
}
It didn't cause any runtime troubles, but it was a code smell. To avoid more broken windows, we've added a custom PHPStan rule.
This OnlyOneClassMethodRule checks classes of specific type(s), and makes sure there is exactly 1 method used from defined list:
# phpstan.neon
services:
-
class: Symplify\PHPStanRules\Rules\OnlyOneClassMethodRule
tags: [phpstan.rules.rule]
arguments:
onlyOneMethodsByType:
Rector\Testing\PHPUnit\AbstractRectorTestCase:
- 'getRectorClass'
- 'getRectorsWithConfiguration'
- 'provideConfigFileInfo'
This rule is not configurable, but it saved us so much duplicated work we have to mention it.
Look at the following code. What would you improve type-wise?
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
class SomeClass
{
public function run(Node $node)
{
if ($node instanceof MethodCall) {
$this->isCheck($node);
}
}
private function isCheck(Node $node)
{
if (! $node instanceof MethodCall) {
return;
}
// ...
}
}
What about this?
- private function isCheck(Node $node)
+ private function isCheck(MethodCall $methodCall)
{
- if (! $node instanceof MethodCall) {
- return;
- }
// ...
}
Yes, why would you check MethodCall
that it's MethodCall
again? It might be evident in the post example with ten lines and nothing else. But in real life, we can easily miss it in any pull-request of 30+ lines.
Now PHPStan has again our back with the CheckTypehintCallerTypeRule:
# phpstan.neon
services:
-
class: Symplify\PHPStanRules\Rules\CheckTypehintCallerTypeRule
tags: [phpstan.rules.rule]
Thanks to @samsonasik
for contributing and further improving this last rule. It's a real time-saver.
You can find all mentioned rules in symplify/phpstan-rules. Get them, use them. Your code will thank you.
That's all for today.
Happy coding!