Skip to content

Keep a merged call without a narrowable argument in the scope - #7064

Merged
ondrejmirtes merged 1 commit into
2.3.xfrom
fix-15727-keep-argumentless-merged-calls
Nov 14, 2026
Merged

ondrejmirtes merged 1 commit into
2.3.xfrom
fix-15727-keep-argumentless-merged-calls

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Closes phpstan/phpstan#15727

Since bcca2b0 ("Narrow remembered count()/sizeof() of an array when the array expression is narrowed", #6953), mergeWith() drops a remembered call whose merged type equals what the call resolves to on its own. The goal is to let later narrowing of an argument reach the call: count($arr) after $arr !== []. But dropping the entry is not neutral for later merges:

  • "Might not be defined" (WordPress): after if (isMulti()) {}, the scope no longer remembers isMulti(): bool. In if (!isMulti() || can('update')), the left operand's scope then has no entry, so the || merge leaves a Maybe holder for isMulti() in the truthy branch. A Maybe holder derives no conditional guard, so the merge after the if loses "if isMulti() is false, $updateData is defined".
  • Call narrowed after an if without else (Dolibarr): the same Maybe holder (non-falsy-string from the right operand of getS() && …) is intersected as if it held the call's type when the conditional expression "if $client is null, getS() is ''|'0'" fires. That makes the else branch never, and getS() ends up non-falsy-string after the if.

Only an argument that narrowing can change can make a remembered call stale. A call without one now keeps its entry, as it did in 2.3.5. This covers calls with no arguments and calls whose arguments are all literals: scalars other than interpolated strings, constants, and class constants of a named class. count($arr), strlen($s) and the rest of bug-13750.php are still dropped as before.

The turbo-ext mirror in MutatingScope.cpp gets the same check, and generated/MutatingScope.h is regenerated with PHP 8.5.

Not covered: the same pattern on a call with a narrowable argument, such as if (f($i)) {} followed by if (!f($i) || …), still reports "might not be defined". It behaves the same way as on 2.3.x-dev today, and keeping that entry would undo #6953 for that call.

Verification:

  • tests/PHPStan/Analyser/nsrt/bug-15727.php and DefinedVariableRuleTest::testBug15727() fail without the fix (4 failing assertions plus the rule error on line 30) and pass with it. They also pass with the extension loaded and only the C++ change in place.
  • The full test suite passes with and without the extension (23702 tests). make phpstan reports no errors with and without the extension. The strict extension build, smoke.php, signature-parity.php and side-by-side.php pass.
  • Not run: make lint-turbo, because clang-tidy 21 is not available here.

Since bcca2b0, a merge drops a remembered call whose merged type is
what the call resolves to on its own, so that later narrowing of the
call's arguments reaches it. A call without an argument that narrowing
can change, such as isMulti() or can('update'), never goes stale, but
dropping its entry still changes later merges: a branch without the
entry merges into a Maybe holder, which derives no conditional guards
and is intersected as if it held the call's type when a conditional
expression narrows the call. Such calls now keep their entry.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1d722-5ae1-772c-a909-1fc9b8e04be4
@ondrejmirtes
ondrejmirtes force-pushed the fix-15727-keep-argumentless-merged-calls branch from 71ae3ab to 25004a0 Compare November 14, 2026 09:12
@ondrejmirtes
ondrejmirtes merged commit b68353e into 2.3.x Nov 14, 2026
387 of 392 checks passed
@ondrejmirtes
ondrejmirtes deleted the fix-15727-keep-argumentless-merged-calls branch November 14, 2026 09: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.

After if (f()), a later if on the same call reports "might not be defined" or narrows f() (regression in 2.3.x-dev)

1 participant