Skip to content
6 changes: 0 additions & 6 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1647,12 +1647,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\.$#'
identifier: phpstanApi.instanceofType
count: 2
path: src/Type/Php/PropertyExistsTypeSpecifyingExtension.php

-
message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#'
identifier: phpstanApi.instanceofType
Expand Down
38 changes: 25 additions & 13 deletions src/Type/Php/PropertyExistsTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use PHPStan\Rules\Properties\PropertyReflectionFinder;
use PHPStan\Type\Accessory\HasPropertyType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\ObjectWithoutClassType;
Expand Down Expand Up @@ -55,8 +54,9 @@ public function specifyTypes(
TypeSpecifierContext $context,
): SpecifiedTypes
{
$propertyNameType = $scope->getType($node->getArgs()[1]->value);
if (!$propertyNameType instanceof ConstantStringType) {
$propertyNameTypes = $scope->getType($node->getArgs()[1]->value)->getConstantStrings();

if ($propertyNameTypes === []) {
return $this->typeSpecifier->create(
new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()),
new ConstantBooleanType(true),
Expand All @@ -65,24 +65,36 @@ public function specifyTypes(
);
}

if ($propertyNameType->getValue() === '') {
return new SpecifiedTypes([], []);
$hasPropertyTypes = [];
foreach ($propertyNameTypes as $propertyNameType) {
$hasPropertyTypes[] = new HasPropertyType($propertyNameType->getValue());
}

$objectType = $scope->getType($node->getArgs()[0]->value);
if ($objectType instanceof ConstantStringType) {

if (!$objectType->isObject()->yes()) {
return new SpecifiedTypes([], []);
} elseif ($objectType->isObject()->yes()) {
$propertyNode = new PropertyFetch(
}

$propertyNodes = [];

foreach ($propertyNameTypes as $propertyNameType) {
if ($propertyNameType->getValue() === '') {
return new SpecifiedTypes([], []);
}

$propertyNodes[] = new PropertyFetch(
$node->getArgs()[0]->value,
new Identifier($propertyNameType->getValue()),
);
} else {
return new SpecifiedTypes([], []);
}

$propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyNode, $scope);
if ($propertyReflection !== null) {
foreach ($propertyNodes as $propertyNode) {
$propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyNode, $scope);
if ($propertyReflection === null) {
continue;
}

if (!$propertyReflection->isNative()) {
return new SpecifiedTypes([], []);
}
Expand All @@ -92,7 +104,7 @@ public function specifyTypes(
$node->getArgs()[0]->value,
new IntersectionType([
new ObjectWithoutClassType(),
new HasPropertyType($propertyNameType->getValue()),
...$hasPropertyTypes,
]),
$context,
$scope,
Expand Down
16 changes: 16 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13304.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Bug13304;

use function PHPStan\Testing\assertType;

function foo(object $foo): void
{
foreach (['qux', 'quux'] as $property) {
if (!property_exists($foo, $property)) {
throw new \Exception;
}

assertType("object&hasProperty(quux)&hasProperty(qux)", $foo);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, this doesn't make sense. The assertType only makes sense below the foreach.

property_exists($foo, $property) with 'qux'|'quux' does not guarantee that both properties are accessible on the object.

}
}
Loading