diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9977a1992d..3fcd01e7bf 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1448,11 +1448,6 @@ parameters: count: 1 path: src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 2 - path: src/Type/Php/PropertyExistsTypeSpecifyingExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" count: 2 diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index 70d08b9098..d95ee72f68 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -13,7 +13,6 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\Accessory\HasPropertyType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectWithoutClassType; @@ -51,36 +50,36 @@ public function specifyTypes( TypeSpecifierContext $context, ): SpecifiedTypes { - $propertyNameType = $scope->getType($node->getArgs()[1]->value); - if (!$propertyNameType instanceof ConstantStringType) { + $propertyNames = $scope->getType($node->getArgs()[1]->value)->getConstantStrings(); + if ($propertyNames === []) { return new SpecifiedTypes([], []); } - $objectType = $scope->getType($node->getArgs()[0]->value); - if ($objectType instanceof ConstantStringType) { - return new SpecifiedTypes([], []); - } elseif ($objectType->isObject()->yes()) { + $types = [new ObjectWithoutClassType()]; + foreach ($propertyNames as $propertyNameType) { + $objectType = $scope->getType($node->getArgs()[0]->value); + if (!$objectType->isObject()->yes()) { + return new SpecifiedTypes([], []); + } + $propertyNode = new PropertyFetch( $node->getArgs()[0]->value, new Identifier($propertyNameType->getValue()), ); - } else { - return new SpecifiedTypes([], []); - } - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyNode, $scope); - if ($propertyReflection !== null) { - if (!$propertyReflection->isNative()) { - return new SpecifiedTypes([], []); + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyNode, $scope); + if ($propertyReflection !== null) { + if (!$propertyReflection->isNative()) { + return new SpecifiedTypes([], []); + } } + + $types[] = new HasPropertyType($propertyNameType->getValue()); } return $this->typeSpecifier->create( $node->getArgs()[0]->value, - new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType($propertyNameType->getValue()), - ]), + new IntersectionType($types), $context, false, $scope, diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 16529f3a74..dfb5d4ddb4 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -262,6 +262,14 @@ public function testImpossibleCheckTypeFunctionCall(): void 927, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], + [ + "Call to function property_exists() with \$this(CheckTypeFunctionCall\\FinalClassWithPropertyExistsOfUnionStrings) and 'foo2Property'|'fooProperty' will always evaluate to true.", + 994, + ], + [ + "Call to function property_exists() with \$this(CheckTypeFunctionCall\\FinalClassWithPropertyExistsOfUnionStrings) and 'barProperty'|'fooProperty' will always evaluate to false.", + 1005, + ], ], ); } @@ -372,6 +380,10 @@ public function testImpossibleCheckTypeFunctionCallWithoutAlwaysTrue(): void 927, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], + [ + "Call to function property_exists() with \$this(CheckTypeFunctionCall\\FinalClassWithPropertyExistsOfUnionStrings) and 'barProperty'|'fooProperty' will always evaluate to false.", + 1005, + ], ], ); } diff --git a/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php b/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php index 814d93ae60..5e3ff999b8 100644 --- a/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php +++ b/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php @@ -976,3 +976,33 @@ function checkClosedResource($resource): void { } } + +final class FinalClassWithPropertyExistsOfUnionStrings +{ + /** @var int */ + private $fooProperty; + /** @var int */ + private $foo2Property; + + public function doFoo() + { + $prop = 'fooProperty'; + if (rand(0, 1) === 0) { + $prop = 'foo2Property'; + } + + if (property_exists($this, $prop)) { + } + } + + public function doFooBar() + { + $prop = 'fooProperty'; + if (rand(0, 1) === 0) { + $prop = 'barProperty'; + } + + if (property_exists($this, $prop)) { + } + } +}