Skip to content

Keep template argument facts of catch and finally blocks - #6608

Merged
ondrejmirtes merged 2 commits into
2.3.xfrom
template-argument-facts-in-terminating-branches
Sep 27, 2026
Merged

ondrejmirtes merged 2 commits into
2.3.xfrom
template-argument-facts-in-terminating-branches

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

With unresolvedTemplateArguments (bleeding edge), a template argument site inside a catch block that ends by throwing or returning, or inside a finally block, was never resolved:

try {
	mayThrow();
} catch (\Exception $e) {
	$collection = new Collection([1]);
	takeInts($collection); // Collection<int> expected
	throw $e;
}
// Parameter #1 $collection of function takeInts expects Collection<int>, Collection<unresolved(1)> given.

TryCatchHandler leaves an always-terminating catch block 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 those blocks were dropped with their scopes. The constraints of every block (try, each catch, finally) are now joined into the scope after the try-catch - constraints are a persistent collection deduplicated by node identity, so joining a shared prefix again costs nothing.

Found on Slevomat through the closure signature inference in #6604, which inherits the same observation pass.

  • tests/PHPStan/Analyser/nsrt/template-argument-try-catch.php fails without the fix (Collection<unresolved(1)> for the throwing catch and the finally block).
  • Turbo twin ported (TryCatchHandler.cpp), side-by-side / smoke green, expected version bumped.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VpbB99tJfUmeArX74xqvHv

ondrejmirtes and others added 2 commits September 27, 2026 03:15
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
@ondrejmirtes
ondrejmirtes force-pushed the template-argument-facts-in-terminating-branches branch from 8b1be1f to 3c01737 Compare September 27, 2026 01:16
@ondrejmirtes
ondrejmirtes merged commit 3c01737 into 2.3.x Sep 27, 2026
1 check passed
@ondrejmirtes
ondrejmirtes deleted the template-argument-facts-in-terminating-branches branch September 27, 2026 01:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant