Skip to content

Commit

Permalink
NEW Add rule to avoid use of "new" keyword
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed Jul 18, 2024
1 parent 62b0416 commit d38c6e0
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 0 deletions.
73 changes: 73 additions & 0 deletions src/PHPStan/KeywordSelfRule.php
Original file line number Diff line number Diff line change
@@ -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()
];
}
}
76 changes: 76 additions & 0 deletions tests/PHPStan/KeywordSelfRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?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);
}


/*
[
[
'@method annotation \'DataObject PolymorphicHasOne()\' is missing or has an invalid syntax.',
16
],
[
'@method annotation \'HasManyList<Group> DotNotationHasMany()\' is missing or has an invalid syntax.',
16
],
],
*/
}
Empty file.
11 changes: 11 additions & 0 deletions tests/PHPStan/KeywordSelfRuleTest/ClassUsesTrait.php
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 11 additions & 0 deletions tests/PHPStan/KeywordSelfRuleTest/ClassUsesTraitCorrect.php
Original file line number Diff line number Diff line change
@@ -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;
}
29 changes: 29 additions & 0 deletions tests/PHPStan/KeywordSelfRuleTest/TestClass.php
Original file line number Diff line number Diff line change
@@ -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(){}
}
21 changes: 21 additions & 0 deletions tests/PHPStan/KeywordSelfRuleTest/TestInterface.php
Original file line number Diff line number Diff line change
@@ -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();
}
28 changes: 28 additions & 0 deletions tests/PHPStan/KeywordSelfRuleTest/TestTrait.php
Original file line number Diff line number Diff line change
@@ -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(){}
}
28 changes: 28 additions & 0 deletions tests/PHPStan/KeywordSelfRuleTest/TestTraitCorrect.php
Original file line number Diff line number Diff line change
@@ -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(){}
}

0 comments on commit d38c6e0

Please sign in to comment.