From a683afb5ed2abb9981b6881b4e4c588b59e35039 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 9 Aug 2025 17:59:55 +0200 Subject: [PATCH] Better support for -0.0 --- src/Analyser/TypeSpecifier.php | 22 ++++++-- .../InitializerExprTypeResolver.php | 4 +- src/Type/Constant/ConstantFloatType.php | 14 ++++- src/Type/NullType.php | 2 + src/Type/StaticTypeFactory.php | 1 + .../ConstantNumericComparisonTypeTrait.php | 3 + src/Type/UnionTypeHelper.php | 6 ++ tests/PHPStan/Analyser/TypeSpecifierTest.php | 26 ++++----- tests/PHPStan/Analyser/nsrt/assert-empty.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-10189.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-12225.php | 44 +++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-3991.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-4117.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-pr-339.php | 8 +-- .../Analyser/nsrt/comparison-operators.php | 2 +- .../Analyser/nsrt/composer-array-bug.php | 4 +- .../composer-non-empty-array-after-unset.php | 2 +- tests/PHPStan/Analyser/nsrt/equal-narrow.php | 56 +++++++++---------- .../Analyser/nsrt/integer-range-types.php | 6 +- tests/PHPStan/Analyser/nsrt/math.php | 10 ++-- .../PHPStan/Analyser/nsrt/mixed-subtract.php | 12 ++-- tests/PHPStan/Analyser/nsrt/more-types.php | 4 +- .../Analyser/nsrt/non-empty-string.php | 2 +- tests/PHPStan/Type/TypeCombinatorTest.php | 8 +-- tests/PHPStan/Type/UnionTypeTest.php | 1 + 25 files changed, 167 insertions(+), 84 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12225.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index e946229f7d..01b102e127 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1990,6 +1990,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), + new ConstantFloatType(-0.0), new ConstantFloatType(0.0), new ConstantStringType(''), new ConstantArrayType([], []), @@ -2022,6 +2023,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), + new ConstantFloatType(-0.0), new ConstantFloatType(0.0), new StringType(), ]; @@ -2030,6 +2032,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), + new ConstantFloatType(-0.0), new ConstantFloatType(0.0), new ConstantStringType('0'), ]; @@ -2046,6 +2049,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), + new ConstantFloatType(-0.0), new ConstantFloatType(0.0), new ConstantStringType(''), ]; @@ -2474,16 +2478,21 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty && !$rightType->equals($leftType) && $rightType->isSuperTypeOf($leftType)->yes()) ) { + $typeToSet = $leftType; + if (in_array(0.0, $leftType->getConstantScalarValues(), true)) { + $typeToSet = TypeCombinator::union(new ConstantFloatType(-0.0), new ConstantFloatType(0.0), $typeToSet); + } + $types = $this->create( $rightExpr, - $leftType, + $typeToSet, $context, $scope, )->setRootExpr($expr); if ($rightExpr instanceof AlwaysRememberedExpr) { $types = $types->unionWith($this->create( $unwrappedRightExpr, - $leftType, + $typeToSet, $context, $scope, ))->setRootExpr($expr); @@ -2498,16 +2507,21 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty && $leftType->isSuperTypeOf($rightType)->yes() ) ) { + $typeToSet = $rightType; + if (in_array(0.0, $rightType->getConstantScalarValues(), true)) { + $typeToSet = TypeCombinator::union(new ConstantFloatType(-0.0), new ConstantFloatType(0.0), $typeToSet); + } + $leftTypes = $this->create( $leftExpr, - $rightType, + $typeToSet, $context, $scope, )->setRootExpr($expr); if ($leftExpr instanceof AlwaysRememberedExpr) { $leftTypes = $leftTypes->unionWith($this->create( $unwrappedLeftExpr, - $rightType, + $typeToSet, $context, $scope, ))->setRootExpr($expr); diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index af6302edfc..2c5e0f3c52 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -1236,14 +1236,14 @@ public function getMulType(Expr $left, Expr $right, callable $getTypeCallback): $leftNumberType = $leftType->toNumber(); if ($leftNumberType instanceof ConstantIntegerType && $leftNumberType->getValue() === 0) { if ($rightType->isFloat()->yes()) { - return new ConstantFloatType(0.0); + return new UnionType([new ConstantFloatType(-0.0), new ConstantFloatType(0.0)]); } return new ConstantIntegerType(0); } $rightNumberType = $rightType->toNumber(); if ($rightNumberType instanceof ConstantIntegerType && $rightNumberType->getValue() === 0) { if ($leftType->isFloat()->yes()) { - return new ConstantFloatType(0.0); + return new UnionType([new ConstantFloatType(-0.0), new ConstantFloatType(0.0)]); } return new ConstantIntegerType(0); } diff --git a/src/Type/Constant/ConstantFloatType.php b/src/Type/Constant/ConstantFloatType.php index 0ac763af76..bf4c4db553 100644 --- a/src/Type/Constant/ConstantFloatType.php +++ b/src/Type/Constant/ConstantFloatType.php @@ -40,7 +40,19 @@ public function getValue(): float public function equals(Type $type): bool { - return $type instanceof self && ($this->value === $type->value || is_nan($this->value) && is_nan($type->value)); + if (!$type instanceof self) { + return false; + } + + if (is_nan($this->value)) { + return is_nan($type->value); + } + + if ($this->value === 0.0 && $type->value === 0.0) { + return (string) $this->value === (string) $type->value; + } + + return $this->value === $type->value; } private function castFloatToString(float $value): string diff --git a/src/Type/NullType.php b/src/Type/NullType.php index ef2408e713..05132e3c3c 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -365,6 +365,7 @@ public function getSmallerOrEqualType(PhpVersion $phpVersion): Type new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), + new ConstantFloatType(-0.0), new ConstantFloatType(0.0), new ConstantStringType(''), new ConstantArrayType([], []), @@ -378,6 +379,7 @@ public function getGreaterType(PhpVersion $phpVersion): Type new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), + new ConstantFloatType(-0.0), new ConstantFloatType(0.0), new ConstantStringType(''), new ConstantArrayType([], []), diff --git a/src/Type/StaticTypeFactory.php b/src/Type/StaticTypeFactory.php index 93fe12d555..ba87092412 100644 --- a/src/Type/StaticTypeFactory.php +++ b/src/Type/StaticTypeFactory.php @@ -20,6 +20,7 @@ public static function falsey(): Type new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), + new ConstantFloatType(-0.0), new ConstantFloatType(0.0), new ConstantStringType(''), new ConstantStringType('0'), diff --git a/src/Type/Traits/ConstantNumericComparisonTypeTrait.php b/src/Type/Traits/ConstantNumericComparisonTypeTrait.php index c6efe96950..2cce73dd4e 100644 --- a/src/Type/Traits/ConstantNumericComparisonTypeTrait.php +++ b/src/Type/Traits/ConstantNumericComparisonTypeTrait.php @@ -24,6 +24,7 @@ public function getSmallerType(PhpVersion $phpVersion): Type if (!(bool) $this->value) { $subtractedTypes[] = new NullType(); $subtractedTypes[] = new ConstantBooleanType(false); + $subtractedTypes[] = new ConstantFloatType(-0.0); // subtract range when we support float-ranges $subtractedTypes[] = new ConstantFloatType(0.0); // subtract range when we support float-ranges } @@ -49,6 +50,7 @@ public function getGreaterType(PhpVersion $phpVersion): Type $subtractedTypes = [ new NullType(), new ConstantBooleanType(false), + new ConstantFloatType(-0.0), // subtract range when we support float-ranges new ConstantFloatType(0.0), // subtract range when we support float-ranges IntegerRangeType::createAllSmallerThanOrEqualTo($this->value), ]; @@ -69,6 +71,7 @@ public function getGreaterOrEqualType(PhpVersion $phpVersion): Type if ((bool) $this->value) { $subtractedTypes[] = new NullType(); $subtractedTypes[] = new ConstantBooleanType(false); + $subtractedTypes[] = new ConstantFloatType(-0.0); // subtract range when we support float-ranges $subtractedTypes[] = new ConstantFloatType(0.0); // subtract range when we support float-ranges } diff --git a/src/Type/UnionTypeHelper.php b/src/Type/UnionTypeHelper.php index f91ea5cb45..890cafa064 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -76,6 +76,12 @@ public static function sortTypes(array $types): array if ($b instanceof ConstantIntegerType && $a instanceof ConstantFloatType) { return 1; } + if ((string) $a->getValue() === '-0' && (string) $b->getValue() === '0') { + return -1; + } + if ((string) $b->getValue() === '-0' && (string) $a->getValue() === '0') { + return 1; + } return 0; } diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 5a06bfb0aa..78a5af392a 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -46,7 +46,7 @@ class TypeSpecifierTest extends PHPStanTestCase { - private const FALSEY_TYPE_DESCRIPTION = '0|0.0|\'\'|\'0\'|array{}|false|null'; + private const FALSEY_TYPE_DESCRIPTION = '0|-0.0|0.0|\'\'|\'0\'|array{}|false|null'; private const TRUTHY_TYPE_DESCRIPTION = 'mixed~(' . self::FALSEY_TYPE_DESCRIPTION . ')'; private const SURE_NOT_FALSEY = '~' . self::FALSEY_TYPE_DESCRIPTION; private const SURE_NOT_TRUTHY = '~' . self::TRUTHY_TYPE_DESCRIPTION; @@ -481,8 +481,8 @@ public static function dataCondition(): iterable new Variable('foo'), new Expr\ConstFetch(new Name('null')), ), - ['$foo' => '0|0.0|\'\'|array{}|false|null'], - ['$foo' => '~0|0.0|\'\'|array{}|false|null'], + ['$foo' => '0|-0.0|0.0|\'\'|array{}|false|null'], + ['$foo' => '~0|-0.0|0.0|\'\'|array{}|false|null'], ], [ new Expr\BinaryOp\Identical( @@ -633,7 +633,7 @@ public static function dataCondition(): iterable [ new Expr\BooleanNot(new Expr\Empty_(new Variable('stringOrNull'))), [ - '$stringOrNull' => '~0|0.0|\'\'|\'0\'|array{}|false|null', + '$stringOrNull' => '~0|-0.0|0.0|\'\'|\'0\'|array{}|false|null', ], [ '$stringOrNull' => '\'\'|\'0\'|null', @@ -655,13 +655,13 @@ public static function dataCondition(): iterable '$array' => 'array{}|null', ], [ - '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', + '$array' => '~0|-0.0|0.0|\'\'|\'0\'|array{}|false|null', ], ], [ new BooleanNot(new Expr\Empty_(new Variable('array'))), [ - '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', + '$array' => '~0|-0.0|0.0|\'\'|\'0\'|array{}|false|null', ], [ '$array' => 'array{}|null', @@ -826,7 +826,7 @@ public static function dataCondition(): iterable '$n' => 'mixed~(int<3, max>|true)', ], [ - '$n' => 'mixed~(0.0|int|false|null)', + '$n' => 'mixed~(-0.0|0.0|int|false|null)', ], ], [ @@ -838,7 +838,7 @@ public static function dataCondition(): iterable '$n' => 'mixed~(int<' . PHP_INT_MIN . ', max>|true)', ], [ - '$n' => 'mixed~(0.0|false|null)', + '$n' => 'mixed~(-0.0|0.0|false|null)', ], ], [ @@ -847,7 +847,7 @@ public static function dataCondition(): iterable new LNumber(PHP_INT_MAX), ), [ - '$n' => 'mixed~(0.0|bool|int|null)', + '$n' => 'mixed~(-0.0|0.0|bool|int|null)', ], [ '$n' => 'mixed', @@ -862,7 +862,7 @@ public static function dataCondition(): iterable '$n' => 'mixed~int<' . (PHP_INT_MIN + 1) . ', max>', ], [ - '$n' => 'mixed~(0.0|bool|int|null)', + '$n' => 'mixed~(-0.0|0.0|bool|int|null)', ], ], [ @@ -871,7 +871,7 @@ public static function dataCondition(): iterable new LNumber(PHP_INT_MAX), ), [ - '$n' => 'mixed~(0.0|int|false|null)', + '$n' => 'mixed~(-0.0|0.0|int|false|null)', ], [ '$n' => 'mixed~(int<' . PHP_INT_MAX . ', max>|true)', @@ -889,7 +889,7 @@ public static function dataCondition(): iterable ), ), [ - '$n' => 'mixed~(0.0|int|int<6, max>|false|null)', + '$n' => 'mixed~(-0.0|0.0|int|int<6, max>|false|null)', ], [ '$n' => 'mixed~(int<3, 5>|true)', @@ -1252,7 +1252,7 @@ public static function dataCondition(): iterable ), [ '$foo' => 'non-empty-array', - 'count($foo)' => 'mixed~(0.0|int|false|null)', + 'count($foo)' => 'mixed~(-0.0|0.0|int|false|null)', ], [], ], diff --git a/tests/PHPStan/Analyser/nsrt/assert-empty.php b/tests/PHPStan/Analyser/nsrt/assert-empty.php index a74e4c1e35..3d0bdd2132 100644 --- a/tests/PHPStan/Analyser/nsrt/assert-empty.php +++ b/tests/PHPStan/Analyser/nsrt/assert-empty.php @@ -20,10 +20,10 @@ function assertNotEmpty(mixed $var): void function ($var) { assertEmpty($var); - assertType("0|0.0|''|'0'|array{}|false|null", $var); + assertType("0|-0.0|0.0|''|'0'|array{}|false|null", $var); }; function ($var) { assertNotEmpty($var); - assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $var); + assertType("mixed~(0|-0.0|0.0|''|'0'|array{}|false|null)", $var); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10189.php b/tests/PHPStan/Analyser/nsrt/bug-10189.php index 6cb97fa4de..b178f63870 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10189.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10189.php @@ -20,7 +20,7 @@ function file_managed_file_save_upload(): array { } assertType('non-empty-array|Bug10189\SomeInterface', $files); $files = array_filter($files); - assertType("array", $files); + assertType("array", $files); return empty($files) ? [] : [1,2]; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12225.php b/tests/PHPStan/Analyser/nsrt/bug-12225.php new file mode 100644 index 0000000000..34f2707d27 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12225.php @@ -0,0 +1,44 @@ +getType() which is `stdClass|array|null` here @@ -26,7 +26,7 @@ public static function email($config = null) assertType('array{}|null', $config); $config = new \stdClass(); } elseif (! (is_array($config) || $config instanceof \stdClass)) { - assertNativeType('mixed~(0|0.0|\'\'|\'0\'|array{}|stdClass|false|null)', $config); + assertNativeType('mixed~(0|-0.0|0.0|\'\'|\'0\'|array{}|stdClass|false|null)', $config); assertType('*NEVER*', $config); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4117.php b/tests/PHPStan/Analyser/nsrt/bug-4117.php index 14732b77b6..eee161d747 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4117.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4117.php @@ -32,9 +32,9 @@ public function broken(int $key) $item = $this->items[$key] ?? null; assertType('T of mixed~null (class Bug4117Types\GenericList, argument)|null', $item); if ($item) { - assertType("T of mixed~(0|0.0|''|'0'|array{}|false|null) (class Bug4117Types\GenericList, argument)", $item); + assertType("T of mixed~(0|-0.0|0.0|''|'0'|array{}|false|null) (class Bug4117Types\GenericList, argument)", $item); } else { - assertType("(T of mixed~null (class Bug4117Types\GenericList, argument)&false)|(0.0&T of mixed~null (class Bug4117Types\GenericList, argument))|(0&T of mixed~null (class Bug4117Types\GenericList, argument))|(list{}&T of mixed~null (class Bug4117Types\GenericList, argument))|(''&T of mixed~null (class Bug4117Types\GenericList, argument))|('0'&T of mixed~null (class Bug4117Types\GenericList, argument))|null", $item); + assertType("(T of mixed~null (class Bug4117Types\GenericList, argument)&false)|(-0.0&T of mixed~null (class Bug4117Types\GenericList, argument))|(0.0&T of mixed~null (class Bug4117Types\GenericList, argument))|(0&T of mixed~null (class Bug4117Types\GenericList, argument))|(list{}&T of mixed~null (class Bug4117Types\GenericList, argument))|(''&T of mixed~null (class Bug4117Types\GenericList, argument))|('0'&T of mixed~null (class Bug4117Types\GenericList, argument))|null", $item); } assertType('T of mixed~null (class Bug4117Types\GenericList, argument)|null', $item); diff --git a/tests/PHPStan/Analyser/nsrt/bug-pr-339.php b/tests/PHPStan/Analyser/nsrt/bug-pr-339.php index 4d841ad783..d55e3120e0 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-pr-339.php +++ b/tests/PHPStan/Analyser/nsrt/bug-pr-339.php @@ -17,17 +17,17 @@ assertType('mixed', $a); assertType('mixed', $c); if ($a) { - assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $a); + assertType("mixed~(0|-0.0|0.0|''|'0'|array{}|false|null)", $a); assertType('mixed', $c); assertVariableCertainty(TrinaryLogic::createYes(), $a); } if ($c) { assertType('mixed', $a); - assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $c); + assertType("mixed~(0|-0.0|0.0|''|'0'|array{}|false|null)", $c); assertVariableCertainty(TrinaryLogic::createYes(), $c); } } else { - assertType("0|0.0|''|'0'|array{}|false|null", $a); - assertType("0|0.0|''|'0'|array{}|false|null", $c); + assertType("0|-0.0|0.0|''|'0'|array{}|false|null", $a); + assertType("0|-0.0|0.0|''|'0'|array{}|false|null", $c); } diff --git a/tests/PHPStan/Analyser/nsrt/comparison-operators.php b/tests/PHPStan/Analyser/nsrt/comparison-operators.php index 14488df983..9533a77c80 100644 --- a/tests/PHPStan/Analyser/nsrt/comparison-operators.php +++ b/tests/PHPStan/Analyser/nsrt/comparison-operators.php @@ -122,7 +122,7 @@ public function null(?int $i, ?float $f, ?string $s, ?bool $b): void assertType('*NEVER*', $f); } if ($f <= null) { - assertType('0.0|null', $f); + assertType('-0.0|0.0|null', $f); } if ($s > null) { diff --git a/tests/PHPStan/Analyser/nsrt/composer-array-bug.php b/tests/PHPStan/Analyser/nsrt/composer-array-bug.php index 0873226897..e93b6c876e 100644 --- a/tests/PHPStan/Analyser/nsrt/composer-array-bug.php +++ b/tests/PHPStan/Analyser/nsrt/composer-array-bug.php @@ -16,7 +16,7 @@ class Foo public function doFoo(): void { if (!empty($this->config['authors'])) { - assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $this->config['authors']); + assertType("mixed~(0|-0.0|0.0|''|'0'|array{}|false|null)", $this->config['authors']); foreach ($this->config['authors'] as $key => $author) { assertType("mixed", $this->config['authors']); @@ -54,7 +54,7 @@ public function doFoo(): void unset($this->config['authors']); assertType("array", $this->config); } else { - assertType("non-empty-array&hasOffsetValue('authors', mixed~(0|0.0|''|'0'|array{}|false|null))", $this->config); + assertType("non-empty-array&hasOffsetValue('authors', mixed~(0|-0.0|0.0|''|'0'|array{}|false|null))", $this->config); } assertType("array", $this->config); diff --git a/tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php b/tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php index 0cec47c79a..62c93046d0 100644 --- a/tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php +++ b/tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php @@ -13,7 +13,7 @@ class Foo public function doFoo() { if (!empty($this->config['authors'])) { - assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $this->config['authors']); + assertType("mixed~(0|-0.0|0.0|''|'0'|array{}|false|null)", $this->config['authors']); foreach ($this->config['authors'] as $key => $author) { assertType("mixed", $this->config['authors']); if (!is_array($author)) { diff --git a/tests/PHPStan/Analyser/nsrt/equal-narrow.php b/tests/PHPStan/Analyser/nsrt/equal-narrow.php index 774377f400..57e3f534e7 100644 --- a/tests/PHPStan/Analyser/nsrt/equal-narrow.php +++ b/tests/PHPStan/Analyser/nsrt/equal-narrow.php @@ -11,21 +11,21 @@ use function PHPStan\Testing\assertType; /** - * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param 0|-0.0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x * @param int|string|null $y * @param mixed $z */ function doNull($x, $y, $z): void { if ($x == null) { - assertType("0|0.0|''|array{}|false|null", $x); + assertType("0|-0.0|0.0|''|array{}|false|null", $x); } else { assertType("1|'0'|'x'|object|true", $x); } if (null != $x) { assertType("1|'0'|'x'|object|true", $x); } else { - assertType("0|0.0|''|array{}|false|null", $x); + assertType("0|-0.0|0.0|''|array{}|false|null", $x); } if ($y == null) { @@ -35,32 +35,32 @@ function doNull($x, $y, $z): void } if ($z == null) { - assertType("0|0.0|''|array{}|false|null", $z); + assertType("0|-0.0|0.0|''|array{}|false|null", $z); } else { - assertType("mixed~(0|0.0|''|array{}|false|null)", $z); + assertType("mixed~(0|-0.0|0.0|''|array{}|false|null)", $z); } } /** - * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param 0|-0.0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x * @param int|string|null $y * @param mixed $z */ function doFalse($x, $y, $z): void { if ($x == false) { - assertType("0|0.0|''|'0'|array{}|false|null", $x); + assertType("0|-0.0|0.0|''|'0'|array{}|false|null", $x); } else { assertType("1|'x'|object|true", $x); } if (false != $x) { assertType("1|'x'|object|true", $x); } else { - assertType("0|0.0|''|'0'|array{}|false|null", $x); + assertType("0|-0.0|0.0|''|'0'|array{}|false|null", $x); } if (!$x) { - assertType("0|0.0|''|'0'|array{}|false|null", $x); + assertType("0|-0.0|0.0|''|'0'|array{}|false|null", $x); } else { assertType("1|'x'|object|true", $x); } @@ -72,14 +72,14 @@ function doFalse($x, $y, $z): void } if ($z == false) { - assertType("0|0.0|''|'0'|array{}|false|null", $z); + assertType("0|-0.0|0.0|''|'0'|array{}|false|null", $z); } else { - assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $z); + assertType("mixed~(0|-0.0|0.0|''|'0'|array{}|false|null)", $z); } } /** - * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param 0|-0.0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x * @param int|string|null $y * @param mixed $z */ @@ -88,10 +88,10 @@ function doTrue($x, $y, $z): void if ($x == true) { assertType("1|'x'|object|true", $x); } else { - assertType("0|0.0|''|'0'|array{}|false|null", $x); + assertType("0|-0.0|0.0|''|'0'|array{}|false|null", $x); } if (true != $x) { - assertType("0|0.0|''|'0'|array{}|false|null", $x); + assertType("0|-0.0|0.0|''|'0'|array{}|false|null", $x); } else { assertType("1|'x'|object|true", $x); } @@ -99,7 +99,7 @@ function doTrue($x, $y, $z): void if ($x) { assertType("1|'x'|object|true", $x); } else { - assertType("0|0.0|''|'0'|array{}|false|null", $x); + assertType("0|-0.0|0.0|''|'0'|array{}|false|null", $x); } if ($y == true) { @@ -109,14 +109,14 @@ function doTrue($x, $y, $z): void } if ($z == true) { - assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $z); + assertType("mixed~(0|-0.0|0.0|''|'0'|array{}|false|null)", $z); } else { - assertType("0|0.0|''|'0'|array{}|false|null", $z); + assertType("0|-0.0|0.0|''|'0'|array{}|false|null", $z); } } /** - * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param 0|-0.0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x * @param int|string|null $y * @param mixed $z */ @@ -124,14 +124,14 @@ function doZero($x, $y, $z): void { // PHP 7.x/8.x compatibility: Keep zero in both cases if ($x == 0) { - assertType("0|0.0|''|'0'|'x'|false|null", $x); + assertType("0|-0.0|0.0|''|'0'|'x'|false|null", $x); } else { assertType("1|''|'x'|array{}|object|true", $x); } if (0 != $x) { assertType("1|''|'x'|array{}|object|true", $x); } else { - assertType("0|0.0|''|'0'|'x'|false|null", $x); + assertType("0|-0.0|0.0|''|'0'|'x'|false|null", $x); } if ($y == 0) { @@ -141,14 +141,14 @@ function doZero($x, $y, $z): void } if ($z == 0) { - assertType("0|0.0|string|false|null", $z); + assertType("0|-0.0|0.0|string|false|null", $z); } else { - assertType("mixed~(0|0.0|'0'|false|null)", $z); + assertType("mixed~(0|-0.0|0.0|'0'|false|null)", $z); } } /** - * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param 0|-0.0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x * @param int|string|null $y * @param mixed $z */ @@ -156,14 +156,14 @@ function doEmptyString($x, $y, $z): void { // PHP 7.x/8.x compatibility: Keep zero in both cases if ($x == '') { - assertType("0|0.0|''|false|null", $x); + assertType("0|-0.0|0.0|''|false|null", $x); } else { - assertType("0|0.0|1|'0'|'x'|array{}|object|true", $x); + assertType("0|-0.0|0.0|1|'0'|'x'|array{}|object|true", $x); } if ('' != $x) { - assertType("0|0.0|1|'0'|'x'|array{}|object|true", $x); + assertType("0|-0.0|0.0|1|'0'|'x'|array{}|object|true", $x); } else { - assertType("0|0.0|''|false|null", $x); + assertType("0|-0.0|0.0|''|false|null", $x); } if ($y == '') { @@ -173,7 +173,7 @@ function doEmptyString($x, $y, $z): void } if ($z == '') { - assertType("0|0.0|''|false|null", $z); + assertType("0|-0.0|0.0|''|false|null", $z); } else { assertType("mixed~(''|false|null)", $z); } diff --git a/tests/PHPStan/Analyser/nsrt/integer-range-types.php b/tests/PHPStan/Analyser/nsrt/integer-range-types.php index 876a791352..c4350e40c3 100644 --- a/tests/PHPStan/Analyser/nsrt/integer-range-types.php +++ b/tests/PHPStan/Analyser/nsrt/integer-range-types.php @@ -449,7 +449,7 @@ public function zeroIssues($positive, $negative) function subtract($m) { if ($m != 0) { - assertType("mixed~(0|0.0|'0'|false|null)", $m); // could be "mixed~(0|0.0|''|'0'|false|null)" + assertType("mixed~(0|-0.0|0.0|'0'|false|null)", $m); // could be "mixed~(0|-0.0|0.0|''|'0'|false|null)" assertType('int', (int) $m); } if ($m !== 0) { @@ -462,7 +462,7 @@ function subtract($m) { } if ($m != true) { - assertType("0|0.0|''|'0'|array{}|false|null", $m); + assertType("0|-0.0|0.0|''|'0'|array{}|false|null", $m); assertType('0', (int) $m); } if ($m !== true) { @@ -471,7 +471,7 @@ function subtract($m) { } if ($m != false) { - assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m); + assertType("mixed~(0|-0.0|0.0|''|'0'|array{}|false|null)", $m); assertType('int', (int) $m); } if ($m !== false) { diff --git a/tests/PHPStan/Analyser/nsrt/math.php b/tests/PHPStan/Analyser/nsrt/math.php index 9d12809783..25be8bd9c0 100644 --- a/tests/PHPStan/Analyser/nsrt/math.php +++ b/tests/PHPStan/Analyser/nsrt/math.php @@ -115,26 +115,26 @@ public function doSit(int $i, int $j): void public function multiplyZero(int $i, float $f, $range): void { assertType('0', $i * false); - assertType('0.0', $f * false); + assertType('-0.0|0.0', $f * false); assertType('0', $range * false); assertType('0', $i * '0'); - assertType('0.0', $f * '0'); + assertType('-0.0|0.0', $f * '0'); assertType('0', $range * '0'); assertType('0', $i * 0); - assertType('0.0', $f * 0); + assertType('-0.0|0.0', $f * 0); assertType('0', $range * 0); assertType('0', 0 * $i); - assertType('0.0', 0 * $f); + assertType('-0.0|0.0', 0 * $f); assertType('0', 0 * $range); $i *= 0; $f *= 0; $range *= 0; assertType('0', $i); - assertType('0.0', $f); + assertType('-0.0|0.0', $f); assertType('0', $range); } diff --git a/tests/PHPStan/Analyser/nsrt/mixed-subtract.php b/tests/PHPStan/Analyser/nsrt/mixed-subtract.php index c544802655..a660625634 100644 --- a/tests/PHPStan/Analyser/nsrt/mixed-subtract.php +++ b/tests/PHPStan/Analyser/nsrt/mixed-subtract.php @@ -26,26 +26,26 @@ function subtract(mixed $m, $moreThenFalsy) { } if ($m) { - assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m); + assertType("mixed~(0|-0.0|0.0|''|'0'|array{}|false|null)", $m); assertType('true', (bool) $m); } if (!$m) { - assertType("0|0.0|''|'0'|array{}|false|null", $m); + assertType("0|-0.0|0.0|''|'0'|array{}|false|null", $m); assertType('false', (bool) $m); } if (!$m) { if (!is_int($m)) { - assertType("0.0|''|'0'|array{}|false|null", $m); + assertType("-0.0|0.0|''|'0'|array{}|false|null", $m); assertType('false', (bool)$m); } if (!is_bool($m)) { - assertType("0|0.0|''|'0'|array{}|null", $m); + assertType("0|-0.0|0.0|''|'0'|array{}|null", $m); assertType('false', (bool)$m); } } if (!$m || is_int($m)) { - assertType("0.0|''|'0'|array{}|int|false|null", $m); + assertType("-0.0|0.0|''|'0'|array{}|int|false|null", $m); assertType('bool', (bool) $m); } @@ -55,7 +55,7 @@ function subtract(mixed $m, $moreThenFalsy) { } if ($m != 0 && !is_array($m) && $m != null && !is_object($m)) { // subtract more types then falsy - assertType("mixed~(0|0.0|''|'0'|array|object|false|null)", $m); + assertType("mixed~(0|-0.0|0.0|''|'0'|array|object|false|null)", $m); assertType('true', (bool) $m); } } diff --git a/tests/PHPStan/Analyser/nsrt/more-types.php b/tests/PHPStan/Analyser/nsrt/more-types.php index c8ae927b2d..29ff3d3963 100644 --- a/tests/PHPStan/Analyser/nsrt/more-types.php +++ b/tests/PHPStan/Analyser/nsrt/more-types.php @@ -45,8 +45,8 @@ public function doFoo( assertType('class-string', $enumString); assertType('literal-string&non-empty-string', $nonEmptyLiteralString); assertType('float|int|int<1, max>|non-falsy-string|true', $nonEmptyScalar); - assertType("0|0.0|''|'0'|false", $emptyScalar); - assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $nonEmptyMixed); + assertType("0|-0.0|0.0|''|'0'|false", $emptyScalar); + assertType("mixed~(0|-0.0|0.0|''|'0'|array{}|false|null)", $nonEmptyMixed); assertType('lowercase-string', $lowercaseString); assertType('lowercase-string&non-empty-string', $nonEmptyLowercaseString); assertType('uppercase-string', $uppercaseString); diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index c8031310ae..b3046fe3c0 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -428,7 +428,7 @@ function multiplesPrintfFormats(string $s) { function subtract($m) { if ($m) { - assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m); + assertType("mixed~(0|-0.0|0.0|''|'0'|array{}|false|null)", $m); assertType('non-falsy-string', (string) $m); } if ($m != '') { diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index a989ec244a..398aedaf64 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -1896,7 +1896,7 @@ public static function dataUnion(): iterable StaticTypeFactory::falsey(), ], UnionType::class, - '0|0.0|\'\'|\'0\'|array{}|false|null', + '0|-0.0|0.0|\'\'|\'0\'|array{}|false|null', ], [ [ @@ -1904,7 +1904,7 @@ public static function dataUnion(): iterable StaticTypeFactory::truthy(), ], MixedType::class, - 'mixed~(0|0.0|\'\'|\'0\'|array{}|false|null)=implicit', + 'mixed~(0|-0.0|0.0|\'\'|\'0\'|array{}|false|null)=implicit', ], [ [ @@ -4933,13 +4933,13 @@ public static function dataRemove(): array StaticTypeFactory::truthy(), StaticTypeFactory::falsey(), MixedType::class, - 'mixed~(0|0.0|\'\'|\'0\'|array{}|false|null)', + 'mixed~(0|-0.0|0.0|\'\'|\'0\'|array{}|false|null)', ], [ StaticTypeFactory::falsey(), StaticTypeFactory::truthy(), UnionType::class, - '0|0.0|\'\'|\'0\'|array{}|false|null', + '0|-0.0|0.0|\'\'|\'0\'|array{}|false|null', ], [ new BooleanType(), diff --git a/tests/PHPStan/Type/UnionTypeTest.php b/tests/PHPStan/Type/UnionTypeTest.php index c93a5e8d47..39cb8d670c 100644 --- a/tests/PHPStan/Type/UnionTypeTest.php +++ b/tests/PHPStan/Type/UnionTypeTest.php @@ -1358,6 +1358,7 @@ public function testSorting(): void new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantFloatType(-1.0), + new ConstantFloatType(-0.0), new ConstantFloatType(0.0), new ConstantFloatType(1.0), new ConstantStringType(''),