From f827d32355b7f7b796e2c14111423c5a3804acb0 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:01:54 +0000 Subject: [PATCH] Fix phpstan/phpstan#14281: type narrowed too much after identical comparison - Root cause: commit a063119ee 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 --- src/Analyser/ExprHandler/NewHandler.php | 4 + tests/PHPStan/Analyser/nsrt/bug-14281.php | 99 +++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14281.php diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 4179b5259d..534b0373c3 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -547,6 +547,10 @@ classReflection: $classReflection->withTypes($types)->asFinal(), continue; } + if ($type instanceof NeverType) { + continue; + } + if (!array_key_exists($ancestorType->getName(), $resolvedTypeMap)) { $resolvedTypeMap[$ancestorType->getName()] = $type; continue; diff --git a/tests/PHPStan/Analyser/nsrt/bug-14281.php b/tests/PHPStan/Analyser/nsrt/bug-14281.php new file mode 100644 index 0000000000..1433cc0008 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14281.php @@ -0,0 +1,99 @@ + 'value'], + ]; + + $collection = (new TestCollection())->assignRecursive($data); + + assert(count($collection)===5); + assertType("array{null, 0, 'some-string', stdClass, array{some: 'value'}}", $data); + + assert($data[0] === $collection->get(0)); + assertType("array{null, 0, 'some-string', stdClass, array{some: 'value'}}", $data); + assert($data[1] === $collection->get(1)); + assertType("array{null, 0, 'some-string', stdClass, array{some: 'value'}}", $data); + assert($data[2] === $collection->get(2)); + assertType("array{null, 0, 'some-string', stdClass, array{some: 'value'}}", $data); + assert($data[3] === $collection->get(3)); + assertType("array{null, 0, 'some-string', stdClass, array{some: 'value'}}", $data); + assert($data[4] === $collection->get(4)); + assertType("array{null, 0, 'some-string', stdClass, array{some: 'value'}}", $data); + } +} + +/** + * @template TElement + * + * @extends Collection + */ +class TestCollection extends Collection +{ +} + +/** + * @template TElement + * + * @implements \IteratorAggregate + */ +abstract class Collection implements \IteratorAggregate, \Countable +{ + /** + * @var array + */ + protected array $elements = []; + + /** + * @param array $elements + */ + public function __construct(array $elements = []) + { + $this->elements = $elements; + } + + /** + * @param array-key $key + * + * @return TElement|null + */ + public function get($key) + { + return $this->elements[$key] ?? null; + } + + /** + * @phpstan-impure + */ + #[\Override] + public function count(): int + { + return \count($this->elements); + } + + /** + * @return \Traversable + */ + #[\Override] + public function getIterator(): \Traversable + { + yield from $this->elements; + } + + /** @param array $options */ + public function assignRecursive(array $options): static + { + return $this; + } +}