diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 0a1f5f83e84..d9c45d8064a 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -2156,6 +2156,13 @@ private function currentTypeForConditionalHolder(NodeScopeResolver $nodeScopeRes // (@phpstan-assert property fetches etc.) answer from scope state or a walk $storedResult = $storage->findExpressionResult($expr); if ($storedResult !== null) { + // a subexpression walked on a narrower branch (the right side of + // `$a['k'] === null || is_int($a['k'])` sees `$a['k']` without null) + // does not answer on this scope - re-price it here + if (!$storedResult->answersOnScope($scope, $scope->nativeTypesPromoted)) { + return $nodeScopeResolver->processExprOnDemand($expr, $scope, new ExpressionResultStorage())->getTypeOnScope($scope, $scope->nativeTypesPromoted); + } + return $storedResult->getTypeOnScope($scope, $scope->nativeTypesPromoted); } diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 374d29043e6..1c3cb2e5662 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = 'a0f7086'; + public const EXPECTED_EXTENSION_VERSION = '7bcf6c4'; private static bool $active = false; diff --git a/tests/PHPStan/Analyser/nsrt/bug-15479.php b/tests/PHPStan/Analyser/nsrt/bug-15479.php new file mode 100644 index 00000000000..a9dc81a7f02 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15479.php @@ -0,0 +1,26 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug15479; + +use function PHPStan\Testing\assertType; + +/** @param array $a */ +function f(array $a): void { + $ok = array_key_exists('k', $a) && ($a['k'] === null || is_int($a['k'])); + if ($ok) { assertType('int|null', $a['k']); } +} +/** @param array $a */ +function g(array $a): void { + $ok = ($a['k'] === null || is_int($a['k'])); + if ($ok) { assertType('int|null', $a['k']); } +} +/** @param array $a */ +function h(array $a): void { + if (array_key_exists('k', $a) && ($a['k'] === null || is_int($a['k']))) { assertType('int|null', $a['k']); } +} +function i(mixed $v): void { + $ok = $v === null || is_int($v); + if ($ok) { assertType('int|null', $v); } +} diff --git a/turbo-ext/src/AssignHandler.cpp b/turbo-ext/src/AssignHandler.cpp index f5a81e3b4bc..95ebcf41251 100644 --- a/turbo-ext/src/AssignHandler.cpp +++ b/turbo-ext/src/AssignHandler.cpp @@ -2447,7 +2447,22 @@ class AssignHandler // on loop-convergence passes); synthetic terms narrowing extensions built // (@phpstan-assert property fetches etc.) answer from scope state or a walk AH_VAL(storedResult, pt_expression_result_storage_find(storage, expr)); - if (!storedResult.isNull()) return ahTypeOnScope(storedResult.raw(), scope); + if (!storedResult.isNull()) { + // a subexpression walked on a narrower branch (the right side of + // `$a['k'] === null || is_int($a['k'])` sees `$a['k']` without null) + // does not answer on this scope - re-price it here + bool promoted; + AH_OK(ahPromoted(scope, promoted)); + bool answers; + AH_OK(pt_expression_result_answers_on_scope(storedResult.raw(), scope, promoted, answers)); + if (!answers) { + AH_VAL(onDemandStorage, pt_expression_result_storage_new()); + AH_VAL(onDemandResult, pt_node_scope_resolver_process_expr_on_demand(nsr, expr, scope, onDemandStorage.raw())); + return ahTypeOnScope(onDemandResult.raw(), scope); + } + + return ahTypeOnScope(storedResult.raw(), scope); + } return nsrReadScopeStateOrSyntheticType(nsr, expr, scope); }