Skip to content

Commit be9eeb4

Browse files
VincentLangletphpstan-bot
authored andcommitted
Narrow array dim fetch types after isset+type guard in BooleanAnd falsey context
After `if (isset($array['key']) && !is_string($array['key'])) { throw; }`, PHPStan now correctly narrows `$array['key'] ?? ''` to `string` instead of `mixed|string`. The fix creates ConditionalExpressionHolders in the BooleanAnd falsey handler that fire when the isset condition is later confirmed (e.g., during coalesce evaluation). Closes phpstan/phpstan#6202
1 parent c6fb24e commit be9eeb4

2 files changed

Lines changed: 172 additions & 0 deletions

File tree

src/Analyser/TypeSpecifier.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,8 @@ public function specifyTypesInCondition(
750750
$this->processBooleanNotSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders, $scope),
751751
$this->processBooleanSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders, $rightScope),
752752
$this->processBooleanSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders, $scope),
753+
$this->processBooleanTruthyConditionalTypes($scope, $expr->left, $leftTypesForHolders, $rightTypesForHolders, $rightScope),
754+
$this->processBooleanTruthyConditionalTypes($scope, $expr->right, $rightTypesForHolders, $leftTypesForHolders, $scope),
753755
))->setRootExpr($expr);
754756
}
755757

@@ -1115,6 +1117,7 @@ public function specifyTypesInCondition(
11151117
}
11161118
}
11171119
}
1120+
11181121
}
11191122

11201123
return new SpecifiedTypes();
@@ -2211,6 +2214,81 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes
22112214
return [];
22122215
}
22132216

2217+
/**
2218+
* @return array<string, ConditionalExpressionHolder[]>
2219+
*/
2220+
private function processBooleanTruthyConditionalTypes(Scope $scope, Expr $leftExpr, SpecifiedTypes $leftFalseyTypes, SpecifiedTypes $rightFalseyTypes, Scope $rightScope): array
2221+
{
2222+
if ($leftFalseyTypes->getSureTypes() !== [] || $leftFalseyTypes->getSureNotTypes() !== []) {
2223+
return [];
2224+
}
2225+
2226+
$leftTruthyTypes = $this->specifyTypesInCondition($scope, $leftExpr, TypeSpecifierContext::createTrue());
2227+
2228+
$conditionExpressionTypes = [];
2229+
foreach ($leftTruthyTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
2230+
if (!$this->isTrackableExpression($expr)) {
2231+
continue;
2232+
}
2233+
2234+
$scopeType = $scope->getType($expr);
2235+
$conditionType = TypeCombinator::remove($scopeType, $type);
2236+
if ($scopeType->equals($conditionType)) {
2237+
continue;
2238+
}
2239+
2240+
$conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes(
2241+
$expr,
2242+
$conditionType,
2243+
);
2244+
}
2245+
2246+
foreach ($leftTruthyTypes->getSureTypes() as $exprString => [$expr, $type]) {
2247+
if (!$this->isTrackableExpression($expr)) {
2248+
continue;
2249+
}
2250+
2251+
if (isset($conditionExpressionTypes[$exprString])) {
2252+
continue;
2253+
}
2254+
2255+
$scopeType = $scope->getType($expr);
2256+
$conditionType = TypeCombinator::intersect($scopeType, $type);
2257+
if ($scopeType->equals($conditionType)) {
2258+
continue;
2259+
}
2260+
2261+
$conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes(
2262+
$expr,
2263+
$conditionType,
2264+
);
2265+
}
2266+
2267+
if ($conditionExpressionTypes === []) {
2268+
return [];
2269+
}
2270+
2271+
$holders = [];
2272+
foreach ($rightFalseyTypes->getSureTypes() as $exprString => [$expr, $type]) {
2273+
if (!$this->isTrackableExpression($expr)) {
2274+
continue;
2275+
}
2276+
2277+
if (!isset($holders[$exprString])) {
2278+
$holders[$exprString] = [];
2279+
}
2280+
2281+
$targetScope = $expr instanceof Expr\Variable ? $scope : $rightScope;
2282+
$holder = new ConditionalExpressionHolder(
2283+
$conditionExpressionTypes,
2284+
ExpressionTypeHolder::createYes($expr, TypeCombinator::intersect($targetScope->getType($expr), $type)),
2285+
);
2286+
$holders[$exprString][$holder->getKey()] = $holder;
2287+
}
2288+
2289+
return $holders;
2290+
}
2291+
22142292
private function isTrackableExpression(Expr $expr): bool
22152293
{
22162294
if ($expr instanceof Expr\Variable) {
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6202;
4+
5+
use Exception;
6+
use function PHPStan\Testing\assertType;
7+
8+
class HelloWorld
9+
{
10+
/**
11+
* @param array<string,mixed> $array
12+
*/
13+
public function sayHello(array $array): void
14+
{
15+
if (isset($array['mightExist']) && !is_string($array['mightExist'])) {
16+
throw new Exception('Has to be string if set');
17+
}
18+
19+
assertType('string', $array['mightExist'] ?? '');
20+
21+
if (!isset($array['hasToExist']) || !is_string($array['hasToExist'])) {
22+
throw new Exception('Has to exist as string');
23+
}
24+
assertType('string', $array['hasToExist']);
25+
}
26+
27+
/**
28+
* @param array<string, mixed> $array
29+
*/
30+
public function otherIsTypeFunctions(array $array): void
31+
{
32+
if (isset($array['intKey']) && !is_int($array['intKey'])) {
33+
throw new Exception();
34+
}
35+
assertType('int', $array['intKey'] ?? 0);
36+
37+
if (isset($array['arrayKey']) && !is_array($array['arrayKey'])) {
38+
throw new Exception();
39+
}
40+
assertType('array<mixed>', $array['arrayKey'] ?? []);
41+
42+
if (isset($array['boolKey']) && !is_bool($array['boolKey'])) {
43+
throw new Exception();
44+
}
45+
assertType('bool', $array['boolKey'] ?? false);
46+
}
47+
48+
/**
49+
* @param array<string, mixed> $array
50+
*/
51+
public function orPattern(array $array): void
52+
{
53+
if (!isset($array['key']) || !is_string($array['key'])) {
54+
throw new Exception();
55+
}
56+
assertType('string', $array['key']);
57+
}
58+
59+
/**
60+
* @param array<string, mixed> $array
61+
*/
62+
public function instanceofCheck(array $array): void
63+
{
64+
if (isset($array['obj']) && !$array['obj'] instanceof \stdClass) {
65+
throw new Exception();
66+
}
67+
assertType('stdClass', $array['obj'] ?? new \stdClass());
68+
}
69+
70+
/**
71+
* @param array<string, mixed> $array
72+
*/
73+
public function nestedArrayDimFetch(array $array): void
74+
{
75+
if (isset($array['nested']['key']) && !is_string($array['nested']['key'])) {
76+
throw new Exception();
77+
}
78+
assertType('string', $array['nested']['key'] ?? '');
79+
}
80+
81+
/**
82+
* @param array<string, mixed> $array
83+
*/
84+
public function directAccessAfterGuard(array $array): void
85+
{
86+
if (isset($array['mightExist']) && !is_string($array['mightExist'])) {
87+
throw new Exception();
88+
}
89+
90+
if (isset($array['mightExist'])) {
91+
assertType('string', $array['mightExist']);
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)