diff --git a/src/Analyser/ExprHandler/IssetHandler.php b/src/Analyser/ExprHandler/IssetHandler.php index 7f0d6eb5e3..82c1ae5af7 100644 --- a/src/Analyser/ExprHandler/IssetHandler.php +++ b/src/Analyser/ExprHandler/IssetHandler.php @@ -196,6 +196,10 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e if ( $issetExpr instanceof ArrayDimFetch && $issetExpr->dim !== null + // When the var is itself an offset access (a nested isset like + // $r['K']['Port']), narrowing it in the falsey branch leaks the + // intermediate offset's existence into the enclosing scope. + && !($issetExpr->var instanceof ArrayDimFetch) ) { $varType = $scope->getType($issetExpr->var); if (!$varType instanceof MixedType) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-15005.php b/tests/PHPStan/Analyser/nsrt/bug-15005.php new file mode 100644 index 0000000000..9589d9503d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15005.php @@ -0,0 +1,73 @@ + $r */ +function nestedIssetLeak(array $r): void +{ + assertType('array{Port: int, Secure: string|null}|null', $r['K'] ?? null); + + $port = isset($r['K']['Port']) ? $r['K']['Port'] : null; + + assertType('array{Port: int, Secure: string|null}|null', $r['K'] ?? null); + + $secure = $r['K']['Secure'] ?? null; + + echo $port, $secure; +} + +/** @param array $r */ +function alsoAfterPlainIf(array $r): void +{ + if (isset($r['K']['Port'])) { + echo $r['K']['Port']; + } + + assertType('array{Port: int, Secure: string|null}|null', $r['K'] ?? null); +} + +/** @param array $r */ +function notWithCoalesce(array $r): void +{ + $port = $r['K']['Port'] ?? null; + assertType('array{Port: int, Secure: string|null}|null', $r['K'] ?? null); + echo $port; +} + +/** @param array $r */ +function notWithSingleLevel(array $r): void +{ + $port = isset($r['K']) ? $r['K'] : null; + assertType('string|null', $r['K'] ?? null); + echo $port; +} + +/** @param array> $r */ +function threeLevels(array $r): void +{ + if (isset($r['A']['B']['Port'])) { + echo $r['A']['B']['Port']; + } + + assertType('array{Port: int}|null', $r['A']['B'] ?? null); + assertType('array|null', $r['A'] ?? null); +} + +class Holder +{ + + /** @var array */ + public array $arr = []; + + public function doFoo(): void + { + if (isset($this->arr['K']['Port'])) { + echo $this->arr['K']['Port']; + } + + assertType('array{Port: int, Secure: string|null}|null', $this->arr['K'] ?? null); + } + +}