Skip to content

Commit c3d80bb

Browse files
ondrejmirtesclaude
andcommitted
Keep template argument facts of catch and finally blocks
A catch block that ends by throwing or returning is left out of the scope after the try-catch, and the finally block reaches it only through processFinallyScope(), which carries variable state. The facts the observation pass collected in them - a new Collection([1]) sent to Collection<int> - were lost with the scopes, the site stayed unresolved and the second pass reported Collection<unresolved(1)>. The constraints of every block are now joined into the scope after the try-catch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpbB99tJfUmeArX74xqvHv
1 parent 1410344 commit c3d80bb

3 files changed

Lines changed: 93 additions & 0 deletions

File tree

‎src/Analyser/StmtHandler/TryCatchHandler.php‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ public function processStmt(
7171
$branchScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context);
7272
$branchScope = $branchScopeResult->getScope();
7373
$finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope;
74+
// what the template argument observation collected in every block,
75+
// including the ones that end by leaving the try-catch
76+
$blockScopes = [$branchScope];
7477

7578
$exitPoints = [];
7679
$finallyExitPoints = [];
@@ -246,6 +249,7 @@ public function processStmt(
246249

247250
$catchScopeResult = $nodeScopeResolver->processStmtNodesInternal($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $storage, $nodeCallback, $context);
248251
$catchScopeForFinally = $catchScopeResult->getScope();
252+
$blockScopes[] = $catchScopeForFinally;
249253
$catchFlows[] = [$originalCatchType, VariableFlow::sequence($catchNode->var !== null ? VariableFlowBuilder::targetWrite($catchNode->var, VariableWrite::KIND_CATCH, $catchScopeForFinally, $storage) : null, $catchScopeResult->getVariableFlow())];
250254

251255
$finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope);
@@ -297,6 +301,7 @@ public function processStmt(
297301
$throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints());
298302
$impurePoints = array_merge($impurePoints, $finallyResult->getImpurePoints());
299303
$finallyScope = $finallyResult->getScope();
304+
$blockScopes[] = $finallyScope;
300305
$finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope);
301306
if (!$finallyResult->isAlwaysTerminating()) {
302307
// the finally block runs after the exit point, so its changes are
@@ -329,6 +334,10 @@ public function processStmt(
329334
$exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints());
330335
}
331336

337+
foreach ($blockScopes as $blockScope) {
338+
$finalScope = $finalScope->addTemplateArgumentConstraints($blockScope->getTemplateArgumentConstraints());
339+
}
340+
332341
return new InternalStatementResult($finalScope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPoints, throwPoints: array_merge($throwPoints, $throwPointsForLater), impurePoints: $impurePoints, variableFlow: VariableFlow::tryCatch($branchScopeResult->getVariableFlow(), $catchFlows, $finallyFlow));
333342
}
334343

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace TemplateArgumentTryCatch;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/** @template T */
8+
class Collection
9+
{
10+
11+
/** @param list<T> $items */
12+
public function __construct(array $items)
13+
{
14+
}
15+
16+
}
17+
18+
/** @param Collection<int> $collection */
19+
function takeInts(Collection $collection): void
20+
{
21+
}
22+
23+
function mayThrow(): void
24+
{
25+
}
26+
27+
function catchThatThrows(): void
28+
{
29+
try {
30+
mayThrow();
31+
} catch (\Exception $e) {
32+
$collection = new Collection([1]);
33+
takeInts($collection);
34+
assertType('TemplateArgumentTryCatch\Collection<int>', $collection);
35+
throw $e;
36+
}
37+
}
38+
39+
function catchThatReturns(): void
40+
{
41+
try {
42+
mayThrow();
43+
} catch (\Exception $e) {
44+
$collection = new Collection([1]);
45+
takeInts($collection);
46+
assertType('TemplateArgumentTryCatch\Collection<int>', $collection);
47+
return;
48+
}
49+
}
50+
51+
function finallyBlock(): void
52+
{
53+
try {
54+
mayThrow();
55+
} finally {
56+
$collection = new Collection([1]);
57+
takeInts($collection);
58+
assertType('TemplateArgumentTryCatch\Collection<int>', $collection);
59+
}
60+
}
61+
62+
function tryThatThrows(): void
63+
{
64+
try {
65+
$collection = new Collection([1]);
66+
takeInts($collection);
67+
assertType('TemplateArgumentTryCatch\Collection<int>', $collection);
68+
throw new \Exception();
69+
} catch (\Exception $e) {
70+
}
71+
}

‎turbo-ext/src/TryCatchHandler.cpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@ class TryCatchHandler
164164
bool alwaysTerminating;
165165
if (UNEXPECTED(!pt_internal_statement_result_is_always_terminating(branch, alwaysTerminating))) return zv::Val();
166166
zv::Val finalScope = alwaysTerminating ? zv::Val::null() : zv::Val::copyOf(branchScope.ref());
167+
// what the template argument observation collected in every block,
168+
// including the ones that end by leaving the try-catch
169+
zv::Arr blockScopes = zv::Arr::empty();
170+
blockScopes.push(zv::Val::copyOf(branchScope.ref()));
167171

168172
zv::Arr exitPoints = zv::Arr::empty();
169173
zv::Arr finallyExitPoints = zv::Arr::empty();
@@ -489,6 +493,7 @@ class TryCatchHandler
489493
if (UNEXPECTED(resultScope == NULL)) return zv::Val();
490494
catchScopeForFinally = zv::Val::copyOf(zv::Ref(resultScope));
491495
}
496+
blockScopes.push(zv::Val::copyOf(catchScopeForFinally.ref()));
492497
{
493498
zv::Val writeFlow = zv::Val::null();
494499
zval *catchVar = ptsh::readNodeProperty(pt_tch_catch_var_site, catchNode, PT_LC("var"));
@@ -626,6 +631,7 @@ class TryCatchHandler
626631
if (UNEXPECTED(resultScope == NULL)) return zv::Val();
627632
finallyScope = zv::Val::copyOf(zv::Ref(resultScope));
628633
}
634+
blockScopes.push(zv::Val::copyOf(finallyScope.ref()));
629635
if (!finallyAlwaysTerminating) {
630636
zv::Val processed = pt_mutating_scope_process_finally_scope(Z_OBJ_P(finalScope.raw()), Z_OBJ_P(finallyScope.raw()), Z_OBJ_P(originalFinallyScope.raw()));
631637
if (UNEXPECTED(processed.isUndef())) return zv::Val();
@@ -695,6 +701,13 @@ class TryCatchHandler
695701
variableFlow = pt_variable_flow_try_catch(bodyFlow, catchFlows.raw(), finallyFlow.raw());
696702
if (UNEXPECTED(variableFlow.isUndef())) return zv::Val();
697703
}
704+
for (auto entry : blockScopes.arrRef()) {
705+
zv::Val constraints = pt_mutating_scope_get_template_argument_constraints(Z_OBJ_P(entry.value().raw()));
706+
if (UNEXPECTED(constraints.isUndef())) return zv::Val();
707+
zv::Val joined = pt_mutating_scope_add_template_argument_constraints(Z_OBJ_P(finalScope.raw()), constraints.raw());
708+
if (UNEXPECTED(joined.isUndef())) return zv::Val();
709+
finalScope = std::move(joined);
710+
}
698711
return pt_internal_statement_result_new(finalScope.raw(), hasYield, alwaysTerminating, exitPoints.raw(), throwPoints.raw(), impurePoints.raw(), NULL, variableFlow.raw());
699712
}
700713

0 commit comments

Comments
 (0)