diff --git a/CLAUDE.md b/CLAUDE.md index c96c8954ee..7e843cd303 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -417,3 +417,7 @@ When methods are called on union types (`Foo|Bar`), the resolved method reflecti - `react/child-process`, `react/async` - Parallel analysis - `symfony/console` - CLI interface - `hoa/compiler` - Used for regex type parsing + +### Ternary expression type narrowing in TypeSpecifier + +`TypeSpecifier::specifyTypesInCondition()` handles ternary expressions (`$cond ? $a : $b`) for type narrowing. In a truthy context (e.g., inside `assert()`), the ternary is semantically equivalent to `($cond && $a) || (!$cond && $b)` — meaning if the condition is true, the "if" branch must be truthy, and if false, the "else" branch must be truthy. The fix converts ternary expressions to this `BooleanOr(BooleanAnd(...), BooleanAnd(...))` form so the existing OR/AND narrowing logic handles both branches correctly. This enables `assert($cond ? $x instanceof A : $x instanceof B)` to narrow `$x` to `A|B`. The `AssertFunctionTypeSpecifyingExtension` calls `specifyTypesInCondition` with `TypeSpecifierContext::createTruthy()` context for the assert argument. diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 64f9841cf0..64e835780b 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1062,12 +1062,12 @@ public function specifyTypesInCondition( } elseif ( $expr instanceof Expr\Ternary && !$context->null() - && $scope->getType($expr->else)->isFalse()->yes() ) { - $conditionExpr = $expr->cond; - if ($expr->if !== null) { - $conditionExpr = new BooleanAnd($conditionExpr, $expr->if); - } + $ifExpr = $expr->if ?? $expr->cond; + $conditionExpr = new BooleanOr( + new BooleanAnd($expr->cond, $ifExpr), + new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else), + ); return $this->specifyTypesInCondition($scope, $conditionExpr, $context)->setRootExpr($expr); diff --git a/tests/PHPStan/Analyser/nsrt/bug-14100.php b/tests/PHPStan/Analyser/nsrt/bug-14100.php new file mode 100644 index 0000000000..013668ddad --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14100.php @@ -0,0 +1,55 @@ +analyse([__DIR__ . '/data/bug-10248.php'], []); } - #[RequiresPhp('>= 8.0')] + #[RequiresPhp('>= 8.2')] public function testBug11815(): void { $this->analyse([__DIR__ . '/data/bug-11815.php'], []);