Skip to content

Commit 588c6c6

Browse files
authored
[symfony] Add RequiredOnlyInAbstractRule (#164)
1 parent 2b10e91 commit 588c6c6

File tree

6 files changed

+134
-0
lines changed

6 files changed

+134
-0
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,17 @@ class SomeClass extends SomeParentClass
794794

795795
<br>
796796

797+
### RequiredOnlyInAbstractRule
798+
799+
`@required` annotation should be used only in abstract classes, to child classes can use clean `__construct()` service injection.
800+
801+
```yaml
802+
rules:
803+
- Symplify\PHPStanRules\Rules\Symfony\RequiredOnlyInAbstractRule
804+
```
805+
806+
<br>
807+
797808
### SingleRequiredMethodRule
798809

799810
There must be maximum 1 @required method in the class. Merge it to one to avoid possible injection collision or duplicated injects.

src/Enum/SymfonyRuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ final class SymfonyRuleIdentifier
2323
public const SYMFONY_NO_ABSTRACT_CONTROLLER_CONSTRUCTOR = 'symfony.noAbstractControllerConstructor';
2424

2525
public const SINGLE_REQUIRED_METHOD = 'symfony.singleRequiredMethod';
26+
27+
public const SYMFONY_REQUIRED_ONLY_IN_ABSTRACT = 'symfony.requiredOnlyInAbstract';
2628
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Symfony;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\Class_;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use Symplify\PHPStanRules\Enum\SymfonyRuleIdentifier;
13+
use Symplify\PHPStanRules\NodeAnalyzer\SymfonyRequiredMethodAnalyzer;
14+
15+
/**
16+
* @see \Symplify\PHPStanRules\Tests\Rules\Symfony\RequiredOnlyInAbstractRule\RequiredOnlyInAbstractRuleTest
17+
*
18+
* @implements Rule<Class_>
19+
*/
20+
final class RequiredOnlyInAbstractRule implements Rule
21+
{
22+
/**
23+
* @var string
24+
*/
25+
public const ERROR_MESSAGE = '#@required is reserved exclusively for abstract classes. For the rest of classes, use clean constructor injection';
26+
27+
public function getNodeType(): string
28+
{
29+
return Class_::class;
30+
}
31+
32+
/**
33+
* @param Class_ $node
34+
*/
35+
public function processNode(Node $node, Scope $scope): array
36+
{
37+
foreach ($node->getMethods() as $classMethod) {
38+
if (! SymfonyRequiredMethodAnalyzer::detect($classMethod)) {
39+
continue;
40+
}
41+
42+
if ($node->isAbstract()) {
43+
continue;
44+
}
45+
46+
$identifierRuleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
47+
->line($classMethod->getLine())
48+
->identifier(SymfonyRuleIdentifier::SYMFONY_REQUIRED_ONLY_IN_ABSTRACT)
49+
->build();
50+
51+
return [$identifierRuleError];
52+
}
53+
54+
return [];
55+
}
56+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\RequiredOnlyInAbstractRule\Fixture;
6+
7+
final class NonAbstractControllerWithRequired
8+
{
9+
/**
10+
* @required
11+
*/
12+
public function someMethod()
13+
{
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\RequiredOnlyInAbstractRule\Fixture;
6+
7+
abstract class SkipAbstractClass
8+
{
9+
/**
10+
* @required
11+
*/
12+
public function someMethod()
13+
{
14+
}
15+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\RequiredOnlyInAbstractRule;
6+
7+
use Iterator;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use Symplify\PHPStanRules\Rules\Symfony\RequiredOnlyInAbstractRule;
12+
13+
final class RequiredOnlyInAbstractRuleTest extends RuleTestCase
14+
{
15+
#[DataProvider('provideData')]
16+
public function testRule(string $filePath, array $expectedErrorMessagesWithLines): void
17+
{
18+
$this->analyse([$filePath], $expectedErrorMessagesWithLines);
19+
}
20+
21+
public static function provideData(): Iterator
22+
{
23+
yield [__DIR__ . '/Fixture/NonAbstractControllerWithRequired.php', [[
24+
RequiredOnlyInAbstractRule::ERROR_MESSAGE,
25+
12,
26+
]]];
27+
28+
yield [__DIR__ . '/Fixture/SkipAbstractClass.php', []];
29+
}
30+
31+
protected function getRule(): Rule
32+
{
33+
return new RequiredOnlyInAbstractRule();
34+
}
35+
}

0 commit comments

Comments
 (0)