Skip to content

Commit f3eb144

Browse files
authored
ClassSuffixNamingRule (#102)
1 parent 54144bb commit f3eb144

File tree

6 files changed

+159
-0
lines changed

6 files changed

+159
-0
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ parameters:
2727
enabled: true
2828
backedEnumGenerics:
2929
enabled: true
30+
classSuffixNaming:
31+
enabled: true
32+
superclassToSuffixMapping: []
3033
enforceEnumMatch:
3134
enabled: true
3235
enforceListReturn:
@@ -138,6 +141,23 @@ enum MyEnum: string { // missing @implements tag
138141
}
139142
```
140143

144+
### classSuffixNaming *
145+
- Allows you to enforce class name suffix for subclasses of configured superclass
146+
- Checks nothing by default, configure it by passing `superclass => suffix` mapping
147+
- Passed superclass is not expected to have such suffix, only subclasses are
148+
- You can use interface as superclass
149+
150+
```neon
151+
shipmonkRules:
152+
classSuffixNaming:
153+
superclassToSuffixMapping:
154+
\Exception: Exception
155+
\PHPStan\Rules\Rule: Rule
156+
\PHPUnit\Framework\TestCase: Test
157+
\Symfony\Component\Console\Command\Command: Command
158+
```
159+
160+
141161
### enforceEnumMatchRule
142162
- Enforces usage of `match ($enum)` instead of exhaustive conditions like `if ($enum === Enum::One) elseif ($enum === Enum::Two)`
143163
- This rule aims to "fix" a bit problematic behaviour of PHPStan (introduced at 1.10). It understands enum cases very well and forces you to adjust following code:

phpstan.neon.dist

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ parameters:
1818
checkUninitializedProperties: true
1919
checkTooWideReturnTypesInProtectedAndPublicMethods: true
2020

21+
shipmonkRules:
22+
classSuffixNaming:
23+
superclassToSuffixMapping:
24+
PHPStan\Rules\Rule: Rule
25+
PhpParser\NodeVisitor: Visitor
26+
ShipMonk\PHPStan\RuleTestCase: RuleTest
27+
2128
ignoreErrors:
2229
-
2330
message: "#Class BackedEnum not found\\.#"

rules.neon

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ parameters:
66
enabled: true
77
backedEnumGenerics:
88
enabled: true
9+
classSuffixNaming:
10+
enabled: true
11+
superclassToSuffixMapping: []
912
enforceEnumMatch:
1013
enabled: true
1114
enforceListReturn:
@@ -73,6 +76,10 @@ parametersSchema:
7376
backedEnumGenerics: structure([
7477
enabled: bool()
7578
])
79+
classSuffixNaming: structure([
80+
enabled: bool()
81+
superclassToSuffixMapping: arrayOf(string(), string())
82+
])
7683
enforceEnumMatch: structure([
7784
enabled: bool()
7885
])
@@ -161,6 +168,8 @@ conditionalTags:
161168
phpstan.rules.rule: %shipmonkRules.allowNamedArgumentOnlyInAttributes.enabled%
162169
ShipMonk\PHPStan\Rule\BackedEnumGenericsRule:
163170
phpstan.rules.rule: %shipmonkRules.backedEnumGenerics.enabled%
171+
ShipMonk\PHPStan\Rule\ClassSuffixNamingRule:
172+
phpstan.rules.rule: %shipmonkRules.classSuffixNaming.enabled%
164173
ShipMonk\PHPStan\Rule\EnforceEnumMatchRule:
165174
phpstan.rules.rule: %shipmonkRules.enforceEnumMatch.enabled%
166175
ShipMonk\PHPStan\Rule\EnforceNativeReturnTypehintRule:
@@ -230,6 +239,10 @@ services:
230239
class: ShipMonk\PHPStan\Rule\AllowNamedArgumentOnlyInAttributesRule
231240
-
232241
class: ShipMonk\PHPStan\Rule\BackedEnumGenericsRule
242+
-
243+
class: ShipMonk\PHPStan\Rule\ClassSuffixNamingRule
244+
arguments:
245+
superclassToSuffixMapping: %shipmonkRules.classSuffixNaming.superclassToSuffixMapping%
233246
-
234247
class: ShipMonk\PHPStan\Rule\EnforceEnumMatchRule
235248
-

src/Rule/ClassSuffixNamingRule.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\Rule;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\InClassNode;
8+
use PHPStan\Rules\Rule;
9+
use function strlen;
10+
use function substr_compare;
11+
12+
/**
13+
* @implements Rule<InClassNode>
14+
*/
15+
class ClassSuffixNamingRule implements Rule
16+
{
17+
18+
/**
19+
* @var array<class-string, string>
20+
*/
21+
private array $superclassToSuffixMapping;
22+
23+
/**
24+
* @param array<class-string, string> $superclassToSuffixMapping
25+
*/
26+
public function __construct(array $superclassToSuffixMapping = [])
27+
{
28+
$this->superclassToSuffixMapping = $superclassToSuffixMapping;
29+
}
30+
31+
public function getNodeType(): string
32+
{
33+
return InClassNode::class;
34+
}
35+
36+
/**
37+
* @param InClassNode $node
38+
* @return list<string>
39+
*/
40+
public function processNode(
41+
Node $node,
42+
Scope $scope
43+
): array
44+
{
45+
$classReflection = $scope->getClassReflection();
46+
47+
if ($classReflection === null) {
48+
return [];
49+
}
50+
51+
if ($classReflection->isAnonymous()) {
52+
return [];
53+
}
54+
55+
foreach ($this->superclassToSuffixMapping as $superClass => $suffix) {
56+
if (!$classReflection->isSubclassOf($superClass)) {
57+
continue;
58+
}
59+
60+
$className = $classReflection->getName();
61+
62+
if (substr_compare($className, $suffix, -strlen($suffix)) !== 0) {
63+
return ["Class name $className should end with $suffix suffix"];
64+
}
65+
}
66+
67+
return [];
68+
}
69+
70+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\Rule;
4+
5+
use PHPStan\Rules\Rule;
6+
use ShipMonk\PHPStan\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<ClassSuffixNamingRule>
10+
*/
11+
class ClassSuffixNamingRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new ClassSuffixNamingRule([ // @phpstan-ignore-line ignore non existing class not being class-string
17+
'ClassSuffixNamingRule\CheckedParent' => 'Suffix',
18+
'ClassSuffixNamingRule\CheckedInterface' => 'Suffix2',
19+
'NotExistingClass' => 'Foo',
20+
]);
21+
}
22+
23+
public function testClass(): void
24+
{
25+
$this->analyseFile(__DIR__ . '/data/ClassSuffixNamingRule/code.php');
26+
}
27+
28+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ClassSuffixNamingRule;
4+
5+
class CheckedParent {}
6+
interface CheckedInterface {}
7+
8+
interface BadSuffixInterface extends CheckedInterface {} // error: Class name ClassSuffixNamingRule\BadSuffixInterface should end with Suffix2 suffix
9+
interface GoodNameSuffix2 extends CheckedInterface {}
10+
11+
class Whatever {}
12+
class BadSuffixClass extends CheckedParent {} // error: Class name ClassSuffixNamingRule\BadSuffixClass should end with Suffix suffix
13+
class GoodNameSuffix extends CheckedParent {}
14+
15+
class InvalidConfigurationAlwaysGeneratesSomeError extends CheckedParent implements CheckedInterface {} // error: Class name ClassSuffixNamingRule\InvalidConfigurationAlwaysGeneratesSomeError should end with Suffix suffix
16+
class InvalidConfigurationAlwaysGeneratesSomeErrorSuffix extends CheckedParent implements CheckedInterface {} // error: Class name ClassSuffixNamingRule\InvalidConfigurationAlwaysGeneratesSomeErrorSuffix should end with Suffix2 suffix
17+
class InvalidConfigurationAlwaysGeneratesSomeErrorSuffix2 extends CheckedParent implements CheckedInterface {} // error: Class name ClassSuffixNamingRule\InvalidConfigurationAlwaysGeneratesSomeErrorSuffix2 should end with Suffix suffix
18+
19+
new class extends CheckedParent {
20+
21+
};

0 commit comments

Comments
 (0)