Skip to content

Commit 772df61

Browse files
staabmphpstan-bot
authored andcommitted
Fix type narrowed too much after identical comparison with never-typed generic method
When a generic class is constructed with an empty array default parameter, the template type resolves to `never`. Method return types using this template (e.g. `TElement|null`) then collapse to `null`, causing `===` comparisons against non-null values to be seen as always-false, which makes subsequent code unreachable and narrows variable types to `*NEVER*`. The fix replaces `never` with the template's declared bound when resolving class-level template types in method return types, but only when the return type does not contain conditional types (which need the actual `never` value to evaluate correctly, e.g. `T is never ? false : bool`). Fixes phpstan/phpstan#14281
1 parent e905481 commit 772df61

File tree

3 files changed

+113
-2
lines changed

3 files changed

+113
-2
lines changed

src/Reflection/ResolvedFunctionVariantWithOriginal.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Reflection;
44

55
use PHPStan\Reflection\Php\ExtendedDummyParameter;
6+
use PHPStan\Type\ConditionalType;
67
use PHPStan\Type\ConditionalTypeForParameter;
78
use PHPStan\Type\ErrorType;
89
use PHPStan\Type\Generic\GenericObjectType;
@@ -12,6 +13,7 @@
1213
use PHPStan\Type\Generic\TemplateTypeMap;
1314
use PHPStan\Type\Generic\TemplateTypeVariance;
1415
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
16+
use PHPStan\Type\NeverType;
1517
use PHPStan\Type\NonAcceptingNeverType;
1618
use PHPStan\Type\Type;
1719
use PHPStan\Type\TypeTraverser;
@@ -245,7 +247,16 @@ private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance
245247
return $traverse($type);
246248
};
247249

248-
return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($references, $objectCb): Type {
250+
$containsConditionalType = false;
251+
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$containsConditionalType): Type {
252+
if ($type instanceof ConditionalType) {
253+
$containsConditionalType = true;
254+
}
255+
256+
return $containsConditionalType ? $type : $traverse($type);
257+
});
258+
259+
return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($references, $objectCb, $containsConditionalType): Type {
249260
if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) {
250261
return TypeTraverser::map($type, $objectCb);
251262
}
@@ -256,6 +267,10 @@ private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance
256267
return $traverse($type);
257268
}
258269

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

tests/PHPStan/Analyser/nsrt/generics.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,7 @@ public function returnStatic(): self
931931
function () {
932932
$stdEmpty = new StdClassCollection([]);
933933
assertType('PHPStan\Generics\FunctionsAssertType\StdClassCollection<*NEVER*, *NEVER*>', $stdEmpty);
934-
assertType('array{}', $stdEmpty->getAll());
934+
assertType('array<stdClass>', $stdEmpty->getAll());
935935

936936
$std = new StdClassCollection([new \stdClass()]);
937937
assertType('PHPStan\Generics\FunctionsAssertType\StdClassCollection<int, stdClass>', $std);

0 commit comments

Comments
 (0)