|
8 | 8 | use PhpParser\Node\Expr; |
9 | 9 | use PhpParser\Node\Stmt; |
10 | 10 | use PhpParser\Node\Stmt\TryCatch; |
| 11 | +use PhpParser\Node\Expr\Variable; |
| 12 | +use PHPStan\Analyser\ConditionalExpressionHolder; |
11 | 13 | use PHPStan\Analyser\ExpressionResultStorage; |
| 14 | +use PHPStan\Analyser\ExpressionTypeHolder; |
12 | 15 | use PHPStan\Analyser\InternalStatementExitPoint; |
13 | 16 | use PHPStan\Analyser\InternalStatementResult; |
14 | 17 | use PHPStan\Analyser\InternalThrowPoint; |
|
26 | 29 | use PHPStan\Node\Variable\VariableWrite; |
27 | 30 | use PHPStan\Node\VariableAssignNode; |
28 | 31 | use PHPStan\ShouldNotHappenException; |
| 32 | +use PHPStan\TrinaryLogic; |
| 33 | +use PHPStan\Type\ErrorType; |
29 | 34 | use PHPStan\Type\NeverType; |
| 35 | +use PHPStan\Type\NullType; |
30 | 36 | use PHPStan\Type\ObjectType; |
31 | 37 | use PHPStan\Type\TypeCombinator; |
32 | 38 | use Throwable; |
@@ -239,7 +245,13 @@ public function processStmt( |
239 | 245 | $catchScopeForFinally = $catchScopeResult->getScope(); |
240 | 246 | $catchFlows[] = [$originalCatchType, VariableFlow::sequence($catchNode->var !== null ? VariableFlowBuilder::targetWrite($catchNode->var, VariableWrite::KIND_CATCH, $catchScopeForFinally, $storage) : null, $catchScopeResult->getVariableFlow())]; |
241 | 247 |
|
242 | | - $finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope); |
| 248 | + if (!$catchScopeResult->isAlwaysTerminating()) { |
| 249 | + $mergedScope = $catchScopeResult->getScope()->mergeWith($finalScope); |
| 250 | + if ($variableName !== null && $finalScope !== null) { |
| 251 | + $mergedScope = $this->addCatchVariableDefinednessConditionals($mergedScope, $finalScope, $catchScopeResult->getScope(), $variableName); |
| 252 | + } |
| 253 | + $finalScope = $mergedScope; |
| 254 | + } |
243 | 255 | $alwaysTerminating = $alwaysTerminating && $catchScopeResult->isAlwaysTerminating(); |
244 | 256 | $hasYield = $hasYield || $catchScopeResult->hasYield(); |
245 | 257 | $catchThrowPoints = $catchScopeResult->getThrowPoints(); |
@@ -323,4 +335,70 @@ public function processStmt( |
323 | 335 | return new InternalStatementResult($finalScope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPoints, throwPoints: array_merge($throwPoints, $throwPointsForLater), impurePoints: $impurePoints, variableFlow: VariableFlow::tryCatch($branchScopeResult->getVariableFlow(), $catchFlows, $finallyFlow)); |
324 | 336 | } |
325 | 337 |
|
| 338 | + /** |
| 339 | + * The catch variable's definedness after the try/catch tells the two joined |
| 340 | + * paths apart: when it is untracked on the non-catch path and certainly |
| 341 | + * defined at the end of the catch body, "the catch variable is undefined" |
| 342 | + * later implies the non-catch path ran. Record that as conditional |
| 343 | + * expression holders with a certainty-No condition on the catch variable, |
| 344 | + * restoring the non-catch certainty (and type) of variables the join |
| 345 | + * demoted to maybe-defined - e.g. `isset($e) || $var instanceof \DateTime` |
| 346 | + * evaluates `$var` only where `$e` is narrowed away. |
| 347 | + */ |
| 348 | + private function addCatchVariableDefinednessConditionals( |
| 349 | + MutatingScope $mergedScope, |
| 350 | + MutatingScope $nonCatchScope, |
| 351 | + MutatingScope $catchEndScope, |
| 352 | + string $variableName, |
| 353 | + ): MutatingScope |
| 354 | + { |
| 355 | + $variableExprString = '$' . $variableName; |
| 356 | + if (isset($nonCatchScope->expressionTypes[$variableExprString])) { |
| 357 | + return $mergedScope; |
| 358 | + } |
| 359 | + |
| 360 | + $catchVariableHolder = $catchEndScope->expressionTypes[$variableExprString] ?? null; |
| 361 | + if ($catchVariableHolder === null || !$catchVariableHolder->getCertainty()->yes()) { |
| 362 | + return $mergedScope; |
| 363 | + } |
| 364 | + |
| 365 | + $conditions = [ |
| 366 | + [ |
| 367 | + $variableExprString => new ExpressionTypeHolder(new Variable($variableName), new ErrorType(), TrinaryLogic::createNo()), |
| 368 | + ], |
| 369 | + ]; |
| 370 | + if ($catchVariableHolder->getType()->isNull()->no()) { |
| 371 | + // In a scope where any variable can exist (e.g. the top level), the |
| 372 | + // isset() machinery models "!isset($e)" on a maybe-defined variable as |
| 373 | + // "maybe defined, null when defined" instead of unsetting it. That |
| 374 | + // state excludes the catch path just the same - it guarantees a |
| 375 | + // defined, non-null catch variable. |
| 376 | + $conditions[] = [ |
| 377 | + $variableExprString => ExpressionTypeHolder::createMaybe(new Variable($variableName), new NullType()), |
| 378 | + ]; |
| 379 | + } |
| 380 | + foreach ($nonCatchScope->expressionTypes as $exprString => $holder) { |
| 381 | + if (!$holder->getCertainty()->yes()) { |
| 382 | + continue; |
| 383 | + } |
| 384 | + $expr = $holder->getExpr(); |
| 385 | + if (!$expr instanceof Variable || !is_string($expr->name)) { |
| 386 | + continue; |
| 387 | + } |
| 388 | + $mergedHolder = $mergedScope->expressionTypes[$exprString] ?? null; |
| 389 | + if ($mergedHolder === null || !$mergedHolder->getCertainty()->maybe()) { |
| 390 | + continue; |
| 391 | + } |
| 392 | + |
| 393 | + $conditionalHolders = []; |
| 394 | + foreach ($conditions as $condition) { |
| 395 | + $conditionalHolder = new ConditionalExpressionHolder($condition, $holder); |
| 396 | + $conditionalHolders[$conditionalHolder->getKey()] = $conditionalHolder; |
| 397 | + } |
| 398 | + $mergedScope = $mergedScope->addConditionalExpressions((string) $exprString, $conditionalHolders); |
| 399 | + } |
| 400 | + |
| 401 | + return $mergedScope; |
| 402 | + } |
| 403 | + |
326 | 404 | } |
0 commit comments