diff --git a/src/PHPStan/KeywordSelfRule.php b/src/PHPStan/KeywordSelfRule.php new file mode 100644 index 0000000..802a847 --- /dev/null +++ b/src/PHPStan/KeywordSelfRule.php @@ -0,0 +1,73 @@ +<?php + +declare(strict_types=1); + +namespace SilverStripe\Standards\PHPStan; + +use PhpParser\Node; +use PhpParser\Node\Name; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Expr\New_; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Expr\StaticPropertyFetch; +use PhpParser\Node\Identifier; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; + +/** + * Validates that the `self` keyword is only used in situations where it's not avoidable. + * + * @implements Rule<Node> + */ +class KeywordSelfRule implements Rule +{ + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $gettingConst = false; + switch (get_class($node)) { + // fetching a constant, e.g. `self::MY_CONST` + case ClassConstFetch::class: + // Traits can use `self` to get const values - but otherwise follow same + // logic as methods or properties. + if ($scope->isInTrait()) { + return []; + } + // static method call, e.g. `self::myMethod()` + case StaticCall::class: + // fetching a static property, e.g. `self::$my_property` + case StaticPropertyFetch::class: + if (!is_a($node->class, Name::class) || $node->class->toString() !== 'self') { + return []; + } + break; + // `self` as a type (for a property, argument, or return type) + case Name::class: + // Trait can use `self` for typehinting + if ($scope->isInTrait() || $node->toString() !== 'self') { + return []; + } + break; + // instantiating a new object from `self`, e.g. `new self()` + case New_::class: + if ($node->class->toString() !== 'self') { + return []; + } + break; + default: + return []; + } + + $actualClass = $scope->isInTrait() ? 'self::class' : $scope->getClassReflection()->getName(); + return [ + RuleErrorBuilder::message( + "Can't use keyword 'self'. Use '$actualClass' instead." + )->build() + ]; + } +} diff --git a/tests/PHPStan/KeywordSelfRuleTest.php b/tests/PHPStan/KeywordSelfRuleTest.php new file mode 100644 index 0000000..6651b65 --- /dev/null +++ b/tests/PHPStan/KeywordSelfRuleTest.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +namespace SilverStripe\Standards\Tests\PHPStan; + +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use SilverStripe\Standards\PHPStan\KeywordSelfRule; +use SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest\ClassUsesTrait; +use SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest\TestClass; +use SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest\TestInterface; +use SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest\TestTrait; + +/** + * @extends RuleTestCase<KeywordSelfRule> + */ +class KeywordSelfRuleTest extends RuleTestCase +{ + protected function getRule(): Rule + { + return new KeywordSelfRule(); + } + + public function provideRule() + { + return [ + 'interface' => [ + 'filePaths' => [__DIR__ . '/KeywordSelfRuleTest/TestInterface.php'], + 'errorMessage' => "Can't use keyword 'self'. Use '" . TestInterface::class . "' instead.", + 'errorLines' => [13, 18, 18], + ], + 'class' => [ + 'filePaths' => [__DIR__ . '/KeywordSelfRuleTest/TestClass.php'], + 'errorMessage' => "Can't use keyword 'self'. Use '" . TestClass::class . "' instead.", + 'errorLines' => [9, 11, 16, 16, 18, 20, 21, 21, 25], + ], + 'trait' => [ + 'filePaths' => [__DIR__ . '/KeywordSelfRuleTest/TestTrait.php', __DIR__ . '/KeywordSelfRuleTest/ClassUsesTrait.php'], + 'errorMessage' => "Can't use keyword 'self'. Use 'self::class' instead.", + 'errorLines' => [17, 19, 20, 24], + ], + 'trait no errors' => [ + 'filePaths' => [__DIR__ . '/KeywordSelfRuleTest/TestTrait.php', __DIR__ . '/KeywordSelfRuleTest/ClassUsesTrait.php'], + 'errorMessage' => "Can't use keyword 'self'. Use 'self::class' instead.", + 'errorLines' => [], + ], + ]; + } + + /** + * @dataProvider provideRule + */ + public function testRule(array $filePaths, string $errorMessage, array $errorLines): void + { + $errors = []; + foreach ($errorLines as $line) { + $errors[] = [$errorMessage, $line]; + } + $this->analyse($filePaths, $errors); + } +} diff --git a/tests/PHPStan/KeywordSelfRuleTest/Anothertest.php b/tests/PHPStan/KeywordSelfRuleTest/Anothertest.php new file mode 100644 index 0000000..e69de29 diff --git a/tests/PHPStan/KeywordSelfRuleTest/ClassUsesTrait.php b/tests/PHPStan/KeywordSelfRuleTest/ClassUsesTrait.php new file mode 100644 index 0000000..8594e10 --- /dev/null +++ b/tests/PHPStan/KeywordSelfRuleTest/ClassUsesTrait.php @@ -0,0 +1,11 @@ +<?php + +namespace SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest; + +/** + * PHPStan doesn't analyse traits unless there's a class that uses it + */ +class ClassUsesTrait +{ + use TestTrait; +} diff --git a/tests/PHPStan/KeywordSelfRuleTest/ClassUsesTraitCorrect.php b/tests/PHPStan/KeywordSelfRuleTest/ClassUsesTraitCorrect.php new file mode 100644 index 0000000..1768848 --- /dev/null +++ b/tests/PHPStan/KeywordSelfRuleTest/ClassUsesTraitCorrect.php @@ -0,0 +1,11 @@ +<?php + +namespace SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest; + +/** + * PHPStan doesn't analyse traits unless there's a class that uses it + */ +class ClassUsesTraitCorrect +{ + use TestTraitCorrect; +} diff --git a/tests/PHPStan/KeywordSelfRuleTest/TestClass.php b/tests/PHPStan/KeywordSelfRuleTest/TestClass.php new file mode 100644 index 0000000..83234d6 --- /dev/null +++ b/tests/PHPStan/KeywordSelfRuleTest/TestClass.php @@ -0,0 +1,29 @@ +<?php + +namespace SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest; + +class TestClass +{ + private const MY_CONST = 'some value'; + + private static string $myProperty = self::MY_CONST; + + private static self $mySecondProperty; + + /** + * self::class is ignored here because this is a comment. + */ + public static function function1(self $someProperty): self + { + $self = new self(); + $self::class; // $self:: isn't seen as self:: + self::self(); + self::$myProperty = self::class; + /* intentionally commented out to prove even multi-line comments are ignored. + self::$myProperty = self::class; + */ + return new self(); + } + + public static function self(){} +} diff --git a/tests/PHPStan/KeywordSelfRuleTest/TestInterface.php b/tests/PHPStan/KeywordSelfRuleTest/TestInterface.php new file mode 100644 index 0000000..848fe55 --- /dev/null +++ b/tests/PHPStan/KeywordSelfRuleTest/TestInterface.php @@ -0,0 +1,21 @@ +<?php + +namespace SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest; + +/** + * Usage of `self` in an interface refers to the interface itself, unlike with traits. + * This means we should avoid using self, since it's not actually needed here. + */ +interface TestInterface +{ + public const MY_CONST = 'some value'; + + public const MY_SECOND_CONST = self::MY_CONST; + + /** + * self::class is ignored here because this is a comment. + */ + public static function function1(self $someProperty): self; + + public static function self(); +} diff --git a/tests/PHPStan/KeywordSelfRuleTest/TestTrait.php b/tests/PHPStan/KeywordSelfRuleTest/TestTrait.php new file mode 100644 index 0000000..701c9fd --- /dev/null +++ b/tests/PHPStan/KeywordSelfRuleTest/TestTrait.php @@ -0,0 +1,28 @@ +<?php + +namespace SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest; + +trait TestTrait +{ + // Can't define a const on a trait, but the trait can access consts on the class that uses it + private static string $myProperty = self::MY_CONST; + + private static self $mySecondProperty; + + /** + * self::class is ignored here because this is a comment. + */ + public static function function1(self $someProperty): self + { + $self = new self(); + $self::class; // $self:: isn't seen as self:: + self::self(); + self::$myProperty = self::class; + /* intentionally commented out to prove even multi-line comments are ignored. + self::$myProperty = self::class; + */ + return new self(); + } + + public static function self(){} +} diff --git a/tests/PHPStan/KeywordSelfRuleTest/TestTraitCorrect.php b/tests/PHPStan/KeywordSelfRuleTest/TestTraitCorrect.php new file mode 100644 index 0000000..4d56826 --- /dev/null +++ b/tests/PHPStan/KeywordSelfRuleTest/TestTraitCorrect.php @@ -0,0 +1,28 @@ +<?php + +namespace SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest; + +trait TestTraitCorrect +{ + // Can't define a const on a trait, but the trait can access consts on the class that uses it + private static string $myProperty = self::MY_CONST; + + private static self $mySecondProperty; + + /** + * self::class is ignored here because this is a comment. + */ + public static function function1(self $someProperty): self + { + $self = new (self::class)(); + $self::class; // $self:: isn't seen as self:: + self::class::self(); + self::class::$myProperty = self::class; + /* intentionally commented out to prove even multi-line comments are ignored. + self::$myProperty = self::class; + */ + return new (self::class)(); + } + + public static function self(){} +}