diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index bd62f071ea6..eb4fdd91f3c 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -3096,6 +3096,11 @@ private static function redundant(ExpressionResult $rhs, Expr $target, Expressio if (count($values) !== 1 || !$values[0]->equals($rhs->getType())) { return null; } + // 0.0 and -0.0 are identical (`===`) but distinguishable (`(string)`, `fdiv()`), + // and `$f === 0.0` narrows $f to 0.0 while it may still hold -0.0 + if (in_array(0.0, $values[0]->getConstantScalarValues(), true)) { + return null; + } $nativeValues = $nativeType->getFiniteTypes(); return count($nativeValues) === 1 && $nativeValues[0]->equals($rhs->getNativeType()) ? $rhs->getType() : null; } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php index 2ceca6991c7..0379c4c06b2 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php @@ -206,6 +206,14 @@ public function testRedundantAssignment(): void ]); } + public function testRedundantAssignmentOfSignedZero(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-redundant-signed-zero.php'], [ + ['Value assigned to variable $x is never read before being overwritten.', 12], + ['Variable $x is assigned value 1.5 but it already has that value.', 50], + ]); + } + public function testByRefReturn(): void { $this->analyse([__DIR__ . '/data/unused-variable-by-ref-return.php'], [ diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-variable-redundant-signed-zero.php b/tests/PHPStan/Rules/DeadCode/data/unused-variable-redundant-signed-zero.php new file mode 100644 index 00000000000..8d7b53c8911 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-variable-redundant-signed-zero.php @@ -0,0 +1,80 @@ + -0.0]; + sink($a); + $a['k'] = 0.0; + sink($a); +} + +function nonZeroFloatIsStillRedundant(): void +{ + $x = 1.5; + sink($x); + $x = 1.5; + sink($x); +} + +function zeroAtNestedOffset(): void +{ + $a = ['x' => ['y' => -0.0]]; + sink($a); + $a['x']['y'] = 0.0; + sink($a); +} + +function repeatedPositiveZeroIsNotReportedEither(): void +{ + // the type of $x cannot tell this 0.0 from a narrowed zero of unknown sign + $x = 0.0; + sink($x); + $x = 0.0; + sink($x); +} + +// https://3v4l.org/GiSbY +function h(): void { + $f = -0.0; + var_dump($f === 0.0); // bool(true) + echo $f, "\n"; // -0 + if ($f === 0.0) { + $f = 0.0; // not redundant: turns -0.0 into 0.0 + } + echo $f, "\n"; // 0 +} diff --git a/turbo-ext/src/AssignHandler.cpp b/turbo-ext/src/AssignHandler.cpp index 393a822d2b8..a4606b7f285 100644 --- a/turbo-ext/src/AssignHandler.cpp +++ b/turbo-ext/src/AssignHandler.cpp @@ -4810,6 +4810,12 @@ class AssignHandler bool equal; AH_OK(ahEquals(zend_hash_index_find(Z_ARRVAL_P(values.raw()), 0), rhsType.raw(), equal)); if (!equal) return ahNull(); + // in_array(0.0, ..., true): matches -0.0 too, never NAN; the sign of a zero is observable + AH_VAL(scalarValues, ahTypeOp(zend_hash_index_find(Z_ARRVAL_P(values.raw()), 0), PT_OP_GET_CONSTANT_SCALAR_VALUES, 0, NULL)); + for (auto entry : zv::TableRef(Z_ARRVAL_P(scalarValues.raw()))) { + zval *value = entry.value().deref().raw(); + if (Z_TYPE_P(value) == IS_DOUBLE && Z_DVAL_P(value) == 0.0) return ahNull(); + } } AH_VAL(nativeValues, ahCall(nativeType.raw(), PT_LC("getfinitetypes"), 0, NULL)); if (zend_hash_num_elements(Z_ARRVAL_P(nativeValues.raw())) != 1) return ahNull();