diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8616d227c86..4744a2a680f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1626,7 +1626,7 @@ parameters: - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' identifier: phpstanApi.instanceofType - count: 2 + count: 1 path: src/Type/Php/PropertyExistsTypeSpecifyingExtension.php - diff --git a/src/Rules/Properties/AccessStaticPropertiesCheck.php b/src/Rules/Properties/AccessStaticPropertiesCheck.php index 7ad441ffed9..c548a2f342c 100644 --- a/src/Rules/Properties/AccessStaticPropertiesCheck.php +++ b/src/Rules/Properties/AccessStaticPropertiesCheck.php @@ -3,8 +3,14 @@ namespace PHPStan\Rules\Properties; use PhpParser\Node; +use PhpParser\Node\Arg; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\StaticPropertyFetch; +use PhpParser\Node\Identifier; use PhpParser\Node\Name; +use PhpParser\Node\Name\FullyQualified; +use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; @@ -256,6 +262,19 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, ]); } + if ($node->class instanceof Name) { + $classExpr = new ClassConstFetch($node->class, new Identifier('class')); + } else { + $classExpr = $node->class; + } + $propertyExistsCall = new FuncCall(new FullyQualified('property_exists'), [ + new Arg($classExpr), + new Arg(new String_($name)), + ]); + if ($scope->getType($propertyExistsCall)->isTrue()->yes()) { + return []; + } + return array_merge($messages, [ RuleErrorBuilder::message(sprintf( 'Access to an undefined static property %s::$%s.', diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index b299f6f14fd..df4c9864d7a 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -15,11 +15,13 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\Accessory\HasPropertyType; +use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\UnionType; use function count; #[AutowiredService] @@ -71,17 +73,35 @@ public function specifyTypes( } $objectType = $scope->getType($args[0]->value); - if ($objectType instanceof ConstantStringType) { - return new SpecifiedTypes([], []); - } elseif ($objectType->isObject()->yes()) { - $propertyNode = new PropertyFetch( + if ($objectType->isString()->yes()) { + return $this->typeSpecifier->create( + new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()), + new ConstantBooleanType(true), + $context, + $scope, + ); + } + + if (!$objectType->isObject()->yes()) { + return $this->typeSpecifier->create( $args[0]->value, - new Identifier($propertyNameType->getValue()), + new UnionType([ + new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType($propertyNameType->getValue()), + ]), + new ClassStringType(), + ]), + $context, + $scope, ); - } else { - return new SpecifiedTypes([], []); } + $propertyNode = new PropertyFetch( + $args[0]->value, + new Identifier($propertyNameType->getValue()), + ); + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyNode, $scope); if ($propertyReflection !== null) { if (!$propertyReflection->isNative()) { diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php index ed52d66ccd7..f8ddba0d610 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php @@ -80,6 +80,11 @@ public function testRuleExpressionNames(): void ]); } + public function testBug2861(): void + { + $this->analyse([__DIR__ . '/data/bug-2861-assign.php'], []); + } + #[RequiresPhp('>= 8.5.0')] public function testAsymmetricVisibility(): void { diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index a411b751980..a6e80a4dff5 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -353,4 +353,9 @@ public function testBug8668Bis(): void ]); } + public function testBug2861(): void + { + $this->analyse([__DIR__ . '/data/bug-2861.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-2861-assign.php b/tests/PHPStan/Rules/Properties/data/bug-2861-assign.php new file mode 100644 index 00000000000..fe16cea9f74 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-2861-assign.php @@ -0,0 +1,20 @@ +value = $value; + } + + /** @return static|null */ + public static function getDefault() { + if (property_exists(static::class, 'default') && null !== static::$default) { + $obj = static::$default; + return new static($obj); + } + return null; + } +} + +class Foo { + use EnumTrait; + public const BLA = 'bla'; +} + +class Bar { + use EnumTrait; + public static $default = 'bla'; + public const BLA = 'bla'; +} + +class Baz { + use EnumTrait; + + /** @return static|null */ + public static function getDefault2() { + if (property_exists(self::class, 'default') && null !== self::$default) { + return new static(self::$default); + } + return null; + } +} + +class ExpressionBased { + /** + * @param class-string $className + */ + public static function test(string $className): void { + if (property_exists($className, 'default')) { + echo $className::$default; + } + } +}