Skip to content

Commit 2fb3dbe

Browse files
authored
Support for @final PHPDoc tag above properties
1 parent ce257d9 commit 2fb3dbe

21 files changed

+127
-20
lines changed

src/Reflection/Annotations/AnnotationPropertyReflection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ public function isAbstract(): TrinaryLogic
119119
return TrinaryLogic::createNo();
120120
}
121121

122+
public function isFinalByKeyword(): TrinaryLogic
123+
{
124+
return TrinaryLogic::createNo();
125+
}
126+
122127
public function isFinal(): TrinaryLogic
123128
{
124129
return TrinaryLogic::createNo();

src/Reflection/Dummy/ChangedTypePropertyReflection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ public function isAbstract(): TrinaryLogic
116116
return $this->reflection->isAbstract();
117117
}
118118

119+
public function isFinalByKeyword(): TrinaryLogic
120+
{
121+
return $this->reflection->isFinalByKeyword();
122+
}
123+
119124
public function isFinal(): TrinaryLogic
120125
{
121126
return $this->reflection->isFinal();

src/Reflection/Dummy/DummyPropertyReflection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ public function isAbstract(): TrinaryLogic
116116
return TrinaryLogic::createNo();
117117
}
118118

119+
public function isFinalByKeyword(): TrinaryLogic
120+
{
121+
return TrinaryLogic::createNo();
122+
}
123+
119124
public function isFinal(): TrinaryLogic
120125
{
121126
return TrinaryLogic::createNo();

src/Reflection/ExtendedPropertyReflection.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public function getNativeType(): Type;
3838

3939
public function isAbstract(): TrinaryLogic;
4040

41+
public function isFinalByKeyword(): TrinaryLogic;
42+
4143
public function isFinal(): TrinaryLogic;
4244

4345
public function isVirtual(): TrinaryLogic;

src/Reflection/Php/EnumPropertyReflection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ public function isAbstract(): TrinaryLogic
112112
return TrinaryLogic::createNo();
113113
}
114114

115+
public function isFinalByKeyword(): TrinaryLogic
116+
{
117+
return TrinaryLogic::createNo();
118+
}
119+
115120
public function isFinal(): TrinaryLogic
116121
{
117122
return TrinaryLogic::createNo();

src/Reflection/Php/PhpClassReflectionExtension.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ private function createProperty(
218218
$types[] = $value;
219219
}
220220

221-
return new PhpPropertyReflection($declaringClassReflection, null, null, TypeCombinator::union(...$types), $classReflection->getNativeReflection()->getProperty($propertyName), null, null, null, false, false, false, false, []);
221+
return new PhpPropertyReflection($declaringClassReflection, null, null, TypeCombinator::union(...$types), $classReflection->getNativeReflection()->getProperty($propertyName), null, null, null, false, false, false, false, [], false);
222222
}
223223
}
224224

@@ -227,6 +227,7 @@ private function createProperty(
227227
$isDeprecated = $deprecation !== null;
228228
$isInternal = false;
229229
$isReadOnlyByPhpDoc = $classReflection->isImmutable();
230+
$isFinal = $classReflection->isFinal() || $propertyReflection->isFinal();
230231
$isAllowedPrivateMutation = false;
231232

232233
if (
@@ -308,6 +309,7 @@ private function createProperty(
308309
}
309310
$isInternal = $resolvedPhpDoc->isInternal();
310311
$isReadOnlyByPhpDoc = $isReadOnlyByPhpDoc || $resolvedPhpDoc->isReadOnly();
312+
$isFinal = $isFinal || $resolvedPhpDoc->isFinal();
311313
$isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation();
312314
}
313315

@@ -435,6 +437,7 @@ private function createProperty(
435437
$isReadOnlyByPhpDoc,
436438
$isAllowedPrivateMutation,
437439
$this->attributeReflectionFactory->fromNativeReflection($propertyReflection->getAttributes(), InitializerExprContext::fromClass($declaringClassReflection->getName(), $declaringClassReflection->getFileName())),
440+
$isFinal,
438441
);
439442
}
440443

src/Reflection/Php/PhpPropertyReflection.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public function __construct(
4444
private bool $isReadOnlyByPhpDoc,
4545
private bool $isAllowedPrivateMutation,
4646
private array $attributes,
47+
private bool $isFinal,
4748
)
4849
{
4950
}
@@ -242,11 +243,16 @@ public function isAbstract(): TrinaryLogic
242243
return TrinaryLogic::createFromBoolean($this->reflection->isAbstract());
243244
}
244245

245-
public function isFinal(): TrinaryLogic
246+
public function isFinalByKeyword(): TrinaryLogic
246247
{
247248
return TrinaryLogic::createFromBoolean($this->reflection->isFinal());
248249
}
249250

251+
public function isFinal(): TrinaryLogic
252+
{
253+
return TrinaryLogic::createFromBoolean($this->isFinal);
254+
}
255+
250256
public function isVirtual(): TrinaryLogic
251257
{
252258
return TrinaryLogic::createFromBoolean($this->reflection->isVirtual());

src/Reflection/Php/SimpleXMLElementProperty.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ public function isAbstract(): TrinaryLogic
127127
return TrinaryLogic::createNo();
128128
}
129129

130+
public function isFinalByKeyword(): TrinaryLogic
131+
{
132+
return TrinaryLogic::createNo();
133+
}
134+
130135
public function isFinal(): TrinaryLogic
131136
{
132137
return TrinaryLogic::createNo();

src/Reflection/Php/UniversalObjectCrateProperty.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ public function isAbstract(): TrinaryLogic
117117
return TrinaryLogic::createNo();
118118
}
119119

120+
public function isFinalByKeyword(): TrinaryLogic
121+
{
122+
return TrinaryLogic::createNo();
123+
}
124+
120125
public function isFinal(): TrinaryLogic
121126
{
122127
return TrinaryLogic::createNo();

src/Reflection/ResolvedPropertyReflection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@ public function isAbstract(): TrinaryLogic
174174
return $this->reflection->isAbstract();
175175
}
176176

177+
public function isFinalByKeyword(): TrinaryLogic
178+
{
179+
return $this->reflection->isFinalByKeyword();
180+
}
181+
177182
public function isFinal(): TrinaryLogic
178183
{
179184
return $this->reflection->isFinal();

src/Reflection/Type/IntersectionTypePropertyReflection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ public function isAbstract(): TrinaryLogic
148148
return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isAbstract());
149149
}
150150

151+
public function isFinalByKeyword(): TrinaryLogic
152+
{
153+
return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinalByKeyword());
154+
}
155+
151156
public function isFinal(): TrinaryLogic
152157
{
153158
return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinal());

src/Reflection/Type/UnionTypePropertyReflection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ public function isAbstract(): TrinaryLogic
148148
return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isAbstract());
149149
}
150150

151+
public function isFinalByKeyword(): TrinaryLogic
152+
{
153+
return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinalByKeyword());
154+
}
155+
151156
public function isFinal(): TrinaryLogic
152157
{
153158
return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinal());

src/Reflection/WrappedExtendedPropertyReflection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ public function isAbstract(): TrinaryLogic
109109
return TrinaryLogic::createNo();
110110
}
111111

112+
public function isFinalByKeyword(): TrinaryLogic
113+
{
114+
return TrinaryLogic::createNo();
115+
}
116+
112117
public function isFinal(): TrinaryLogic
113118
{
114119
return TrinaryLogic::createNo();

src/Rules/Properties/FoundPropertyReflection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ public function isAbstract(): TrinaryLogic
143143
return $this->originalPropertyReflection->isAbstract();
144144
}
145145

146+
public function isFinalByKeyword(): TrinaryLogic
147+
{
148+
return $this->originalPropertyReflection->isFinalByKeyword();
149+
}
150+
146151
public function isFinal(): TrinaryLogic
147152
{
148153
return $this->originalPropertyReflection->isFinal();

src/Rules/Properties/OverridingPropertyRule.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public function processNode(Node $node, Scope $scope): array
135135
))->identifier('property.visibility')->nonIgnorable()->build();
136136
}
137137

138-
if ($prototype->isFinal()->yes()) {
138+
if ($prototype->isFinalByKeyword()->yes()) {
139139
$errors[] = RuleErrorBuilder::message(sprintf(
140140
'Property %s::$%s overrides final property %s::$%s.',
141141
$classReflection->getDisplayName(),
@@ -145,6 +145,15 @@ public function processNode(Node $node, Scope $scope): array
145145
))->identifier('property.parentPropertyFinal')
146146
->nonIgnorable()
147147
->build();
148+
} elseif ($prototype->isFinal()->yes()) {
149+
$errors[] = RuleErrorBuilder::message(sprintf(
150+
'Property %s::$%s overrides @final property %s::$%s.',
151+
$classReflection->getDisplayName(),
152+
$node->getName(),
153+
$prototype->getDeclaringClass()->getDisplayName(),
154+
$node->getName(),
155+
))->identifier('property.parentPropertyFinalByPhpDoc')
156+
->build();
148157
}
149158

150159
$typeErrors = [];

src/Rules/RestrictedUsage/RewrittenDeclaringClassPropertyReflection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ public function isAbstract(): TrinaryLogic
7373
return $this->propertyReflection->isAbstract();
7474
}
7575

76+
public function isFinalByKeyword(): TrinaryLogic
77+
{
78+
return $this->propertyReflection->isFinalByKeyword();
79+
}
80+
7681
public function isFinal(): TrinaryLogic
7782
{
7883
return $this->propertyReflection->isFinal();

src/Type/ObjectShapePropertyReflection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ public function isAbstract(): TrinaryLogic
114114
return TrinaryLogic::createNo();
115115
}
116116

117+
public function isFinalByKeyword(): TrinaryLogic
118+
{
119+
return TrinaryLogic::createNo();
120+
}
121+
117122
public function isFinal(): TrinaryLogic
118123
{
119124
return TrinaryLogic::createNo();

tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,19 +187,27 @@ public function testFinal(): void
187187
$this->analyse([__DIR__ . '/data/overriding-final-property.php'], [
188188
[
189189
'Property OverridingFinalProperty\Bar::$a overrides final property OverridingFinalProperty\Foo::$a.',
190-
21,
190+
27,
191191
],
192192
[
193193
'Property OverridingFinalProperty\Bar::$b overrides final property OverridingFinalProperty\Foo::$b.',
194-
23,
194+
29,
195195
],
196196
[
197197
'Property OverridingFinalProperty\Bar::$c overrides final property OverridingFinalProperty\Foo::$c.',
198-
25,
198+
31,
199199
],
200200
[
201201
'Property OverridingFinalProperty\Bar::$d overrides final property OverridingFinalProperty\Foo::$d.',
202-
27,
202+
33,
203+
],
204+
[
205+
'Property OverridingFinalProperty\Bar::$e overrides @final property OverridingFinalProperty\Foo::$e.',
206+
35,
207+
],
208+
[
209+
'Property OverridingFinalProperty\Bar::$f overrides @final property OverridingFinalProperty\Foo::$f.',
210+
37,
203211
],
204212
]);
205213
}

tests/PHPStan/Rules/Properties/data/overriding-final-property.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ class Foo
1313

1414
protected private(set) int $d;
1515

16+
/** @final */
17+
public $e;
18+
19+
/** @final */
20+
protected $f;
21+
1622
}
1723

1824
class Bar extends Foo
@@ -26,4 +32,8 @@ class Bar extends Foo
2632

2733
public int $d;
2834

35+
public $e;
36+
37+
protected $f;
38+
2939
}

tests/PHPStan/Rules/Variables/UnsetRuleTest.php

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -167,55 +167,55 @@ public function testUnsetHookedProperty(): void
167167
],
168168
[
169169
'Cannot unset property UnsetHookedProperty\NonFinalClass::$publicProperty because it might have hooks in a subclass.',
170-
13,
170+
14,
171171
],
172172
[
173173
'Cannot unset property UnsetHookedProperty\ContainerClass::$finalClass because it might have hooks in a subclass.',
174-
83,
174+
86,
175175
],
176176
[
177177
'Cannot unset property UnsetHookedProperty\ContainerClass::$nonFinalClass because it might have hooks in a subclass.',
178-
87,
178+
91,
179179
],
180180
[
181181
'Cannot unset hooked UnsetHookedProperty\Foo::$iii property.',
182-
89,
182+
93,
183183
],
184184
[
185185
'Cannot unset property UnsetHookedProperty\ContainerClass::$foo because it might have hooks in a subclass.',
186-
90,
186+
94,
187187
],
188188
[
189189
'Cannot unset hooked UnsetHookedProperty\User::$name property.',
190-
92,
190+
96,
191191
],
192192
[
193193
'Cannot unset hooked UnsetHookedProperty\User::$fullName property.',
194-
93,
194+
97,
195195
],
196196
[
197197
'Cannot unset property UnsetHookedProperty\ContainerClass::$user because it might have hooks in a subclass.',
198-
94,
198+
98,
199199
],
200200
[
201201
'Cannot unset hooked UnsetHookedProperty\User::$name property.',
202-
96,
202+
100,
203203
],
204204
[
205205
'Cannot unset hooked UnsetHookedProperty\User::$name property.',
206-
97,
206+
101,
207207
],
208208
[
209209
'Cannot unset hooked UnsetHookedProperty\User::$fullName property.',
210-
98,
210+
102,
211211
],
212212
[
213213
'Cannot unset hooked UnsetHookedProperty\User::$fullName property.',
214-
99,
214+
103,
215215
],
216216
[
217217
'Cannot unset property UnsetHookedProperty\ContainerClass::$arrayOfUsers because it might have hooks in a subclass.',
218-
100,
218+
104,
219219
],
220220
]);
221221
}

tests/PHPStan/Rules/Variables/data/unset-hooked-property.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ function doUnset(Foo $foo, User $user, NonFinalClass $nonFinalClass, FinalClass
99
unset($foo->ii);
1010
unset($foo->iii);
1111

12+
unset($nonFinalClass->publicAnnotatedFinalProperty);
1213
unset($nonFinalClass->publicFinalProperty);
1314
unset($nonFinalClass->publicProperty);
1415

@@ -49,6 +50,8 @@ class NonFinalClass {
4950
private string $privateProperty;
5051
public string $publicProperty;
5152
final public string $publicFinalProperty;
53+
/** @final */
54+
public string $publicAnnotatedFinalProperty;
5255

5356
function doFoo() {
5457
unset($this->privateProperty);
@@ -82,6 +85,7 @@ function dooNestedUnset(ContainerClass $containerClass) {
8285
unset($containerClass->finalClass->publicProperty);
8386
unset($containerClass->finalClass);
8487

88+
unset($containerClass->nonFinalClass->publicAnnotatedFinalProperty);
8589
unset($containerClass->nonFinalClass->publicFinalProperty);
8690
unset($containerClass->nonFinalClass->publicProperty);
8791
unset($containerClass->nonFinalClass);

0 commit comments

Comments
 (0)