Skip to content

Commit dfcb186

Browse files
gnutixclaude
andcommitted
Check static property access through constant class-name strings and class-string unions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e4551cc commit dfcb186

3 files changed

Lines changed: 181 additions & 2 deletions

File tree

‎src/Rules/Properties/AccessStaticPropertiesCheck.php‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PHPStan\DependencyInjection\AutowiredParameter;
1717
use PHPStan\DependencyInjection\AutowiredService;
1818
use PHPStan\Internal\SprintfHelper;
19+
use PHPStan\Node\Expr\TypeExpr;
1920
use PHPStan\Php\PhpVersion;
2021
use PHPStan\Reflection\ReflectionProvider;
2122
use PHPStan\Rules\ClassNameCheck;
@@ -163,9 +164,24 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node,
163164
$classType = $scope->resolveTypeByName($node->class);
164165
}
165166
} else {
167+
$exprToCheck = NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class);
168+
$exprType = $scope->getType($exprToCheck);
169+
if ($exprType->isClassString()->yes()) {
170+
// A constant class-name string or a class-string type describes the
171+
// object it names, so route the check through that object type. This
172+
// makes constant-string / class-string unions behave exactly like
173+
// object unions with regard to rule levels (member filtering when
174+
// checkUnionTypes is off, whole-union check when it is on).
175+
$objectType = $exprType->getClassStringObjectType();
176+
if (count($objectType->getObjectClassNames()) === 0) {
177+
// plain class-string resolves to ObjectWithoutClassType, keep silent
178+
return [];
179+
}
180+
$exprToCheck = new TypeExpr($objectType);
181+
}
166182
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
167183
$scope,
168-
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class),
184+
$exprToCheck,
169185
sprintf('Access to static property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)),
170186
static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasStaticProperty($name)->yes(),
171187
);

‎tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Rules\Rule;
1010
use PHPStan\Rules\RuleLevelHelper;
1111
use PHPStan\Testing\RuleTestCase;
12+
use PHPUnit\Framework\Attributes\RequiresPhp;
1213
use const PHP_VERSION_ID;
1314

1415
/**
@@ -17,6 +18,8 @@
1718
class AccessStaticPropertiesRuleTest extends RuleTestCase
1819
{
1920

21+
private bool $checkUnionTypes = true;
22+
2023
protected function getRule(): Rule
2124
{
2225
$reflectionProvider = self::createReflectionProvider();
@@ -27,7 +30,7 @@ protected function getRule(): Rule
2730
$reflectionProvider,
2831
checkNullables: true,
2932
checkThisOnly: false,
30-
checkUnionTypes: true,
33+
checkUnionTypes: $this->checkUnionTypes,
3134
checkExplicitMixed: false,
3235
checkImplicitMixed: false,
3336
checkBenevolentUnionTypes: false,
@@ -358,4 +361,55 @@ public function testBug2861(): void
358361
$this->analyse([__DIR__ . '/data/bug-2861.php'], []);
359362
}
360363

364+
#[RequiresPhp('>= 8.0.0')]
365+
public function testStaticPropertyAccessOnStringUnions(): void
366+
{
367+
$this->analyse([__DIR__ . '/data/static-property-string-unions.php'], [
368+
[
369+
'Access to an undefined static property StaticPropertyStringUnions\Baz::$create.',
370+
32,
371+
],
372+
[
373+
'Access to an undefined static property StaticPropertyStringUnions\Baz|StaticPropertyStringUnions\Foo::$create.',
374+
38,
375+
],
376+
[
377+
'Access to an undefined static property StaticPropertyStringUnions\Baz|StaticPropertyStringUnions\Foo::$create.',
378+
50,
379+
],
380+
[
381+
'Access to an undefined static property StaticPropertyStringUnions\Baz|StaticPropertyStringUnions\Foo::$create.',
382+
58,
383+
],
384+
[
385+
'Access to an undefined static property StaticPropertyStringUnions\Baz::$create.',
386+
66,
387+
],
388+
[
389+
'Static access to instance property StaticPropertyStringUnions\Foo::$doBar.',
390+
80,
391+
],
392+
]);
393+
}
394+
395+
#[RequiresPhp('>= 8.0.0')]
396+
public function testStaticPropertyAccessOnStringUnionsWithoutCheckUnionTypes(): void
397+
{
398+
$this->checkUnionTypes = false;
399+
$this->analyse([__DIR__ . '/data/static-property-string-unions.php'], [
400+
[
401+
'Access to an undefined static property StaticPropertyStringUnions\Baz::$create.',
402+
32,
403+
],
404+
[
405+
'Access to an undefined static property StaticPropertyStringUnions\Baz::$create.',
406+
66,
407+
],
408+
[
409+
'Static access to instance property StaticPropertyStringUnions\Foo::$doBar.',
410+
80,
411+
],
412+
]);
413+
}
414+
361415
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php declare(strict_types = 1); // lint >= 8.0
2+
3+
namespace StaticPropertyStringUnions;
4+
5+
class Foo
6+
{
7+
8+
public static $create;
9+
10+
public $doBar;
11+
12+
}
13+
14+
class Bar
15+
{
16+
17+
public static $create;
18+
19+
}
20+
21+
class Baz
22+
{
23+
24+
}
25+
26+
class Runner
27+
{
28+
29+
public function singleConstantString(): void
30+
{
31+
$class = Baz::class;
32+
echo $class::$create;
33+
}
34+
35+
public function unionOfConstantStrings(bool $flag): void
36+
{
37+
$class = $flag ? Foo::class : Baz::class;
38+
echo $class::$create;
39+
}
40+
41+
public function unionOfConstantStringsAllValid(bool $flag): void
42+
{
43+
$class = $flag ? Foo::class : Bar::class;
44+
echo $class::$create;
45+
}
46+
47+
public function getClassUnion(Foo|Baz $object): void
48+
{
49+
$class = get_class($object);
50+
echo $class::$create;
51+
}
52+
53+
/**
54+
* @param class-string<Foo>|class-string<Baz> $class
55+
*/
56+
public function genericClassStringUnion(string $class): void
57+
{
58+
echo $class::$create;
59+
}
60+
61+
/**
62+
* @param class-string<Baz> $class
63+
*/
64+
public function singleGenericClassString(string $class): void
65+
{
66+
echo $class::$create;
67+
}
68+
69+
/**
70+
* @param class-string $class
71+
*/
72+
public function plainClassString(string $class): void
73+
{
74+
echo $class::$create;
75+
}
76+
77+
public function instancePropertyThroughConstantString(): void
78+
{
79+
$class = Foo::class;
80+
echo $class::$doBar;
81+
}
82+
83+
/**
84+
* @param class-string<Foo> $class
85+
*/
86+
public function propertyExistsNarrowing(string $class): void
87+
{
88+
if (property_exists($class, 'doesNotExistStatically')) {
89+
echo $class::$doesNotExistStatically;
90+
}
91+
}
92+
93+
/**
94+
* @param class-string<Foo>|class-string<Bar> $class
95+
*/
96+
public function genericClassStringUnionAllValid(string $class): void
97+
{
98+
echo $class::$create;
99+
}
100+
101+
/**
102+
* @param class-string<Foo> $class
103+
*/
104+
public function singleGenericClassStringValid(string $class): void
105+
{
106+
echo $class::$create;
107+
}
108+
109+
}

0 commit comments

Comments
 (0)