Skip to content
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
13 changes: 13 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
@@ -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
78 changes: 78 additions & 0 deletions src/Rules/ProtectedMethodRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace PMarki\PHPStanRules\Rules;

use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* Reports an error when a method is protected in a non-abstract class unless it is inherited from a parent class.
*
* @implements Rule<ClassMethod>
*/
class ProtectedMethodRule implements Rule
{
public function getNodeType(): string
{
return ClassMethod::class;
}

/**
* @return array<int, \PHPStan\Rules\RuleError>
*/
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;
}
}
95 changes: 95 additions & 0 deletions tests/ProtectedMethodRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace PMarki\PHPStanRules\Tests;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PMarki\PHPStanRules\Rules\ProtectedMethodRule;

class ProtectedMethodRuleTest extends RuleTestCase
{
private const TIP = "\n 💡 Protected methods can be used only in abstract classes or when inherited.";

public function testNoAbstractClass(): void
{
$this->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();
}
}
12 changes: 12 additions & 0 deletions tests/fixtures/ProtectedMethodRule/AbstractClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace MethodRules;

abstract class AbstractClass {
protected function protectedMethod() {} // good
public function publicMethod() {} // good
private function privateMethod() {} // good
private function _underscore() {} // no underscore
protected function _underscoreProtected() {} // no underscore
public function __construct() {} // good
}
14 changes: 14 additions & 0 deletions tests/fixtures/ProtectedMethodRule/ChildClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace MethodRules;

class ChildClass extends AbstractClass
{
protected function protectedMethod() {} // good
protected function childMethod() {} // no inherited
public function publicMethod() {} // good
private function privateMethod() {} // good
private function _underscore() {} // no underscore
protected function _underscoreProtected() {} // good, inherited
public function __get($name) {} // good
}
13 changes: 13 additions & 0 deletions tests/fixtures/ProtectedMethodRule/Interface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace MethodRules;

interface Foo
{
function _underscoreProtected(); // bad, no leading underscore
}

class ChildClass implements Foo
{
public function _underscoreProtected() {} // good, inherited
}
12 changes: 12 additions & 0 deletions tests/fixtures/ProtectedMethodRule/NoAbstractClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace MethodRules;

class NoAbstractClass {
protected function protectedMethod() {} // bad
public function publicMethod() {} // good
private function privateMethod() {} // good
private function _underscore() {} // no underscore
protected function _underscoreProtected() {} // no underscore
public function __construct() {} // good
}