Skip to content

Commit af3d0af

Browse files
committed
Implement TraitAttributesRule
1 parent 24f916f commit af3d0af

File tree

5 files changed

+174
-0
lines changed

5 files changed

+174
-0
lines changed

conf/config.level0.neon

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ rules:
117117
- PHPStan\Rules\Properties\ReadOnlyPropertyRule
118118
- PHPStan\Rules\Traits\ConflictingTraitConstantsRule
119119
- PHPStan\Rules\Traits\ConstantsInTraitsRule
120+
- PHPStan\Rules\Traits\TraitAttributesRule
120121
- PHPStan\Rules\Types\InvalidTypesInUnionRule
121122
- PHPStan\Rules\Variables\UnsetRule
122123
- PHPStan\Rules\Whitespace\FileWhitespaceRule
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Traits;
4+
5+
use Attribute;
6+
use PhpParser\Node;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\ReflectionProvider;
9+
use PHPStan\Rules\AttributesCheck;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use function count;
13+
14+
/**
15+
* @implements Rule<Node\Stmt\Trait_>
16+
*/
17+
final class TraitAttributesRule implements Rule
18+
{
19+
20+
public function __construct(
21+
private AttributesCheck $attributesCheck,
22+
private ReflectionProvider $reflectionProvider,
23+
)
24+
{
25+
}
26+
27+
public function getNodeType(): string
28+
{
29+
return Node\Stmt\Trait_::class;
30+
}
31+
32+
public function processNode(Node $node, Scope $scope): array
33+
{
34+
$traitName = $node->namespacedName;
35+
if ($traitName === null) {
36+
return [];
37+
}
38+
39+
if (!$this->reflectionProvider->hasClass($traitName->toString())) {
40+
return [];
41+
}
42+
43+
$errors = $this->attributesCheck->check(
44+
$scope,
45+
$node->attrGroups,
46+
Attribute::TARGET_ALL, // there is no dedicated trait target
47+
'trait',
48+
);
49+
50+
$classReflection = $this->reflectionProvider->getClass($traitName->toString());
51+
if (count($classReflection->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) {
52+
$errors[] = RuleErrorBuilder::message('Attribute class AllowDynamicProperties cannot be used with trait.')
53+
->identifier('class.allowDynamicPropertiesTrait')
54+
->nonIgnorable()
55+
->build();
56+
}
57+
58+
return $errors;
59+
}
60+
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Traits;
4+
5+
use PHPStan\Php\PhpVersion;
6+
use PHPStan\Rules\AttributesCheck;
7+
use PHPStan\Rules\ClassCaseSensitivityCheck;
8+
use PHPStan\Rules\Classes\ClassAttributesRule;
9+
use PHPStan\Rules\ClassForbiddenNameCheck;
10+
use PHPStan\Rules\ClassNameCheck;
11+
use PHPStan\Rules\FunctionCallParametersCheck;
12+
use PHPStan\Rules\NullsafeCheck;
13+
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
14+
use PHPStan\Rules\Properties\PropertyReflectionFinder;
15+
use PHPStan\Rules\Rule;
16+
use PHPStan\Rules\RuleLevelHelper;
17+
use PHPStan\Rules\Traits\TraitAttributesRule;
18+
use PHPStan\Testing\RuleTestCase;
19+
use const PHP_VERSION_ID;
20+
21+
/**
22+
* @extends RuleTestCase<TraitAttributesRule>
23+
*/
24+
class TraitAttributesRuleTest extends RuleTestCase
25+
{
26+
27+
private bool $checkExplicitMixed = false;
28+
29+
private bool $checkImplicitMixed = false;
30+
31+
protected function getRule(): Rule
32+
{
33+
$reflectionProvider = $this->createReflectionProvider();
34+
return new TraitAttributesRule(
35+
new AttributesCheck(
36+
$reflectionProvider,
37+
new FunctionCallParametersCheck(
38+
new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false),
39+
new NullsafeCheck(),
40+
new PhpVersion(80000),
41+
new UnresolvableTypeHelper(),
42+
new PropertyReflectionFinder(),
43+
true,
44+
true,
45+
true,
46+
true,
47+
true,
48+
),
49+
new ClassNameCheck(
50+
new ClassCaseSensitivityCheck($reflectionProvider, false),
51+
new ClassForbiddenNameCheck(self::getContainer()),
52+
),
53+
true,
54+
),
55+
$reflectionProvider,
56+
);
57+
}
58+
59+
public function testRule(): void
60+
{
61+
$this->analyse([__DIR__ . '/data/trait-attributes.php'], [
62+
[
63+
'Attribute class TraitAttributes\AbstractAttribute is abstract.',
64+
8,
65+
],
66+
]);
67+
}
68+
69+
public function testBug12281(): void
70+
{
71+
$this->analyse([__DIR__ . '/data/bug-12281.php'], [
72+
[
73+
'Attribute class AllowDynamicProperties cannot be used with trait.',
74+
11,
75+
],
76+
]);
77+
}
78+
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php // lint >= 8.2
2+
3+
namespace Bug12281Traits;
4+
5+
#[\AllowDynamicProperties]
6+
enum BlogDataEnum { /* … */ } // reported by ClassAttributesRule
7+
8+
#[\AllowDynamicProperties]
9+
interface BlogDataInterface { /* … */ } // reported by ClassAttributesRule
10+
11+
#[\AllowDynamicProperties]
12+
trait BlogDataTrait { /* … */ }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace TraitAttributes;
4+
5+
#[\Attribute]
6+
abstract class AbstractAttribute {}
7+
8+
#[AbstractAttribute]
9+
trait MyTrait {}
10+
11+
#[\Attribute]
12+
class MyAttribute {}
13+
14+
#[MyAttribute]
15+
trait MyTrait2 {}
16+
17+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
18+
class MyTargettedAttribute {}
19+
20+
#[MyTargettedAttribute]
21+
trait MyTrait3 {}

0 commit comments

Comments
 (0)