mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
Add better understanding of when floats and ints can be equal
This commit is contained in:
parent
08d9940259
commit
425b6321aa
@ -703,7 +703,7 @@ class AssertionFinder
|
||||
&& $conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical
|
||||
&& $source instanceof StatementsSource
|
||||
) {
|
||||
if (!TypeAnalyzer::canBeIdenticalTo(
|
||||
if (!TypeAnalyzer::canExpressionTypesBeIdentical(
|
||||
$codebase,
|
||||
$other_type,
|
||||
$var_type
|
||||
@ -745,7 +745,7 @@ class AssertionFinder
|
||||
&& $conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical
|
||||
&& $source instanceof StatementsSource
|
||||
) {
|
||||
if (!TypeAnalyzer::canBeIdenticalTo($codebase, $var_type, $other_type)) {
|
||||
if (!TypeAnalyzer::canExpressionTypesBeIdentical($codebase, $var_type, $other_type)) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TypeDoesNotContainType(
|
||||
$var_type . ' does not contain ' . $other_type,
|
||||
|
@ -1328,6 +1328,7 @@ class BinaryOpAnalyzer
|
||||
$left_type_part,
|
||||
new Type\Atomic\TString,
|
||||
false,
|
||||
false,
|
||||
$left_has_scalar_match,
|
||||
$left_type_coerced,
|
||||
$left_type_coerced_from_mixed,
|
||||
@ -1376,6 +1377,7 @@ class BinaryOpAnalyzer
|
||||
$right_type_part,
|
||||
new Type\Atomic\TString,
|
||||
false,
|
||||
false,
|
||||
$right_has_scalar_match,
|
||||
$right_type_coerced,
|
||||
$right_type_coerced_from_mixed,
|
||||
|
@ -1531,7 +1531,7 @@ class CallAnalyzer
|
||||
}
|
||||
|
||||
if (!$type_coerced && !$type_match_found) {
|
||||
$types_can_be_identical = TypeAnalyzer::canBeIdenticalTo(
|
||||
$types_can_be_identical = TypeAnalyzer::canExpressionTypesBeIdentical(
|
||||
$codebase,
|
||||
$input_type,
|
||||
$closure_param_type
|
||||
|
@ -95,6 +95,7 @@ class TypeAnalyzer
|
||||
$input_type_part,
|
||||
$container_type_part,
|
||||
$allow_interface_equality,
|
||||
true,
|
||||
$scalar_type_match_found,
|
||||
$type_coerced,
|
||||
$type_coerced_from_mixed,
|
||||
@ -178,10 +179,6 @@ class TypeAnalyzer
|
||||
* @param Type\Union $container_type
|
||||
* @param bool $ignore_null
|
||||
* @param bool $ignore_false
|
||||
* @param bool &$has_scalar_match
|
||||
* @param bool &$type_coerced whether or not there was type coercion involved
|
||||
* @param bool &$type_coerced_from_mixed
|
||||
* @param bool &$to_string_cast
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
@ -218,6 +215,7 @@ class TypeAnalyzer
|
||||
$input_type_part,
|
||||
$container_type_part,
|
||||
false,
|
||||
false,
|
||||
$scalar_type_match_found,
|
||||
$type_coerced,
|
||||
$type_coerced_from_mixed,
|
||||
@ -238,7 +236,7 @@ class TypeAnalyzer
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function canBeIdenticalTo(
|
||||
public static function canExpressionTypesBeIdentical(
|
||||
Codebase $codebase,
|
||||
Type\Union $type1,
|
||||
Type\Union $type2
|
||||
@ -252,25 +250,19 @@ class TypeAnalyzer
|
||||
}
|
||||
|
||||
foreach ($type1->getTypes() as $type1_part) {
|
||||
if ($type1_part instanceof TNull) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($type2->getTypes() as $type2_part) {
|
||||
if ($type2_part instanceof TNull) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$either_contains = self::isAtomicContainedBy(
|
||||
$codebase,
|
||||
$type1_part,
|
||||
$type2_part,
|
||||
true
|
||||
true,
|
||||
false
|
||||
) || self::isAtomicContainedBy(
|
||||
$codebase,
|
||||
$type2_part,
|
||||
$type1_part,
|
||||
true
|
||||
true,
|
||||
false
|
||||
);
|
||||
|
||||
if ($either_contains) {
|
||||
@ -380,6 +372,7 @@ class TypeAnalyzer
|
||||
* @param bool &$to_string_cast
|
||||
* @param bool &$type_coerced_from_scalar
|
||||
* @param bool $allow_interface_equality
|
||||
* @param bool $allow_float_int_equality whether or not floats and its can be equal
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
@ -388,6 +381,7 @@ class TypeAnalyzer
|
||||
Type\Atomic $input_type_part,
|
||||
Type\Atomic $container_type_part,
|
||||
$allow_interface_equality = false,
|
||||
$allow_float_int_equality = true,
|
||||
&$has_scalar_match = null,
|
||||
&$type_coerced = null,
|
||||
&$type_coerced_from_mixed = null,
|
||||
@ -463,7 +457,10 @@ class TypeAnalyzer
|
||||
// from https://wiki.php.net/rfc/scalar_type_hints_v5:
|
||||
//
|
||||
// > int types can resolve a parameter type of float
|
||||
if ($input_type_part instanceof TInt && $container_type_part instanceof TFloat) {
|
||||
if ($input_type_part instanceof TInt
|
||||
&& $container_type_part instanceof TFloat
|
||||
&& $allow_float_int_equality
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1158,6 +1155,7 @@ class TypeAnalyzer
|
||||
$type_part,
|
||||
$container_type_part,
|
||||
false,
|
||||
false,
|
||||
$has_scalar_match,
|
||||
$type_coerced,
|
||||
$type_coerced_from_mixed,
|
||||
|
@ -777,6 +777,7 @@ class Reconciler
|
||||
$existing_var_type_part,
|
||||
$new_type_part,
|
||||
false,
|
||||
false,
|
||||
$scalar_type_match_found,
|
||||
$type_coerced,
|
||||
$type_coerced_from_mixed,
|
||||
@ -844,6 +845,7 @@ class Reconciler
|
||||
$new_type_part,
|
||||
$existing_var_type_part,
|
||||
false,
|
||||
false,
|
||||
$scalar_type_match_found,
|
||||
$type_coerced,
|
||||
$type_coerced_from_mixed,
|
||||
@ -1380,6 +1382,7 @@ class Reconciler
|
||||
$existing_var_type_part,
|
||||
$new_type_part,
|
||||
false,
|
||||
false,
|
||||
$scalar_type_match_found,
|
||||
$type_coerced,
|
||||
$type_coerced_from_mixed,
|
||||
@ -1483,7 +1486,7 @@ class Reconciler
|
||||
} else {
|
||||
$existing_var_type = new Type\Union([new Type\Atomic\TLiteralInt($value)]);
|
||||
}
|
||||
} elseif (!$existing_var_type->hasFloat() && $var_id && $code_location && !$is_loose_equality) {
|
||||
} elseif ($var_id && $code_location && !$is_loose_equality) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
@ -1493,6 +1496,41 @@ class Reconciler
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
} elseif ($is_loose_equality && $existing_var_type->hasFloat()) {
|
||||
// convert floats to ints
|
||||
$existing_float_types = $existing_var_type->getLiteralFloats();
|
||||
|
||||
if ($existing_float_types) {
|
||||
$can_be_equal = false;
|
||||
$did_remove_type = false;
|
||||
|
||||
foreach ($existing_var_atomic_types as $atomic_key => $_) {
|
||||
if (substr($atomic_key, 0, 6) === 'float(') {
|
||||
$atomic_key = 'int(' . substr($atomic_key, 6);
|
||||
}
|
||||
if ($atomic_key !== $new_var_type) {
|
||||
$existing_var_type->removeType($atomic_key);
|
||||
$did_remove_type = true;
|
||||
} else {
|
||||
$can_be_equal = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($var_id
|
||||
&& $code_location
|
||||
&& (!$can_be_equal || (!$did_remove_type && count($existing_var_atomic_types) === 1))
|
||||
) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$new_var_type,
|
||||
$can_be_equal,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($scalar_type === 'string' || $scalar_type === 'class-string') {
|
||||
if ($existing_var_type->isMixed()) {
|
||||
@ -1601,6 +1639,41 @@ class Reconciler
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
} elseif ($is_loose_equality && $existing_var_type->hasInt()) {
|
||||
// convert ints to floats
|
||||
$existing_float_types = $existing_var_type->getLiteralInts();
|
||||
|
||||
if ($existing_float_types) {
|
||||
$can_be_equal = false;
|
||||
$did_remove_type = false;
|
||||
|
||||
foreach ($existing_var_atomic_types as $atomic_key => $_) {
|
||||
if (substr($atomic_key, 0, 4) === 'int(') {
|
||||
$atomic_key = 'float(' . substr($atomic_key, 4);
|
||||
}
|
||||
if ($atomic_key !== $new_var_type) {
|
||||
$existing_var_type->removeType($atomic_key);
|
||||
$did_remove_type = true;
|
||||
} else {
|
||||
$can_be_equal = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($var_id
|
||||
&& $code_location
|
||||
&& (!$can_be_equal || (!$did_remove_type && count($existing_var_atomic_types) === 1))
|
||||
) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$new_var_type,
|
||||
$can_be_equal,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -266,7 +266,7 @@ class AssertTest extends TestCase
|
||||
assertInstanceOf(A::class, $a);
|
||||
}',
|
||||
],
|
||||
'allowCanBeEqualAfterAssertion' => [
|
||||
'allowCanBeSameAfterAssertion' => [
|
||||
'<?php
|
||||
|
||||
/**
|
||||
@ -299,6 +299,76 @@ class AssertTest extends TestCase
|
||||
assertSame($a, $b);
|
||||
}',
|
||||
],
|
||||
'allowCanBeNotSameAfterAssertion' => [
|
||||
'<?php
|
||||
|
||||
/**
|
||||
* Asserts that two variables are the same.
|
||||
*
|
||||
* @template T
|
||||
* @param T $expected
|
||||
* @param mixed $actual
|
||||
* @psalm-assert !=T $actual
|
||||
*/
|
||||
function assertNotSame($expected, $actual) : void {}
|
||||
|
||||
$a = rand(0, 1) ? "goodbye" : "hello";
|
||||
$b = rand(0, 1) ? "hello" : "goodbye";
|
||||
assertNotSame($a, $b);
|
||||
|
||||
$c = "hello";
|
||||
$d = rand(0, 1) ? "hello" : "goodbye";
|
||||
assertNotSame($c, $d);
|
||||
|
||||
$c = "hello";
|
||||
$d = rand(0, 1) ? "hello" : "goodbye";
|
||||
assertNotSame($d, $c);
|
||||
|
||||
$c = 4;
|
||||
$d = rand(0, 1) ? 4 : 5;
|
||||
assertNotSame($d, $c);
|
||||
|
||||
function foo(string $a, string $b) : void {
|
||||
assertNotSame($a, $b);
|
||||
}',
|
||||
],
|
||||
'allowCanBeEqualAfterAssertion' => [
|
||||
'<?php
|
||||
|
||||
/**
|
||||
* Asserts that two variables are the same.
|
||||
*
|
||||
* @template T
|
||||
* @param T $expected
|
||||
* @param mixed $actual
|
||||
* @psalm-assert ~T $actual
|
||||
*/
|
||||
function assertEqual($expected, $actual) : void {}
|
||||
|
||||
$a = rand(0, 1) ? "goodbye" : "hello";
|
||||
$b = rand(0, 1) ? "hello" : "goodbye";
|
||||
assertEqual($a, $b);
|
||||
|
||||
$c = "hello";
|
||||
$d = rand(0, 1) ? "hello" : "goodbye";
|
||||
assertEqual($c, $d);
|
||||
|
||||
$c = "hello";
|
||||
$d = rand(0, 1) ? "hello" : "goodbye";
|
||||
assertEqual($d, $c);
|
||||
|
||||
$c = 4;
|
||||
$d = rand(0, 1) ? 3.0 : 4.0;
|
||||
assertEqual($d, $c);
|
||||
|
||||
$c = 4.0;
|
||||
$d = rand(0, 1) ? 3 : 4;
|
||||
assertEqual($d, $c);
|
||||
|
||||
function foo(string $a, string $b) : void {
|
||||
assertEqual($a, $b);
|
||||
}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -494,7 +564,7 @@ class AssertTest extends TestCase
|
||||
assertSame($a, $b);',
|
||||
'error_message' => 'RedundantCondition'
|
||||
],
|
||||
'detectNeverCanBeEqualAfterAssertion' => [
|
||||
'detectNeverCanBeSameAfterAssertion' => [
|
||||
'<?php
|
||||
|
||||
/**
|
||||
@ -512,6 +582,78 @@ class AssertTest extends TestCase
|
||||
assertSame($c, $d);',
|
||||
'error_message' => 'TypeDoesNotContainType'
|
||||
],
|
||||
'detectNeverCanBeNotSameAfterAssertion' => [
|
||||
'<?php
|
||||
|
||||
/**
|
||||
* Asserts that two variables are the same.
|
||||
*
|
||||
* @template T
|
||||
* @param T $expected
|
||||
* @param mixed $actual
|
||||
* @psalm-assert !=T $actual
|
||||
*/
|
||||
function assertNotSame($expected, $actual) : void {}
|
||||
|
||||
$c = "helloa";
|
||||
$d = rand(0, 1) ? "hello" : "goodbye";
|
||||
assertNotSame($c, $d);',
|
||||
'error_message' => 'RedundantCondition'
|
||||
],
|
||||
'detectNeverCanBeEqualAfterAssertion' => [
|
||||
'<?php
|
||||
|
||||
/**
|
||||
* Asserts that two variables are the same.
|
||||
*
|
||||
* @template T
|
||||
* @param T $expected
|
||||
* @param mixed $actual
|
||||
* @psalm-assert ~T $actual
|
||||
*/
|
||||
function assertEqual($expected, $actual) : void {}
|
||||
|
||||
$c = "helloa";
|
||||
$d = rand(0, 1) ? "hello" : "goodbye";
|
||||
assertEqual($c, $d);',
|
||||
'error_message' => 'TypeDoesNotContainType'
|
||||
],
|
||||
'detectIntFloatNeverCanBeEqualAfterAssertion' => [
|
||||
'<?php
|
||||
|
||||
/**
|
||||
* Asserts that two variables are the same.
|
||||
*
|
||||
* @template T
|
||||
* @param T $expected
|
||||
* @param mixed $actual
|
||||
* @psalm-assert ~T $actual
|
||||
*/
|
||||
function assertEqual($expected, $actual) : void {}
|
||||
|
||||
$c = 4;
|
||||
$d = rand(0, 1) ? 5.0 : 6.0;
|
||||
assertEqual($c, $d);',
|
||||
'error_message' => 'TypeDoesNotContainType'
|
||||
],
|
||||
'detectFloatIntNeverCanBeEqualAfterAssertion' => [
|
||||
'<?php
|
||||
|
||||
/**
|
||||
* Asserts that two variables are the same.
|
||||
*
|
||||
* @template T
|
||||
* @param T $expected
|
||||
* @param mixed $actual
|
||||
* @psalm-assert ~T $actual
|
||||
*/
|
||||
function assertEqual($expected, $actual) : void {}
|
||||
|
||||
$c = 4.0;
|
||||
$d = rand(0, 1) ? 5 : 6;
|
||||
assertEqual($c, $d);',
|
||||
'error_message' => 'TypeDoesNotContainType'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -860,11 +860,6 @@ class TypeReconciliationTest extends TestCase
|
||||
function foo(int $i) : void {
|
||||
if ($i == "5") {}
|
||||
if ("5" == $i) {}
|
||||
}
|
||||
function bar(float $f) : void {
|
||||
if ($f === 0) {}
|
||||
|
||||
if (0 === $f) {}
|
||||
}',
|
||||
],
|
||||
'filterSubclassBasedOnParentInstanceof' => [
|
||||
@ -1289,6 +1284,20 @@ class TypeReconciliationTest extends TestCase
|
||||
}',
|
||||
'error_message' => 'InvalidReturnStatement',
|
||||
],
|
||||
'preventWeakEqualityScalarType' => [
|
||||
'<?php
|
||||
function bar(float $f) : void {
|
||||
if ($f === 0) {}
|
||||
}',
|
||||
'error_message' => 'TypeDoesNotContainType',
|
||||
],
|
||||
'preventYodaWeakEqualityScalarType' => [
|
||||
'<?php
|
||||
function bar(float $f) : void {
|
||||
if (0 === $f) {}
|
||||
}',
|
||||
'error_message' => 'TypeDoesNotContainType',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user