Skip to content

Commit

Permalink
ClassSuffixNamingRule (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
janedbal authored Apr 18, 2023
1 parent 54144bb commit f3eb144
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 0 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ parameters:
enabled: true
backedEnumGenerics:
enabled: true
classSuffixNaming:
enabled: true
superclassToSuffixMapping: []
enforceEnumMatch:
enabled: true
enforceListReturn:
Expand Down Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -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\\.#"
Expand Down
13 changes: 13 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ parameters:
enabled: true
backedEnumGenerics:
enabled: true
classSuffixNaming:
enabled: true
superclassToSuffixMapping: []
enforceEnumMatch:
enabled: true
enforceListReturn:
Expand Down Expand Up @@ -73,6 +76,10 @@ parametersSchema:
backedEnumGenerics: structure([
enabled: bool()
])
classSuffixNaming: structure([
enabled: bool()
superclassToSuffixMapping: arrayOf(string(), string())
])
enforceEnumMatch: structure([
enabled: bool()
])
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
-
Expand Down
70 changes: 70 additions & 0 deletions src/Rule/ClassSuffixNamingRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\Rule;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use function strlen;
use function substr_compare;

/**
* @implements Rule<InClassNode>
*/
class ClassSuffixNamingRule implements Rule
{

/**
* @var array<class-string, string>
*/
private array $superclassToSuffixMapping;

/**
* @param array<class-string, string> $superclassToSuffixMapping
*/
public function __construct(array $superclassToSuffixMapping = [])
{
$this->superclassToSuffixMapping = $superclassToSuffixMapping;
}

public function getNodeType(): string
{
return InClassNode::class;
}

/**
* @param InClassNode $node
* @return list<string>
*/
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 [];
}

}
28 changes: 28 additions & 0 deletions tests/Rule/ClassSuffixNamingRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\Rule;

use PHPStan\Rules\Rule;
use ShipMonk\PHPStan\RuleTestCase;

/**
* @extends RuleTestCase<ClassSuffixNamingRule>
*/
class ClassSuffixNamingRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new ClassSuffixNamingRule([ // @phpstan-ignore-line ignore non existing class not being class-string
'ClassSuffixNamingRule\CheckedParent' => 'Suffix',
'ClassSuffixNamingRule\CheckedInterface' => 'Suffix2',
'NotExistingClass' => 'Foo',
]);
}

public function testClass(): void
{
$this->analyseFile(__DIR__ . '/data/ClassSuffixNamingRule/code.php');
}

}
21 changes: 21 additions & 0 deletions tests/Rule/data/ClassSuffixNamingRule/code.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types = 1);

namespace ClassSuffixNamingRule;

class CheckedParent {}
interface CheckedInterface {}

interface BadSuffixInterface extends CheckedInterface {} // error: Class name ClassSuffixNamingRule\BadSuffixInterface should end with Suffix2 suffix
interface GoodNameSuffix2 extends CheckedInterface {}

class Whatever {}
class BadSuffixClass extends CheckedParent {} // error: Class name ClassSuffixNamingRule\BadSuffixClass should end with Suffix suffix
class GoodNameSuffix extends CheckedParent {}

class InvalidConfigurationAlwaysGeneratesSomeError extends CheckedParent implements CheckedInterface {} // error: Class name ClassSuffixNamingRule\InvalidConfigurationAlwaysGeneratesSomeError should end with Suffix suffix
class InvalidConfigurationAlwaysGeneratesSomeErrorSuffix extends CheckedParent implements CheckedInterface {} // error: Class name ClassSuffixNamingRule\InvalidConfigurationAlwaysGeneratesSomeErrorSuffix should end with Suffix2 suffix
class InvalidConfigurationAlwaysGeneratesSomeErrorSuffix2 extends CheckedParent implements CheckedInterface {} // error: Class name ClassSuffixNamingRule\InvalidConfigurationAlwaysGeneratesSomeErrorSuffix2 should end with Suffix suffix

new class extends CheckedParent {

};

0 comments on commit f3eb144

Please sign in to comment.