Skip to content

Commit f827d32

Browse files
committed
Fix phpstan/phpstan#14281: type narrowed too much after identical comparison
- Root cause: commit a063119 introduced parent constructor template inference that resolved TElement to *NEVER* from empty default array parameters - When new TestCollection() was called without arguments, TElement was inferred as *NEVER* (from default []), causing get() to return null instead of mixed - This made assert($data[1] === $collection->get(1)) resolve as assert(0 === null), narrowing $data to *NEVER* (dead code) - Fix: skip NeverType when mapping parent constructor template types to child class - New regression test in tests/PHPStan/Analyser/nsrt/bug-14281.php
1 parent a5be566 commit f827d32

2 files changed

Lines changed: 103 additions & 0 deletions

File tree

src/Analyser/ExprHandler/NewHandler.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,10 @@ classReflection: $classReflection->withTypes($types)->asFinal(),
547547
continue;
548548
}
549549

550+
if ($type instanceof NeverType) {
551+
continue;
552+
}
553+
550554
if (!array_key_exists($ancestorType->getName(), $resolvedTypeMap)) {
551555
$resolvedTypeMap[$ancestorType->getName()] = $type;
552556
continue;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug14281;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class CollectionTest
8+
{
9+
public function testFromAssociative(): void
10+
{
11+
$data = [
12+
null,
13+
0,
14+
'some-string',
15+
new \stdClass(),
16+
['some' => 'value'],
17+
];
18+
19+
$collection = (new TestCollection())->assignRecursive($data);
20+
21+
assert(count($collection)===5);
22+
assertType("array{null, 0, 'some-string', stdClass, array{some: 'value'}}", $data);
23+
24+
assert($data[0] === $collection->get(0));
25+
assertType("array{null, 0, 'some-string', stdClass, array{some: 'value'}}", $data);
26+
assert($data[1] === $collection->get(1));
27+
assertType("array{null, 0, 'some-string', stdClass, array{some: 'value'}}", $data);
28+
assert($data[2] === $collection->get(2));
29+
assertType("array{null, 0, 'some-string', stdClass, array{some: 'value'}}", $data);
30+
assert($data[3] === $collection->get(3));
31+
assertType("array{null, 0, 'some-string', stdClass, array{some: 'value'}}", $data);
32+
assert($data[4] === $collection->get(4));
33+
assertType("array{null, 0, 'some-string', stdClass, array{some: 'value'}}", $data);
34+
}
35+
}
36+
37+
/**
38+
* @template TElement
39+
*
40+
* @extends Collection<TElement>
41+
*/
42+
class TestCollection extends Collection
43+
{
44+
}
45+
46+
/**
47+
* @template TElement
48+
*
49+
* @implements \IteratorAggregate<array-key, TElement>
50+
*/
51+
abstract class Collection implements \IteratorAggregate, \Countable
52+
{
53+
/**
54+
* @var array<array-key, TElement>
55+
*/
56+
protected array $elements = [];
57+
58+
/**
59+
* @param array<TElement> $elements
60+
*/
61+
public function __construct(array $elements = [])
62+
{
63+
$this->elements = $elements;
64+
}
65+
66+
/**
67+
* @param array-key $key
68+
*
69+
* @return TElement|null
70+
*/
71+
public function get($key)
72+
{
73+
return $this->elements[$key] ?? null;
74+
}
75+
76+
/**
77+
* @phpstan-impure
78+
*/
79+
#[\Override]
80+
public function count(): int
81+
{
82+
return \count($this->elements);
83+
}
84+
85+
/**
86+
* @return \Traversable<TElement>
87+
*/
88+
#[\Override]
89+
public function getIterator(): \Traversable
90+
{
91+
yield from $this->elements;
92+
}
93+
94+
/** @param array<mixed> $options */
95+
public function assignRecursive(array $options): static
96+
{
97+
return $this;
98+
}
99+
}

0 commit comments

Comments
 (0)