Skip to content

Commit 65e6142

Browse files
authored
[explicit] Add NoProtectedClassStmtRule (#185)
1 parent 64fc9e9 commit 65e6142

File tree

10 files changed

+206
-1
lines changed

10 files changed

+206
-1
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,17 @@ abstract class AbstractClass
245245

246246
<br>
247247

248+
### NoProtectedClassStmtRule
249+
250+
Avoid protected class stmts as they yield unexpected behavior. Use clear interface contract instead
251+
252+
```yaml
253+
rules:
254+
- Symplify\PHPStanRules\Rules\Explicit\NoProtectedClassStmtRule
255+
```
256+
257+
<br>
258+
248259
### ForbiddenArrayMethodCallRule
249260

250261
Array method calls [$this, "method"] are not allowed. Use explicit method instead to help PhpStorm, PHPStan and Rector understand your code

src/Enum/MethodName.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,18 @@ final class MethodName
1111
*/
1212
public const INVOKE = '__invoke';
1313

14+
/**
15+
* @var string
16+
*/
1417
public const CONSTRUCTOR = '__construct';
18+
19+
/**
20+
* @var string
21+
*/
22+
public const SET_UP = 'setUp';
23+
24+
/**
25+
* @var string
26+
*/
27+
public const TEAR_DOWN = 'tearDown';
1528
}

src/Enum/RuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,6 @@ final class RuleIdentifier
6969
public const STRING_FILE_ABSOLUTE_PATH_EXISTS = 'symplify.stringFileAbsolutePathExists';
7070

7171
public const NO_JUST_PROPERTY_ASSIGN = 'symplify.noJustPropertyAssign';
72+
73+
public const NO_PROTECTED_CLASS_STMT = 'symplify.noProtectedClassStmt';
7274
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Explicit;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\Class_;
9+
use PhpParser\Node\Stmt\ClassConst;
10+
use PhpParser\Node\Stmt\ClassMethod;
11+
use PhpParser\Node\Stmt\Property;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\Node\InClassNode;
14+
use PHPStan\Reflection\ClassReflection;
15+
use PHPStan\Rules\Rule;
16+
use PHPStan\Rules\RuleError;
17+
use PHPStan\Rules\RuleErrorBuilder;
18+
use Symplify\PHPStanRules\Enum\MethodName;
19+
use Symplify\PHPStanRules\Enum\RuleIdentifier;
20+
21+
/**
22+
* @implements Rule<InClassNode>
23+
*
24+
* @see \Symplify\PHPStanRules\Tests\Rules\Explicit\NoProtectedClassStmtRule\NoProtectedClassStmtRuleTest
25+
*/
26+
final class NoProtectedClassStmtRule implements Rule
27+
{
28+
/**
29+
* @var string
30+
*/
31+
public const ERROR_MESSAGE = 'Avoid protected class stmts as they yield unexpected behavior. Use clear interface contract instead';
32+
33+
public function getNodeType(): string
34+
{
35+
return InClassNode::class;
36+
}
37+
38+
/**
39+
* @param InClassNode $node
40+
* @return RuleError[]
41+
*/
42+
public function processNode(Node $node, Scope $scope): array
43+
{
44+
$classLike = $node->getOriginalNode();
45+
if (! $classLike instanceof Class_) {
46+
return [];
47+
}
48+
49+
// skip abstract classes for onw
50+
if ($classLike->isAbstract()) {
51+
return [];
52+
}
53+
54+
$ruleErrors = [];
55+
56+
foreach ($classLike->stmts as $classStmt) {
57+
if (! $classStmt instanceof ClassMethod && ! $classStmt instanceof ClassConst && ! $classStmt instanceof Property) {
58+
continue;
59+
}
60+
61+
// skip test one
62+
if ($this->shouldSkipClassMethod($classStmt, $scope)) {
63+
continue;
64+
}
65+
66+
if (! $classStmt->isProtected()) {
67+
continue;
68+
}
69+
70+
$ruleErrors[] = RuleErrorBuilder::message(self::ERROR_MESSAGE)
71+
->line($classStmt->getStartLine())
72+
->identifier(RuleIdentifier::NO_PROTECTED_CLASS_STMT)
73+
->build();
74+
}
75+
76+
return $ruleErrors;
77+
}
78+
79+
private function shouldSkipClassMethod(ClassMethod|ClassConst|Property $classStmt, Scope $scope): bool
80+
{
81+
if (! $classStmt instanceof ClassMethod) {
82+
return false;
83+
}
84+
85+
// PHPUnit test methods
86+
if (in_array($classStmt->name->toString(), [MethodName::SET_UP, MethodName::TEAR_DOWN])) {
87+
return false;
88+
}
89+
90+
return $this->isParentMethodOverride($classStmt, $scope);
91+
}
92+
93+
private function isParentMethodOverride(ClassMethod $classMethod, Scope $scope): bool
94+
{
95+
$classReflection = $scope->getClassReflection();
96+
if (! $classReflection instanceof ClassReflection) {
97+
return false;
98+
}
99+
100+
$parentClassReflection = $classReflection->getParentClass();
101+
if (! $parentClassReflection instanceof ClassReflection) {
102+
return false;
103+
}
104+
105+
return $parentClassReflection->hasMethod($classMethod->name->toString());
106+
}
107+
}

src/Rules/Symfony/RequireInvokableControllerRule.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use PHPStan\Rules\Rule;
1111
use PHPStan\Rules\RuleErrorBuilder;
1212
use Symplify\PHPStanRules\Enum\MethodName;
13-
use Symplify\PHPStanRules\Enum\SymfonyClass;
1413
use Symplify\PHPStanRules\Enum\SymfonyRuleIdentifier;
1514
use Symplify\PHPStanRules\Symfony\NodeAnalyzer\SymfonyControllerAnalyzer;
1615

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Explicit\NoProtectedClassStmtRule\Fixture;
4+
5+
final class ClassWithProtected
6+
{
7+
protected int $item;
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Explicit\NoProtectedClassStmtRule\Fixture;
4+
5+
abstract class SkipAbstractWithProtected
6+
{
7+
protected int $item;
8+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Explicit\NoProtectedClassStmtRule\Fixture;
4+
5+
use Symplify\PHPStanRules\Tests\Rules\Explicit\NoProtectedClassStmtRule\Source\ParentClassWithMethod;
6+
7+
final class SkipParentRequired extends ParentClassWithMethod
8+
{
9+
protected function some()
10+
{
11+
}
12+
}
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\Explicit\NoProtectedClassStmtRule;
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\Explicit\NoProtectedClassStmtRule;
12+
13+
final class NoProtectedClassStmtRuleTest extends RuleTestCase
14+
{
15+
#[DataProvider('provideData')]
16+
public function testRule(string $filePath, array $expectedErrorsWithLines): void
17+
{
18+
$this->analyse([$filePath], $expectedErrorsWithLines);
19+
}
20+
21+
public static function provideData(): Iterator
22+
{
23+
yield [__DIR__ . '/Fixture/ClassWithProtected.php', [
24+
[NoProtectedClassStmtRule::ERROR_MESSAGE, 7],
25+
]];
26+
27+
yield [__DIR__ . '/Fixture/SkipParentRequired.php', []];
28+
yield [__DIR__ . '/Fixture/SkipAbstractWithProtected.php', []];
29+
}
30+
31+
protected function getRule(): Rule
32+
{
33+
return new NoProtectedClassStmtRule();
34+
}
35+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Explicit\NoProtectedClassStmtRule\Source;
4+
5+
class ParentClassWithMethod
6+
{
7+
protected function some()
8+
{
9+
}
10+
}

0 commit comments

Comments
 (0)