From a1a4608c6d1b2a9a6d489959ed658b8b501747cf Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 13 Apr 2023 16:50:35 +0200 Subject: [PATCH] ClassSuffixNamingRule --- README.md | 20 ++++++ phpstan.neon.dist | 7 ++ rules.neon | 13 ++++ src/Rule/ClassSuffixNamingRule.php | 70 +++++++++++++++++++ tests/Rule/ClassSuffixNamingRuleTest.php | 26 +++++++ .../Rule/data/ClassSuffixNamingRule/code.php | 12 ++++ 6 files changed, 148 insertions(+) create mode 100644 src/Rule/ClassSuffixNamingRule.php create mode 100644 tests/Rule/ClassSuffixNamingRuleTest.php create mode 100644 tests/Rule/data/ClassSuffixNamingRule/code.php diff --git a/README.md b/README.md index 0424725..48b39d9 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ parameters: enabled: true backedEnumGenerics: enabled: true + classSuffixNaming: + enabled: true + superclassToSuffixMapping: [] enforceEnumMatch: enabled: true enforceListReturn: @@ -138,6 +141,23 @@ enum MyEnum: string { // missing @implements tag } ``` +### classSuffixNaming * +- Allows you to enforce class name suffix for subclasses of configured superclass +- Checks nothing by default, configure it by passing `superclass => suffix` mapping +- Passed superclass is not expected to have such suffix, only subclasses are +- You can use interface as superclass + +```neon + shipmonkRules: + classSuffixNaming: + superclassToSuffixMapping: + \Exception: Exception + \PHPStan\Rules\Rule: Rule + \PHPUnit\Framework\TestCase: Test + \Symfony\Component\Console\Command\Command: Command +``` + + ### enforceEnumMatchRule - Enforces usage of `match ($enum)` instead of exhaustive conditions like `if ($enum === Enum::One) elseif ($enum === Enum::Two)` - 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: diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 8b29c5e..b447290 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -18,6 +18,13 @@ parameters: checkUninitializedProperties: true checkTooWideReturnTypesInProtectedAndPublicMethods: true + shipmonkRules: + classSuffixNaming: + superclassToSuffixMapping: + PHPStan\Rules\Rule: Rule + PhpParser\NodeVisitor: Visitor + ShipMonk\PHPStan\RuleTestCase: RuleTest + ignoreErrors: - message: "#Class BackedEnum not found\\.#" diff --git a/rules.neon b/rules.neon index 6ef613d..3c7667e 100644 --- a/rules.neon +++ b/rules.neon @@ -6,6 +6,9 @@ parameters: enabled: true backedEnumGenerics: enabled: true + classSuffixNaming: + enabled: true + superclassToSuffixMapping: [] enforceEnumMatch: enabled: true enforceListReturn: @@ -73,6 +76,10 @@ parametersSchema: backedEnumGenerics: structure([ enabled: bool() ]) + classSuffixNaming: structure([ + enabled: bool() + superclassToSuffixMapping: arrayOf(string(), string()) + ]) enforceEnumMatch: structure([ enabled: bool() ]) @@ -161,6 +168,8 @@ conditionalTags: phpstan.rules.rule: %shipmonkRules.allowNamedArgumentOnlyInAttributes.enabled% ShipMonk\PHPStan\Rule\BackedEnumGenericsRule: phpstan.rules.rule: %shipmonkRules.backedEnumGenerics.enabled% + ShipMonk\PHPStan\Rule\ClassSuffixNamingRule: + phpstan.rules.rule: %shipmonkRules.classSuffixNaming.enabled% ShipMonk\PHPStan\Rule\EnforceEnumMatchRule: phpstan.rules.rule: %shipmonkRules.enforceEnumMatch.enabled% ShipMonk\PHPStan\Rule\EnforceNativeReturnTypehintRule: @@ -230,6 +239,10 @@ services: class: ShipMonk\PHPStan\Rule\AllowNamedArgumentOnlyInAttributesRule - class: ShipMonk\PHPStan\Rule\BackedEnumGenericsRule + - + class: ShipMonk\PHPStan\Rule\ClassSuffixNamingRule + arguments: + superclassToSuffixMapping: %shipmonkRules.classSuffixNaming.superclassToSuffixMapping% - class: ShipMonk\PHPStan\Rule\EnforceEnumMatchRule - diff --git a/src/Rule/ClassSuffixNamingRule.php b/src/Rule/ClassSuffixNamingRule.php new file mode 100644 index 0000000..699022e --- /dev/null +++ b/src/Rule/ClassSuffixNamingRule.php @@ -0,0 +1,70 @@ + + */ +class ClassSuffixNamingRule implements Rule +{ + + /** + * @var array + */ + private array $superclassToSuffixMapping; + + /** + * @param array $superclassToSuffixMapping + */ + public function __construct(array $superclassToSuffixMapping = []) + { + $this->superclassToSuffixMapping = $superclassToSuffixMapping; + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + /** + * @param InClassNode $node + * @return list + */ + public function processNode( + Node $node, + Scope $scope + ): array + { + $classReflection = $scope->getClassReflection(); + + if ($classReflection === null) { + return []; + } + + if ($classReflection->isAnonymous()) { + return []; + } + + foreach ($this->superclassToSuffixMapping as $superClass => $suffix) { + if (!$classReflection->isSubclassOf($superClass)) { + continue; + } + + $className = $classReflection->getName(); + + if (substr_compare($className, $suffix, -strlen($suffix)) !== 0) { + return ["Class name $className should end with $suffix suffix"]; + } + } + + return []; + } + +} diff --git a/tests/Rule/ClassSuffixNamingRuleTest.php b/tests/Rule/ClassSuffixNamingRuleTest.php new file mode 100644 index 0000000..0f333c0 --- /dev/null +++ b/tests/Rule/ClassSuffixNamingRuleTest.php @@ -0,0 +1,26 @@ + + */ +class ClassSuffixNamingRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + /** @var class-string $superclass */ + $superclass = 'ClassSuffixNamingRule\CheckedParent'; // @phpstan-ignore-line + return new ClassSuffixNamingRule([$superclass => 'Suffix']); + } + + public function testClass(): void + { + $this->analyseFile(__DIR__ . '/data/ClassSuffixNamingRule/code.php'); + } + +} diff --git a/tests/Rule/data/ClassSuffixNamingRule/code.php b/tests/Rule/data/ClassSuffixNamingRule/code.php new file mode 100644 index 0000000..a1e70db --- /dev/null +++ b/tests/Rule/data/ClassSuffixNamingRule/code.php @@ -0,0 +1,12 @@ +