diff --git a/extension.neon b/extension.neon index 80cf7ed..9721f9a 100644 --- a/extension.neon +++ b/extension.neon @@ -7,3 +7,4 @@ parameters: - stubs/Money/MoneyParser.stub rules: - Ibexa\PHPStan\Rules\NoConfigResolverParametersInConstructorRule + - Ibexa\PHPStan\Rules\FinalClassRule diff --git a/rules/FinalClassRule.php b/rules/FinalClassRule.php new file mode 100644 index 0000000..962cb9b --- /dev/null +++ b/rules/FinalClassRule.php @@ -0,0 +1,77 @@ + + */ +final class FinalClassRule implements Rule +{ + private ReflectionProvider $reflectionProvider; + + public function __construct( + ReflectionProvider $reflectionProvider + ) { + $this->reflectionProvider = $reflectionProvider; + } + + public function getNodeType(): string + { + return Node\Stmt\Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // Skip anonymous classes + if (!isset($node->namespacedName)) { + return []; + } + + $className = $node->namespacedName->toString(); + + if (!$this->reflectionProvider->hasClass($className)) { + return []; + } + + $reflection = $this->reflectionProvider->getClass($className); + + // Skip if already final + if ($reflection->isFinal()) { + return []; + } + + // Skip if abstract (abstract classes shouldn't be final) + if ($reflection->isAbstract()) { + return []; + } + + // Skip interfaces and traits + if ($reflection->isInterface() || $reflection->isTrait()) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf( + 'Class %s is not final. All non-abstract classes should be final.', + $reflection->getName() + ) + ) + ->identifier('class.notFinal') + ->tip('Add "final" keyword to the class declaration.') + ->build(), + ]; + } +} diff --git a/tests/rules/FinalClassRuleTest.php b/tests/rules/FinalClassRuleTest.php new file mode 100644 index 0000000..6d91f24 --- /dev/null +++ b/tests/rules/FinalClassRuleTest.php @@ -0,0 +1,53 @@ + + */ +final class FinalClassRuleTest extends RuleTestCase +{ + protected function getRule(): Rule + { + return new FinalClassRule($this->createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse( + [ + __DIR__ . '/Fixtures/FinalClass/NonFinalClass.php', + ], + [ + [ + 'Class Ibexa\Tests\PHPStan\Rules\Fixtures\FinalClass\NonFinalClass is not final. All non-abstract classes should be final.', + 11, + 'Add "final" keyword to the class declaration.', + ], + ] + ); + } + + public function testNoErrorsOnFinalAndAbstractClassesAndInterfaces(): void + { + $this->analyse( + [ + __DIR__ . '/Fixtures/FinalClass/FinalClass.php', + __DIR__ . '/Fixtures/FinalClass/AbstractClass.php', + __DIR__ . '/Fixtures/FinalClass/SomeInterface.php', + __DIR__ . '/Fixtures/FinalClass/SomeTrait.php', + ], + [] + ); + } +} diff --git a/tests/rules/Fixtures/FinalClass/AbstractClass.php b/tests/rules/Fixtures/FinalClass/AbstractClass.php new file mode 100644 index 0000000..39ee675 --- /dev/null +++ b/tests/rules/Fixtures/FinalClass/AbstractClass.php @@ -0,0 +1,13 @@ +