Fix phpstan/phpstan#14411: Incorrect type narrowing with dependent types#5371
Fix phpstan/phpstan#14411: Incorrect type narrowing with dependent types#5371phpstan-bot wants to merge 3 commits intophpstan:2.1.xfrom
Conversation
- In createConditionalExpressions(), type guards were created for variables whose branch types overlapped, allowing narrowing of one variable to incorrectly narrow a dependent variable from a different branch - Added a disjointness check: a variable is only used as a type guard when its "our" branch type and "their" branch type don't overlap - New regression test in tests/PHPStan/Analyser/nsrt/bug-14411.php
ondrejmirtes
left a comment
There was a problem hiding this comment.
-
Please add regression tests for phpstan/phpstan#10843, phpstan/phpstan#14211, phpstan/phpstan#11328, phpstan/phpstan#10085
-
You broke Sylius:
------ --------------------------------------------------------------------------------------------------------------------------------------------------------------
Line Sylius/Bundle/ShopBundle/EventListener/OrderCustomerIpListener.php
------ --------------------------------------------------------------------------------------------------------------------------------------------------------------
52 Variable $order might not be defined.
🪪 variable.undefined
✏️ https://github.com/Sylius/Sylius/blob/da59129e4c249cdc3d4b57840c86880008315592/src/Sylius/Bundle/ShopBundle/EventListener/OrderCustomerIpListener.php#L52
------ --------------------------------------------------------------------------------------------------------------------------------------------------------------
Please reproduce the problem (a test that passes without changes to src/ but fails with current src/ changes) and then fix it too.
…sions The type disjointness check in createConditionalExpressions() was too aggressive, rejecting type guards when isSuperTypeOf returned Maybe for object types. For class/interface combinations (e.g. Event vs OrderInterface), the Maybe result is purely theoretical (some hypothetical class could be both), whereas for non-object types (e.g. mixed~null vs int|null, or non-empty-array vs array) it indicates genuine value overlap. The fix now only skips guards when isSuperTypeOf returns yes (complete containment) or when it returns maybe AND at least one type is not an object type. Object-to-object Maybe results (interface/class combos) are preserved as valid guards. Adds integration test reproducing the Sylius OrderCustomerIpListener pattern where $order is assigned in separate instanceof branches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add NSRT regression tests for: - phpstan/phpstan#10843: match(true) with instanceof type narrowing - phpstan/phpstan#14211: repeated if($m) check with isset-based bool - phpstan/phpstan#11328: backed enum in_array conditional narrowing - phpstan/phpstan#10085: list type incorrectly inferred as non-empty Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Both commits have been pushed. Here's a summary of what was done: Fix (commit 1): The original
This fixes the Sylius regression where Regression tests (commit 2): Added NSRT tests for:
|
Summary
When variables are assigned in different branches of an if/else and then one is checked for null, PHPStan could incorrectly narrow the type of a dependent variable from the other branch. For example, checking
$b !== nullafter$bwas assigned in both branches would incorrectly narrow$ato non-null, even though$bcould be non-null from the else branch where$aIS null.Changes
src/Analyser/MutatingScope.phpin thecreateConditionalExpressions()methodRoot cause
The
createConditionalExpressions()method creates conditional type relationships like "if$bhas type X, then$ahas type Y". These are used to propagate type narrowing across dependent variables. However, the method didn't check whether the guard variable's types from both branches were disjoint. When they overlapped (e.g.,mixed~nullfrom the if-branch andint|nullfrom the else-branch shareint), narrowing the merged type could produce the exact branch type even for values originating from the other branch, causing incorrect type narrowing.The fix adds a check:
$holder->getType()->isSuperTypeOf($theirExpressionTypes[$exprString]->getType())->no()— ensuring that the guard variable's types from both branches don't overlap before using it as a discriminator.Test
Added
tests/PHPStan/Analyser/nsrt/bug-14411.phpwhich reproduces the original issue:$a = get_mixed(), then$bassigned conditionally from$aorget_optional_int(), then checking$b !== nullshould not narrow$ato non-null.Fixes phpstan/phpstan#14411