Skip to content

Keep never in MutatingScope::intersectButNotNever() instead of widening it to null - #6507

Merged
ondrejmirtes merged 1 commit into
2.2.xfrom
cherry-pick-6504-2.2.x
Sep 22, 2026
Merged

ondrejmirtes merged 1 commit into
2.2.xfrom
cherry-pick-6504-2.2.x

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Cherry-pick of #6504 (merged into 2.3.x as 7c9cd26) onto 2.2.x.

The regression it fixes is on 2.2.x too: #6493 was merged into 2.2.x, so fn (): null => $neverReturning() and fn (): ?int => throw ... are inferred as Closure(): null instead of Closure(): never there as well.

Verified on 2.2.x:

  • nsrt/bug-15285.php and CallCallablesRuleTest::testBug15285 fail without the MutatingScope change (Closure(): null / static-Closure(): null where never is expected, plus the argument.type error) and pass with it.
  • Full test suite and self-analysis are green.
  • A PHAR built from this branch, compared with current 2.2.x-dev on a large private codebase (cold result cache): the only difference is the 6 Parameter #2 of closure expects (Closure(): never)|null, Closure(): null given. errors going away.

Refs phpstan/phpstan#15285

🤖 Generated with Claude Code

https://claude.ai/code/session_01Dhb4ssXuKVWcVBcNFpsdn7

…dening it to `null`

* `intersectButNotNever()` restores the native type's nullability onto the
  intersection result. It relied on the old `TypeCombinator::addNull()`
  behaviour, where `addNull(never)` returned `never`. Since `addNull()` was
  improved it returns `null` for `never`, so a nullable native type combined
  with an inferred `never` produced `null`.
* Return the intersection unchanged as soon as it is `never`, before the
  nullability is added back.
* Fixes closure and arrow function return types resolved in
  `ClosureTypeResolver` (`fn (): null => neverReturning()` and
  `function (): ?int { throw ...; }` are `Closure(): never` again), including
  the `static` variants and closures declared `: ?Generator` whose body only
  throws.
* Fixes the same helper's other call sites - closure and arrow function
  parameter types in `MutatingScope::enterAnonymousFunctionWithoutReflection()`
  and `enterArrowFunctionWithoutReflection()`. A `?int` parameter in a
  `callable(never): void` context (e.g. `array_map()` over `list<never>`) is
  `never` inside the body again, for both the fixed and the variadic parameter
  branch.
* Probed the other `TypeCombinator::addNull()` call sites that can receive a
  `never` by instrumenting `addNull()` and running the whole test suite. The
  nullsafe short-circuit sites (`MethodCallHandler`, `StaticCallHandler`,
  `PropertyFetchHandler`, `StaticPropertyFetchHandler`, `ArrayDimFetchHandler`,
  `MutatingScope::resolveScopeStateType()`) and
  `ArrayFindFunctionReturnTypeExtension` are correct as they are: there `null`
  really is the result of the short circuit / of not finding anything.
  `TypehintHelper::decideType()` cannot reach its `addNull()` with a `never`,
  because an explicit `@return never` returns early and an implicit one is
  never picked as the result type.

(cherry picked from commit 7c9cd26)
@ondrejmirtes
ondrejmirtes merged commit bba555b into 2.2.x Sep 22, 2026
245 of 246 checks passed
@ondrejmirtes
ondrejmirtes deleted the cherry-pick-6504-2.2.x branch September 22, 2026 06:56
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.

2 participants