diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 62ee67ed27b..fa52e2b2a49 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -2170,8 +2170,10 @@ private function currentTypeForConditionalHolder(NodeScopeResolver $nodeScopeRes // them from the walk's own storage (the scope's storage stack misses it // on loop-convergence passes); synthetic terms narrowing extensions built // (@phpstan-assert property fetches etc.) answer from scope state or a walk + // - unless the walk read them on a narrowed branch (the right operand of + // `$a['k'] === null || is_int($a['k'])`), then they are re-priced here $storedResult = $storage->findExpressionResult($expr); - if ($storedResult !== null) { + if ($storedResult !== null && $storedResult->answersOnScope($scope, $scope->nativeTypesPromoted)) { return $storedResult->getTypeOnScope($scope, $scope->nativeTypesPromoted); } diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 9f69558d9b1..4b6c7d89d52 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -699,7 +699,19 @@ public function answersOnScope(MutatingScope $scope, bool $useNativeTypes): bool $useNativeTypes = $useNativeTypes || $scope->nativeTypesPromoted; $readScope = $useNativeTypes ? $scope->doNotTreatPhpDocTypesAsCertain() : $scope; - return $this->isScopeAuthoritative($readScope) || $this->askScopeVariableStateMatches($scope, $useNativeTypes); + if ($this->isScopeAuthoritative($readScope)) { + return true; + } + + // the walk position narrowed this very expression (e.g. the right + // operand of `$a['k'] === null || is_int($a['k'])`) - a position not + // carrying that narrowing must not see it through the memoized type + $positionScope = $useNativeTypes ? $this->beforeScope->doNotTreatPhpDocTypesAsCertain() : $this->beforeScope; + if ($this->isScopeAuthoritative($positionScope)) { + return false; + } + + return $this->askScopeVariableStateMatches($scope, $useNativeTypes); } /** diff --git a/tests/PHPStan/Analyser/nsrt/bug-15479.php b/tests/PHPStan/Analyser/nsrt/bug-15479.php new file mode 100644 index 00000000000..ea2285df247 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15479.php @@ -0,0 +1,71 @@ += 8.0 + +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); } +} + +/** @param array $a */ +function negated(array $a): void { + $ok = ($a['k'] !== null && !is_int($a['k'])); + if (!$ok) { + assertType('int|null', $a['k']); + } else { + assertType('mixed~(int|null)', $a['k']); + } +} + +/** @param array $a */ +function ternary(array $a): void { + $ok = $a['k'] === null || is_int($a['k']) ? 1 : 0; + if ($ok === 1) { assertType('int|null', $a['k']); } +} + +/** @param array> $a */ +function nested(array $a): void { + $ok = ($a['k']['j'] === null || is_int($a['k']['j'])); + if ($ok) { assertType('int|null', $a['k']['j']); } +} + +/** @param array $a */ +function typeChecks(array $a): void { + $ok = (is_string($a['k']) || is_int($a['k'])); + if ($ok) { assertType('int|string', $a['k']); } +} + +/** @param array $a */ +function constantOrType(array $a): void { + $ok = ($a['k'] === 1 || is_string($a['k'])); + if ($ok) { assertType('1|string', $a['k']); } +} + +/** @param array $a */ +function elseBranch(array $a): void { + $ok = ($a['k'] === null || is_string($a['k'])); + if ($ok) { + assertType('string|null', $a['k']); + } else { + assertType('mixed~(string|null)', $a['k']); + } +} diff --git a/turbo-ext/src/AssignHandler.cpp b/turbo-ext/src/AssignHandler.cpp index e249818c5d4..f5dd909b880 100644 --- a/turbo-ext/src/AssignHandler.cpp +++ b/turbo-ext/src/AssignHandler.cpp @@ -2489,8 +2489,16 @@ class AssignHandler // them from the walk's own storage (the scope's storage stack misses it // on loop-convergence passes); synthetic terms narrowing extensions built // (@phpstan-assert property fetches etc.) answer from scope state or a walk + // - unless the walk read them on a narrowed branch (the right operand of + // `$a['k'] === null || is_int($a['k'])`), then they are re-priced here AH_VAL(storedResult, pt_expression_result_storage_find(storage, expr)); - if (!storedResult.isNull()) return ahTypeOnScope(storedResult.raw(), scope); + if (!storedResult.isNull()) { + bool promoted; + AH_OK(ahPromoted(scope, promoted)); + bool answers; + AH_OK(pt_expression_result_answers_on_scope(storedResult.raw(), scope, promoted, answers)); + if (answers) return ahTypeOnScope(storedResult.raw(), scope); + } return nsrReadScopeStateOrSyntheticType(nsr, expr, scope); } diff --git a/turbo-ext/src/ExpressionResult.cpp b/turbo-ext/src/ExpressionResult.cpp index c72c7984242..7d2a5b9a9cd 100644 --- a/turbo-ext/src/ExpressionResult.cpp +++ b/turbo-ext/src/ExpressionResult.cpp @@ -600,6 +600,21 @@ class ExpressionResult zend_long authoritative = isScopeAuthoritative(readScope); if (authoritative != 0) return authoritative; + + // the walk position narrowed this very expression (e.g. the right + // operand of `$a['k'] === null || is_int($a['k'])`) - a position not + // carrying that narrowing must not see it through the memoized type + zv::Val positionScopeHolder; + zval *positionScope = slot(slots::beforeScope); + if (useNativeTypes) { + positionScopeHolder = scopeNativeView(positionScope); + if (UNEXPECTED(positionScopeHolder.isUndef())) return -1; + positionScope = positionScopeHolder.raw(); + } + zend_long positionAuthoritative = isScopeAuthoritative(positionScope); + if (positionAuthoritative < 0) return -1; + if (positionAuthoritative == 1) return 0; + return askScopeVariableStateMatches(scope, useNativeTypes, false); }