Skip to content

Commit acf44c2

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) - For non-final classes, accounts for child classes that could override visibility: protected keeps its type (invariant in PHP), private becomes mixed (child can redeclare with any type) - For final classes, inaccessible properties return empty array - Handles NeverType in index position by falling back to integer keys Fixes phpstan/phpstan#13573
1 parent 024a65c commit acf44c2

3 files changed

Lines changed: 319 additions & 2 deletions

File tree

src/Type/Php/ArrayColumnHelper.php

Lines changed: 25 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,25 @@ 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+
if (!$classReflection->isFinal()) {
163+
if ($property->isPrivate()) {
164+
return [new MixedType(), TrinaryLogic::createMaybe()];
165+
}
166+
167+
return [$property->getReadableType(), TrinaryLogic::createMaybe()];
168+
}
169+
}
170+
continue;
171+
}
172+
173+
$returnTypes[] = $property->getReadableType();
151174
}
152175
}
153176

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

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

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

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

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

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

0 commit comments

Comments
 (0)