From 9fd38e56e662e503f035369b62dcc33dc1f566a9 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:15:01 +0000 Subject: [PATCH 1/2] Add regression test for `$this` narrowing to `never` inside isset()-guarded boolean decomposition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds tests/PHPStan/Analyser/nsrt/bug-14804.php reproducing the reported case verbatim from the issue's container code: an outer `$enum === X && isset($this->prop[$key])` guard followed by an inner `isset($this->other[$key])` block and a nested `if ($enum === X)`, where `$this` was incorrectly narrowed to `*NEVER*`. - The test asserts `$this` keeps its `$this(Bug14804\Container)` type both before and inside the nested narrowing, and that `$lifecycle` stays the full enum union. - The root fix already lives in ConditionalExpressionHolderHelper: the boolean-decomposition holder that collapsed an expression to `NeverType` (an artifact of the cross-kind holder + isset() truthy/falsey swap) is skipped. Verified the new test fails with `*NEVER*` when that guard is removed and passes with it. - Probed analogous constructs (empty(), array_key_exists(), `??`, BooleanOr early returns, static properties, narrowing non-`$this` locals and match results, and deeper nesting) — all already handled by the shared guard, no residual bug. --- tests/PHPStan/Analyser/nsrt/bug-14804.php | 82 +++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14804.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-14804.php b/tests/PHPStan/Analyser/nsrt/bug-14804.php new file mode 100644 index 0000000000..f43320ad63 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14804.php @@ -0,0 +1,82 @@ + */ + private array $registry = []; + + /** @var array */ + private array $persistentDependencies = []; + + /** @var array $arguments): object)> */ + private array $initializers = []; + + /** @var array */ + private array $resolving = []; + + /** + * @template TClassName of object + * @param class-string $className + * @return TClassName + */ + public function resolve(string $className, array $arguments = []): object + { + $lifecycle = $this->registry[$className] ?? Lifecycle::TRANSIENT; + + if ( + $lifecycle === Lifecycle::PERSISTENT && + isset($this->persistentDependencies[$className]) + ) { + /** @var TClassName */ + return $this->persistentDependencies[$className]; + } + + if (isset($this->resolving[$className])) { + throw new \Exception(); + } + + $this->resolving[$className] = true; + + try { + if (isset($this->initializers[$className])) { + assertType('$this(Bug14804\Container)', $this); + assertType('Bug14804\Lifecycle::PERSISTENT|Bug14804\Lifecycle::TRANSIENT', $lifecycle); + + /** @var TClassName $instance */ + $instance = ($this->initializers[$className])($this, $arguments); + + if ($lifecycle === Lifecycle::PERSISTENT) { + assertType('$this(Bug14804\Container)', $this); + + unset($this->initializers[$className]); + $this->persistentDependencies[$className] = $instance; + } + + return $instance; + } + + throw new \Exception(); + } finally { + unset($this->resolving[$className]); + } + } + +} From 7817bbc68e5efcd6c9a7d04f282a5dae29fe0dfb Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 11 Jun 2026 07:13:25 +0000 Subject: [PATCH 2/2] Assert narrowed $lifecycle inside nested if to mirror both dumped types from the issue Co-Authored-By: Claude Opus 4.8 --- tests/PHPStan/Analyser/nsrt/bug-14804.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-14804.php b/tests/PHPStan/Analyser/nsrt/bug-14804.php index f43320ad63..0ab2307aa6 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14804.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14804.php @@ -64,6 +64,7 @@ public function resolve(string $className, array $arguments = []): object $instance = ($this->initializers[$className])($this, $arguments); if ($lifecycle === Lifecycle::PERSISTENT) { + assertType('Bug14804\Lifecycle::PERSISTENT', $lifecycle); assertType('$this(Bug14804\Container)', $this); unset($this->initializers[$className]);