Skip to content

Commit 8b2b97f

Browse files
committed
[PHP 8.5] Support for asymmetric visibility in static properties
1 parent ecab490 commit 8b2b97f

11 files changed

+165
-8
lines changed

src/Node/ClassPropertyNode.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,21 @@ public function isPrivate(): bool
9696
return (bool) ($this->flags & Modifiers::PRIVATE);
9797
}
9898

99+
public function isPrivateSet(): bool
100+
{
101+
return (bool) ($this->flags & Modifiers::PRIVATE_SET);
102+
}
103+
104+
public function isProtectedSet(): bool
105+
{
106+
return (bool) ($this->flags & Modifiers::PROTECTED_SET);
107+
}
108+
109+
public function isPublicSet(): bool
110+
{
111+
return (bool) ($this->flags & Modifiers::PUBLIC_SET);
112+
}
113+
99114
public function isFinal(): bool
100115
{
101116
return (bool) ($this->flags & Modifiers::FINAL);

src/Php/PhpVersion.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,11 @@ public function supportsAsymmetricVisibility(): bool
371371
return $this->versionId >= 80400;
372372
}
373373

374+
public function supportsAsymmetricVisibilityForStaticProperties(): bool
375+
{
376+
return $this->versionId >= 80500;
377+
}
378+
374379
public function supportsLazyObjects(): bool
375380
{
376381
return $this->versionId >= 80400;

src/Rules/Properties/AccessStaticPropertiesCheck.php

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\DependencyInjection\AutowiredParameter;
1111
use PHPStan\DependencyInjection\AutowiredService;
1212
use PHPStan\Internal\SprintfHelper;
13+
use PHPStan\Php\PhpVersion;
1314
use PHPStan\Reflection\ReflectionProvider;
1415
use PHPStan\Rules\ClassNameCheck;
1516
use PHPStan\Rules\ClassNameNodePair;
@@ -40,6 +41,7 @@ public function __construct(
4041
private ReflectionProvider $reflectionProvider,
4142
private RuleLevelHelper $ruleLevelHelper,
4243
private ClassNameCheck $classCheck,
44+
private PhpVersion $phpVersion,
4345
#[AutowiredParameter(ref: '%tips.discoveringSymbols%')]
4446
private bool $discoveringSymbolsTip,
4547
)
@@ -49,7 +51,7 @@ public function __construct(
4951
/**
5052
* @return list<IdentifierRuleError>
5153
*/
52-
public function check(StaticPropertyFetch $node, Scope $scope): array
54+
public function check(StaticPropertyFetch $node, Scope $scope, bool $write): array
5355
{
5456
if ($node->name instanceof Node\VarLikeIdentifier) {
5557
$names = [$node->name->name];
@@ -59,7 +61,7 @@ public function check(StaticPropertyFetch $node, Scope $scope): array
5961

6062
$errors = [];
6163
foreach ($names as $name) {
62-
$errors = array_merge($errors, $this->processSingleProperty($scope, $node, $name));
64+
$errors = array_merge($errors, $this->processSingleProperty($scope, $node, $name, $write));
6365
}
6466

6567
return $errors;
@@ -68,7 +70,7 @@ public function check(StaticPropertyFetch $node, Scope $scope): array
6870
/**
6971
* @return list<IdentifierRuleError>
7072
*/
71-
private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, string $name): array
73+
private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, string $name, bool $write): array
7274
{
7375
$messages = [];
7476
if ($node->class instanceof Name) {
@@ -198,7 +200,11 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node,
198200

199201
while ($parentClassReflection !== null) {
200202
if ($parentClassReflection->hasStaticProperty($name)) {
201-
if ($scope->canReadProperty($parentClassReflection->getStaticProperty($name))) {
203+
if ($write) {
204+
if ($scope->canWriteProperty($parentClassReflection->getStaticProperty($name))) {
205+
return [];
206+
}
207+
} elseif ($scope->canReadProperty($parentClassReflection->getStaticProperty($name))) {
202208
return [];
203209
}
204210
return [
@@ -241,7 +247,19 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node,
241247
}
242248

243249
$property = $classType->getStaticProperty($name, $scope);
244-
if (!$scope->canReadProperty($property)) {
250+
if ($write) {
251+
if ($scope->canWriteProperty($property)) {
252+
return $messages;
253+
}
254+
} elseif ($scope->canReadProperty($property)) {
255+
return $messages;
256+
}
257+
258+
if (
259+
!$this->phpVersion->supportsAsymmetricVisibilityForStaticProperties()
260+
|| !$write
261+
|| (!$property->isPrivateSet() && !$property->isProtectedSet())
262+
) {
245263
return array_merge($messages, [
246264
RuleErrorBuilder::message(sprintf(
247265
'Access to %s property $%s of class %s.',
@@ -252,7 +270,14 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node,
252270
]);
253271
}
254272

255-
return $messages;
273+
return array_merge($messages, [
274+
RuleErrorBuilder::message(sprintf(
275+
'Access to %s property $%s of class %s.',
276+
$property->isPrivateSet() ? 'private(set)' : 'protected(set)',
277+
$name,
278+
$property->getDeclaringClass()->getDisplayName(),
279+
))->identifier(sprintf('assign.staticProperty%s', $property->isPrivateSet() ? 'PrivateSet' : 'ProtectedSet'))->build(),
280+
]);
256281
}
257282

258283
}

src/Rules/Properties/AccessStaticPropertiesInAssignRule.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array
3434
return [];
3535
}
3636

37-
return $this->check->check($node->getPropertyFetch(), $scope);
37+
return $this->check->check($node->getPropertyFetch(), $scope, true);
3838
}
3939

4040
}

src/Rules/Properties/AccessStaticPropertiesRule.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function getNodeType(): string
2626

2727
public function processNode(Node $node, Scope $scope): array
2828
{
29-
return $this->check->check($node, $scope);
29+
return $this->check->check($node, $scope, false);
3030
}
3131

3232
}

src/Rules/Properties/PropertyInClassRule.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,21 @@ public function processNode(Node $node, Scope $scope): array
176176
->build(),
177177
];
178178
}
179+
if (
180+
!$this->phpVersion->supportsAsymmetricVisibilityForStaticProperties()
181+
&& (
182+
$node->isPrivateSet()
183+
|| $node->isProtectedSet()
184+
|| $node->isPublicSet()
185+
)
186+
) {
187+
return [
188+
RuleErrorBuilder::message('Asymmetric visibility for static properties is supported only on PHP 8.5 and later.')
189+
->nonIgnorable()
190+
->identifier('property.staticAsymmetricVisibility')
191+
->build(),
192+
];
193+
}
179194
}
180195

181196
if ($node->isVirtual()) {

tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
namespace PHPStan\Rules\Properties;
44

5+
use PHPStan\Php\PhpVersion;
56
use PHPStan\Rules\ClassCaseSensitivityCheck;
67
use PHPStan\Rules\ClassForbiddenNameCheck;
78
use PHPStan\Rules\ClassNameCheck;
89
use PHPStan\Rules\Rule;
910
use PHPStan\Rules\RuleLevelHelper;
1011
use PHPStan\Testing\RuleTestCase;
12+
use PHPUnit\Framework\Attributes\RequiresPhp;
13+
use const PHP_VERSION_ID;
1114

1215
/**
1316
* @extends RuleTestCase<AccessStaticPropertiesInAssignRule>
@@ -28,6 +31,7 @@ protected function getRule(): Rule
2831
$reflectionProvider,
2932
self::getContainer(),
3033
),
34+
new PhpVersion(PHP_VERSION_ID),
3135
true,
3236
),
3337
);
@@ -67,4 +71,23 @@ public function testRuleExpressionNames(): void
6771
]);
6872
}
6973

74+
#[RequiresPhp('>= 8.5')]
75+
public function testAsymmetricVisibility(): void
76+
{
77+
$this->analyse([__DIR__ . '/data/static-properties-asymmetric-visibility.php'], [
78+
[
79+
'Access to private(set) property $foo of class StaticPropertiesAsymmetricVisibility\Foo.',
80+
25,
81+
],
82+
[
83+
'Access to private(set) property $foo of class StaticPropertiesAsymmetricVisibility\Foo.',
84+
32,
85+
],
86+
[
87+
'Access to protected(set) property $bar of class StaticPropertiesAsymmetricVisibility\Foo.',
88+
33,
89+
],
90+
]);
91+
}
92+
7093
}

tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace PHPStan\Rules\Properties;
44

5+
use PHPStan\Php\PhpVersion;
56
use PHPStan\Rules\ClassCaseSensitivityCheck;
67
use PHPStan\Rules\ClassForbiddenNameCheck;
78
use PHPStan\Rules\ClassNameCheck;
89
use PHPStan\Rules\Rule;
910
use PHPStan\Rules\RuleLevelHelper;
1011
use PHPStan\Testing\RuleTestCase;
12+
use const PHP_VERSION_ID;
1113

1214
/**
1315
* @extends RuleTestCase<AccessStaticPropertiesRule>
@@ -28,6 +30,7 @@ protected function getRule(): Rule
2830
$reflectionProvider,
2931
self::getContainer(),
3032
),
33+
new PhpVersion(PHP_VERSION_ID),
3134
true,
3235
),
3336
);

tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,4 +281,27 @@ public function testPhp84FinalPropertyHooks(): void
281281
]);
282282
}
283283

284+
public function testAsymmetricVisibilityForStaticProperties(): void
285+
{
286+
$errors = [];
287+
if (PHP_VERSION_ID < 80500) {
288+
$errors = [
289+
[
290+
'Asymmetric visibility for static properties is supported only on PHP 8.5 and later.',
291+
8,
292+
],
293+
[
294+
'Asymmetric visibility for static properties is supported only on PHP 8.5 and later.',
295+
10,
296+
],
297+
[
298+
'Asymmetric visibility for static properties is supported only on PHP 8.5 and later.',
299+
12,
300+
],
301+
];
302+
}
303+
304+
$this->analyse([__DIR__ . '/data/static-properties-asymmetric-visibility-support.php'], $errors);
305+
}
306+
284307
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php // lint >= 8.5
2+
3+
namespace StaticPropertiesAsymmetricVisibilitySupport;
4+
5+
class Foo
6+
{
7+
8+
public private(set) static int $foo = 1;
9+
10+
public protected(set) static int $bar = 1;
11+
12+
public public(set) static int $baz = 1;
13+
14+
}

0 commit comments

Comments
 (0)