Skip to content

Commit 8b1cf40

Browse files
Merge pull request #22 from DaveLiddament/feature/restrict-trait-to
ADD RestrictTraitTo rule
2 parents db24f98 + 203aa8a commit 8b1cf40

File tree

4 files changed

+166
-0
lines changed

4 files changed

+166
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This is an extension for [PHPStan](https://phpstan.org) for adding analysis for
1717
- [MustUseResult](https://github.com/DaveLiddament/php-language-extensions#mustUseResult)
1818
- [NamespaceVisibility](https://github.com/DaveLiddament/php-language-extensions#namespaceVisibility)
1919
- [Package](https://github.com/DaveLiddament/php-language-extensions#package)
20+
- [Restrict Trait To](https://github.com/DaveLiddament/php-language-extensions#retrictTraitTo)
2021
- [Test Tag](https://github.com/DaveLiddament/php-language-extensions#testtag)
2122

2223
## Installation

src/Rules/RestrictTraitToRule.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace DaveLiddament\PhpstanPhpLanguageExtensions\Rules;
4+
5+
use DaveLiddament\PhpLanguageExtensions\RestrictTraitTo;
6+
use DaveLiddament\PhpstanPhpLanguageExtensions\AttributeValueReaders\AttributeFinder;
7+
use DaveLiddament\PhpstanPhpLanguageExtensions\AttributeValueReaders\AttributeValueReader;
8+
use DaveLiddament\PhpstanPhpLanguageExtensions\Helpers\Cache;
9+
use PhpParser\Node;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Reflection\ReflectionProvider;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use PHPStan\Type\ObjectType;
15+
16+
/** @implements Rule<Node\Stmt\TraitUse> */
17+
final class RestrictTraitToRule implements Rule
18+
{
19+
/**
20+
* @var Cache<string|null>
21+
*/
22+
private Cache $cache;
23+
24+
public function __construct(
25+
private ReflectionProvider $reflectionProvider,
26+
) {
27+
$this->cache = new Cache();
28+
}
29+
30+
public function getNodeType(): string
31+
{
32+
return Node\Stmt\TraitUse::class;
33+
}
34+
35+
public function processNode(Node $node, Scope $scope): array
36+
{
37+
$containingClassName = $scope->getClassReflection()?->getName();
38+
39+
if (null === $containingClassName) {
40+
return [];
41+
}
42+
43+
$containingClassObjectType = new ObjectType($containingClassName);
44+
45+
foreach ($node->traits as $trait) {
46+
$classReflection = $this->reflectionProvider->getClass($trait->toCodeString())->getNativeReflection();
47+
48+
if ($this->cache->hasEntry($classReflection)) {
49+
$restrictTraitToClassName = $this->cache->getEntry($classReflection);
50+
} else {
51+
$restrictTraitTo = AttributeFinder::getAttributeOnClass($classReflection, RestrictTraitTo::class);
52+
53+
if (null === $restrictTraitTo) {
54+
$restrictTraitToClassName = null;
55+
} else {
56+
$restrictTraitToClassName = AttributeValueReader::getString($restrictTraitTo, 0, 'className');
57+
}
58+
59+
$this->cache->addEntry($classReflection, $restrictTraitToClassName);
60+
}
61+
62+
if (null === $restrictTraitToClassName) {
63+
continue;
64+
}
65+
66+
$restrictTraitToObjectType = new ObjectType($restrictTraitToClassName);
67+
68+
if (!$restrictTraitToObjectType->isSuperTypeOf($containingClassObjectType)->yes()) {
69+
return [
70+
RuleErrorBuilder::message("Trait can only be used on class or child of: $restrictTraitToClassName")
71+
->identifier('phpExtensionLibrary.restrictTraitTo')
72+
->build(),
73+
];
74+
}
75+
}
76+
77+
return [];
78+
}
79+
}

tests/Rules/RestrictTraitToTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DaveLiddament\PhpstanPhpLanguageExtensions\Tests\Rules;
6+
7+
use DaveLiddament\PhpstanPhpLanguageExtensions\Rules\RestrictTraitToRule;
8+
use DaveLiddament\PhpstanRuleTestHelper\AbstractRuleTestCase;
9+
use PHPStan\Rules\Rule;
10+
11+
/** @extends AbstractRuleTestCase<RestrictTraitToRule> */
12+
final class RestrictTraitToTest extends AbstractRuleTestCase
13+
{
14+
protected function getRule(): Rule
15+
{
16+
return new RestrictTraitToRule($this->createReflectionProvider());
17+
}
18+
19+
public function testRestrictTraitTo(): void
20+
{
21+
$this->assertIssuesReported(
22+
__DIR__.'/data/restrictTraitTo/restrictTraitTo.php',
23+
);
24+
}
25+
26+
protected function getErrorFormatter(): string
27+
{
28+
return 'Trait can only be used on class or child of: {0}';
29+
}
30+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RestrictTraitTo;
6+
7+
use DaveLiddament\PhpLanguageExtensions\RestrictTraitTo;
8+
9+
trait UseAnywhere {}
10+
11+
#[RestrictTraitTo(Interface1::class)]
12+
trait UseOnlyOnInterface1 {}
13+
14+
#[RestrictTraitTo(AbstractClass1::class)]
15+
trait UseOnlyOnAbstractClass1 {}
16+
17+
#[RestrictTraitTo(Class2::class)]
18+
trait UseOnlyOnClass2 {}
19+
20+
21+
interface Interface1 {}
22+
23+
24+
class AClass {
25+
use UseAnywhere; // OK
26+
use UseOnlyOnInterface1; // ERROR RestrictTraitTo\Interface1
27+
use UseOnlyOnAbstractClass1; // ERROR RestrictTraitTo\AbstractClass1
28+
use UseOnlyOnClass2; // ERROR RestrictTraitTo\Class2
29+
}
30+
31+
32+
class ImplementsInterface1 implements Interface1 {
33+
use UseAnywhere; // OK
34+
use UseOnlyOnInterface1; // OK
35+
use UseOnlyOnAbstractClass1; // ERROR RestrictTraitTo\AbstractClass1
36+
use UseOnlyOnClass2; // ERROR RestrictTraitTo\Class2
37+
}
38+
39+
abstract class AbstractClass1
40+
{
41+
42+
}
43+
44+
class ExtendsAbstractClass1 extends AbstractClass1 {
45+
use UseAnywhere; // OK
46+
use UseOnlyOnInterface1; // ERROR RestrictTraitTo\Interface1
47+
use UseOnlyOnAbstractClass1; // OK
48+
use UseOnlyOnClass2; // ERROR RestrictTraitTo\Class2
49+
}
50+
51+
class Class2 {
52+
use UseAnywhere; // OK
53+
use UseOnlyOnInterface1; // ERROR RestrictTraitTo\Interface1
54+
use UseOnlyOnAbstractClass1; // ERROR RestrictTraitTo\AbstractClass1
55+
use UseOnlyOnClass2; // OK
56+
}

0 commit comments

Comments
 (0)