Skip to content

Commit 202dd81

Browse files
authored
Readonly classes cannot be combined with #[AllowDynamicProperties] + check trait attributes
1 parent 1dc44d1 commit 202dd81

File tree

10 files changed

+296
-1
lines changed

10 files changed

+296
-1
lines changed

Diff for: Makefile

+2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ lint:
8787
--exclude tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-class.php \
8888
--exclude tests/PHPStan/Rules/Properties/data/hooked-properties-in-class.php \
8989
--exclude tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php \
90+
--exclude tests/PHPStan/Rules/Classes/data/bug-12281.php \
91+
--exclude tests/PHPStan/Rules/Traits/data/bug-12281.php \
9092
src tests
9193

9294
cs:

Diff for: conf/config.level0.neon

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ rules:
103103
- PHPStan\Rules\Regexp\RegularExpressionPatternRule
104104
- PHPStan\Rules\Traits\ConflictingTraitConstantsRule
105105
- PHPStan\Rules\Traits\ConstantsInTraitsRule
106+
- PHPStan\Rules\Traits\TraitAttributesRule
106107
- PHPStan\Rules\Types\InvalidTypesInUnionRule
107108
- PHPStan\Rules\Variables\UnsetRule
108109
- PHPStan\Rules\Whitespace\FileWhitespaceRule

Diff for: src/Rules/Classes/ClassAttributesRule.php

+31-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
use PHPStan\Node\InClassNode;
99
use PHPStan\Rules\AttributesCheck;
1010
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use function count;
13+
use function sprintf;
1114

1215
/**
1316
* @implements Rule<InClassNode>
@@ -28,12 +31,39 @@ public function processNode(Node $node, Scope $scope): array
2831
{
2932
$classLikeNode = $node->getOriginalNode();
3033

31-
return $this->attributesCheck->check(
34+
$errors = $this->attributesCheck->check(
3235
$scope,
3336
$classLikeNode->attrGroups,
3437
Attribute::TARGET_CLASS,
3538
'class',
3639
);
40+
41+
$classReflection = $node->getClassReflection();
42+
if (
43+
$classReflection->isReadOnly()
44+
|| $classReflection->isEnum()
45+
|| $classReflection->isInterface()
46+
) {
47+
$typeName = 'readonly class';
48+
$identifier = 'class.allowDynamicPropertiesReadonly';
49+
if ($classReflection->isEnum()) {
50+
$typeName = 'enum';
51+
$identifier = 'enum.allowDynamicProperties';
52+
}
53+
if ($classReflection->isInterface()) {
54+
$typeName = 'interface';
55+
$identifier = 'interface.allowDynamicProperties';
56+
}
57+
58+
if (count($classReflection->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) {
59+
$errors[] = RuleErrorBuilder::message(sprintf('Attribute class AllowDynamicProperties cannot be used with %s.', $typeName))
60+
->identifier($identifier)
61+
->nonIgnorable()
62+
->build();
63+
}
64+
}
65+
66+
return $errors;
3767
}
3868

3969
}

Diff for: src/Rules/Traits/TraitAttributesRule.php

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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\Node\InTraitNode;
9+
use PHPStan\Rules\AttributesCheck;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use function count;
13+
14+
/**
15+
* @implements Rule<InTraitNode>
16+
*/
17+
final class TraitAttributesRule implements Rule
18+
{
19+
20+
public function __construct(
21+
private AttributesCheck $attributesCheck,
22+
)
23+
{
24+
}
25+
26+
public function getNodeType(): string
27+
{
28+
return InTraitNode::class;
29+
}
30+
31+
public function processNode(Node $node, Scope $scope): array
32+
{
33+
$originalNode = $node->getOriginalNode();
34+
$errors = $this->attributesCheck->check(
35+
$scope,
36+
$originalNode->attrGroups,
37+
Attribute::TARGET_CLASS,
38+
'class',
39+
);
40+
41+
if (count($node->getTraitReflection()->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) {
42+
$errors[] = RuleErrorBuilder::message('Attribute class AllowDynamicProperties cannot be used with trait.')
43+
->identifier('trait.allowDynamicProperties')
44+
->nonIgnorable()
45+
->build();
46+
}
47+
48+
return $errors;
49+
}
50+
51+
}

Diff for: tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php

+24
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,28 @@ public function testBug12011(): void
167167
]);
168168
}
169169

170+
public function testBug12281(): void
171+
{
172+
if (PHP_VERSION_ID < 80200) {
173+
$this->markTestSkipped('Test requires PHP 8.2.');
174+
}
175+
176+
$this->checkExplicitMixed = true;
177+
$this->checkImplicitMixed = true;
178+
$this->analyse([__DIR__ . '/data/bug-12281.php'], [
179+
[
180+
'Attribute class AllowDynamicProperties cannot be used with readonly class.',
181+
05,
182+
],
183+
[
184+
'Attribute class AllowDynamicProperties cannot be used with enum.',
185+
12,
186+
],
187+
[
188+
'Attribute class AllowDynamicProperties cannot be used with interface.',
189+
15,
190+
],
191+
]);
192+
}
193+
170194
}

Diff for: tests/PHPStan/Rules/Classes/data/bug-12281.php

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php // lint >= 8.2
2+
3+
namespace Bug12281;
4+
5+
#[\AllowDynamicProperties]
6+
readonly class BlogData { /* … */ }
7+
8+
/** @readonly */
9+
#[\AllowDynamicProperties]
10+
class BlogDataPhpdoc { /* … */ }
11+
12+
#[\AllowDynamicProperties]
13+
enum BlogDataEnum { /* … */ }
14+
15+
#[\AllowDynamicProperties]
16+
interface BlogDataInterface { /* … */ }
+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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\ClassForbiddenNameCheck;
9+
use PHPStan\Rules\ClassNameCheck;
10+
use PHPStan\Rules\FunctionCallParametersCheck;
11+
use PHPStan\Rules\NullsafeCheck;
12+
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
13+
use PHPStan\Rules\Properties\PropertyReflectionFinder;
14+
use PHPStan\Rules\Rule;
15+
use PHPStan\Rules\RuleLevelHelper;
16+
use PHPStan\Testing\RuleTestCase;
17+
use const PHP_VERSION_ID;
18+
19+
/**
20+
* @extends RuleTestCase<TraitAttributesRule>
21+
*/
22+
class TraitAttributesRuleTest extends RuleTestCase
23+
{
24+
25+
private bool $checkExplicitMixed = false;
26+
27+
private bool $checkImplicitMixed = false;
28+
29+
protected function getRule(): Rule
30+
{
31+
$reflectionProvider = $this->createReflectionProvider();
32+
return new TraitAttributesRule(
33+
new AttributesCheck(
34+
$reflectionProvider,
35+
new FunctionCallParametersCheck(
36+
new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false),
37+
new NullsafeCheck(),
38+
new PhpVersion(80000),
39+
new UnresolvableTypeHelper(),
40+
new PropertyReflectionFinder(),
41+
true,
42+
true,
43+
true,
44+
true,
45+
),
46+
new ClassNameCheck(
47+
new ClassCaseSensitivityCheck($reflectionProvider, false),
48+
new ClassForbiddenNameCheck(self::getContainer()),
49+
),
50+
true,
51+
),
52+
);
53+
}
54+
55+
public function testRule(): void
56+
{
57+
$this->analyse([__DIR__ . '/data/trait-attributes.php'], [
58+
[
59+
'Attribute class TraitAttributes\AbstractAttribute is abstract.',
60+
8,
61+
],
62+
[
63+
'Attribute class TraitAttributes\MyTargettedAttribute does not have the class target.',
64+
20,
65+
],
66+
]);
67+
}
68+
69+
public function testBug12011(): void
70+
{
71+
if (PHP_VERSION_ID < 80300) {
72+
$this->markTestSkipped('Test requires PHP 8.3.');
73+
}
74+
75+
$this->checkExplicitMixed = true;
76+
$this->checkImplicitMixed = true;
77+
78+
$this->analyse([__DIR__ . '/data/bug-12011.php'], [
79+
[
80+
'Parameter #1 $name of attribute class Bug12011Trait\Table constructor expects string|null, int given.',
81+
8,
82+
],
83+
]);
84+
}
85+
86+
public function testBug12281(): void
87+
{
88+
$this->analyse([__DIR__ . '/data/bug-12281.php'], [
89+
[
90+
'Attribute class AllowDynamicProperties cannot be used with trait.',
91+
11,
92+
],
93+
]);
94+
}
95+
96+
}

Diff for: tests/PHPStan/Rules/Traits/data/bug-12011.php

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php // lint >= 8.3
2+
3+
namespace Bug12011Trait;
4+
5+
use Attribute;
6+
7+
8+
#[Table(self::TABLE_NAME)]
9+
trait MyTrait
10+
{
11+
private const int TABLE_NAME = 1;
12+
}
13+
14+
class X {
15+
use MyTrait;
16+
}
17+
18+
#[Attribute(Attribute::TARGET_CLASS)]
19+
final class Table
20+
{
21+
public function __construct(
22+
public readonly string|null $name = null,
23+
public readonly string|null $schema = null,
24+
) {
25+
}
26+
}

Diff for: tests/PHPStan/Rules/Traits/data/bug-12281.php

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 { /* … */ }
13+
14+
class Uses
15+
{
16+
17+
use BlogDataTrait;
18+
19+
}

Diff for: tests/PHPStan/Rules/Traits/data/trait-attributes.php

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 {}
22+
23+
class Uses
24+
{
25+
26+
use MyTrait;
27+
use MyTrait2;
28+
use MyTrait3;
29+
30+
}

0 commit comments

Comments
 (0)