diff --git a/src/Type/Php/ArrayColumnHelper.php b/src/Type/Php/ArrayColumnHelper.php index 179d85bc04..0cbf24793b 100644 --- a/src/Type/Php/ArrayColumnHelper.php +++ b/src/Type/Php/ArrayColumnHelper.php @@ -53,6 +53,9 @@ public function getReturnIndexType(Type $arrayType, Type $indexType, Scope $scop $iterableValueType = $arrayType->getIterableValueType(); [$type, $certainty] = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope); + if ($type instanceof NeverType) { + return new IntegerType(); + } if ($certainty->yes()) { return $type; } @@ -98,7 +101,9 @@ public function handleConstantArray(ConstantArrayType $arrayType, Type $columnTy if (!$indexType->isNull()->yes()) { [$type, $certainty] = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope); - if ($certainty->yes()) { + if ($type instanceof NeverType) { + $keyType = null; + } elseif ($certainty->yes()) { $keyType = $type; } else { $keyType = TypeCombinator::union($type, new IntegerType()); @@ -147,7 +152,17 @@ private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $ continue; } - $returnTypes[] = $type->getInstanceProperty($propertyName, $scope)->getReadableType(); + $property = $type->getInstanceProperty($propertyName, $scope); + if (!$scope->canReadProperty($property)) { + foreach ($type->getObjectClassReflections() as $classReflection) { + if ($classReflection->hasMethod('__isset') && $classReflection->hasMethod('__get')) { + return [new MixedType(), TrinaryLogic::createMaybe()]; + } + } + continue; + } + + $returnTypes[] = $property->getReadableType(); } } diff --git a/tests/PHPStan/Analyser/nsrt/array-column-php82.php b/tests/PHPStan/Analyser/nsrt/array-column-php82.php index e55e7a38ba..c0b8c7d8e1 100644 --- a/tests/PHPStan/Analyser/nsrt/array-column-php82.php +++ b/tests/PHPStan/Analyser/nsrt/array-column-php82.php @@ -237,3 +237,122 @@ public function doFoo(array $a): void } } + +class ObjectWithVisibility +{ + public int $pub = 1; + protected int $prot = 2; + private int $priv = 3; +} + +class ArrayColumnVisibilityTest +{ + + /** @param array $objects */ + public function testNonPublicProperties(array $objects): void + { + assertType('list', array_column($objects, 'pub')); + assertType('array{}', array_column($objects, 'prot')); + assertType('array{}', array_column($objects, 'priv')); + } + + /** @param array{ObjectWithVisibility} $objects */ + public function testNonPublicPropertiesConstant(array $objects): void + { + assertType('array{int}', array_column($objects, 'pub')); + assertType('array{}', array_column($objects, 'prot')); + assertType('array{}', array_column($objects, 'priv')); + } + + /** @param array $objects */ + public function testNonPublicAsIndex(array $objects): void + { + assertType('array', array_column($objects, 'pub', 'pub')); + assertType('array', array_column($objects, 'pub', 'priv')); + } + +} + +class ArrayColumnVisibilityFromInsideTest +{ + + public int $pub = 1; + private int $priv = 2; + + /** @param list $objects */ + public function testFromInside(array $objects): void + { + assertType('list', array_column($objects, 'pub')); + assertType('list', array_column($objects, 'priv')); + } + +} + +class ArrayColumnVisibilityFromChildTest extends ObjectWithVisibility +{ + + /** @param list $objects */ + public function testFromChild(array $objects): void + { + assertType('list', array_column($objects, 'pub')); + assertType('list', array_column($objects, 'prot')); + assertType('array{}', array_column($objects, 'priv')); + } + +} + +class ObjectWithIssetOnly +{ + private int $priv = 2; + + public function __isset(string $name): bool + { + return true; + } +} + +class ArrayColumnVisibilityWithIssetOnlyTest +{ + + /** @param array $objects */ + public function testWithIssetOnly(array $objects): void + { + assertType('array{}', array_column($objects, 'priv')); + } + +} + +class ObjectWithIsset +{ + public int $pub = 1; + private int $priv = 2; + + public function __isset(string $name): bool + { + return true; + } + + public function __get(string $name): mixed + { + return $this->$name; + } +} + +class ArrayColumnVisibilityWithIssetTest +{ + + /** @param array $objects */ + public function testWithIsset(array $objects): void + { + assertType('list', array_column($objects, 'pub')); + assertType('list', array_column($objects, 'priv')); + } + + /** @param array{ObjectWithIsset} $objects */ + public function testWithIssetConstant(array $objects): void + { + assertType('array{int}', array_column($objects, 'pub')); + assertType('list', array_column($objects, 'priv')); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-column.php b/tests/PHPStan/Analyser/nsrt/array-column.php index 7049a5130b..f6ebf69259 100644 --- a/tests/PHPStan/Analyser/nsrt/array-column.php +++ b/tests/PHPStan/Analyser/nsrt/array-column.php @@ -252,3 +252,122 @@ public function doFoo(array $a): void } } + +class ObjectWithVisibility +{ + public int $pub = 1; + protected int $prot = 2; + private int $priv = 3; +} + +class ArrayColumnVisibilityTest +{ + + /** @param array $objects */ + public function testNonPublicProperties(array $objects): void + { + assertType('list', array_column($objects, 'pub')); + assertType('array{}', array_column($objects, 'prot')); + assertType('array{}', array_column($objects, 'priv')); + } + + /** @param array{ObjectWithVisibility} $objects */ + public function testNonPublicPropertiesConstant(array $objects): void + { + assertType('array{int}', array_column($objects, 'pub')); + assertType('array{}', array_column($objects, 'prot')); + assertType('array{}', array_column($objects, 'priv')); + } + + /** @param array $objects */ + public function testNonPublicAsIndex(array $objects): void + { + assertType('array', array_column($objects, 'pub', 'pub')); + assertType('array', array_column($objects, 'pub', 'priv')); + } + +} + +class ArrayColumnVisibilityFromInsideTest +{ + + public int $pub = 1; + private int $priv = 2; + + /** @param list $objects */ + public function testFromInside(array $objects): void + { + assertType('list', array_column($objects, 'pub')); + assertType('list', array_column($objects, 'priv')); + } + +} + +class ArrayColumnVisibilityFromChildTest extends ObjectWithVisibility +{ + + /** @param list $objects */ + public function testFromChild(array $objects): void + { + assertType('list', array_column($objects, 'pub')); + assertType('list', array_column($objects, 'prot')); + assertType('array{}', array_column($objects, 'priv')); + } + +} + +class ObjectWithIssetOnly +{ + private int $priv = 2; + + public function __isset(string $name): bool + { + return true; + } +} + +class ArrayColumnVisibilityWithIssetOnlyTest +{ + + /** @param array $objects */ + public function testWithIssetOnly(array $objects): void + { + assertType('array{}', array_column($objects, 'priv')); + } + +} + +class ObjectWithIsset +{ + public int $pub = 1; + private int $priv = 2; + + public function __isset(string $name): bool + { + return true; + } + + public function __get(string $name): mixed + { + return $this->$name; + } +} + +class ArrayColumnVisibilityWithIssetTest +{ + + /** @param array $objects */ + public function testWithIsset(array $objects): void + { + assertType('list', array_column($objects, 'pub')); + assertType('list', array_column($objects, 'priv')); + } + + /** @param array{ObjectWithIsset} $objects */ + public function testWithIssetConstant(array $objects): void + { + assertType('array{int}', array_column($objects, 'pub')); + assertType('list', array_column($objects, 'priv')); + } + +}