Skip to content

Commit 1521691

Browse files
janedbalstaabmVincentLanglet
authored andcommitted
array_column should not extract non-public properties from objects (phpstan#5210)
Co-authored-by: Markus Staab <maggus.staab@googlemail.com> Co-authored-by: Vincent Langlet <VincentLanglet@users.noreply.github.com>
1 parent bb97d47 commit 1521691

File tree

3 files changed

+332
-2
lines changed

3 files changed

+332
-2
lines changed

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->isFinalByKeyword()) {
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: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,163 @@ 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')); // Could be array{}|array{int}
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 ArrayColumnVisibilityImplicitFinalTest
305+
{
306+
307+
public function testNewExpression(): void
308+
{
309+
$objects = [new NonFinalObjectWithVisibility()];
310+
assertType('array{int}', array_column($objects, 'pub'));
311+
assertType('array{}', array_column($objects, 'prot'));
312+
assertType('array{}', array_column($objects, 'priv'));
313+
}
314+
315+
}
316+
317+
class ArrayColumnVisibilityFromInsideTest
318+
{
319+
320+
public int $pub = 1;
321+
private int $priv = 2;
322+
323+
/** @param list<self> $objects */
324+
public function testFromInside(array $objects): void
325+
{
326+
assertType('list<int>', array_column($objects, 'pub'));
327+
assertType('list<int>', array_column($objects, 'priv'));
328+
}
329+
330+
}
331+
332+
class ArrayColumnVisibilityFromChildTest extends NonFinalObjectWithVisibility
333+
{
334+
335+
/** @param list<NonFinalObjectWithVisibility> $objects */
336+
public function testFromChild(array $objects): void
337+
{
338+
assertType('list<int>', array_column($objects, 'pub'));
339+
assertType('list<int>', array_column($objects, 'prot'));
340+
assertType('list', array_column($objects, 'priv'));
341+
}
342+
343+
}
344+
345+
final class ObjectWithIssetOnly
346+
{
347+
private int $priv = 2;
348+
349+
public function __isset(string $name): bool
350+
{
351+
return true;
352+
}
353+
}
354+
355+
class ArrayColumnVisibilityWithIssetOnlyTest
356+
{
357+
358+
/** @param array<int, ObjectWithIssetOnly> $objects */
359+
public function testWithIssetOnly(array $objects): void
360+
{
361+
assertType('array{}', array_column($objects, 'priv'));
362+
}
363+
364+
}
365+
366+
class ObjectWithIsset
367+
{
368+
public int $pub = 1;
369+
private int $priv = 2;
370+
371+
public function __isset(string $name): bool
372+
{
373+
return true;
374+
}
375+
376+
public function __get(string $name): mixed
377+
{
378+
return $this->$name;
379+
}
380+
}
381+
382+
class ArrayColumnVisibilityWithIssetTest
383+
{
384+
385+
/** @param array<int, ObjectWithIsset> $objects */
386+
public function testWithIsset(array $objects): void
387+
{
388+
assertType('list<int>', array_column($objects, 'pub'));
389+
assertType('list', array_column($objects, 'priv'));
390+
}
391+
392+
/** @param array{ObjectWithIsset} $objects */
393+
public function testWithIssetConstant(array $objects): void
394+
{
395+
assertType('array{int}', array_column($objects, 'pub'));
396+
assertType('list', array_column($objects, 'priv'));
397+
}
398+
399+
}

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)