diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 51995ac6657..fcc12f5b12b 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -241,6 +241,12 @@ public function processAssignVar( $assignedExpr = $this->unwrapAssign($assignedExpr); $type = $scopeBeforeAssignEval->getType($assignedExpr); + $nativeType = $scope->getNativeType($assignedExpr); + if ($isAssignOp && $type instanceof ErrorType) { + $type = $scopeBeforeAssignEval->getType($var); + $nativeType = $scopeBeforeAssignEval->getNativeType($var); + } + $conditionalExpressions = []; if ($assignedExpr instanceof Ternary) { $if = $assignedExpr->if; @@ -314,7 +320,7 @@ public function processAssignVar( } $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage); - $scope = $scope->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr), TrinaryLogic::createYes()); + $scope = $scope->assignVariable($var->name, $type, $nativeType, TrinaryLogic::createYes()); foreach ($conditionalExpressions as $exprString => $holders) { $scope = $scope->addConditionalExpressions($exprString, $holders); } @@ -414,6 +420,28 @@ public function processAssignVar( $valueToWrite = $scope->getType($assignedExpr); $nativeValueToWrite = $scope->getNativeType($assignedExpr); + + if ($isAssignOp && $valueToWrite instanceof ErrorType) { + $originalType = $scope->getType($var); + foreach ($offsetTypes as [$offsetType]) { + if ($offsetType === null) { + break; + } + $originalType = $originalType->getOffsetValueType($offsetType); + } + $valueToWrite = $originalType; + } + if ($isAssignOp && $nativeValueToWrite instanceof ErrorType) { + $originalNativeType = $scope->getNativeType($var); + foreach ($offsetNativeTypes as [$offsetNativeType]) { + if ($offsetNativeType === null) { + break; + } + $originalNativeType = $originalNativeType->getOffsetValueType($offsetNativeType); + } + $nativeValueToWrite = $originalNativeType; + } + $scopeBeforeAssignEval = $scope; // 3. eval assigned expr @@ -781,6 +809,21 @@ public function processAssignVar( $varType = $scope->getType($var); $varNativeType = $scope->getNativeType($var); + if ($isAssignOp && $valueToWrite instanceof ErrorType) { + $originalType = $varType; + foreach ($offsetTypes as [$offsetType]) { + $originalType = $originalType->getOffsetValueType($offsetType); + } + $valueToWrite = $originalType; + } + if ($isAssignOp && $nativeValueToWrite instanceof ErrorType) { + $originalNativeType = $varNativeType; + foreach ($offsetNativeTypes as [$offsetNativeType]) { + $originalNativeType = $originalNativeType->getOffsetValueType($offsetNativeType); + } + $nativeValueToWrite = $originalNativeType; + } + $offsetValueType = $varType; $offsetNativeValueType = $varNativeType; $offsetValueTypeStack = [$offsetValueType]; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10349.php b/tests/PHPStan/Analyser/nsrt/bug-10349.php new file mode 100644 index 00000000000..2db8674eb0c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10349.php @@ -0,0 +1,34 @@ +> $expected + */ + public function testTypePreservationAfterErrorAssignOp(array $expected, int $ptr, string $key): void + { + assertType('array>', $expected); + + $expected[$key]['number-1'] += $ptr; + + // After += with ErrorType result, the array type should be preserved + assertType('bool|float|int|string', $expected[$key]['number-2']); + } + + /** + * @param array $arr + */ + public function testSimpleArrayTypePreservation(array $arr, int $ptr): void + { + assertType('bool|float|int|string', $arr['a']); + + $arr['a'] += $ptr; + + // After += with ErrorType result, sibling keys should keep their type + assertType('bool|float|int|string', $arr['b']); + } +} diff --git a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php index 735f7577116..64694f3b2d1 100644 --- a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php @@ -835,4 +835,86 @@ public function testBug14080(): void $this->analyse([__DIR__ . '/data/bug-14080.php'], []); } + public function testBug10349(): void + { + $this->analyse([__DIR__ . '/data/bug-10349.php'], [ + [ + 'Binary operation "+=" between bool|float|int|string and int results in an error.', + 15, + ], + [ + 'Binary operation "+=" between bool|float|int|string and int results in an error.', + 20, + ], + [ + 'Binary operation "+=" between bool|float|int|string and int results in an error.', + 37, + ], + [ + 'Binary operation "+=" between bool|float|int|string and int results in an error.', + 47, + ], + [ + 'Binary operation "+=" between bool|float|int|string and int results in an error.', + 49, + ], + [ + 'Binary operation "+=" between bool|float|int|string and int results in an error.', + 57, + ], + [ + 'Binary operation "+=" between bool|float|int|string and int results in an error.', + 59, + ], + [ + 'Binary operation "-=" between bool|float|int|string and int results in an error.', + 67, + ], + [ + 'Binary operation "-=" between bool|float|int|string and int results in an error.', + 68, + ], + [ + 'Binary operation "*=" between bool|float|int|string and int results in an error.', + 69, + ], + [ + 'Binary operation "*=" between bool|float|int|string and int results in an error.', + 70, + ], + [ + 'Binary operation ".=" between array|int and \'foo\' results in an error.', + 78, + ], + [ + 'Binary operation ".=" between array|int and \'foo\' results in an error.', + 79, + ], + [ + 'Binary operation "/=" between bool|float|int|string and int results in an error.', + 87, + ], + [ + 'Binary operation "/=" between bool|float|int|string and int results in an error.', + 88, + ], + [ + 'Binary operation "%=" between bool|float|int|string and int results in an error.', + 89, + ], + [ + 'Binary operation "%=" between bool|float|int|string and int results in an error.', + 90, + ], + [ + 'Binary operation "<<=" between bool|float|int|string and int results in an error.', + 98, + ], + [ + 'Binary operation "<<=" between bool|float|int|string and int results in an error.', + 99, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Operators/data/bug-10349.php b/tests/PHPStan/Rules/Operators/data/bug-10349.php new file mode 100644 index 00000000000..1404d8ed612 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-10349.php @@ -0,0 +1,101 @@ +> $expected + */ + public function issue1A(array $expected, int $ptr): void + { + foreach ($expected as $key => $param) { + if ($param['number-1'] !== false) { + // This gets flagged + $expected[$key]['number-1'] += $ptr; + } + + if ($param['number-2'] !== false) { + // This should also get flagged but doesn't + $expected[$key]['number-2'] += $ptr; + } + } + } + + /** + * @param array> $expected + */ + public function issue1B(array $expected, int $ptr): void + { + foreach ($expected as $key => $param) { + if (is_int($expected[$key]['number-1'])) { + $expected[$key]['number-1'] += $ptr; + } + + // Even after fixing the first, the second one still doesn't get flagged + if ($param['number-2'] !== false) { + $expected[$key]['number-2'] += $ptr; + } + } + } + + /** + * @param array> $expected + */ + public function multipleOpsNoLoop(array $expected, int $ptr, string $key): void + { + $expected[$key]['number-1'] += $ptr; + // After the first += corrupts the array type, this should still be flagged + $expected[$key]['number-2'] += $ptr; + } + + /** + * @param array $arr + */ + public function simpleArray(array $arr, int $ptr): void + { + $arr['a'] += $ptr; + // After the first += corrupts the array type, this should still be flagged + $arr['b'] += $ptr; + } + + /** + * @param array $arr + */ + public function otherAssignOps(array $arr, int $ptr): void + { + $arr['a'] -= $ptr; + $arr['b'] -= $ptr; + $arr['c'] *= $ptr; + $arr['d'] *= $ptr; + } + + /** + * @param array|int> $arr + */ + public function concatAssignOps(array $arr): void + { + $arr['a'] .= 'foo'; + $arr['b'] .= 'foo'; + } + + /** + * @param array $arr + */ + public function divAndModAssignOps(array $arr, int $ptr): void + { + $arr['a'] /= $ptr; + $arr['b'] /= $ptr; + $arr['c'] %= $ptr; + $arr['d'] %= $ptr; + } + + /** + * @param array $arr + */ + public function bitwiseAssignOps(array $arr, int $ptr): void + { + $arr['a'] <<= $ptr; + $arr['b'] <<= $ptr; + } +}