2018-05-03 19:56:30 +02:00
|
|
|
|
<?php
|
2020-08-08 05:22:30 +02:00
|
|
|
|
namespace Psalm\Tests\TypeReconciliation;
|
2018-05-03 19:56:30 +02:00
|
|
|
|
|
2020-08-23 16:32:07 +02:00
|
|
|
|
use Psalm\Internal\RuntimeCaches;
|
|
|
|
|
|
2020-08-08 05:22:30 +02:00
|
|
|
|
class ValueTest extends \Psalm\Tests\TestCase
|
2018-05-03 19:56:30 +02:00
|
|
|
|
{
|
2020-08-08 05:22:30 +02:00
|
|
|
|
use \Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
|
|
|
|
|
use \Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
|
2018-05-03 19:56:30 +02:00
|
|
|
|
|
2019-05-17 00:36:36 +02:00
|
|
|
|
public function setUp() : void
|
2019-04-22 16:01:25 +02:00
|
|
|
|
{
|
2020-08-23 16:32:07 +02:00
|
|
|
|
RuntimeCaches::clearAll();
|
2019-04-22 16:01:25 +02:00
|
|
|
|
|
|
|
|
|
$this->file_provider = new \Psalm\Tests\Internal\Provider\FakeFileProvider();
|
|
|
|
|
|
|
|
|
|
$this->project_analyzer = new \Psalm\Internal\Analyzer\ProjectAnalyzer(
|
2020-08-08 05:22:30 +02:00
|
|
|
|
new \Psalm\Tests\TestConfig(),
|
2019-04-22 16:01:25 +02:00
|
|
|
|
new \Psalm\Internal\Provider\Providers(
|
|
|
|
|
$this->file_provider,
|
|
|
|
|
new \Psalm\Tests\Internal\Provider\FakeParserCacheProvider()
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->project_analyzer->setPhpVersion('7.3');
|
|
|
|
|
$this->project_analyzer->getCodebase()->config->parse_sql = true;
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-03 19:56:30 +02:00
|
|
|
|
/**
|
2019-03-01 21:55:20 +01:00
|
|
|
|
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
|
2018-05-03 19:56:30 +02:00
|
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
|
public function providerValidCodeParse()
|
2018-05-03 19:56:30 +02:00
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'whileCountUpdate' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$array = [1, 2, 3];
|
|
|
|
|
while (rand(1, 10) === 1) {
|
|
|
|
|
$array[] = 4;
|
|
|
|
|
$array[] = 5;
|
|
|
|
|
$array[] = 6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (count($array) === 7) {}',
|
|
|
|
|
],
|
|
|
|
|
'tryCountCatch' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$errors = [];
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
throw new Exception("bad");
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception $e) {
|
|
|
|
|
$errors[] = $e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (count($errors) !== 0) {
|
|
|
|
|
echo "Errors";
|
|
|
|
|
}',
|
|
|
|
|
],
|
|
|
|
|
'ternaryDifferentString' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$foo = rand(0, 1) ? "bar" : "bat";
|
|
|
|
|
|
|
|
|
|
if ($foo === "bar") {}
|
|
|
|
|
|
|
|
|
|
if ($foo !== "bar") {}
|
|
|
|
|
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
$foo = "baz";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($foo === "baz") {}
|
|
|
|
|
|
|
|
|
|
if ($foo !== "bat") {}',
|
|
|
|
|
],
|
|
|
|
|
'ifDifferentString' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$foo = "bar";
|
|
|
|
|
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
$foo = "bat";
|
|
|
|
|
} elseif (rand(0, 1)) {
|
|
|
|
|
$foo = "baz";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$bar = "bar";
|
|
|
|
|
$baz = "baz";
|
|
|
|
|
|
|
|
|
|
if ($foo === "bar") {}
|
|
|
|
|
if ($foo !== "bar") {}
|
|
|
|
|
if ($foo === "baz") {}
|
|
|
|
|
if ($foo === $bar) {}
|
|
|
|
|
if ($foo !== $bar) {}
|
|
|
|
|
if ($foo === $baz) {}',
|
|
|
|
|
],
|
|
|
|
|
'ifThisOrThat' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$foo = "bar";
|
|
|
|
|
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
$foo = "bat";
|
|
|
|
|
} elseif (rand(0, 1)) {
|
|
|
|
|
$foo = "baz";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($foo === "baz" || $foo === "bar") {}',
|
|
|
|
|
],
|
|
|
|
|
'ifDifferentNullableString' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$foo = null;
|
|
|
|
|
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
$foo = "bar";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$bar = "bar";
|
|
|
|
|
|
|
|
|
|
if ($foo === "bar") {}
|
|
|
|
|
if ($foo !== "bar") {}',
|
|
|
|
|
],
|
|
|
|
|
'whileIncremented' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$i = 1;
|
|
|
|
|
$j = 2;
|
|
|
|
|
while (rand(0, 1)) {
|
|
|
|
|
if ($i === $j) {}
|
|
|
|
|
$i++;
|
2019-03-23 19:27:54 +01:00
|
|
|
|
}',
|
2018-05-03 19:56:30 +02:00
|
|
|
|
],
|
|
|
|
|
'checkStringKeyValue' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$foo = [
|
|
|
|
|
"0" => 3,
|
|
|
|
|
"1" => 4,
|
|
|
|
|
"2" => 5,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function takesInt(int $s) : void {}
|
|
|
|
|
|
|
|
|
|
foreach ($foo as $i => $b) {
|
|
|
|
|
takesInt($i);
|
|
|
|
|
}',
|
|
|
|
|
],
|
|
|
|
|
'getValidIntStringOffset' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$foo = [
|
|
|
|
|
"0" => 3,
|
|
|
|
|
"1" => 4,
|
|
|
|
|
"2" => 5,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$a = "2";
|
|
|
|
|
|
|
|
|
|
echo $foo["2"];
|
|
|
|
|
echo $foo[$a];',
|
|
|
|
|
],
|
|
|
|
|
'checkStringKeyValueAfterKnownIntStringOffset' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$foo = [
|
|
|
|
|
"0" => 3,
|
|
|
|
|
"1" => 4,
|
|
|
|
|
"2" => 5,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$a = "2";
|
|
|
|
|
$foo[$a] = 6;
|
|
|
|
|
|
|
|
|
|
function takesInt(int $s) : void {}
|
|
|
|
|
|
|
|
|
|
foreach ($foo as $i => $b) {
|
|
|
|
|
takesInt($i);
|
|
|
|
|
}',
|
|
|
|
|
],
|
2018-05-07 22:00:56 +02:00
|
|
|
|
'regularComparison1' => [
|
2018-05-07 20:52:45 +02:00
|
|
|
|
'<?php
|
2018-05-07 22:00:56 +02:00
|
|
|
|
function foo(string $s1, string $s2, ?int $i) : string {
|
2018-05-07 20:52:45 +02:00
|
|
|
|
if ($s1 !== $s2) {
|
|
|
|
|
return $s1;
|
|
|
|
|
}
|
2018-05-07 22:00:56 +02:00
|
|
|
|
|
|
|
|
|
return $s2;
|
|
|
|
|
}',
|
|
|
|
|
],
|
|
|
|
|
'regularComparison2' => [
|
|
|
|
|
'<?php
|
|
|
|
|
function foo(string $s1, string $s2) : string {
|
|
|
|
|
if ($s1 !== "hello") {
|
|
|
|
|
if ($s1 !== "goodbye") {
|
|
|
|
|
return $s1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $s2;
|
|
|
|
|
}',
|
|
|
|
|
],
|
|
|
|
|
'regularComparison3' => [
|
|
|
|
|
'<?php
|
|
|
|
|
class A {
|
|
|
|
|
const B = 1;
|
|
|
|
|
const C = 2;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
function foo(string $s1, string $s2, ?int $i) : string {
|
|
|
|
|
if ($i !== A::B && $i !== A::C) {}
|
|
|
|
|
|
2018-05-07 20:52:45 +02:00
|
|
|
|
return $s2;
|
|
|
|
|
}',
|
|
|
|
|
],
|
2018-05-07 22:00:56 +02:00
|
|
|
|
'regularComparisonOnPossiblyNull' => [
|
|
|
|
|
'<?php
|
|
|
|
|
/** @psalm-ignore-nullable-return */
|
|
|
|
|
function generate() : ?string {
|
|
|
|
|
return rand(0, 1000) ? "hello" : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function foo() : string {
|
|
|
|
|
$str = generate();
|
|
|
|
|
|
|
|
|
|
if ($str[0] === "h") {
|
|
|
|
|
return $str;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "hello";
|
|
|
|
|
}',
|
|
|
|
|
],
|
2018-05-08 23:42:02 +02:00
|
|
|
|
'incrementAndCheck' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$i = 0;
|
|
|
|
|
if (rand(0, 1)) $i++;
|
2019-03-23 19:27:54 +01:00
|
|
|
|
if ($i === 1) {}',
|
2018-05-08 23:42:02 +02:00
|
|
|
|
],
|
|
|
|
|
'incrementInClosureAndCheck' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$i = 0;
|
|
|
|
|
$a = function() use (&$i) : void {
|
2020-07-31 18:44:01 +02:00
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
$i++;
|
|
|
|
|
}
|
2018-05-08 23:42:02 +02:00
|
|
|
|
};
|
|
|
|
|
$a();
|
|
|
|
|
if ($i === 0) {}',
|
2018-06-18 19:16:51 +02:00
|
|
|
|
'assertions' => [],
|
2020-07-31 18:44:01 +02:00
|
|
|
|
'error_levels' => ['MixedOperand', 'MixedAssignment'],
|
2018-05-08 23:42:02 +02:00
|
|
|
|
],
|
2018-05-09 00:11:10 +02:00
|
|
|
|
'incrementMixedCall' => [
|
|
|
|
|
'<?php
|
|
|
|
|
function foo($f) : void {
|
|
|
|
|
$i = 0;
|
|
|
|
|
$f->add(function() use (&$i) : void {
|
|
|
|
|
if (rand(0, 1)) $i++;
|
|
|
|
|
});
|
|
|
|
|
if ($i === 0) {}
|
|
|
|
|
}',
|
|
|
|
|
'assertions' => [],
|
2020-07-31 18:44:01 +02:00
|
|
|
|
'error_levels' => ['MissingParamType', 'MixedMethodCall', 'MixedOperand', 'MixedAssignment'],
|
2018-05-09 00:11:10 +02:00
|
|
|
|
],
|
2018-05-13 00:46:47 +02:00
|
|
|
|
'regularValueReconciliation' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$s = rand(0, 1) ? "a" : "b";
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
$s = "c";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($s === "a" || $s === "b") {
|
|
|
|
|
if ($s === "a") {}
|
2019-03-23 19:27:54 +01:00
|
|
|
|
}',
|
2018-05-13 00:46:47 +02:00
|
|
|
|
],
|
2018-05-18 17:02:50 +02:00
|
|
|
|
'moreValueReconciliation' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$a = rand(0, 1) ? "a" : "b";
|
|
|
|
|
$b = rand(0, 1) ? "a" : "b";
|
|
|
|
|
|
|
|
|
|
$s = rand(0, 1) ? $a : $b;
|
|
|
|
|
if (rand(0, 1)) $s = "c";
|
|
|
|
|
|
|
|
|
|
if ($s === $a) {
|
|
|
|
|
} elseif ($s === $b) {}',
|
|
|
|
|
],
|
2018-05-13 00:46:47 +02:00
|
|
|
|
'negativeInts' => [
|
|
|
|
|
'<?php
|
|
|
|
|
class C {
|
|
|
|
|
const A = 1;
|
|
|
|
|
const B = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const A = 1;
|
|
|
|
|
const B = -1;
|
|
|
|
|
|
|
|
|
|
$i = rand(0, 1) ? A : B;
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
$i = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($i === A) {
|
|
|
|
|
echo "here";
|
|
|
|
|
} elseif ($i === B) {
|
|
|
|
|
echo "here";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$i = rand(0, 1) ? C::A : C::B;
|
|
|
|
|
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
$i = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($i === C::A) {
|
|
|
|
|
echo "here";
|
|
|
|
|
} elseif ($i === C::B) {
|
|
|
|
|
echo "here";
|
|
|
|
|
}',
|
|
|
|
|
],
|
2018-05-18 17:02:50 +02:00
|
|
|
|
'falsyReconciliation' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$s = rand(0, 1) ? 200 : null;
|
2019-03-23 19:27:54 +01:00
|
|
|
|
if (!$s) {}',
|
2018-05-18 17:02:50 +02:00
|
|
|
|
],
|
|
|
|
|
'redefinedIntInIfAndPossibleComparison' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$s = rand(0, 1) ? 0 : 1;
|
|
|
|
|
|
|
|
|
|
if ($s && rand(0, 1)) {
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
$s = 2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($s == 2) {}',
|
|
|
|
|
],
|
2018-05-22 00:33:39 +02:00
|
|
|
|
'noEmpties' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$context = \'a\';
|
|
|
|
|
while ( true ) {
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
exit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$context = \'b\';
|
|
|
|
|
} elseif (rand(0, 1)) {
|
|
|
|
|
if ($context !== \'c\' && $context !== \'b\') {
|
|
|
|
|
exit;
|
|
|
|
|
}
|
2018-05-18 17:02:50 +02:00
|
|
|
|
|
2018-05-22 00:33:39 +02:00
|
|
|
|
$context = \'c\';
|
|
|
|
|
}
|
|
|
|
|
}',
|
|
|
|
|
],
|
2018-06-08 19:53:42 +02:00
|
|
|
|
'ifOrAssertionWithSwitch' => [
|
|
|
|
|
'<?php
|
|
|
|
|
function foo(string $s) : void {
|
|
|
|
|
switch ($s) {
|
|
|
|
|
case "a":
|
|
|
|
|
case "b":
|
|
|
|
|
case "c":
|
|
|
|
|
if ($s === "a" || $s === "b") {
|
|
|
|
|
throw new \InvalidArgumentException;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}',
|
|
|
|
|
],
|
|
|
|
|
'inArrayAssertionProperty' => [
|
|
|
|
|
'<?php
|
|
|
|
|
class Foo
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* @psalm-var "a"|"b"
|
|
|
|
|
*/
|
|
|
|
|
private $s;
|
|
|
|
|
|
|
|
|
|
public function __construct(string $s)
|
|
|
|
|
{
|
|
|
|
|
if (!in_array($s, ["a", "b"], true)) {
|
|
|
|
|
throw new \InvalidArgumentException;
|
|
|
|
|
}
|
|
|
|
|
$this->s = $s;
|
|
|
|
|
}
|
|
|
|
|
}',
|
|
|
|
|
],
|
|
|
|
|
'inArrayAssertionWithSwitch' => [
|
|
|
|
|
'<?php
|
|
|
|
|
function foo(string $s) : void {
|
|
|
|
|
switch ($s) {
|
|
|
|
|
case "a":
|
|
|
|
|
case "b":
|
|
|
|
|
case "c":
|
|
|
|
|
if (in_array($s, ["a", "b"], true)) {
|
|
|
|
|
throw new \InvalidArgumentException;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}',
|
|
|
|
|
],
|
2018-06-12 17:19:35 +02:00
|
|
|
|
'removeLiteralStringForNotIsString' => [
|
|
|
|
|
'<?php
|
|
|
|
|
function takesInt(int $i) : void {}
|
|
|
|
|
|
|
|
|
|
$f = ["a", "b", "c"];
|
|
|
|
|
$f[rand(0, 2)] = 5;
|
|
|
|
|
|
|
|
|
|
$i = rand(0, 2);
|
|
|
|
|
if (isset($f[$i]) && !is_string($f[$i])) {
|
|
|
|
|
takesInt($f[$i]);
|
2019-03-23 19:27:54 +01:00
|
|
|
|
}',
|
2018-06-12 17:19:35 +02:00
|
|
|
|
],
|
|
|
|
|
'removeLiteralIntForNotIsInt' => [
|
|
|
|
|
'<?php
|
|
|
|
|
function takesString(string $i) : void {}
|
|
|
|
|
|
|
|
|
|
$f = [0, 1, 2];
|
|
|
|
|
$f[rand(0, 2)] = "hello";
|
|
|
|
|
|
|
|
|
|
$i = rand(0, 2);
|
|
|
|
|
if (isset($f[$i]) && !is_int($f[$i])) {
|
|
|
|
|
takesString($f[$i]);
|
2019-03-23 19:27:54 +01:00
|
|
|
|
}',
|
2018-06-12 17:19:35 +02:00
|
|
|
|
],
|
|
|
|
|
'removeLiteralFloatForNotIsFloat' => [
|
|
|
|
|
'<?php
|
|
|
|
|
function takesString(string $i) : void {}
|
|
|
|
|
|
|
|
|
|
$f = [1.1, 1.2, 1.3];
|
|
|
|
|
$f[rand(0, 2)] = "hello";
|
|
|
|
|
|
|
|
|
|
$i = rand(0, 2);
|
|
|
|
|
if (isset($f[$i]) && !is_float($f[$i])) {
|
|
|
|
|
takesString($f[$i]);
|
2019-03-23 19:27:54 +01:00
|
|
|
|
}',
|
2018-06-12 17:19:35 +02:00
|
|
|
|
],
|
2018-09-09 19:01:16 +02:00
|
|
|
|
'coerceFromMixed' => [
|
|
|
|
|
'<?php
|
|
|
|
|
function type(int $b): void {}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param mixed $a
|
|
|
|
|
*/
|
|
|
|
|
function foo($a) : void {
|
|
|
|
|
if ($a === 1 || $a === 2) {
|
|
|
|
|
type($a);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (in_array($a, [1, 2], true)) {
|
|
|
|
|
type($a);
|
|
|
|
|
}
|
|
|
|
|
}',
|
|
|
|
|
],
|
|
|
|
|
'coerceFromString' => [
|
|
|
|
|
'<?php
|
|
|
|
|
/** @param "a"|"b" $b */
|
|
|
|
|
function type(string $b): void {}
|
|
|
|
|
|
|
|
|
|
function foo(string $a) : void {
|
|
|
|
|
if ($a === "a" || $a === "b") {
|
|
|
|
|
type($a);
|
|
|
|
|
}
|
|
|
|
|
}',
|
|
|
|
|
],
|
2018-11-02 04:31:40 +01:00
|
|
|
|
'coercePossibleOffset' => [
|
|
|
|
|
'<?php
|
|
|
|
|
class A {
|
|
|
|
|
const FOO = "foo";
|
|
|
|
|
const BAR = "bar";
|
|
|
|
|
const BAT = "bat";
|
|
|
|
|
const BAM = "bam";
|
|
|
|
|
|
|
|
|
|
/** @var self::FOO|self::BAR|self::BAT|null $s */
|
|
|
|
|
public $s;
|
|
|
|
|
|
|
|
|
|
public function isFooOrBar() : void {
|
|
|
|
|
$map = [
|
|
|
|
|
A::FOO => 1,
|
|
|
|
|
A::BAR => 1,
|
|
|
|
|
A::BAM => 1,
|
|
|
|
|
];
|
|
|
|
|
|
2020-02-13 23:58:15 +01:00
|
|
|
|
if ($this->s !== null && isset($map[$this->s])) {}
|
2018-11-02 04:31:40 +01:00
|
|
|
|
}
|
2019-03-23 19:27:54 +01:00
|
|
|
|
}',
|
2018-11-02 04:31:40 +01:00
|
|
|
|
],
|
2018-12-08 19:18:55 +01:00
|
|
|
|
'noRedundantConditionWithMixed' => [
|
|
|
|
|
'<?php
|
|
|
|
|
function foo($a) : void {
|
|
|
|
|
if ($a == "a") {
|
|
|
|
|
} else {
|
|
|
|
|
if ($a == "b" && rand(0, 1)) {}
|
|
|
|
|
}
|
|
|
|
|
}',
|
2019-03-01 21:55:20 +01:00
|
|
|
|
'assertions' => [],
|
2018-12-08 19:18:55 +01:00
|
|
|
|
'error_levels' => ['MissingParamType', 'MixedAssignment'],
|
|
|
|
|
],
|
2019-03-12 06:26:19 +01:00
|
|
|
|
'numericToStringComparison' => [
|
|
|
|
|
'<?php
|
|
|
|
|
/** @psalm-suppress MissingParamType */
|
|
|
|
|
function foo($s) : void {
|
|
|
|
|
if (is_numeric($s)) {
|
|
|
|
|
if ($s === 1) {}
|
|
|
|
|
}
|
|
|
|
|
}',
|
|
|
|
|
],
|
2019-03-16 17:40:19 +01:00
|
|
|
|
'newlineIssue' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$a = "foo";
|
|
|
|
|
$b = "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
";
|
|
|
|
|
|
|
|
|
|
$c = $a;
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
$c = $b;
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-23 19:27:54 +01:00
|
|
|
|
if ($c === $b) {}',
|
2019-03-16 17:40:19 +01:00
|
|
|
|
],
|
2019-03-21 21:57:42 +01:00
|
|
|
|
'don’tChangeType' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$x = 0;
|
|
|
|
|
$y = rand(0, 1);
|
|
|
|
|
$x++;
|
|
|
|
|
if ($x !== $y) {
|
|
|
|
|
chr($x);
|
|
|
|
|
}',
|
|
|
|
|
],
|
2019-03-21 22:26:10 +01:00
|
|
|
|
'don’tChangeTypeInElse' => [
|
|
|
|
|
'<?php
|
|
|
|
|
/** @var 0|string */
|
|
|
|
|
$x = 0;
|
|
|
|
|
$y = rand(0, 1) ? 0 : 1;
|
|
|
|
|
if ($x !== $y) {
|
|
|
|
|
} else {
|
|
|
|
|
if (!is_string($x)) {
|
|
|
|
|
chr($x);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @var int|string */
|
|
|
|
|
$x = 0;
|
|
|
|
|
if ($x !== $y) {
|
|
|
|
|
} else {
|
|
|
|
|
if (!is_string($x)) {
|
|
|
|
|
chr($x);
|
|
|
|
|
}
|
2019-03-23 19:27:54 +01:00
|
|
|
|
}',
|
2019-03-21 22:26:10 +01:00
|
|
|
|
],
|
2019-03-22 21:45:17 +01:00
|
|
|
|
'convertNullArrayKeyToEmptyString' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$a = [
|
|
|
|
|
1 => 1,
|
|
|
|
|
2 => 2,
|
|
|
|
|
null => "hello",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$b = $a[""];',
|
|
|
|
|
'assertions' => [
|
|
|
|
|
'$b' => 'string',
|
|
|
|
|
],
|
|
|
|
|
],
|
2019-05-02 06:50:35 +02:00
|
|
|
|
'yodaConditionalsShouldHaveSameOutput1' => [
|
|
|
|
|
'<?php
|
|
|
|
|
class Foo {
|
|
|
|
|
/**
|
|
|
|
|
* @var array{from:bool, to:bool}
|
|
|
|
|
*/
|
|
|
|
|
protected $things = ["from" => false, "to" => false];
|
|
|
|
|
|
|
|
|
|
public function foo(string ...$things) : void {
|
|
|
|
|
foreach ($things as $thing) {
|
|
|
|
|
if ($thing !== "from" && $thing !== "to") {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->things[$thing] = !$this->things[$thing];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
',
|
|
|
|
|
],
|
|
|
|
|
'yodaConditionalsShouldHaveSameOutput2' => [
|
|
|
|
|
'<?php
|
|
|
|
|
class Foo {
|
|
|
|
|
/**
|
|
|
|
|
* @var array{from:bool, to:bool}
|
|
|
|
|
*/
|
|
|
|
|
protected $things = ["from" => false, "to" => false];
|
|
|
|
|
|
|
|
|
|
public function foo(string ...$things) : void {
|
|
|
|
|
foreach ($things as $thing) {
|
|
|
|
|
if ("from" !== $thing && "to" !== $thing) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->things[$thing] = !$this->things[$thing];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
',
|
|
|
|
|
],
|
2019-11-21 16:44:24 +01:00
|
|
|
|
'supportSingleLiteralType' => [
|
|
|
|
|
'<?php
|
|
|
|
|
class A {
|
|
|
|
|
/**
|
|
|
|
|
* @var string
|
|
|
|
|
* @psalm-var "easy"
|
|
|
|
|
*/
|
|
|
|
|
private $type = "easy";
|
|
|
|
|
}'
|
|
|
|
|
],
|
2020-01-04 22:36:19 +01:00
|
|
|
|
'typecastTrueToInt' => [
|
|
|
|
|
'<?php
|
|
|
|
|
/**
|
|
|
|
|
* @param 0|1 $int
|
|
|
|
|
*/
|
|
|
|
|
function foo(int $int) : void {
|
|
|
|
|
echo (string) $int;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foo((int) true);',
|
|
|
|
|
],
|
|
|
|
|
'typecastFalseToInt' => [
|
|
|
|
|
'<?php
|
|
|
|
|
/**
|
|
|
|
|
* @param 0|1 $int
|
|
|
|
|
*/
|
|
|
|
|
function foo(int $int) : void {
|
|
|
|
|
echo (string) $int;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foo((int) false);',
|
|
|
|
|
],
|
|
|
|
|
'typecastedBoolToInt' => [
|
|
|
|
|
'<?php
|
|
|
|
|
/**
|
|
|
|
|
* @param 0|1 $int
|
|
|
|
|
*/
|
|
|
|
|
function foo(int $int) : void {
|
|
|
|
|
echo (string) $int;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foo((int) ((bool) 2));',
|
|
|
|
|
],
|
2020-01-30 03:25:44 +01:00
|
|
|
|
'notEqualToEachOther' => [
|
|
|
|
|
'<?php
|
|
|
|
|
function example(object $a, object $b): bool {
|
|
|
|
|
if ($a !== $b && \get_class($a) === \get_class($b)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}'
|
|
|
|
|
],
|
2020-02-22 06:16:15 +01:00
|
|
|
|
'numericStringValue' => [
|
|
|
|
|
'<?php
|
|
|
|
|
/** @psalm-return numeric-string */
|
|
|
|
|
function makeNumeric() : string {
|
|
|
|
|
return "12.34";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @psalm-param numeric-string $string */
|
|
|
|
|
function consumeNumeric(string $string) : void {
|
|
|
|
|
\error_log($string);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
consumeNumeric("12.34");'
|
|
|
|
|
],
|
2020-02-26 23:28:31 +01:00
|
|
|
|
'resolveScalarClassConstant' => [
|
|
|
|
|
'<?php
|
|
|
|
|
class PaymentFailure {
|
|
|
|
|
const NO_CLIENT = "no_client";
|
|
|
|
|
const NO_CARD = "no_card";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return PaymentFailure::NO_CARD|PaymentFailure::NO_CLIENT
|
|
|
|
|
*/
|
|
|
|
|
function something() {
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
return PaymentFailure::NO_CARD;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return PaymentFailure::NO_CLIENT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function blah(): void {
|
|
|
|
|
$test = something();
|
|
|
|
|
if ($test === PaymentFailure::NO_CLIENT) {}
|
|
|
|
|
}'
|
|
|
|
|
],
|
2020-05-18 14:52:37 +02:00
|
|
|
|
'removeNullAfterLessThanZero' => [
|
|
|
|
|
'<?php
|
|
|
|
|
function fcn(?int $val): int {
|
|
|
|
|
if ($val < 0) {
|
|
|
|
|
return $val;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 5;
|
|
|
|
|
}',
|
|
|
|
|
],
|
2020-05-18 19:42:47 +02:00
|
|
|
|
'numericStringCastFromInt' => [
|
|
|
|
|
'<?php
|
|
|
|
|
/**
|
|
|
|
|
* @return numeric-string
|
|
|
|
|
*/
|
|
|
|
|
function makeNumStringFromInt(int $v) {
|
|
|
|
|
return (string) $v;
|
|
|
|
|
}',
|
|
|
|
|
],
|
|
|
|
|
'numericStringCastFromFloat' => [
|
|
|
|
|
'<?php
|
|
|
|
|
/**
|
|
|
|
|
* @return numeric-string
|
|
|
|
|
*/
|
|
|
|
|
function makeNumStringFromFloat(float $v) {
|
|
|
|
|
return (string) $v;
|
|
|
|
|
}'
|
2020-07-27 01:09:26 +02:00
|
|
|
|
],
|
|
|
|
|
'compareNegatedValue' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$i = rand(-1, 5);
|
|
|
|
|
|
|
|
|
|
if (!($i > 0)) {
|
|
|
|
|
echo $i;
|
|
|
|
|
}',
|
|
|
|
|
],
|
2020-07-30 17:25:47 +02:00
|
|
|
|
'refinePositiveInt' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$f = rand(0, 1) ? -1 : 1;
|
|
|
|
|
if ($f > 0) {}'
|
|
|
|
|
],
|
2020-07-31 17:26:54 +02:00
|
|
|
|
'assignOpThenCheck' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$data = ["e" => 0];
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
$data["e"]++;
|
|
|
|
|
}
|
|
|
|
|
if ($data["e"] > 0) {}'
|
|
|
|
|
],
|
2020-08-26 23:58:01 +02:00
|
|
|
|
'compareToNullImplicitly' => [
|
|
|
|
|
'<?php
|
|
|
|
|
final class Foo {
|
|
|
|
|
public const VALUE_ANY = null;
|
|
|
|
|
public const VALUE_ONE = "one";
|
|
|
|
|
|
|
|
|
|
/** @return self::VALUE_* */
|
|
|
|
|
public static function getValues() {
|
|
|
|
|
return rand(0, 1) ? null : self::VALUE_ONE;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$data = Foo::getValues();
|
|
|
|
|
|
|
|
|
|
if ($data === Foo::VALUE_ANY) {
|
|
|
|
|
$data = "default";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
echo strlen($data);'
|
|
|
|
|
],
|
2018-05-03 19:56:30 +02:00
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-03-01 21:55:20 +01:00
|
|
|
|
* @return iterable<string,array{string,error_message:string,2?:string[],3?:bool,4?:string}>
|
2018-05-03 19:56:30 +02:00
|
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
|
public function providerInvalidCodeParse()
|
2018-05-03 19:56:30 +02:00
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'neverEqualsType' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$a = 4;
|
|
|
|
|
if ($a === 5) {
|
|
|
|
|
// do something
|
|
|
|
|
}',
|
|
|
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
|
|
|
],
|
|
|
|
|
'alwaysIdenticalType' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$a = 4;
|
|
|
|
|
if ($a === 4) {
|
|
|
|
|
// do something
|
|
|
|
|
}',
|
|
|
|
|
'error_message' => 'RedundantCondition',
|
|
|
|
|
],
|
|
|
|
|
'alwaysNotIdenticalType' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$a = 4;
|
|
|
|
|
if ($a !== 5) {
|
|
|
|
|
// do something
|
|
|
|
|
}',
|
|
|
|
|
'error_message' => 'RedundantCondition',
|
|
|
|
|
],
|
|
|
|
|
'neverNotIdenticalType' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$a = 4;
|
|
|
|
|
if ($a !== 4) {
|
|
|
|
|
// do something
|
|
|
|
|
}',
|
2018-05-18 17:02:50 +02:00
|
|
|
|
'error_message' => 'TypeDoesNotContainType',
|
2018-05-03 19:56:30 +02:00
|
|
|
|
],
|
2018-06-10 16:48:19 +02:00
|
|
|
|
'neverEqualsFloatType' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$a = 4.0;
|
|
|
|
|
if ($a === 4.1) {
|
|
|
|
|
// do something
|
|
|
|
|
}',
|
|
|
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
|
|
|
],
|
|
|
|
|
'alwaysIdenticalFloatType' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$a = 4.1;
|
|
|
|
|
if ($a === 4.1) {
|
|
|
|
|
// do something
|
|
|
|
|
}',
|
|
|
|
|
'error_message' => 'RedundantCondition',
|
|
|
|
|
],
|
|
|
|
|
'alwaysNotIdenticalFloatType' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$a = 4.0;
|
|
|
|
|
if ($a !== 4.1) {
|
|
|
|
|
// do something
|
|
|
|
|
}',
|
|
|
|
|
'error_message' => 'RedundantCondition',
|
|
|
|
|
],
|
|
|
|
|
'neverNotIdenticalFloatType' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$a = 4.1;
|
|
|
|
|
if ($a !== 4.1) {
|
|
|
|
|
// do something
|
|
|
|
|
}',
|
|
|
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
|
|
|
],
|
2018-05-03 19:56:30 +02:00
|
|
|
|
'ifImpossibleString' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$foo = rand(0, 1) ? "bar" : "bat";
|
|
|
|
|
|
|
|
|
|
if ($foo === "baz") {}',
|
|
|
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
|
|
|
],
|
|
|
|
|
'arrayOffsetImpossibleValue' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$foo = [
|
|
|
|
|
"a" => 1,
|
|
|
|
|
"b" => 2,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if ($foo["a"] === 2) {}',
|
|
|
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
|
|
|
],
|
|
|
|
|
'impossibleKeyInForeach' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$foo = [
|
|
|
|
|
"0" => 3,
|
|
|
|
|
"1" => 4,
|
|
|
|
|
"2" => 5,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function takesInt(int $s) : void {}
|
|
|
|
|
|
|
|
|
|
foreach ($foo as $i => $b) {
|
|
|
|
|
if ($i === 3) {}
|
|
|
|
|
}',
|
|
|
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
|
|
|
],
|
|
|
|
|
'impossibleValueInForeach' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$foo = [
|
|
|
|
|
"0" => 3,
|
|
|
|
|
"1" => 4,
|
|
|
|
|
"2" => 5,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function takesInt(int $s) : void {}
|
|
|
|
|
|
|
|
|
|
foreach ($foo as $i => $b) {
|
|
|
|
|
if ($b === $i) {}
|
|
|
|
|
}',
|
|
|
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
|
|
|
],
|
|
|
|
|
'invalidIntStringOffset' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$foo = [
|
|
|
|
|
"0" => 3,
|
|
|
|
|
"1" => 4,
|
|
|
|
|
"2" => 5,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$a = "3";
|
|
|
|
|
|
|
|
|
|
echo $foo[$a];',
|
|
|
|
|
'error_message' => 'InvalidArrayOffset',
|
|
|
|
|
],
|
2018-05-12 00:09:11 +02:00
|
|
|
|
'noChangeToVariable' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$i = 0;
|
|
|
|
|
|
|
|
|
|
$a = function() use ($i) : void {
|
|
|
|
|
$i++;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$a();
|
|
|
|
|
|
|
|
|
|
if ($i === 0) {}',
|
|
|
|
|
'error_message' => 'RedundantCondition',
|
|
|
|
|
],
|
2018-05-18 17:02:50 +02:00
|
|
|
|
'redefinedIntInIfAndImpossbleComparison' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$s = rand(0, 1) ? 0 : 1;
|
|
|
|
|
|
|
|
|
|
if ($s && rand(0, 1)) {
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
|
$s = 2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($s == 3) {}',
|
|
|
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
|
|
|
],
|
2018-06-08 19:53:42 +02:00
|
|
|
|
'badIfOrAssertionWithSwitch' => [
|
|
|
|
|
'<?php
|
|
|
|
|
function foo(string $s) : void {
|
|
|
|
|
switch ($s) {
|
|
|
|
|
case "a":
|
|
|
|
|
case "b":
|
|
|
|
|
case "c":
|
|
|
|
|
if ($s === "a" || $s === "b") {
|
|
|
|
|
throw new \InvalidArgumentException;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($s === "c") {}
|
|
|
|
|
}
|
|
|
|
|
}',
|
|
|
|
|
'error_message' => 'RedundantCondition',
|
|
|
|
|
],
|
2019-05-28 21:46:56 +02:00
|
|
|
|
'casedComparison' => [
|
|
|
|
|
'<?php
|
|
|
|
|
if ("C" === "c") {}',
|
|
|
|
|
'error_message' => 'TypeDoesNotContainType',
|
2019-07-05 22:24:00 +02:00
|
|
|
|
],
|
2020-07-27 00:29:17 +02:00
|
|
|
|
'compareValueTwice' => [
|
|
|
|
|
'<?php
|
|
|
|
|
$i = rand(-1, 5);
|
|
|
|
|
|
|
|
|
|
if ($i > 0 && $i > 0) {}',
|
|
|
|
|
'error_message' => 'RedundantCondition',
|
|
|
|
|
],
|
2020-05-25 04:42:08 +02:00
|
|
|
|
'numericStringCoerceToLiteral' => [
|
|
|
|
|
'<?php
|
|
|
|
|
/** @param "0"|"1" $s */
|
|
|
|
|
function foo(string $s) : void {}
|
|
|
|
|
|
|
|
|
|
function bar(string $s) : void {
|
|
|
|
|
if (is_numeric($s)) {
|
|
|
|
|
foo($s);
|
|
|
|
|
}
|
|
|
|
|
}',
|
|
|
|
|
'error_message' => 'ArgumentTypeCoercion'
|
|
|
|
|
],
|
2018-05-03 19:56:30 +02:00
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|