diff --git a/README.md b/README.md index 317a83c..19b0c20 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# phpstan-rules +# PHPStan Rules Set of additional PHPStan rules - check if property and constant shouldn't be set as protected (when is not inherited or class is not abstract) diff --git a/extension.neon b/extension.neon new file mode 100644 index 0000000..3c6f403 --- /dev/null +++ b/extension.neon @@ -0,0 +1,13 @@ +services: + - + class: PMarki\PHPStanRules\Rules\ConstRule + tags: + - phpstan.rules.rule + - + class: PMarki\PHPStanRules\Rules\ProtectedMethodRule + tags: + - phpstan.rules.rule + - + class: PMarki\PHPStanRules\Rules\ProtectedPropertyRule + tags: + - phpstan.rules.rule diff --git a/src/Rules/ProtectedMethodRule.php b/src/Rules/ProtectedMethodRule.php new file mode 100644 index 0000000..bcf45b5 --- /dev/null +++ b/src/Rules/ProtectedMethodRule.php @@ -0,0 +1,78 @@ + + */ +class ProtectedMethodRule implements Rule +{ + public function getNodeType(): string + { + return ClassMethod::class; + } + + /** + * @return array + */ + public function processNode(Node $node, Scope $scope): array + { + if (!$node instanceof ClassMethod) { + return []; + } + + $errors = []; + $methodName = $node->name->toString(); + if ($this->methodInherited($methodName, $scope)) { + return []; + } + + if (\str_starts_with($methodName, '_') && !\str_starts_with($methodName, '__')) { + $errors[] = RuleErrorBuilder::message("Method {$methodName}() must not start with underscore.") + ->identifier('extraMethodRules') + ->build(); + } + + if ($node->isProtected() && !$this->isInAbstractClass($scope)) { + $errors[] = RuleErrorBuilder::message("Method {$methodName}() should have private visibility.") + ->identifier('extraMethodRules') + ->tip("Protected methods can be used only in abstract classes or when inherited.") + ->build(); + } + + return $errors; + } + + private function isInAbstractClass(Scope $scope): bool + { + $classReflection = $scope->getClassReflection(); + return $classReflection === null || $classReflection->isAbstract(); + } + + private function methodInherited(string $methodName, Scope $scope): bool + { + $classReflection = $scope->getClassReflection(); + + foreach (\array_merge($classReflection->getParents(), $classReflection->getInterfaces()) as $ancestor) { + if ($ancestor->hasMethod($methodName)) { + $method = $ancestor->getMethod($methodName, $scope); + + if (!$method->isPrivate()) { + return true; + } + } + } + + return false; + } +} diff --git a/tests/ProtectedMethodRuleTest.php b/tests/ProtectedMethodRuleTest.php new file mode 100644 index 0000000..e62ba02 --- /dev/null +++ b/tests/ProtectedMethodRuleTest.php @@ -0,0 +1,95 @@ +analyse( + [__DIR__ . '/fixtures/ProtectedMethodRule/NoAbstractClass.php'], + [ + [ + 'Method protectedMethod() should have private visibility.' . self::TIP, + 6, + ], + [ + 'Method _underscore() must not start with underscore.', + 9, + ], + [ + 'Method _underscoreProtected() must not start with underscore.', + 10, + ], + [ + 'Method _underscoreProtected() should have private visibility.' . self::TIP, + 10, + ], + ], + ); + } + + public function testAbstractClass(): void + { + $this->analyse( + [__DIR__ . '/fixtures/ProtectedMethodRule/AbstractClass.php'], + [ + [ + 'Method _underscore() must not start with underscore.', + 9, + ], + [ + 'Method _underscoreProtected() must not start with underscore.', + 10, + ], + ], + ); + } + + public function testChildClass(): void + { + $this->analyse( + [ + __DIR__ . '/fixtures/ProtectedMethodRule/ChildClass.php', + ], + [ + [ + 'Method childMethod() should have private visibility.' . self::TIP, + 8, + ], + [ + 'Method _underscore() must not start with underscore.', + 11, + ], + ], + ); + } + + public function testInterface(): void + { + $this->analyse( + [ + __DIR__ . '/fixtures/ProtectedMethodRule/Interface.php', + ], + [ + [ + 'Method _underscoreProtected() must not start with underscore.', + 7, + ], + ], + ); + } + + protected function getRule(): Rule + { + return new ProtectedMethodRule(); + } +} diff --git a/tests/fixtures/ProtectedMethodRule/AbstractClass.php b/tests/fixtures/ProtectedMethodRule/AbstractClass.php new file mode 100644 index 0000000..8a1ff94 --- /dev/null +++ b/tests/fixtures/ProtectedMethodRule/AbstractClass.php @@ -0,0 +1,12 @@ +