|
5 | 5 | use PhpParser\Node\Expr; |
6 | 6 | use PhpParser\Node\Expr\BinaryOp\BooleanAnd; |
7 | 7 | use PhpParser\Node\Expr\BinaryOp\BooleanOr; |
| 8 | +use PhpParser\Node\Expr\BinaryOp\Identical; |
| 9 | +use PhpParser\Node\Expr\BinaryOp\NotIdentical; |
| 10 | +use PhpParser\Node\Expr\ConstFetch; |
8 | 11 | use PhpParser\Node\Expr\Ternary; |
| 12 | +use PhpParser\Node\Name; |
9 | 13 | use PhpParser\Node\Stmt; |
10 | 14 | use PHPStan\Analyser\ExpressionContext; |
11 | 15 | use PHPStan\Analyser\ExpressionResult; |
@@ -85,19 +89,54 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e |
85 | 89 | return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); |
86 | 90 | } |
87 | 91 |
|
88 | | - if ($expr->if !== null) { |
89 | | - $conditionExpr = new BooleanOr( |
90 | | - new BooleanAnd($expr->cond, $expr->if), |
91 | | - new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else), |
92 | | - ); |
93 | | - } else { |
94 | | - $conditionExpr = new BooleanOr( |
95 | | - $expr->cond, |
96 | | - new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else), |
97 | | - ); |
| 92 | + if ($context === TypeSpecifierContext::createTruthy() || $context === TypeSpecifierContext::createFalsey()) { |
| 93 | + if ($expr->if !== null) { |
| 94 | + $conditionExpr = new BooleanOr( |
| 95 | + new BooleanAnd($expr->cond, $expr->if), |
| 96 | + new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else), |
| 97 | + ); |
| 98 | + } else { |
| 99 | + $conditionExpr = new BooleanOr( |
| 100 | + $expr->cond, |
| 101 | + new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else), |
| 102 | + ); |
| 103 | + } |
| 104 | + |
| 105 | + return $typeSpecifier->specifyTypesInCondition($scope, $conditionExpr, $context)->setRootExpr($expr); |
| 106 | + } |
| 107 | + |
| 108 | + // An exact context (`=== true`, `!== false`, ...) is about the value of the taken arm, not |
| 109 | + // its truthiness - the boolean rewrite above would read `0 !== false` as `0` being truthy. |
| 110 | + $ifComparison = $this->createArmComparison($expr->if ?? $expr->cond, $context); |
| 111 | + $elseComparison = $this->createArmComparison($expr->else, $context); |
| 112 | + if ($ifComparison === null || $elseComparison === null) { |
| 113 | + return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); |
| 114 | + } |
| 115 | + |
| 116 | + $conditionExpr = new BooleanOr( |
| 117 | + new BooleanAnd($expr->cond, $ifComparison), |
| 118 | + new BooleanAnd(new Expr\BooleanNot($expr->cond), $elseComparison), |
| 119 | + ); |
| 120 | + |
| 121 | + return $typeSpecifier->specifyTypesInCondition($scope, $conditionExpr, TypeSpecifierContext::createTruthy())->setRootExpr($expr); |
| 122 | + } |
| 123 | + |
| 124 | + private function createArmComparison(Expr $arm, TypeSpecifierContext $context): ?Expr |
| 125 | + { |
| 126 | + if ($context === TypeSpecifierContext::createTrue()) { |
| 127 | + return new Identical($arm, new ConstFetch(new Name('true'))); |
| 128 | + } |
| 129 | + if ($context === TypeSpecifierContext::createFalse()) { |
| 130 | + return new Identical($arm, new ConstFetch(new Name('false'))); |
| 131 | + } |
| 132 | + if ($context === TypeSpecifierContext::createTrue()->negate()) { |
| 133 | + return new NotIdentical($arm, new ConstFetch(new Name('true'))); |
| 134 | + } |
| 135 | + if ($context === TypeSpecifierContext::createFalse()->negate()) { |
| 136 | + return new NotIdentical($arm, new ConstFetch(new Name('false'))); |
98 | 137 | } |
99 | 138 |
|
100 | | - return $typeSpecifier->specifyTypesInCondition($scope, $conditionExpr, $context)->setRootExpr($expr); |
| 139 | + return null; |
101 | 140 | } |
102 | 141 |
|
103 | 142 | public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult |
|
0 commit comments