diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index e939a12fab9..734e482efb1 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -3027,11 +3027,31 @@ private static function redundant(ExpressionResult $rhs, Expr $target, Expressio $nativeType = $nativeType->getOffsetValueType($nativeOffset); } $values = $type->getFiniteTypes(); - if (count($values) !== 1 || !$values[0]->equals($rhs->getType())) { + if (count($values) !== 1 || !$values[0]->equals($rhs->getType()) || self::containsFloatZero($values[0])) { return null; } $nativeValues = $nativeType->getFiniteTypes(); return count($nativeValues) === 1 && $nativeValues[0]->equals($rhs->getNativeType()) ? $rhs->getType() : 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, so a type + * holding a float zero cannot prove that assigning it again changes nothing. + */ + private static function containsFloatZero(Type $type): bool + { + if (in_array(0.0, $type->getConstantScalarValues(), true)) { + return true; + } + foreach ($type->getConstantArrays() as $array) { + foreach ($array->getValueTypes() as $valueType) { + if (self::containsFloatZero($valueType)) { + return true; + } + } + } + return false; + } + } diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 2b54632598c..0be3c456b29 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '63cd54c'; + public const EXPECTED_EXTENSION_VERSION = '06be8c5'; private static bool $active = false; diff --git a/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php index a16a5965174..5cccd994698 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php @@ -201,6 +201,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.', 58], + ]); + } + 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..18323eefe56 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-variable-redundant-signed-zero.php @@ -0,0 +1,96 @@ + -0.0]; + sink($a); + $a['k'] = 0.0; + sink($a); +} + +function nonZeroFloatIsStillRedundant(): void +{ + $x = 1.5; + sink($x); + $x = 1.5; + sink($x); +} + +function zeroInNestedArray(): void +{ + $a = [[-0.0]]; + sink($a); + $a = [[0.0]]; + sink($a); +} + +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 648ad14dd3d..21e44b774d7 100644 --- a/turbo-ext/src/AssignHandler.cpp +++ b/turbo-ext/src/AssignHandler.cpp @@ -4679,6 +4679,9 @@ class AssignHandler bool equal; AH_OK(ahEquals(zend_hash_index_find(Z_ARRVAL_P(values.raw()), 0), rhsType.raw(), equal)); if (!equal) return ahNull(); + bool floatZero; + AH_OK(containsFloatZero(zend_hash_index_find(Z_ARRVAL_P(values.raw()), 0), floatZero)); + if (floatZero) 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(); @@ -4688,6 +4691,35 @@ class AssignHandler if (!equal) return ahNull(); return pt_expression_result_get_type(rhs); } + + /* (twin 3042); false = pending exception */ + static bool containsFloatZero(zval *type, bool &result) + { + result = false; + { + AH_VALB(scalarValues, ahTypeOp(type, PT_OP_GET_CONSTANT_SCALAR_VALUES, 0, NULL)); + for (auto entry : zv::TableRef(Z_ARRVAL_P(scalarValues.raw()))) { + zval *value = entry.value().deref().raw(); + // in_array(0.0, ..., true): -0.0 is identical to 0.0, NAN to nothing + if (Z_TYPE_P(value) == IS_DOUBLE && Z_DVAL_P(value) == 0.0) { + result = true; + return true; + } + } + } + AH_VALB(constantArrays, ahTypeOp(type, PT_OP_GET_CONSTANT_ARRAYS, 0, NULL)); + for (auto arrayEntry : zv::TableRef(Z_ARRVAL_P(constantArrays.raw()))) { + AH_VALB(valueTypes, ahTypeOp(arrayEntry.value().deref().raw(), PT_OP_GET_VALUE_TYPES, 0, NULL)); + for (auto entry : zv::TableRef(Z_ARRVAL_P(valueTypes.raw()))) { + zval *valueType = entry.value().deref().raw(); + bool ok = true; + pt_engine_with_stack([&]() { ok = containsFloatZero(valueType, result); }); + AH_OKB(ok); + if (result) return true; + } + } + return true; + } }; } // namespace phpstanturbo diff --git a/turbo-ext/src/generated/AssignHandler.h b/turbo-ext/src/generated/AssignHandler.h index 5edaadc5cbe..4110afacd9e 100644 --- a/turbo-ext/src/generated/AssignHandler.h +++ b/turbo-ext/src/generated/AssignHandler.h @@ -227,7 +227,9 @@ inline constexpr char strings[] = "array\0" /* 3789 */ "hasArrayReference\0" /* 3795 */ "rhs\0" /* 3813 */ - "redundant"; /* 3817 */ + "redundant\0" /* 3817 */ + "type\0" /* 3827 */ + "containsFloatZero"; /* 3832 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 25), /* __construct $templateArgumentObserver */ reg::packed(76, 0, 99), /* __construct $varAnnotationProcessor */ @@ -473,6 +475,8 @@ inline constexpr reg::PackedArg args[] = { reg::packed(1975, 0, 1376), /* redundant $target */ reg::packed(1521, 0, 1529), /* redundant $storage */ reg::packed(1396, MAY_BE_NULL, 2038), /* redundant return */ + reg::packed(3827, 0, 2038), /* containsFloatZero $type */ + reg::packed(1396, MAY_BE_BOOL), /* containsFloatZero return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -519,6 +523,7 @@ inline constexpr sigtab::Sig getLiteralArrayKeyType = { { 3654 /* getLiteralArra 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 containsFloatZero = { { 3832 /* containsFloatZero */, 1, 244, 1, 245, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::AssignHandler