Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
14 changes: 13 additions & 1 deletion src/Analyser/ExpressionResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
71 changes: 71 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15479.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php // lint >= 8.0

namespace Bug15479;

use function PHPStan\Testing\assertType;

/** @param array<string, mixed> $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<string, mixed> $a */
function g(array $a): void {
$ok = ($a['k'] === null || is_int($a['k']));
if ($ok) { assertType('int|null', $a['k']); }
}

/** @param array<string, mixed> $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<string, mixed> $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<string, mixed> $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<string, array<string, mixed>> $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<string, mixed> $a */
function typeChecks(array $a): void {
$ok = (is_string($a['k']) || is_int($a['k']));
if ($ok) { assertType('int|string', $a['k']); }
}

/** @param array<string, mixed> $a */
function constantOrType(array $a): void {
$ok = ($a['k'] === 1 || is_string($a['k']));
if ($ok) { assertType('1|string', $a['k']); }
}

/** @param array<string, mixed> $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']);
}
}
10 changes: 9 additions & 1 deletion turbo-ext/src/AssignHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
15 changes: 15 additions & 0 deletions turbo-ext/src/ExpressionResult.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading