Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ClassSuffixNamingRule #102

Merged
merged 4 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {

};