Skip to content

Commit 4252b95

Browse files
committed
array_column should not extract non-accessible properties from objects
PHP's array_column respects calling scope visibility. This change: - Checks scope->canReadProperty() before including a property type - When both __isset and __get are defined, treats inaccessible properties as maybe-accessible (returns generic array) - Handles NeverType in index position by falling back to integer keys Fixes phpstan/phpstan#13573
1 parent 024a65c commit 4252b95

3 files changed

Lines changed: 255 additions & 2 deletions

File tree

src/Type/Php/ArrayColumnHelper.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public function getReturnIndexType(Type $arrayType, Type $indexType, Scope $scop
5353
$iterableValueType = $arrayType->getIterableValueType();
5454

5555
[$type, $certainty] = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope);
56+
if ($type instanceof NeverType) {
57+
return new IntegerType();
58+
}
5659
if ($certainty->yes()) {
5760
return $type;
5861
}
@@ -98,7 +101,9 @@ public function handleConstantArray(ConstantArrayType $arrayType, Type $columnTy
98101

99102
if (!$indexType->isNull()->yes()) {
100103
[$type, $certainty] = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope);
101-
if ($certainty->yes()) {
104+
if ($type instanceof NeverType) {
105+
$keyType = null;
106+
} elseif ($certainty->yes()) {
102107
$keyType = $type;
103108
} else {
104109
$keyType = TypeCombinator::union($type, new IntegerType());
@@ -147,7 +152,17 @@ private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $
147152
continue;
148153
}
149154

150-
$returnTypes[] = $type->getInstanceProperty($propertyName, $scope)->getReadableType();
155+
$property = $type->getInstanceProperty($propertyName, $scope);
156+
if (!$scope->canReadProperty($property)) {
157+
foreach ($type->getObjectClassReflections() as $classReflection) {
158+
if ($classReflection->hasMethod('__isset') && $classReflection->hasMethod('__get')) {
159+
return [new MixedType(), TrinaryLogic::createMaybe()];
160+
}
161+
}
162+
continue;
163+
}
164+
165+
$returnTypes[] = $property->getReadableType();
151166
}
152167
}
153168

tests/PHPStan/Analyser/nsrt/array-column-php82.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,122 @@ public function doFoo(array $a): void
237237
}
238238

239239
}
240+
241+
class ObjectWithVisibility
242+
{
243+
public int $pub = 1;
244+
protected int $prot = 2;
245+
private int $priv = 3;
246+
}
247+
248+
class ArrayColumnVisibilityTest
249+
{
250+
251+
/** @param array<int, ObjectWithVisibility> $objects */
252+
public function testNonPublicProperties(array $objects): void
253+
{
254+
assertType('list<int>', array_column($objects, 'pub'));
255+
assertType('array{}', array_column($objects, 'prot'));
256+
assertType('array{}', array_column($objects, 'priv'));
257+
}
258+
259+
/** @param array{ObjectWithVisibility} $objects */
260+
public function testNonPublicPropertiesConstant(array $objects): void
261+
{
262+
assertType('array{int}', array_column($objects, 'pub'));
263+
assertType('array{}', array_column($objects, 'prot'));
264+
assertType('array{}', array_column($objects, 'priv'));
265+
}
266+
267+
/** @param array<int, ObjectWithVisibility> $objects */
268+
public function testNonPublicAsIndex(array $objects): void
269+
{
270+
assertType('array<int, int>', array_column($objects, 'pub', 'pub'));
271+
assertType('array<int, int>', array_column($objects, 'pub', 'priv'));
272+
}
273+
274+
}
275+
276+
class ArrayColumnVisibilityFromInsideTest
277+
{
278+
279+
public int $pub = 1;
280+
private int $priv = 2;
281+
282+
/** @param list<self> $objects */
283+
public function testFromInside(array $objects): void
284+
{
285+
assertType('list<int>', array_column($objects, 'pub'));
286+
assertType('list<int>', array_column($objects, 'priv'));
287+
}
288+
289+
}
290+
291+
class ArrayColumnVisibilityFromChildTest extends ObjectWithVisibility
292+
{
293+
294+
/** @param list<ObjectWithVisibility> $objects */
295+
public function testFromChild(array $objects): void
296+
{
297+
assertType('list<int>', array_column($objects, 'pub'));
298+
assertType('list<int>', array_column($objects, 'prot'));
299+
assertType('array{}', array_column($objects, 'priv'));
300+
}
301+
302+
}
303+
304+
class ObjectWithIssetOnly
305+
{
306+
private int $priv = 2;
307+
308+
public function __isset(string $name): bool
309+
{
310+
return true;
311+
}
312+
}
313+
314+
class ArrayColumnVisibilityWithIssetOnlyTest
315+
{
316+
317+
/** @param array<int, ObjectWithIssetOnly> $objects */
318+
public function testWithIssetOnly(array $objects): void
319+
{
320+
assertType('array{}', array_column($objects, 'priv'));
321+
}
322+
323+
}
324+
325+
class ObjectWithIsset
326+
{
327+
public int $pub = 1;
328+
private int $priv = 2;
329+
330+
public function __isset(string $name): bool
331+
{
332+
return true;
333+
}
334+
335+
public function __get(string $name): mixed
336+
{
337+
return $this->$name;
338+
}
339+
}
340+
341+
class ArrayColumnVisibilityWithIssetTest
342+
{
343+
344+
/** @param array<int, ObjectWithIsset> $objects */
345+
public function testWithIsset(array $objects): void
346+
{
347+
assertType('list<int>', array_column($objects, 'pub'));
348+
assertType('list', array_column($objects, 'priv'));
349+
}
350+
351+
/** @param array{ObjectWithIsset} $objects */
352+
public function testWithIssetConstant(array $objects): void
353+
{
354+
assertType('array{int}', array_column($objects, 'pub'));
355+
assertType('list', array_column($objects, 'priv'));
356+
}
357+
358+
}

tests/PHPStan/Analyser/nsrt/array-column.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,122 @@ public function doFoo(array $a): void
252252
}
253253

254254
}
255+
256+
class ObjectWithVisibility
257+
{
258+
public int $pub = 1;
259+
protected int $prot = 2;
260+
private int $priv = 3;
261+
}
262+
263+
class ArrayColumnVisibilityTest
264+
{
265+
266+
/** @param array<int, ObjectWithVisibility> $objects */
267+
public function testNonPublicProperties(array $objects): void
268+
{
269+
assertType('list<int>', array_column($objects, 'pub'));
270+
assertType('array{}', array_column($objects, 'prot'));
271+
assertType('array{}', array_column($objects, 'priv'));
272+
}
273+
274+
/** @param array{ObjectWithVisibility} $objects */
275+
public function testNonPublicPropertiesConstant(array $objects): void
276+
{
277+
assertType('array{int}', array_column($objects, 'pub'));
278+
assertType('array{}', array_column($objects, 'prot'));
279+
assertType('array{}', array_column($objects, 'priv'));
280+
}
281+
282+
/** @param array<int, ObjectWithVisibility> $objects */
283+
public function testNonPublicAsIndex(array $objects): void
284+
{
285+
assertType('array<int, int>', array_column($objects, 'pub', 'pub'));
286+
assertType('array<int, int>', array_column($objects, 'pub', 'priv'));
287+
}
288+
289+
}
290+
291+
class ArrayColumnVisibilityFromInsideTest
292+
{
293+
294+
public int $pub = 1;
295+
private int $priv = 2;
296+
297+
/** @param list<self> $objects */
298+
public function testFromInside(array $objects): void
299+
{
300+
assertType('list<int>', array_column($objects, 'pub'));
301+
assertType('list<int>', array_column($objects, 'priv'));
302+
}
303+
304+
}
305+
306+
class ArrayColumnVisibilityFromChildTest extends ObjectWithVisibility
307+
{
308+
309+
/** @param list<ObjectWithVisibility> $objects */
310+
public function testFromChild(array $objects): void
311+
{
312+
assertType('list<int>', array_column($objects, 'pub'));
313+
assertType('list<int>', array_column($objects, 'prot'));
314+
assertType('array{}', array_column($objects, 'priv'));
315+
}
316+
317+
}
318+
319+
class ObjectWithIssetOnly
320+
{
321+
private int $priv = 2;
322+
323+
public function __isset(string $name): bool
324+
{
325+
return true;
326+
}
327+
}
328+
329+
class ArrayColumnVisibilityWithIssetOnlyTest
330+
{
331+
332+
/** @param array<int, ObjectWithIssetOnly> $objects */
333+
public function testWithIssetOnly(array $objects): void
334+
{
335+
assertType('array{}', array_column($objects, 'priv'));
336+
}
337+
338+
}
339+
340+
class ObjectWithIsset
341+
{
342+
public int $pub = 1;
343+
private int $priv = 2;
344+
345+
public function __isset(string $name): bool
346+
{
347+
return true;
348+
}
349+
350+
public function __get(string $name): mixed
351+
{
352+
return $this->$name;
353+
}
354+
}
355+
356+
class ArrayColumnVisibilityWithIssetTest
357+
{
358+
359+
/** @param array<int, ObjectWithIsset> $objects */
360+
public function testWithIsset(array $objects): void
361+
{
362+
assertType('list<int>', array_column($objects, 'pub'));
363+
assertType('list', array_column($objects, 'priv'));
364+
}
365+
366+
/** @param array{ObjectWithIsset} $objects */
367+
public function testWithIssetConstant(array $objects): void
368+
{
369+
assertType('array{int}', array_column($objects, 'pub'));
370+
assertType('list', array_column($objects, 'priv'));
371+
}
372+
373+
}

0 commit comments

Comments
 (0)