Skip to content

Commit

Permalink
Merge pull request #22 from DaveLiddament/feature/restrict-trait-to
Browse files Browse the repository at this point in the history
ADD RestrictTraitTo rule
  • Loading branch information
DaveLiddament authored Aug 12, 2024
2 parents db24f98 + 203aa8a commit 8b1cf40
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This is an extension for [PHPStan](https://phpstan.org) for adding analysis for
- [MustUseResult](https://github.com/DaveLiddament/php-language-extensions#mustUseResult)
- [NamespaceVisibility](https://github.com/DaveLiddament/php-language-extensions#namespaceVisibility)
- [Package](https://github.com/DaveLiddament/php-language-extensions#package)
- [Restrict Trait To](https://github.com/DaveLiddament/php-language-extensions#retrictTraitTo)
- [Test Tag](https://github.com/DaveLiddament/php-language-extensions#testtag)

## Installation
Expand Down
79 changes: 79 additions & 0 deletions src/Rules/RestrictTraitToRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace DaveLiddament\PhpstanPhpLanguageExtensions\Rules;

use DaveLiddament\PhpLanguageExtensions\RestrictTraitTo;
use DaveLiddament\PhpstanPhpLanguageExtensions\AttributeValueReaders\AttributeFinder;
use DaveLiddament\PhpstanPhpLanguageExtensions\AttributeValueReaders\AttributeValueReader;
use DaveLiddament\PhpstanPhpLanguageExtensions\Helpers\Cache;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;

/** @implements Rule<Node\Stmt\TraitUse> */
final class RestrictTraitToRule implements Rule
{
/**
* @var Cache<string|null>
*/
private Cache $cache;

public function __construct(
private ReflectionProvider $reflectionProvider,
) {
$this->cache = new Cache();
}

public function getNodeType(): string
{
return Node\Stmt\TraitUse::class;
}

public function processNode(Node $node, Scope $scope): array
{
$containingClassName = $scope->getClassReflection()?->getName();

if (null === $containingClassName) {
return [];
}

$containingClassObjectType = new ObjectType($containingClassName);

foreach ($node->traits as $trait) {
$classReflection = $this->reflectionProvider->getClass($trait->toCodeString())->getNativeReflection();

if ($this->cache->hasEntry($classReflection)) {
$restrictTraitToClassName = $this->cache->getEntry($classReflection);
} else {
$restrictTraitTo = AttributeFinder::getAttributeOnClass($classReflection, RestrictTraitTo::class);

if (null === $restrictTraitTo) {
$restrictTraitToClassName = null;
} else {
$restrictTraitToClassName = AttributeValueReader::getString($restrictTraitTo, 0, 'className');
}

$this->cache->addEntry($classReflection, $restrictTraitToClassName);
}

if (null === $restrictTraitToClassName) {
continue;
}

$restrictTraitToObjectType = new ObjectType($restrictTraitToClassName);

if (!$restrictTraitToObjectType->isSuperTypeOf($containingClassObjectType)->yes()) {
return [
RuleErrorBuilder::message("Trait can only be used on class or child of: $restrictTraitToClassName")
->identifier('phpExtensionLibrary.restrictTraitTo')
->build(),
];
}
}

return [];
}
}
30 changes: 30 additions & 0 deletions tests/Rules/RestrictTraitToTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace DaveLiddament\PhpstanPhpLanguageExtensions\Tests\Rules;

use DaveLiddament\PhpstanPhpLanguageExtensions\Rules\RestrictTraitToRule;
use DaveLiddament\PhpstanRuleTestHelper\AbstractRuleTestCase;
use PHPStan\Rules\Rule;

/** @extends AbstractRuleTestCase<RestrictTraitToRule> */
final class RestrictTraitToTest extends AbstractRuleTestCase
{
protected function getRule(): Rule
{
return new RestrictTraitToRule($this->createReflectionProvider());
}

public function testRestrictTraitTo(): void
{
$this->assertIssuesReported(
__DIR__.'/data/restrictTraitTo/restrictTraitTo.php',
);
}

protected function getErrorFormatter(): string
{
return 'Trait can only be used on class or child of: {0}';
}
}
56 changes: 56 additions & 0 deletions tests/Rules/data/restrictTraitTo/restrictTraitTo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace RestrictTraitTo;

use DaveLiddament\PhpLanguageExtensions\RestrictTraitTo;

trait UseAnywhere {}

#[RestrictTraitTo(Interface1::class)]
trait UseOnlyOnInterface1 {}

#[RestrictTraitTo(AbstractClass1::class)]
trait UseOnlyOnAbstractClass1 {}

#[RestrictTraitTo(Class2::class)]
trait UseOnlyOnClass2 {}


interface Interface1 {}


class AClass {
use UseAnywhere; // OK
use UseOnlyOnInterface1; // ERROR RestrictTraitTo\Interface1
use UseOnlyOnAbstractClass1; // ERROR RestrictTraitTo\AbstractClass1
use UseOnlyOnClass2; // ERROR RestrictTraitTo\Class2
}


class ImplementsInterface1 implements Interface1 {
use UseAnywhere; // OK
use UseOnlyOnInterface1; // OK
use UseOnlyOnAbstractClass1; // ERROR RestrictTraitTo\AbstractClass1
use UseOnlyOnClass2; // ERROR RestrictTraitTo\Class2
}

abstract class AbstractClass1
{

}

class ExtendsAbstractClass1 extends AbstractClass1 {
use UseAnywhere; // OK
use UseOnlyOnInterface1; // ERROR RestrictTraitTo\Interface1
use UseOnlyOnAbstractClass1; // OK
use UseOnlyOnClass2; // ERROR RestrictTraitTo\Class2
}

class Class2 {
use UseAnywhere; // OK
use UseOnlyOnInterface1; // ERROR RestrictTraitTo\Interface1
use UseOnlyOnAbstractClass1; // ERROR RestrictTraitTo\AbstractClass1
use UseOnlyOnClass2; // OK
}

0 comments on commit 8b1cf40

Please sign in to comment.