1
0
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:
Brown 2018-11-16 11:04:45 -05:00
parent 08d9940259
commit 425b6321aa
7 changed files with 251 additions and 27 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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
);
}
}
}
}

View File

@ -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'
],
];
}
}

View File

@ -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',
],
];
}
}