Skip to content

Better support for -0.0 #4228

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 2.1.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -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([], []),
Expand Down Expand Up @@ -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(),
];
Expand All @@ -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'),
];
Expand All @@ -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(''),
];
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
14 changes: 13 additions & 1 deletion src/Type/Constant/ConstantFloatType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/Type/NullType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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([], []),
Expand All @@ -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([], []),
Expand Down
1 change: 1 addition & 0 deletions src/Type/StaticTypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
3 changes: 3 additions & 0 deletions src/Type/Traits/ConstantNumericComparisonTypeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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),
];
Expand All @@ -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
}

Expand Down
6 changes: 6 additions & 0 deletions src/Type/UnionTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
26 changes: 13 additions & 13 deletions tests/PHPStan/Analyser/TypeSpecifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -826,7 +826,7 @@ public static function dataCondition(): iterable
'$n' => 'mixed~(int<3, max>|true)',
],
[
'$n' => 'mixed~(0.0|int<min, 2>|false|null)',
'$n' => 'mixed~(-0.0|0.0|int<min, 2>|false|null)',
],
],
[
Expand All @@ -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)',
],
],
[
Expand All @@ -847,7 +847,7 @@ public static function dataCondition(): iterable
new LNumber(PHP_INT_MAX),
),
[
'$n' => 'mixed~(0.0|bool|int<min, ' . PHP_INT_MAX . '>|null)',
'$n' => 'mixed~(-0.0|0.0|bool|int<min, ' . PHP_INT_MAX . '>|null)',
],
[
'$n' => 'mixed',
Expand All @@ -862,7 +862,7 @@ public static function dataCondition(): iterable
'$n' => 'mixed~int<' . (PHP_INT_MIN + 1) . ', max>',
],
[
'$n' => 'mixed~(0.0|bool|int<min, ' . PHP_INT_MIN . '>|null)',
'$n' => 'mixed~(-0.0|0.0|bool|int<min, ' . PHP_INT_MIN . '>|null)',
],
],
[
Expand All @@ -871,7 +871,7 @@ public static function dataCondition(): iterable
new LNumber(PHP_INT_MAX),
),
[
'$n' => 'mixed~(0.0|int<min, ' . (PHP_INT_MAX - 1) . '>|false|null)',
'$n' => 'mixed~(-0.0|0.0|int<min, ' . (PHP_INT_MAX - 1) . '>|false|null)',
],
[
'$n' => 'mixed~(int<' . PHP_INT_MAX . ', max>|true)',
Expand All @@ -889,7 +889,7 @@ public static function dataCondition(): iterable
),
),
[
'$n' => 'mixed~(0.0|int<min, 2>|int<6, max>|false|null)',
'$n' => 'mixed~(-0.0|0.0|int<min, 2>|int<6, max>|false|null)',
],
[
'$n' => 'mixed~(int<3, 5>|true)',
Expand Down Expand Up @@ -1252,7 +1252,7 @@ public static function dataCondition(): iterable
),
[
'$foo' => 'non-empty-array<mixed, mixed>',
'count($foo)' => 'mixed~(0.0|int<min, 1>|false|null)',
'count($foo)' => 'mixed~(-0.0|0.0|int<min, 1>|false|null)',
],
[],
],
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/assert-empty.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-10189.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function file_managed_file_save_upload(): array {
}
assertType('non-empty-array|Bug10189\SomeInterface', $files);
$files = array_filter($files);
assertType("array<mixed~(0|0.0|''|'0'|array{}|false|null)>", $files);
assertType("array<mixed~(0|-0.0|0.0|''|'0'|array{}|false|null)>", $files);

return empty($files) ? [] : [1,2];
}
44 changes: 44 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-12225.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);

namespace Bug12225;

use function PHPStan\Testing\assertType;

function floatAsserts(float $num): ?int
{
if ($num === 0.0) {
assertType('-0.0|0.0', $num);
assertType("'-0'|'0'", (string) $num);
}

if ($num == 0) {
assertType('-0.0|0.0', $num);
assertType("'-0'|'0'", (string) $num);
}

assertType("'0'", (string) 0.0);
assertType("'-0'", (string) -0.0);

assertType('-0.0', -1.0 * 0.0);
assertType('0.0', 1.0 * 0.0);
assertType('0.0', -1.0 * -0.0);
assertType('-0.0', 1.0 * -0.0);

assertType('true', 0.0 === -0.0);
assertType('true', 0.0 == -0.0);

return null;
}

/** @param mixed $num */
function withMixed($num): ?int
{
if ($num === 0.0) {
assertType('-0.0|0.0', $num);
assertType("'-0'|'0'", (string) $num);
}

assertType('true', 0.0 === -0.0);

return null;
}
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/bug-3991.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ public static function email($config = null)
assertType('array|stdClass|null', $config);
if (empty($config))
{
// the native type should be `0|0.0|''|'0'|array{}|false|null`
// the native type should be `0|-0.0|0.0|''|'0'|array{}|false|null`
// the problem is that `empty($config)` translates to `!isset($config) || !$config`
// and before specified types of the left and right side are intersected they are "normalized"
// by removing the sureNotType from $scope->getType() which is `stdClass|array|null` here
assertNativeType('array{}|null', $config);
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);
}

Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/bug-4117.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading