|
8 | 8 | use PhpParser\Node\Expr; |
9 | 9 | use PhpParser\Node\Expr\ArrayDimFetch; |
10 | 10 | use PhpParser\Node\Expr\Assign; |
| 11 | +use PhpParser\Node\Expr\AssignOp; |
11 | 12 | use PhpParser\Node\Expr\AssignRef; |
| 13 | +use PhpParser\Node\Expr\BinaryOp\BooleanAnd; |
| 14 | +use PhpParser\Node\Expr\BinaryOp\BooleanOr; |
| 15 | +use PhpParser\Node\Expr\BinaryOp\LogicalAnd; |
| 16 | +use PhpParser\Node\Expr\BinaryOp\LogicalOr; |
12 | 17 | use PhpParser\Node\Expr\ConstFetch; |
13 | 18 | use PhpParser\Node\Expr\FuncCall; |
14 | 19 | use PhpParser\Node\Expr\List_; |
|
94 | 99 | use function array_slice; |
95 | 100 | use function count; |
96 | 101 | use function in_array; |
| 102 | +use function is_array; |
97 | 103 | use function is_int; |
98 | 104 | use function is_string; |
99 | 105 | use function spl_object_id; |
@@ -1191,6 +1197,29 @@ public function applyWrite( |
1191 | 1197 | : $this->defaultNarrowingHelper->specifyTypesForNode($scope, $assignedExpr, TypeSpecifierContext::createFalsey()); |
1192 | 1198 | $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $storage, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr, $storedAssignedExprResult); |
1193 | 1199 | $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $storage, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr, $storedAssignedExprResult); |
| 1200 | + |
| 1201 | + // An assignment inside a short-circuit operand may or may not have run |
| 1202 | + // (its variable is only maybe-defined after the RHS walk), but a truthy |
| 1203 | + // && (or falsey ||) guarantees the right operand was evaluated. The |
| 1204 | + // operand walk's truthy/falsey scope knows exactly which variables it |
| 1205 | + // defined - record their certainty as a consequence of the assigned |
| 1206 | + // boolean, e.g. "$bool = $x && ($var = 'foo'); if ($bool) { … $var … }". |
| 1207 | + if ( |
| 1208 | + $storedAssignedExprResult !== null |
| 1209 | + && ( |
| 1210 | + $assignedExpr instanceof BooleanAnd |
| 1211 | + || $assignedExpr instanceof BooleanOr |
| 1212 | + || $assignedExpr instanceof LogicalAnd |
| 1213 | + || $assignedExpr instanceof LogicalOr |
| 1214 | + ) |
| 1215 | + && self::containsVariableAssignment($assignedExpr) |
| 1216 | + ) { |
| 1217 | + if ($assignedExpr instanceof BooleanAnd || $assignedExpr instanceof LogicalAnd) { |
| 1218 | + $conditionalExpressions = $this->processDefinednessForConditionalExpressionsAfterAssign($conditionalExpressions, $var->name, $truthyType, $storedAssignedExprResult->getTruthyScope(), $scope); |
| 1219 | + } else { |
| 1220 | + $conditionalExpressions = $this->processDefinednessForConditionalExpressionsAfterAssign($conditionalExpressions, $var->name, $falseyType, $storedAssignedExprResult->getFalseyScope(), $scope); |
| 1221 | + } |
| 1222 | + } |
1194 | 1223 | } |
1195 | 1224 |
|
1196 | 1225 | foreach ([null, false, 0, 0.0, '', '0', []] as $falseyScalar) { |
@@ -1958,6 +1987,76 @@ private function processSureNotTypesForConditionalExpressionsAfterAssign(NodeSco |
1958 | 1987 | return $conditionalExpressions; |
1959 | 1988 | } |
1960 | 1989 |
|
| 1990 | + /** |
| 1991 | + * Records "if the assigned variable has $variableType, the target variable is |
| 1992 | + * certainly defined (with its branch-scope type)" holders for variables whose |
| 1993 | + * certainty is Yes in the given branch continuation scope of the RHS (the |
| 1994 | + * truthy scope of a `&&`, the falsey scope of a `||` - the scopes where every |
| 1995 | + * short-circuit operand was guaranteed evaluated) but only Maybe in the |
| 1996 | + * merged after-RHS scope. |
| 1997 | + * |
| 1998 | + * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions |
| 1999 | + * @return array<string, ConditionalExpressionHolder[]> |
| 2000 | + */ |
| 2001 | + private function processDefinednessForConditionalExpressionsAfterAssign( |
| 2002 | + array $conditionalExpressions, |
| 2003 | + string $variableName, |
| 2004 | + Type $variableType, |
| 2005 | + MutatingScope $branchScope, |
| 2006 | + MutatingScope $mergedScope, |
| 2007 | + ): array |
| 2008 | + { |
| 2009 | + foreach ($branchScope->expressionTypes as $exprString => $holder) { |
| 2010 | + if (!$holder->getCertainty()->yes()) { |
| 2011 | + continue; |
| 2012 | + } |
| 2013 | + $expr = $holder->getExpr(); |
| 2014 | + if (!$expr instanceof Variable || !is_string($expr->name) || $expr->name === $variableName) { |
| 2015 | + continue; |
| 2016 | + } |
| 2017 | + $mergedHolder = $mergedScope->expressionTypes[$exprString] ?? null; |
| 2018 | + if ($mergedHolder === null || !$mergedHolder->getCertainty()->maybe()) { |
| 2019 | + continue; |
| 2020 | + } |
| 2021 | + |
| 2022 | + $conditionalHolder = new ConditionalExpressionHolder([ |
| 2023 | + '$' . $variableName => ExpressionTypeHolder::createYes(new Variable($variableName), $variableType), |
| 2024 | + ], $holder); |
| 2025 | + $conditionalExpressions[(string) $exprString][$conditionalHolder->getKey()] = $conditionalHolder; |
| 2026 | + } |
| 2027 | + |
| 2028 | + return $conditionalExpressions; |
| 2029 | + } |
| 2030 | + |
| 2031 | + /** |
| 2032 | + * Whether the expression contains an assignment whose execution short-circuit |
| 2033 | + * evaluation may have skipped - a cheap AST gate so the truthy/falsey scope |
| 2034 | + * is only derived for boolean RHS expressions that can define a variable. |
| 2035 | + */ |
| 2036 | + private static function containsVariableAssignment(Node $node): bool |
| 2037 | + { |
| 2038 | + if ($node instanceof Assign || $node instanceof AssignOp || $node instanceof AssignRef) { |
| 2039 | + return true; |
| 2040 | + } |
| 2041 | + |
| 2042 | + foreach ($node->getSubNodeNames() as $subNodeName) { |
| 2043 | + $subNode = $node->$subNodeName; |
| 2044 | + if ($subNode instanceof Node) { |
| 2045 | + if (self::containsVariableAssignment($subNode)) { |
| 2046 | + return true; |
| 2047 | + } |
| 2048 | + } elseif (is_array($subNode)) { |
| 2049 | + foreach ($subNode as $subNodeItem) { |
| 2050 | + if ($subNodeItem instanceof Node && self::containsVariableAssignment($subNodeItem)) { |
| 2051 | + return true; |
| 2052 | + } |
| 2053 | + } |
| 2054 | + } |
| 2055 | + } |
| 2056 | + |
| 2057 | + return false; |
| 2058 | + } |
| 2059 | + |
1961 | 2060 | /** |
1962 | 2061 | * Current type of a conditional-holder expression, used to refine the holder's |
1963 | 2062 | * projected type. Prefers the tracked scope state over the stored result, |
|
0 commit comments