From 84ee93e027b9bc13e702e69fec56aa9a66255d48 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:48:35 +0000 Subject: [PATCH 1/3] Pass only the right-hand side of `??=` as the written expression to `PropertyAssignNode` and `VariableAssignNode` - `$lvalue ??= value` writes only `value`, but AssignHandler handed the whole `AssignOp\Coalesce` node to PropertyAssignNode/VariableAssignNode, whose type is `nonNull($lvalue)|value`. The rules checking the write therefore saw a widened type. - Add AssignHandler::getWrittenExpr() and use it for every PropertyAssignNode (instance + static property, resolved + fallback branches), for the Set*OffsetValueTypeExpr chain built for offset writes on properties (ArrayDimFetch and ExistingArrayDimFetch), and for the VariableAssignNode of plain variable targets. Scope updates still use the full `??=` type. - Mirror the change in turbo-ext/src/AssignHandler.cpp and regenerate turbo-ext/src/generated/AssignHandler.h. - Fixes in TypesAssignedToPropertiesRule: false negative below level 7 (`?int` ??= 'boom' not reported) and false positive at level 7+ (stale narrowed property type merged into the reported type), for instance properties, static properties and property offsets. - Same root cause fixed for ParameterOutAssignedTypeRule (`@param-out` checks on `$p ??= ...`) and TooWidePropertyTypeRule (the kept old value hid "never assigned X" reports). --- src/Analyser/ExprHandler/AssignHandler.php | 27 +- .../TypesAssignedToPropertiesRuleTest.php | 73 +++- .../Rules/Properties/data/bug-15228.php | 87 +++++ .../TooWidePropertyTypeRuleTest.php | 11 + .../Rules/TooWideTypehints/data/bug-15228.php | 20 ++ .../ParameterOutAssignedTypeRuleTest.php | 14 + .../Rules/Variables/data/bug-15228.php | 22 ++ turbo-ext/src/AssignHandler.cpp | 20 +- turbo-ext/src/generated/AssignHandler.h | 312 +++++++++--------- 9 files changed, 419 insertions(+), 167 deletions(-) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-15228.php create mode 100644 tests/PHPStan/Rules/TooWideTypehints/data/bug-15228.php create mode 100644 tests/PHPStan/Rules/Variables/data/bug-15228.php diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index bd62f071ea6..3416417c328 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -760,7 +760,7 @@ private function doPrepareTarget( // 1b. build the write chain (Set*OffsetValueTypeExpr nesting) after // the root walk, so a property base's holder and current types are // read from its stored result instead of pricing the unwalked fetch - $assignedPropertyExpr = $assignedExpr; + $assignedPropertyExpr = $this->getWrittenExpr($assignedExpr); $chainVar = $originalVar; while ($chainVar instanceof ArrayDimFetch) { $varForSetOffsetValue = $chainVar->var; @@ -1097,7 +1097,7 @@ private function doPrepareTarget( if ($var instanceof ExistingArrayDimFetch) { $originalVar = $var; $dimFetchStack = []; - $assignedPropertyExpr = $assignedExpr; + $assignedPropertyExpr = $this->getWrittenExpr($assignedExpr); while ($var instanceof ExistingArrayDimFetch) { $varForSetOffsetValue = $var->getVar(); if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) { @@ -1374,7 +1374,7 @@ public function applyWrite( $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $storage, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType, $impurePoints, $assignedExpr, $storedAssignedExprResult); } - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $this->getWrittenExpr($assignedExpr)), $scopeBeforeAssignEval, $storage); $nativeType = $this->readAssignedValueType($nodeScopeResolver, $storedAssignedExprResult, $assignedExpr, $scope->doNotTreatPhpDocTypesAsCertain()); $scope = $scope->assignVariable( @@ -1596,7 +1596,7 @@ public function applyWrite( if ($templateArgumentFrame !== null) { $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectSend($propertyReflection->getWritableType(), $assignedExprType)); } - $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $this->getWrittenExpr($assignedExpr), $isAssignOp), $scopeBeforeAssignEval, $storage); if ($propertyReflection->canChangeTypeAfterAssignment()) { if ($propertyReflection->hasNativeType()) { $propertyNativeType = $propertyReflection->getNativeType(); @@ -1657,7 +1657,7 @@ public function applyWrite( } else { // fallback $assignedExprType = $this->readAssignedValueType($nodeScopeResolver, $assignedValueResult, $assignedExpr, $scope); - $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $this->getWrittenExpr($assignedExpr), $isAssignOp), $scopeBeforeAssignEval, $storage); $scope = $scope->assignExpression($var, $assignedExprType, $this->readAssignedValueType($nodeScopeResolver, $assignedValueResult, $assignedExpr, $scope->doNotTreatPhpDocTypesAsCertain())); // simulate dynamic property assign by __set to get throw points if (!$propertyHolderType->hasMethod('__set')->no()) { @@ -1693,7 +1693,7 @@ public function applyWrite( $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectSend($propertyReflection->getWritableType(), $assignedExprType)); } } - $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $this->getWrittenExpr($assignedExpr), $isAssignOp), $scopeBeforeAssignEval, $storage); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { if ($propertyReflection->hasNativeType()) { $propertyNativeType = $propertyReflection->getNativeType(); @@ -1724,7 +1724,7 @@ public function applyWrite( } else { // fallback $assignedExprType = $this->readAssignedValueType($nodeScopeResolver, $assignedValueResult, $assignedExpr, $scope); - $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $this->getWrittenExpr($assignedExpr), $isAssignOp), $scopeBeforeAssignEval, $storage); $scope = $scope->assignExpression($var, $assignedExprType, $this->readAssignedValueType($nodeScopeResolver, $assignedValueResult, $assignedExpr, $scope->doNotTreatPhpDocTypesAsCertain())); } } elseif ($kind === PreparedAssignTarget::KIND_LIST) { @@ -2051,6 +2051,19 @@ private function assignDynamicVariable(MutatingScope $scope, ExpressionResult $n return $scope; } + /** + * `$lvalue ??= value` writes only `value` - the type of the whole + * expression also covers the non-null old value it keeps in place. + */ + private function getWrittenExpr(Expr $assignedExpr): Expr + { + if ($assignedExpr instanceof Expr\AssignOp\Coalesce) { + return $assignedExpr->expr; + } + + return $assignedExpr; + } + private function unwrapAssign(Expr $expr): Expr { if ($expr instanceof Assign) { diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 885902a72b7..58c02e04740 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -15,6 +15,8 @@ class TypesAssignedToPropertiesRuleTest extends RuleTestCase private bool $checkNullables = true; + private bool $checkUnionTypes = true; + private bool $checkExplicitMixed = false; private bool $checkImplicitMixed = false; @@ -26,7 +28,7 @@ protected function getRule(): Rule self::createReflectionProvider(), checkNullables: $this->checkNullables, checkThisOnly: false, - checkUnionTypes: true, + checkUnionTypes: $this->checkUnionTypes, checkExplicitMixed: $this->checkExplicitMixed, checkImplicitMixed: $this->checkImplicitMixed, checkBenevolentUnionTypes: false, @@ -1145,4 +1147,73 @@ public function testIntersectionWithTemplateMixed(): void ]); } + public function testBug15228(): void + { + $this->analyse([__DIR__ . '/data/bug-15228.php'], [ + [ + 'Property Bug15228\\CoalesceAssignDemo::$a (int|null) does not accept string.', + 20, + ], + [ + 'Property Bug15228\\CoalesceAssignDemo::$a (int|null) does not accept string.', + 26, + ], + [ + 'Property Bug15228\\CoalesceAssignDemo::$b (list|null) does not accept list.', + 32, + ], + [ + 'Property Bug15228\\CoalesceAssignDemo::$b (list|null) does not accept list.', + 38, + ], + [ + 'Static property Bug15228\\StaticCoalesceAssignDemo::$a (int|null) does not accept string.', + 60, + ], + [ + 'Static property Bug15228\\StaticCoalesceAssignDemo::$b (list|null) does not accept list.', + 65, + ], + [ + 'Property Bug15228\\OffsetCoalesceAssignDemo::$a (array) does not accept array.', + 80, + ], + ]); + } + + public function testBug15228WithoutCheckUnionTypes(): void + { + $this->checkUnionTypes = false; + $this->analyse([__DIR__ . '/data/bug-15228.php'], [ + [ + 'Property Bug15228\\CoalesceAssignDemo::$a (int|null) does not accept string.', + 20, + ], + [ + 'Property Bug15228\\CoalesceAssignDemo::$a (int|null) does not accept string.', + 26, + ], + [ + 'Property Bug15228\\CoalesceAssignDemo::$b (list|null) does not accept list.', + 32, + ], + [ + 'Property Bug15228\\CoalesceAssignDemo::$b (list|null) does not accept list.', + 38, + ], + [ + 'Static property Bug15228\\StaticCoalesceAssignDemo::$a (int|null) does not accept string.', + 60, + ], + [ + 'Static property Bug15228\\StaticCoalesceAssignDemo::$b (list|null) does not accept list.', + 65, + ], + [ + 'Property Bug15228\\OffsetCoalesceAssignDemo::$a (array) does not accept array.', + 80, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-15228.php b/tests/PHPStan/Rules/Properties/data/bug-15228.php new file mode 100644 index 00000000000..5c5e39c2624 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-15228.php @@ -0,0 +1,87 @@ +|null */ + public $b; + + /** @return list */ + private function strings(): array + { + return ['x']; + } + + public function coalesceInvalid(): void + { + $this->a ??= 'boom'; + } + + public function ifInvalid(): void + { + if (null === $this->a) { + $this->a = 'boom'; + } + } + + public function coalesceValid(): void + { + $this->b = $this->strings(); + $this->b ??= [1, 2]; + } + + public function ifValid(): void + { + $this->b = $this->strings(); + if (null === $this->b) { + $this->b = [1, 2]; + } + } +} + +class StaticCoalesceAssignDemo +{ + public static ?int $a = null; + + /** @var list|null */ + public static $b; + + /** @return list */ + private static function strings(): array + { + return ['x']; + } + + public static function coalesceInvalid(): void + { + self::$a ??= 'boom'; + } + + public static function coalesceValid(): void + { + self::$b = self::strings(); + self::$b ??= [1, 2]; + } +} + +class OffsetCoalesceAssignDemo +{ + /** @var array */ + public array $a = []; + + /** @var array|null> */ + public array $b = []; + + public function coalesceInvalid(): void + { + $this->a['x'] ??= 'boom'; + } + + public function coalesceValid(): void + { + $this->b['x'] ??= [1, 2]; + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php index ee62bdef57c..ea02709654a 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php @@ -144,4 +144,15 @@ public function testBug11844(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11844.php'], []); } + #[RequiresPhp('>= 8.0.0')] + public function testBug15228(): void + { + $this->analyse([__DIR__ . '/data/bug-15228.php'], [ + [ + 'Property Bug15228TooWide\\Foo::$x (int|string|null) is never assigned string so it can be removed from the property type.', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-15228.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-15228.php new file mode 100644 index 00000000000..b6fe7efefa7 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-15228.php @@ -0,0 +1,20 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug15228TooWide; + +class Foo +{ + private int|string|null $x = null; + + public function doFoo(): void + { + $this->x ??= 1; + } + + public function getX(): int|string|null + { + return $this->x; + } +} diff --git a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php index 9a6dd9e6354..c6868efe7c3 100644 --- a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -145,4 +145,18 @@ public function testBug15428(): void $this->analyse([__DIR__ . '/data/bug-15428.php'], []); } + public function testBug15228(): void + { + $this->analyse([__DIR__ . '/data/bug-15228.php'], [ + [ + 'Parameter &$p @param-out type of function Bug15228ParamOut\\coalesceInvalid() expects int|null, string given.', + 10, + ], + [ + 'Parameter &$p @param-out type of function Bug15228ParamOut\\coalesceValid() expects list|null, list given.', + 20, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-15228.php b/tests/PHPStan/Rules/Variables/data/bug-15228.php new file mode 100644 index 00000000000..e91e8e3ad9b --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-15228.php @@ -0,0 +1,22 @@ +|null $p + * @param list|null $p + * @param list $s + */ +function coalesceValid(?array &$p, array $s): void +{ + $p = $s; + $p ??= [1, 2]; +} diff --git a/turbo-ext/src/AssignHandler.cpp b/turbo-ext/src/AssignHandler.cpp index 393a822d2b8..c171bf981b8 100644 --- a/turbo-ext/src/AssignHandler.cpp +++ b/turbo-ext/src/AssignHandler.cpp @@ -1476,7 +1476,7 @@ class AssignHandler // 1b. build the write chain (Set*OffsetValueTypeExpr nesting) after // the root walk, so a property base's holder and current types are // read from its stored result instead of pricing the unwalked fetch - zv::Val assignedPropertyExpr = zv::Val::copyOf(zv::Ref(assignedExpr)); + zv::Val assignedPropertyExpr = zv::Val::copyOf(zv::Ref(getWrittenExpr(assignedExpr))); zval *chainVar = originalVar; while (ahIs(chainVar, PT_CLASS_ARRAY_DIM_FETCH)) { zval *chainVarVar = AH_PROP(chainVar, var); @@ -1803,7 +1803,7 @@ class AssignHandler if (ahIs(var, PT_CLASS_EXISTING_ARRAY_DIM_FETCH)) { zval *originalVar = var; zv::Val dimFetchStack = ahEmptyArray(); - zv::Val assignedPropertyExpr = zv::Val::copyOf(zv::Ref(assignedExpr)); + zv::Val assignedPropertyExpr = zv::Val::copyOf(zv::Ref(getWrittenExpr(assignedExpr))); while (ahIs(var, PT_CLASS_EXISTING_ARRAY_DIM_FETCH)) { zval *varVar = AH_PROP(var, var); zval *varDim = AH_PROP(var, dim); @@ -2137,7 +2137,7 @@ class AssignHandler } { - AH_VALB(assignNode, newVariableAssignNode(var, assignedExpr.raw())); + AH_VALB(assignNode, newVariableAssignNode(var, getWrittenExpr(assignedExpr.raw()))); AH_OKB(nsrCallNodeCallback(nsr, nodeCallback, assignNode.raw(), scopeBeforeAssignEval.raw(), storage)); } @@ -2389,6 +2389,16 @@ class AssignHandler return scope; } + /* (twin 2058) `$lvalue ??= value` writes only `value` */ + static zval *getWrittenExpr(zval *assignedExpr) + { + if (ahIs(assignedExpr, PT_CLASS_COALESCE_ASSIGN_OP_EXPR)) { + return AH_PROP(assignedExpr, expr); + } + + return assignedExpr; + } + /* (twin 1980) */ static zval *unwrapAssign(zval *expr) { @@ -3870,7 +3880,7 @@ class AssignHandler } } { - AH_VALB(assignNode, newPropertyAssignNode(var, assignedExpr, isAssignOp)); + AH_VALB(assignNode, newPropertyAssignNode(var, getWrittenExpr(assignedExpr), isAssignOp)); AH_OKB(nsrCallNodeCallback(nsr, nodeCallback, assignNode.raw(), scopeBeforeAssignEval, storage)); } if (propertyReflection == NULL) return true; @@ -3929,7 +3939,7 @@ class AssignHandler { AH_VALB(assignedExprType, readAssignedValueType(nsr, assignedValueResult, assignedExpr, scope.raw())); { - AH_VALB(assignNode, newPropertyAssignNode(var, assignedExpr, isAssignOp)); + AH_VALB(assignNode, newPropertyAssignNode(var, getWrittenExpr(assignedExpr), isAssignOp)); AH_OKB(nsrCallNodeCallback(nsr, nodeCallback, assignNode.raw(), scopeBeforeAssignEval, storage)); } AH_VALB(nativeScope, pt_mutating_scope_do_not_treat_phpdoc_types_as_certain(Z_OBJ_P(scope.raw()))); diff --git a/turbo-ext/src/generated/AssignHandler.h b/turbo-ext/src/generated/AssignHandler.h index 5edaadc5cbe..90adffcb2c7 100644 --- a/turbo-ext/src/generated/AssignHandler.h +++ b/turbo-ext/src/generated/AssignHandler.h @@ -165,69 +165,70 @@ inline constexpr char strings[] = "nameResult\0" /* 2405 */ "nativeValueType\0" /* 2416 */ "assignDynamicVariable\0" /* 2432 */ - "unwrapAssign\0" /* 2454 */ - "variableName\0" /* 2467 */ - "conditionalExpressions\0" /* 2480 */ - "specifiedTypes\0" /* 2503 */ - "variableType\0" /* 2518 */ - "rhsImpurePoints\0" /* 2531 */ - "processSureTypesForConditionalExpressionsAfterAssign\0" /* 2547 */ - "processSureNotTypesForConditionalExpressionsAfterAssign\0" /* 2600 */ - "currentTypeForConditionalHolder\0" /* 2656 */ - "armScope\0" /* 2688 */ - "armExpr\0" /* 2697 */ - "remainderType\0" /* 2705 */ - "otherArmType\0" /* 2719 */ - "processTernaryArmValueImpliedTypesAfterAssign\0" /* 2732 */ - "holderExpr\0" /* 2778 */ - "holderExprString\0" /* 2789 */ - "holderType\0" /* 2806 */ - "holderCertainty\0" /* 2817 */ - "PHPStan\\TrinaryLogic\0" /* 2833 */ - "addConditionalExpressionHolder\0" /* 2854 */ - "newConditionalExpressions\0" /* 2885 */ - "mergeConditionalExpressions\0" /* 2911 */ - "PhpParser\\Node\\Expr\\Match_\0" /* 2939 */ - "processMatchForConditionalExpressionsAfterAssign\0" /* 2966 */ - "assignedType\0" /* 3015 */ - "processDerivedConditionalExpressionsAfterAssign\0" /* 3028 */ - "PhpParser\\Node\\Expr\\FuncCall\0" /* 3076 */ - "processInArrayForConditionalExpressionsAfterAssign\0" /* 3105 */ - "isExprSafeToProjectThroughVariable\0" /* 3156 */ - "dimFetchStack\0" /* 3191 */ - "PHPStan\\Analyser\\Scope\0" /* 3205 */ - "isImplicitArrayCreation\0" /* 3228 */ - "rootVarName\0" /* 3252 */ - "arrayExpr\0" /* 3264 */ - "PhpParser\\Node\\Expr\\Array_\0" /* 3274 */ - "parentExpr\0" /* 3301 */ - "processArrayByRefItems\0" /* 3312 */ - "implicitIndex\0" /* 3335 */ - "processArrayByRefItemsWithImplicitIndex\0" /* 3349 */ - "isFlattenableUnpackedArray\0" /* 3389 */ - "index\0" /* 3416 */ - "advanceImplicitIndex\0" /* 3422 */ - "offsetTypes\0" /* 3443 */ - "offsetValueType\0" /* 3455 */ - "valueToWrite\0" /* 3471 */ - "produceArrayDimFetchAssignValueToWrite\0" /* 3484 */ - "arrayDimFetch\0" /* 3523 */ - "PhpParser\\Node\\Expr\\ArrayDimFetch\0" /* 3537 */ - "shouldKeepList\0" /* 3571 */ - "offsetType\0" /* 3586 */ - "trackedLinkImpliesOffset\0" /* 3597 */ - "a\0" /* 3622 */ - "b\0" /* 3624 */ - "isSameVariable\0" /* 3626 */ - "isSameOffset\0" /* 3641 */ - "getLiteralArrayKeyType\0" /* 3654 */ - "propertyFetch\0" /* 3677 */ - "PhpParser\\Node\\Expr\\PropertyFetch|PhpParser\\Node\\Expr\\StaticPropertyFetch\0" /* 3691 */ - "getOriginalPropertyType\0" /* 3765 */ - "array\0" /* 3789 */ - "hasArrayReference\0" /* 3795 */ - "rhs\0" /* 3813 */ - "redundant"; /* 3817 */ + "getWrittenExpr\0" /* 2454 */ + "unwrapAssign\0" /* 2469 */ + "variableName\0" /* 2482 */ + "conditionalExpressions\0" /* 2495 */ + "specifiedTypes\0" /* 2518 */ + "variableType\0" /* 2533 */ + "rhsImpurePoints\0" /* 2546 */ + "processSureTypesForConditionalExpressionsAfterAssign\0" /* 2562 */ + "processSureNotTypesForConditionalExpressionsAfterAssign\0" /* 2615 */ + "currentTypeForConditionalHolder\0" /* 2671 */ + "armScope\0" /* 2703 */ + "armExpr\0" /* 2712 */ + "remainderType\0" /* 2720 */ + "otherArmType\0" /* 2734 */ + "processTernaryArmValueImpliedTypesAfterAssign\0" /* 2747 */ + "holderExpr\0" /* 2793 */ + "holderExprString\0" /* 2804 */ + "holderType\0" /* 2821 */ + "holderCertainty\0" /* 2832 */ + "PHPStan\\TrinaryLogic\0" /* 2848 */ + "addConditionalExpressionHolder\0" /* 2869 */ + "newConditionalExpressions\0" /* 2900 */ + "mergeConditionalExpressions\0" /* 2926 */ + "PhpParser\\Node\\Expr\\Match_\0" /* 2954 */ + "processMatchForConditionalExpressionsAfterAssign\0" /* 2981 */ + "assignedType\0" /* 3030 */ + "processDerivedConditionalExpressionsAfterAssign\0" /* 3043 */ + "PhpParser\\Node\\Expr\\FuncCall\0" /* 3091 */ + "processInArrayForConditionalExpressionsAfterAssign\0" /* 3120 */ + "isExprSafeToProjectThroughVariable\0" /* 3171 */ + "dimFetchStack\0" /* 3206 */ + "PHPStan\\Analyser\\Scope\0" /* 3220 */ + "isImplicitArrayCreation\0" /* 3243 */ + "rootVarName\0" /* 3267 */ + "arrayExpr\0" /* 3279 */ + "PhpParser\\Node\\Expr\\Array_\0" /* 3289 */ + "parentExpr\0" /* 3316 */ + "processArrayByRefItems\0" /* 3327 */ + "implicitIndex\0" /* 3350 */ + "processArrayByRefItemsWithImplicitIndex\0" /* 3364 */ + "isFlattenableUnpackedArray\0" /* 3404 */ + "index\0" /* 3431 */ + "advanceImplicitIndex\0" /* 3437 */ + "offsetTypes\0" /* 3458 */ + "offsetValueType\0" /* 3470 */ + "valueToWrite\0" /* 3486 */ + "produceArrayDimFetchAssignValueToWrite\0" /* 3499 */ + "arrayDimFetch\0" /* 3538 */ + "PhpParser\\Node\\Expr\\ArrayDimFetch\0" /* 3552 */ + "shouldKeepList\0" /* 3586 */ + "offsetType\0" /* 3601 */ + "trackedLinkImpliesOffset\0" /* 3612 */ + "a\0" /* 3637 */ + "b\0" /* 3639 */ + "isSameVariable\0" /* 3641 */ + "isSameOffset\0" /* 3656 */ + "getLiteralArrayKeyType\0" /* 3669 */ + "propertyFetch\0" /* 3692 */ + "PhpParser\\Node\\Expr\\PropertyFetch|PhpParser\\Node\\Expr\\StaticPropertyFetch\0" /* 3706 */ + "getOriginalPropertyType\0" /* 3780 */ + "array\0" /* 3804 */ + "hasArrayReference\0" /* 3810 */ + "rhs\0" /* 3828 */ + "redundant"; /* 3832 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 25), /* __construct $templateArgumentObserver */ reg::packed(76, 0, 99), /* __construct $varAnnotationProcessor */ @@ -337,27 +338,29 @@ inline constexpr reg::PackedArg args[] = { reg::packed(2064, 0, 2038), /* assignDynamicVariable $valueType */ reg::packed(2416, 0, 2038), /* assignDynamicVariable $nativeValueType */ reg::packed(1396, 0, 1490), /* assignDynamicVariable return */ + reg::packed(1672, 0, 1376), /* getWrittenExpr $assignedExpr */ + reg::packed(1396, 0, 1376), /* getWrittenExpr return */ reg::packed(1371, 0, 1376), /* unwrapAssign $expr */ reg::packed(1396, 0, 1376), /* unwrapAssign return */ reg::packed(1406, 0, 1424), /* processSureTypesForConditionalExpressionsAfterAssign $nodeScopeResolver */ reg::packed(1484, 0, 1490), /* processSureTypesForConditionalExpressionsAfterAssign $scope */ reg::packed(1521, 0, 1529), /* processSureTypesForConditionalExpressionsAfterAssign $storage */ - reg::packed(2467, MAY_BE_STRING), /* processSureTypesForConditionalExpressionsAfterAssign $variableName */ - reg::packed(2480, MAY_BE_ARRAY), /* processSureTypesForConditionalExpressionsAfterAssign $conditionalExpressions */ - reg::packed(2503, 0, 2224), /* processSureTypesForConditionalExpressionsAfterAssign $specifiedTypes */ - reg::packed(2518, 0, 2038), /* processSureTypesForConditionalExpressionsAfterAssign $variableType */ - reg::packed(2531, MAY_BE_ARRAY), /* processSureTypesForConditionalExpressionsAfterAssign $rhsImpurePoints */ + reg::packed(2482, MAY_BE_STRING), /* processSureTypesForConditionalExpressionsAfterAssign $variableName */ + reg::packed(2495, MAY_BE_ARRAY), /* processSureTypesForConditionalExpressionsAfterAssign $conditionalExpressions */ + reg::packed(2518, 0, 2224), /* processSureTypesForConditionalExpressionsAfterAssign $specifiedTypes */ + reg::packed(2533, 0, 2038), /* processSureTypesForConditionalExpressionsAfterAssign $variableType */ + reg::packed(2546, MAY_BE_ARRAY), /* processSureTypesForConditionalExpressionsAfterAssign $rhsImpurePoints */ reg::packed(1672, 0, 1376), /* processSureTypesForConditionalExpressionsAfterAssign $assignedExpr */ reg::packed(1994, MAY_BE_NULL, 1626), /* processSureTypesForConditionalExpressionsAfterAssign $assignedValueResult */ reg::packed(1396, MAY_BE_ARRAY), /* processSureTypesForConditionalExpressionsAfterAssign return */ reg::packed(1406, 0, 1424), /* processSureNotTypesForConditionalExpressionsAfterAssign $nodeScopeResolver */ reg::packed(1484, 0, 1490), /* processSureNotTypesForConditionalExpressionsAfterAssign $scope */ reg::packed(1521, 0, 1529), /* processSureNotTypesForConditionalExpressionsAfterAssign $storage */ - reg::packed(2467, MAY_BE_STRING), /* processSureNotTypesForConditionalExpressionsAfterAssign $variableName */ - reg::packed(2480, MAY_BE_ARRAY), /* processSureNotTypesForConditionalExpressionsAfterAssign $conditionalExpressions */ - reg::packed(2503, 0, 2224), /* processSureNotTypesForConditionalExpressionsAfterAssign $specifiedTypes */ - reg::packed(2518, 0, 2038), /* processSureNotTypesForConditionalExpressionsAfterAssign $variableType */ - reg::packed(2531, MAY_BE_ARRAY), /* processSureNotTypesForConditionalExpressionsAfterAssign $rhsImpurePoints */ + reg::packed(2482, MAY_BE_STRING), /* processSureNotTypesForConditionalExpressionsAfterAssign $variableName */ + reg::packed(2495, MAY_BE_ARRAY), /* processSureNotTypesForConditionalExpressionsAfterAssign $conditionalExpressions */ + reg::packed(2518, 0, 2224), /* processSureNotTypesForConditionalExpressionsAfterAssign $specifiedTypes */ + reg::packed(2533, 0, 2038), /* processSureNotTypesForConditionalExpressionsAfterAssign $variableType */ + reg::packed(2546, MAY_BE_ARRAY), /* processSureNotTypesForConditionalExpressionsAfterAssign $rhsImpurePoints */ reg::packed(1672, 0, 1376), /* processSureNotTypesForConditionalExpressionsAfterAssign $assignedExpr */ reg::packed(1994, MAY_BE_NULL, 1626), /* processSureNotTypesForConditionalExpressionsAfterAssign $assignedValueResult */ reg::packed(1396, MAY_BE_ARRAY), /* processSureNotTypesForConditionalExpressionsAfterAssign return */ @@ -369,107 +372,107 @@ inline constexpr reg::PackedArg args[] = { reg::packed(1994, MAY_BE_NULL, 1626), /* currentTypeForConditionalHolder $assignedValueResult */ reg::packed(1396, 0, 2038), /* currentTypeForConditionalHolder return */ reg::packed(1406, 0, 1424), /* processTernaryArmValueImpliedTypesAfterAssign $nodeScopeResolver */ - reg::packed(2688, 0, 1490), /* processTernaryArmValueImpliedTypesAfterAssign $armScope */ + reg::packed(2703, 0, 1490), /* processTernaryArmValueImpliedTypesAfterAssign $armScope */ reg::packed(1521, 0, 1529), /* processTernaryArmValueImpliedTypesAfterAssign $storage */ - reg::packed(2467, MAY_BE_STRING), /* processTernaryArmValueImpliedTypesAfterAssign $variableName */ - reg::packed(2480, MAY_BE_ARRAY), /* processTernaryArmValueImpliedTypesAfterAssign $conditionalExpressions */ - reg::packed(2697, 0, 1376), /* processTernaryArmValueImpliedTypesAfterAssign $armExpr */ - reg::packed(2705, 0, 2038), /* processTernaryArmValueImpliedTypesAfterAssign $remainderType */ - reg::packed(2719, 0, 2038), /* processTernaryArmValueImpliedTypesAfterAssign $otherArmType */ - reg::packed(2531, MAY_BE_ARRAY), /* processTernaryArmValueImpliedTypesAfterAssign $rhsImpurePoints */ + reg::packed(2482, MAY_BE_STRING), /* processTernaryArmValueImpliedTypesAfterAssign $variableName */ + reg::packed(2495, MAY_BE_ARRAY), /* processTernaryArmValueImpliedTypesAfterAssign $conditionalExpressions */ + reg::packed(2712, 0, 1376), /* processTernaryArmValueImpliedTypesAfterAssign $armExpr */ + reg::packed(2720, 0, 2038), /* processTernaryArmValueImpliedTypesAfterAssign $remainderType */ + reg::packed(2734, 0, 2038), /* processTernaryArmValueImpliedTypesAfterAssign $otherArmType */ + reg::packed(2546, MAY_BE_ARRAY), /* processTernaryArmValueImpliedTypesAfterAssign $rhsImpurePoints */ reg::packed(1672, 0, 1376), /* processTernaryArmValueImpliedTypesAfterAssign $assignedExpr */ reg::packed(1994, MAY_BE_NULL, 1626), /* processTernaryArmValueImpliedTypesAfterAssign $assignedValueResult */ reg::packed(1396, MAY_BE_ARRAY), /* processTernaryArmValueImpliedTypesAfterAssign return */ - reg::packed(2480, MAY_BE_ARRAY), /* addConditionalExpressionHolder $conditionalExpressions */ - reg::packed(2467, MAY_BE_STRING), /* addConditionalExpressionHolder $variableName */ - reg::packed(2518, 0, 2038), /* addConditionalExpressionHolder $variableType */ - reg::packed(2778, 0, 1376), /* addConditionalExpressionHolder $holderExpr */ - reg::packed(2789, MAY_BE_STRING), /* addConditionalExpressionHolder $holderExprString */ - reg::packed(2806, 0, 2038), /* addConditionalExpressionHolder $holderType */ - reg::packed(2817, 0, 2833), /* addConditionalExpressionHolder $holderCertainty */ + reg::packed(2495, MAY_BE_ARRAY), /* addConditionalExpressionHolder $conditionalExpressions */ + reg::packed(2482, MAY_BE_STRING), /* addConditionalExpressionHolder $variableName */ + reg::packed(2533, 0, 2038), /* addConditionalExpressionHolder $variableType */ + reg::packed(2793, 0, 1376), /* addConditionalExpressionHolder $holderExpr */ + reg::packed(2804, MAY_BE_STRING), /* addConditionalExpressionHolder $holderExprString */ + reg::packed(2821, 0, 2038), /* addConditionalExpressionHolder $holderType */ + reg::packed(2832, 0, 2848), /* addConditionalExpressionHolder $holderCertainty */ reg::packed(1396, MAY_BE_ARRAY), /* addConditionalExpressionHolder return */ - reg::packed(2480, MAY_BE_ARRAY), /* mergeConditionalExpressions $conditionalExpressions */ - reg::packed(2885, MAY_BE_ARRAY), /* mergeConditionalExpressions $newConditionalExpressions */ + reg::packed(2495, MAY_BE_ARRAY), /* mergeConditionalExpressions $conditionalExpressions */ + reg::packed(2900, MAY_BE_ARRAY), /* mergeConditionalExpressions $newConditionalExpressions */ reg::packed(1396, MAY_BE_ARRAY), /* mergeConditionalExpressions return */ reg::packed(1484, 0, 1490), /* processMatchForConditionalExpressionsAfterAssign $scope */ - reg::packed(2467, MAY_BE_STRING), /* processMatchForConditionalExpressionsAfterAssign $variableName */ - reg::packed(1371, 0, 2939), /* processMatchForConditionalExpressionsAfterAssign $expr */ + reg::packed(2482, MAY_BE_STRING), /* processMatchForConditionalExpressionsAfterAssign $variableName */ + reg::packed(1371, 0, 2954), /* processMatchForConditionalExpressionsAfterAssign $expr */ reg::packed(1396, MAY_BE_ARRAY), /* processMatchForConditionalExpressionsAfterAssign return */ reg::packed(1406, 0, 1424), /* processDerivedConditionalExpressionsAfterAssign $nodeScopeResolver */ reg::packed(1484, 0, 1490), /* processDerivedConditionalExpressionsAfterAssign $scope */ - reg::packed(2467, MAY_BE_STRING), /* processDerivedConditionalExpressionsAfterAssign $variableName */ - reg::packed(2480, MAY_BE_ARRAY), /* processDerivedConditionalExpressionsAfterAssign $conditionalExpressions */ + reg::packed(2482, MAY_BE_STRING), /* processDerivedConditionalExpressionsAfterAssign $variableName */ + reg::packed(2495, MAY_BE_ARRAY), /* processDerivedConditionalExpressionsAfterAssign $conditionalExpressions */ reg::packed(1672, 0, 1376), /* processDerivedConditionalExpressionsAfterAssign $assignedExpr */ - reg::packed(3015, 0, 2038), /* processDerivedConditionalExpressionsAfterAssign $assignedType */ - reg::packed(2531, MAY_BE_ARRAY), /* processDerivedConditionalExpressionsAfterAssign $rhsImpurePoints */ + reg::packed(3030, 0, 2038), /* processDerivedConditionalExpressionsAfterAssign $assignedType */ + reg::packed(2546, MAY_BE_ARRAY), /* processDerivedConditionalExpressionsAfterAssign $rhsImpurePoints */ reg::packed(1396, MAY_BE_ARRAY), /* processDerivedConditionalExpressionsAfterAssign return */ reg::packed(1406, 0, 1424), /* processInArrayForConditionalExpressionsAfterAssign $nodeScopeResolver */ reg::packed(1484, 0, 1490), /* processInArrayForConditionalExpressionsAfterAssign $scope */ - reg::packed(2467, MAY_BE_STRING), /* processInArrayForConditionalExpressionsAfterAssign $variableName */ - reg::packed(2480, MAY_BE_ARRAY), /* processInArrayForConditionalExpressionsAfterAssign $conditionalExpressions */ - reg::packed(1672, 0, 3076), /* processInArrayForConditionalExpressionsAfterAssign $assignedExpr */ - reg::packed(3015, 0, 2038), /* processInArrayForConditionalExpressionsAfterAssign $assignedType */ - reg::packed(2531, MAY_BE_ARRAY), /* processInArrayForConditionalExpressionsAfterAssign $rhsImpurePoints */ + reg::packed(2482, MAY_BE_STRING), /* processInArrayForConditionalExpressionsAfterAssign $variableName */ + reg::packed(2495, MAY_BE_ARRAY), /* processInArrayForConditionalExpressionsAfterAssign $conditionalExpressions */ + reg::packed(1672, 0, 3091), /* processInArrayForConditionalExpressionsAfterAssign $assignedExpr */ + reg::packed(3030, 0, 2038), /* processInArrayForConditionalExpressionsAfterAssign $assignedType */ + reg::packed(2546, MAY_BE_ARRAY), /* processInArrayForConditionalExpressionsAfterAssign $rhsImpurePoints */ reg::packed(1396, MAY_BE_ARRAY), /* processInArrayForConditionalExpressionsAfterAssign return */ reg::packed(1371, 0, 1376), /* isExprSafeToProjectThroughVariable $expr */ - reg::packed(2467, MAY_BE_STRING), /* isExprSafeToProjectThroughVariable $variableName */ - reg::packed(2531, MAY_BE_ARRAY), /* isExprSafeToProjectThroughVariable $rhsImpurePoints */ + reg::packed(2482, MAY_BE_STRING), /* isExprSafeToProjectThroughVariable $variableName */ + reg::packed(2546, MAY_BE_ARRAY), /* isExprSafeToProjectThroughVariable $rhsImpurePoints */ reg::packed(1672, 0, 1376), /* isExprSafeToProjectThroughVariable $assignedExpr */ reg::packed(1396, MAY_BE_BOOL), /* isExprSafeToProjectThroughVariable return */ - reg::packed(3191, MAY_BE_ARRAY), /* isImplicitArrayCreation $dimFetchStack */ - reg::packed(1484, 0, 3205), /* isImplicitArrayCreation $scope */ - reg::packed(1396, 0, 2833), /* isImplicitArrayCreation return */ + reg::packed(3206, MAY_BE_ARRAY), /* isImplicitArrayCreation $dimFetchStack */ + reg::packed(1484, 0, 3220), /* isImplicitArrayCreation $scope */ + reg::packed(1396, 0, 2848), /* isImplicitArrayCreation return */ reg::packed(1406, 0, 1424), /* processArrayByRefItems $nodeScopeResolver */ reg::packed(1484, 0, 1490), /* processArrayByRefItems $scope */ reg::packed(1521, 0, 1529), /* processArrayByRefItems $storage */ - reg::packed(3252, MAY_BE_STRING), /* processArrayByRefItems $rootVarName */ - reg::packed(3264, 0, 3274), /* processArrayByRefItems $arrayExpr */ - reg::packed(3301, 0, 1376), /* processArrayByRefItems $parentExpr */ + reg::packed(3267, MAY_BE_STRING), /* processArrayByRefItems $rootVarName */ + reg::packed(3279, 0, 3289), /* processArrayByRefItems $arrayExpr */ + reg::packed(3316, 0, 1376), /* processArrayByRefItems $parentExpr */ reg::packed(1396, 0, 1490), /* processArrayByRefItems return */ reg::packed(1406, 0, 1424), /* processArrayByRefItemsWithImplicitIndex $nodeScopeResolver */ reg::packed(1484, 0, 1490), /* processArrayByRefItemsWithImplicitIndex $scope */ reg::packed(1521, 0, 1529), /* processArrayByRefItemsWithImplicitIndex $storage */ - reg::packed(3252, MAY_BE_STRING), /* processArrayByRefItemsWithImplicitIndex $rootVarName */ - reg::packed(3264, 0, 3274), /* processArrayByRefItemsWithImplicitIndex $arrayExpr */ - reg::packed(3301, 0, 1376), /* processArrayByRefItemsWithImplicitIndex $parentExpr */ - reg::packed(3335, MAY_BE_NULL | MAY_BE_LONG), /* processArrayByRefItemsWithImplicitIndex $implicitIndex */ + reg::packed(3267, MAY_BE_STRING), /* processArrayByRefItemsWithImplicitIndex $rootVarName */ + reg::packed(3279, 0, 3289), /* processArrayByRefItemsWithImplicitIndex $arrayExpr */ + reg::packed(3316, 0, 1376), /* processArrayByRefItemsWithImplicitIndex $parentExpr */ + reg::packed(3350, MAY_BE_NULL | MAY_BE_LONG), /* processArrayByRefItemsWithImplicitIndex $implicitIndex */ reg::packed(1396, MAY_BE_ARRAY), /* processArrayByRefItemsWithImplicitIndex return */ - reg::packed(3264, 0, 3274), /* isFlattenableUnpackedArray $arrayExpr */ + reg::packed(3279, 0, 3289), /* isFlattenableUnpackedArray $arrayExpr */ reg::packed(1396, MAY_BE_BOOL), /* isFlattenableUnpackedArray return */ - reg::packed(3416, MAY_BE_LONG), /* advanceImplicitIndex $index */ + reg::packed(3431, MAY_BE_LONG), /* advanceImplicitIndex $index */ reg::packed(1396, MAY_BE_NULL | MAY_BE_LONG), /* advanceImplicitIndex return */ reg::packed(1406, 0, 1424), /* produceArrayDimFetchAssignValueToWrite $nodeScopeResolver */ - reg::packed(3191, MAY_BE_ARRAY), /* produceArrayDimFetchAssignValueToWrite $dimFetchStack */ - reg::packed(3443, MAY_BE_ARRAY), /* produceArrayDimFetchAssignValueToWrite $offsetTypes */ - reg::packed(3455, 0, 2038), /* produceArrayDimFetchAssignValueToWrite $offsetValueType */ - reg::packed(3471, 0, 2038), /* produceArrayDimFetchAssignValueToWrite $valueToWrite */ + reg::packed(3206, MAY_BE_ARRAY), /* produceArrayDimFetchAssignValueToWrite $dimFetchStack */ + reg::packed(3458, MAY_BE_ARRAY), /* produceArrayDimFetchAssignValueToWrite $offsetTypes */ + reg::packed(3470, 0, 2038), /* produceArrayDimFetchAssignValueToWrite $offsetValueType */ + reg::packed(3486, 0, 2038), /* produceArrayDimFetchAssignValueToWrite $valueToWrite */ reg::packed(1484, 0, 1490), /* produceArrayDimFetchAssignValueToWrite $scope */ reg::packed(1521, 0, 1529), /* produceArrayDimFetchAssignValueToWrite $storage */ reg::packed(1396, MAY_BE_ARRAY), /* produceArrayDimFetchAssignValueToWrite return */ reg::packed(1406, 0, 1424), /* shouldKeepList $nodeScopeResolver */ - reg::packed(3523, 0, 3537), /* shouldKeepList $arrayDimFetch */ + reg::packed(3538, 0, 3552), /* shouldKeepList $arrayDimFetch */ reg::packed(1484, 0, 1490), /* shouldKeepList $scope */ reg::packed(1521, 0, 1529), /* shouldKeepList $storage */ - reg::packed(3455, 0, 2038), /* shouldKeepList $offsetValueType */ + reg::packed(3470, 0, 2038), /* shouldKeepList $offsetValueType */ reg::packed(1396, MAY_BE_BOOL), /* shouldKeepList return */ - reg::packed(3455, 0, 2038), /* trackedLinkImpliesOffset $offsetValueType */ - reg::packed(3586, 0, 2038), /* trackedLinkImpliesOffset $offsetType */ + reg::packed(3470, 0, 2038), /* trackedLinkImpliesOffset $offsetValueType */ + reg::packed(3601, 0, 2038), /* trackedLinkImpliesOffset $offsetType */ reg::packed(1396, MAY_BE_BOOL), /* trackedLinkImpliesOffset return */ - reg::packed(3622, 0, 1376), /* isSameVariable $a */ - reg::packed(3624, 0, 1376), /* isSameVariable $b */ + reg::packed(3637, 0, 1376), /* isSameVariable $a */ + reg::packed(3639, 0, 1376), /* isSameVariable $b */ reg::packed(1396, MAY_BE_BOOL), /* isSameVariable return */ - reg::packed(3622, 0, 1376), /* isSameOffset $a */ - reg::packed(3624, 0, 1376), /* isSameOffset $b */ + reg::packed(3637, 0, 1376), /* isSameOffset $a */ + reg::packed(3639, 0, 1376), /* isSameOffset $b */ reg::packed(1396, MAY_BE_BOOL), /* isSameOffset return */ reg::packed(1371, 0, 1376), /* getLiteralArrayKeyType $expr */ reg::packed(1396, MAY_BE_NULL, 2038), /* getLiteralArrayKeyType return */ reg::packed(1406, 0, 1424), /* getOriginalPropertyType $nodeScopeResolver */ - reg::packed(3677, 0, 3691), /* getOriginalPropertyType $propertyFetch */ + reg::packed(3692, 0, 3706), /* getOriginalPropertyType $propertyFetch */ reg::packed(1484, 0, 1490), /* getOriginalPropertyType $scope */ reg::packed(1396, 0, 2038), /* getOriginalPropertyType return */ - reg::packed(3789, 0, 3274), /* hasArrayReference $array */ + reg::packed(3804, 0, 3289), /* hasArrayReference $array */ reg::packed(1396, MAY_BE_BOOL), /* hasArrayReference return */ - reg::packed(3813, 0, 1626), /* redundant $rhs */ + reg::packed(3828, 0, 1626), /* redundant $rhs */ reg::packed(1975, 0, 1376), /* redundant $target */ reg::packed(1521, 0, 1529), /* redundant $storage */ reg::packed(1396, MAY_BE_NULL, 2038), /* redundant return */ @@ -494,31 +497,32 @@ inline constexpr sigtab::Sig createArrayDimFetchConditionalExpressionHolder = { inline constexpr sigtab::Sig readAssignedValueType = { { 2303 /* readAssignedValueType */, 4, 91, 4, 95, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig resolveContainerTypesAfterAssignedExprEval = { { 2362 /* resolveContainerTypesAfterAssignedExprEval */, 6, 96, 6, 102, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig assignDynamicVariable = { { 2432 /* assignDynamicVariable */, 4, 103, 4, 107, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig unwrapAssign = { { 2454 /* unwrapAssign */, 1, 108, 1, 109, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processSureTypesForConditionalExpressionsAfterAssign = { { 2547 /* processSureTypesForConditionalExpressionsAfterAssign */, 10, 110, 10, 120, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processSureNotTypesForConditionalExpressionsAfterAssign = { { 2600 /* processSureNotTypesForConditionalExpressionsAfterAssign */, 10, 121, 10, 131, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig currentTypeForConditionalHolder = { { 2656 /* currentTypeForConditionalHolder */, 6, 132, 6, 138, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processTernaryArmValueImpliedTypesAfterAssign = { { 2732 /* processTernaryArmValueImpliedTypesAfterAssign */, 11, 139, 11, 150, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig addConditionalExpressionHolder = { { 2854 /* addConditionalExpressionHolder */, 7, 151, 7, 158, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig mergeConditionalExpressions = { { 2911 /* mergeConditionalExpressions */, 2, 159, 2, 161, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processMatchForConditionalExpressionsAfterAssign = { { 2966 /* processMatchForConditionalExpressionsAfterAssign */, 3, 162, 3, 165, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processDerivedConditionalExpressionsAfterAssign = { { 3028 /* processDerivedConditionalExpressionsAfterAssign */, 7, 166, 7, 173, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processInArrayForConditionalExpressionsAfterAssign = { { 3105 /* processInArrayForConditionalExpressionsAfterAssign */, 7, 174, 7, 181, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isExprSafeToProjectThroughVariable = { { 3156 /* isExprSafeToProjectThroughVariable */, 4, 182, 4, 186, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isImplicitArrayCreation = { { 3228 /* isImplicitArrayCreation */, 2, 187, 2, 189, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processArrayByRefItems = { { 3312 /* processArrayByRefItems */, 6, 190, 6, 196, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processArrayByRefItemsWithImplicitIndex = { { 3349 /* processArrayByRefItemsWithImplicitIndex */, 7, 197, 7, 204, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isFlattenableUnpackedArray = { { 3389 /* isFlattenableUnpackedArray */, 1, 205, 1, 206, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig advanceImplicitIndex = { { 3422 /* advanceImplicitIndex */, 1, 207, 1, 208, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig produceArrayDimFetchAssignValueToWrite = { { 3484 /* produceArrayDimFetchAssignValueToWrite */, 7, 209, 7, 216, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig shouldKeepList = { { 3571 /* shouldKeepList */, 5, 217, 5, 222, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig trackedLinkImpliesOffset = { { 3597 /* trackedLinkImpliesOffset */, 2, 223, 2, 225, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isSameVariable = { { 3626 /* isSameVariable */, 2, 226, 2, 228, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isSameOffset = { { 3641 /* isSameOffset */, 2, 229, 2, 231, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getLiteralArrayKeyType = { { 3654 /* getLiteralArrayKeyType */, 1, 232, 1, 233, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getOriginalPropertyType = { { 3765 /* getOriginalPropertyType */, 3, 234, 3, 237, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig hasArrayReference = { { 3795 /* hasArrayReference */, 1, 238, 1, 239, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig redundant = { { 3817 /* redundant */, 3, 240, 3, 243, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getWrittenExpr = { { 2454 /* getWrittenExpr */, 1, 108, 1, 109, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig unwrapAssign = { { 2469 /* unwrapAssign */, 1, 110, 1, 111, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processSureTypesForConditionalExpressionsAfterAssign = { { 2562 /* processSureTypesForConditionalExpressionsAfterAssign */, 10, 112, 10, 122, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processSureNotTypesForConditionalExpressionsAfterAssign = { { 2615 /* processSureNotTypesForConditionalExpressionsAfterAssign */, 10, 123, 10, 133, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig currentTypeForConditionalHolder = { { 2671 /* currentTypeForConditionalHolder */, 6, 134, 6, 140, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processTernaryArmValueImpliedTypesAfterAssign = { { 2747 /* processTernaryArmValueImpliedTypesAfterAssign */, 11, 141, 11, 152, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig addConditionalExpressionHolder = { { 2869 /* addConditionalExpressionHolder */, 7, 153, 7, 160, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig mergeConditionalExpressions = { { 2926 /* mergeConditionalExpressions */, 2, 161, 2, 163, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processMatchForConditionalExpressionsAfterAssign = { { 2981 /* processMatchForConditionalExpressionsAfterAssign */, 3, 164, 3, 167, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processDerivedConditionalExpressionsAfterAssign = { { 3043 /* processDerivedConditionalExpressionsAfterAssign */, 7, 168, 7, 175, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processInArrayForConditionalExpressionsAfterAssign = { { 3120 /* processInArrayForConditionalExpressionsAfterAssign */, 7, 176, 7, 183, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isExprSafeToProjectThroughVariable = { { 3171 /* isExprSafeToProjectThroughVariable */, 4, 184, 4, 188, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isImplicitArrayCreation = { { 3243 /* isImplicitArrayCreation */, 2, 189, 2, 191, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processArrayByRefItems = { { 3327 /* processArrayByRefItems */, 6, 192, 6, 198, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processArrayByRefItemsWithImplicitIndex = { { 3364 /* processArrayByRefItemsWithImplicitIndex */, 7, 199, 7, 206, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isFlattenableUnpackedArray = { { 3404 /* isFlattenableUnpackedArray */, 1, 207, 1, 208, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig advanceImplicitIndex = { { 3437 /* advanceImplicitIndex */, 1, 209, 1, 210, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig produceArrayDimFetchAssignValueToWrite = { { 3499 /* produceArrayDimFetchAssignValueToWrite */, 7, 211, 7, 218, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig shouldKeepList = { { 3586 /* shouldKeepList */, 5, 219, 5, 224, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig trackedLinkImpliesOffset = { { 3612 /* trackedLinkImpliesOffset */, 2, 225, 2, 227, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isSameVariable = { { 3641 /* isSameVariable */, 2, 228, 2, 230, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isSameOffset = { { 3656 /* isSameOffset */, 2, 231, 2, 233, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getLiteralArrayKeyType = { { 3669 /* getLiteralArrayKeyType */, 1, 234, 1, 235, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getOriginalPropertyType = { { 3780 /* getOriginalPropertyType */, 3, 236, 3, 239, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig hasArrayReference = { { 3810 /* hasArrayReference */, 1, 240, 1, 241, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig redundant = { { 3832 /* redundant */, 3, 242, 3, 245, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::AssignHandler From cded8d547a917ac5a2cf2480e8f5c1c1502bd089 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 4 Nov 2026 10:20:16 +0000 Subject: [PATCH 2/3] Test that other compound assignment operators check the same type as their expanded form Co-Authored-By: Claude Opus 5.5 --- .../TypesAssignedToPropertiesRuleTest.php | 24 ++++++++++++++++ .../Rules/Properties/data/bug-15228.php | 28 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 58c02e04740..85c59cff21e 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -1178,6 +1178,22 @@ public function testBug15228(): void 'Property Bug15228\\OffsetCoalesceAssignDemo::$a (array) does not accept array.', 80, ], + [ + 'Property Bug15228\\OtherAssignOpsDemo::$a (int|null) does not accept string.', + 98, + ], + [ + 'Property Bug15228\\OtherAssignOpsDemo::$a (int|null) does not accept string.', + 103, + ], + [ + 'Property Bug15228\\OtherAssignOpsDemo::$r (int<0, 10>) does not accept int<5, 15>.', + 108, + ], + [ + 'Property Bug15228\\OtherAssignOpsDemo::$r (int<0, 10>) does not accept int<5, 15>.', + 113, + ], ]); } @@ -1213,6 +1229,14 @@ public function testBug15228WithoutCheckUnionTypes(): void 'Property Bug15228\\OffsetCoalesceAssignDemo::$a (array) does not accept array.', 80, ], + [ + 'Property Bug15228\\OtherAssignOpsDemo::$a (int|null) does not accept string.', + 98, + ], + [ + 'Property Bug15228\\OtherAssignOpsDemo::$a (int|null) does not accept string.', + 103, + ], ]); } diff --git a/tests/PHPStan/Rules/Properties/data/bug-15228.php b/tests/PHPStan/Rules/Properties/data/bug-15228.php index 5c5e39c2624..6dcb1f1344d 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-15228.php +++ b/tests/PHPStan/Rules/Properties/data/bug-15228.php @@ -85,3 +85,31 @@ public function coalesceValid(): void $this->b['x'] ??= [1, 2]; } } + +class OtherAssignOpsDemo +{ + public ?int $a = null; + + /** @var int<0, 10> */ + public int $r = 0; + + public function concat(): void + { + $this->a .= 'x'; + } + + public function concatExpanded(): void + { + $this->a = $this->a . 'x'; + } + + public function plus(): void + { + $this->r += 5; + } + + public function plusExpanded(): void + { + $this->r = $this->r + 5; + } +} From d81c4c1a02497b90b89e94c1b64b596b15a95206 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 4 Nov 2026 12:24:32 +0000 Subject: [PATCH 3/3] Check only the right-hand side of `??=` in OffsetAccessValueAssignmentRule Co-Authored-By: Claude Opus 5.5 --- .../OffsetAccessValueAssignmentRule.php | 3 +- .../OffsetAccessValueAssignmentRuleTest.php | 14 +++++ ...ffset-access-value-assignment-coalesce.php | 55 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment-coalesce.php diff --git a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php index 71d5e48014f..ea6e39aba97 100644 --- a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php @@ -58,7 +58,8 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($node instanceof Assign || $node instanceof Expr\AssignRef) { + if ($node instanceof Assign || $node instanceof Expr\AssignRef || $node instanceof AssignOp\Coalesce) { + // `$lvalue ??= value` writes only `value` $assignedValueType = $scope->getType($node->expr); } else { $assignedValueType = $scope->getType($node); diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php index f59a882a1df..49d0486b99a 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php @@ -90,4 +90,18 @@ public function testBug10783(): void $this->analyse([__DIR__ . '/../Variables/data/bug-10783.php'], []); } + public function testCoalesceAssign(): void + { + $this->analyse([__DIR__ . '/data/offset-access-value-assignment-coalesce.php'], [ + [ + 'OffsetAccessValueAssignmentCoalesce\\Foo does not accept string.', + 40, + ], + [ + 'OffsetAccessValueAssignmentCoalesce\\Foo does not accept string.', + 53, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment-coalesce.php b/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment-coalesce.php new file mode 100644 index 00000000000..71512e74434 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment-coalesce.php @@ -0,0 +1,55 @@ + + */ +class Foo implements \ArrayAccess +{ + public function offsetExists($offset): bool + { + return true; + } + + public function offsetGet($offset): ?string + { + return 'x'; + } + + /** + * @param string|null $offset + * @param int $value + */ + public function offsetSet($offset, $value): void + { + } + + public function offsetUnset($offset): void + { + } +} + +function coalesceValid(Foo $foo): void +{ + $foo['x'] ??= 5; +} + +function coalesceInvalid(Foo $foo): void +{ + $foo['x'] ??= 'boom'; +} + +function ifValid(Foo $foo): void +{ + if (null === $foo['x']) { + $foo['x'] = 5; + } +} + +function ifInvalid(Foo $foo): void +{ + if (null === $foo['x']) { + $foo['x'] = 'boom'; + } +}