Skip to content

Commit 096ab28

Browse files
VincentLangletphpstan-bot
authored andcommitted
Suppress undefined static property error when property_exists() guard is present
- Handle class-string first argument in `PropertyExistsTypeSpecifyingExtension` by marking the `property_exists()` FuncCall expression as `true` in scope, instead of trying to intersect `HasPropertyType` with the class-string type (which produces `never` due to ObjectTypeTrait incompatibility) - Add a virtual `property_exists()` check in `AccessStaticPropertiesCheck` before reporting "Access to an undefined static property" — constructs a `property_exists(ClassName::class, 'propName')` FuncCall and checks if the scope evaluates it as `true` - Covers `static::$prop`, `self::$prop`, `ClassName::$prop`, and expression-based `$className::$prop` access patterns - Also works for the assign context via `AccessStaticPropertiesInAssignRule` which shares the same `AccessStaticPropertiesCheck` - Updated phpstan-baseline.neon to reflect one fewer `instanceof ConstantStringType`
1 parent 7604335 commit 096ab28

7 files changed

Lines changed: 133 additions & 8 deletions

File tree

phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1626,7 +1626,7 @@ parameters:
16261626
-
16271627
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
16281628
identifier: phpstanApi.instanceofType
1629-
count: 2
1629+
count: 1
16301630
path: src/Type/Php/PropertyExistsTypeSpecifyingExtension.php
16311631

16321632
-

src/Rules/Properties/AccessStaticPropertiesCheck.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
namespace PHPStan\Rules\Properties;
44

55
use PhpParser\Node;
6+
use PhpParser\Node\Arg;
7+
use PhpParser\Node\Expr\ClassConstFetch;
8+
use PhpParser\Node\Expr\FuncCall;
69
use PhpParser\Node\Expr\StaticPropertyFetch;
10+
use PhpParser\Node\Identifier;
711
use PhpParser\Node\Name;
12+
use PhpParser\Node\Name\FullyQualified;
13+
use PhpParser\Node\Scalar\String_;
814
use PHPStan\Analyser\NullsafeOperatorHelper;
915
use PHPStan\Analyser\Scope;
1016
use PHPStan\DependencyInjection\AutowiredParameter;
@@ -256,6 +262,19 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node,
256262
]);
257263
}
258264

265+
if ($node->class instanceof Name) {
266+
$classExpr = new ClassConstFetch($node->class, new Identifier('class'));
267+
} else {
268+
$classExpr = $node->class;
269+
}
270+
$propertyExistsCall = new FuncCall(new FullyQualified('property_exists'), [
271+
new Arg($classExpr),
272+
new Arg(new String_($name)),
273+
]);
274+
if ($scope->getType($propertyExistsCall)->isTrue()->yes()) {
275+
return [];
276+
}
277+
259278
return array_merge($messages, [
260279
RuleErrorBuilder::message(sprintf(
261280
'Access to an undefined static property %s::$%s.',

src/Type/Php/PropertyExistsTypeSpecifyingExtension.php

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
use PHPStan\Reflection\FunctionReflection;
1616
use PHPStan\Rules\Properties\PropertyReflectionFinder;
1717
use PHPStan\Type\Accessory\HasPropertyType;
18+
use PHPStan\Type\ClassStringType;
1819
use PHPStan\Type\Constant\ConstantBooleanType;
1920
use PHPStan\Type\Constant\ConstantStringType;
2021
use PHPStan\Type\FunctionTypeSpecifyingExtension;
2122
use PHPStan\Type\IntersectionType;
2223
use PHPStan\Type\ObjectWithoutClassType;
24+
use PHPStan\Type\UnionType;
2325
use function count;
2426

2527
#[AutowiredService]
@@ -71,17 +73,35 @@ public function specifyTypes(
7173
}
7274

7375
$objectType = $scope->getType($args[0]->value);
74-
if ($objectType instanceof ConstantStringType) {
75-
return new SpecifiedTypes([], []);
76-
} elseif ($objectType->isObject()->yes()) {
77-
$propertyNode = new PropertyFetch(
76+
if ($objectType->isString()->yes()) {
77+
return $this->typeSpecifier->create(
78+
new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()),
79+
new ConstantBooleanType(true),
80+
$context,
81+
$scope,
82+
);
83+
}
84+
85+
if (!$objectType->isObject()->yes()) {
86+
return $this->typeSpecifier->create(
7887
$args[0]->value,
79-
new Identifier($propertyNameType->getValue()),
88+
new UnionType([
89+
new IntersectionType([
90+
new ObjectWithoutClassType(),
91+
new HasPropertyType($propertyNameType->getValue()),
92+
]),
93+
new ClassStringType(),
94+
]),
95+
$context,
96+
$scope,
8097
);
81-
} else {
82-
return new SpecifiedTypes([], []);
8398
}
8499

100+
$propertyNode = new PropertyFetch(
101+
$args[0]->value,
102+
new Identifier($propertyNameType->getValue()),
103+
);
104+
85105
$propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyNode, $scope);
86106
if ($propertyReflection !== null) {
87107
if (!$propertyReflection->isNative()) {

tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ public function testRuleExpressionNames(): void
8080
]);
8181
}
8282

83+
public function testBug2861(): void
84+
{
85+
$this->analyse([__DIR__ . '/data/bug-2861-assign.php'], []);
86+
}
87+
8388
#[RequiresPhp('>= 8.5.0')]
8489
public function testAsymmetricVisibility(): void
8590
{

tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,4 +353,9 @@ public function testBug8668Bis(): void
353353
]);
354354
}
355355

356+
public function testBug2861(): void
357+
{
358+
$this->analyse([__DIR__ . '/data/bug-2861.php'], []);
359+
}
360+
356361
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug2861Assign;
4+
5+
class Foo {
6+
public static function test(): void {
7+
if (property_exists(static::class, 'default')) {
8+
static::$default = 'value';
9+
}
10+
}
11+
12+
/**
13+
* @param class-string $className
14+
*/
15+
public static function testExpr(string $className): void {
16+
if (property_exists($className, 'default')) {
17+
$className::$default = 'value';
18+
}
19+
}
20+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug2861;
4+
5+
trait EnumTrait {
6+
/** @var mixed */
7+
protected $value;
8+
9+
/** @param mixed $value */
10+
final public function __construct($value) {
11+
$this->value = $value;
12+
}
13+
14+
/** @return static|null */
15+
public static function getDefault() {
16+
if (property_exists(static::class, 'default') && null !== static::$default) {
17+
$obj = static::$default;
18+
return new static($obj);
19+
}
20+
return null;
21+
}
22+
}
23+
24+
class Foo {
25+
use EnumTrait;
26+
public const BLA = 'bla';
27+
}
28+
29+
class Bar {
30+
use EnumTrait;
31+
public static $default = 'bla';
32+
public const BLA = 'bla';
33+
}
34+
35+
class Baz {
36+
use EnumTrait;
37+
38+
/** @return static|null */
39+
public static function getDefault2() {
40+
if (property_exists(self::class, 'default') && null !== self::$default) {
41+
return new static(self::$default);
42+
}
43+
return null;
44+
}
45+
}
46+
47+
class ExpressionBased {
48+
/**
49+
* @param class-string $className
50+
*/
51+
public static function test(string $className): void {
52+
if (property_exists($className, 'default')) {
53+
echo $className::$default;
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)