mirror of
https://github.com/danog/psalm.git
synced 2024-12-15 02:47:02 +01:00
9e4e468907
Related: #7771
3256 lines
112 KiB
PHP
3256 lines
112 KiB
PHP
<?php
|
|
|
|
namespace Psalm\Tests\TypeReconciliation;
|
|
|
|
use Psalm\Tests\TestCase;
|
|
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
|
|
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
|
|
|
|
class ConditionalTest extends TestCase
|
|
{
|
|
use InvalidCodeAnalysisTestTrait;
|
|
use ValidCodeAnalysisTestTrait;
|
|
|
|
/**
|
|
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
|
|
*/
|
|
public function providerValidCodeParse(): iterable
|
|
{
|
|
return [
|
|
'intIsMixed' => [
|
|
'<?php
|
|
/** @param mixed $a */
|
|
function foo($a): void {
|
|
$b = 5;
|
|
|
|
if ($b === $a) { }
|
|
}',
|
|
],
|
|
'typeResolutionFromDocblock' => [
|
|
'<?php
|
|
class A { }
|
|
|
|
/**
|
|
* @param A $a
|
|
* @return void
|
|
*/
|
|
function fooFoo($a) {
|
|
if ($a instanceof A) {
|
|
}
|
|
}',
|
|
'assertions' => [],
|
|
'error_levels' => ['RedundantConditionGivenDocblockType'],
|
|
],
|
|
'arrayTypeResolutionFromDocblock' => [
|
|
'<?php
|
|
/**
|
|
* @param string[] $strs
|
|
* @return void
|
|
*/
|
|
function foo(array $strs) {
|
|
foreach ($strs as $str) {
|
|
if (is_string($str)) {}
|
|
}
|
|
}',
|
|
'assertions' => [],
|
|
'error_levels' => ['RedundantConditionGivenDocblockType'],
|
|
],
|
|
'typeResolutionFromDocblockInside' => [
|
|
'<?php
|
|
/**
|
|
* @param int $length
|
|
* @return void
|
|
*/
|
|
function foo($length) {
|
|
if (!is_int($length)) {
|
|
if (is_numeric($length)) {
|
|
}
|
|
}
|
|
}',
|
|
'assertions' => [],
|
|
'error_levels' => ['DocblockTypeContradiction'],
|
|
],
|
|
'notInstanceof' => [
|
|
'<?php
|
|
class A { }
|
|
|
|
class B extends A { }
|
|
|
|
$a = new A();
|
|
|
|
$out = null;
|
|
|
|
if ($a instanceof B) {
|
|
// do something
|
|
}
|
|
else {
|
|
$out = $a;
|
|
}',
|
|
'assertions' => [
|
|
'$out' => 'A|null',
|
|
],
|
|
],
|
|
'notInstanceOfProperty' => [
|
|
'<?php
|
|
class B { }
|
|
|
|
class C extends B { }
|
|
|
|
class A {
|
|
/** @var B */
|
|
public $foo;
|
|
|
|
public function __construct() {
|
|
$this->foo = new B();
|
|
}
|
|
}
|
|
|
|
$a = new A();
|
|
|
|
$out = null;
|
|
|
|
if ($a->foo instanceof C) {
|
|
// do something
|
|
}
|
|
else {
|
|
$out = $a->foo;
|
|
}',
|
|
'assertions' => [
|
|
'$out' => 'B|null',
|
|
],
|
|
'error_levels' => [],
|
|
],
|
|
'notInstanceOfPropertyElseif' => [
|
|
'<?php
|
|
class B { }
|
|
|
|
class C extends B { }
|
|
|
|
class A {
|
|
/** @var string|B */
|
|
public $foo = "";
|
|
}
|
|
|
|
$a = new A();
|
|
|
|
$out = null;
|
|
|
|
if (is_string($a->foo)) {
|
|
|
|
}
|
|
elseif ($a->foo instanceof C) {
|
|
// do something
|
|
}
|
|
else {
|
|
$out = $a->foo;
|
|
}',
|
|
'assertions' => [
|
|
'$out' => 'B|null',
|
|
],
|
|
'error_levels' => [],
|
|
],
|
|
'typeRefinementWithIsNumericOnIntOrFalse' => [
|
|
'<?php
|
|
/** @return void */
|
|
function fooFoo(string $a) {
|
|
if (is_numeric($a)) { }
|
|
|
|
if (is_numeric($a) && $a === "1") { }
|
|
}
|
|
|
|
$b = rand(0, 1) ? 5 : false;
|
|
if (is_numeric($b)) { }',
|
|
],
|
|
'typeRefinementWithIsNumericAndIsString' => [
|
|
'<?php
|
|
/**
|
|
* @param mixed $a
|
|
* @return void
|
|
*/
|
|
function foo ($a) {
|
|
if (is_numeric($a)) {
|
|
if (is_string($a)) {
|
|
}
|
|
}
|
|
}',
|
|
],
|
|
'typeRefinementWithIsNumericOnIntOrString' => [
|
|
'<?php
|
|
$a = rand(0, 5) > 4 ? "hello" : 5;
|
|
|
|
if (is_numeric($a)) {
|
|
exit;
|
|
}',
|
|
'assertions' => [
|
|
'$a' => 'string',
|
|
],
|
|
],
|
|
'typeRefinementWithStringOrTrue' => [
|
|
'<?php
|
|
$a = rand(0, 5) > 4 ? "hello" : true;
|
|
|
|
if (is_bool($a)) {
|
|
exit;
|
|
}',
|
|
'assertions' => [
|
|
'$a' => 'string',
|
|
],
|
|
],
|
|
'updateMultipleIssetVars' => [
|
|
'<?php
|
|
/** @return void **/
|
|
function foo(string $s) {}
|
|
|
|
$a = rand(0, 1) ? ["hello"] : null;
|
|
if (isset($a[0])) {
|
|
foo($a[0]);
|
|
}',
|
|
],
|
|
'updateMultipleIssetVarsWithVariableOffset' => [
|
|
'<?php
|
|
/** @return void **/
|
|
function foo(string $s) {}
|
|
|
|
$a = rand(0, 1) ? ["hello"] : null;
|
|
$b = 0;
|
|
if (isset($a[$b])) {
|
|
foo($a[$b]);
|
|
}',
|
|
],
|
|
'instanceOfSubtypes' => [
|
|
'<?php
|
|
abstract class A {}
|
|
class B extends A {}
|
|
|
|
abstract class C {}
|
|
class D extends C {}
|
|
|
|
function makeA(): A {
|
|
return new B();
|
|
}
|
|
|
|
function makeC(): C {
|
|
return new D();
|
|
}
|
|
|
|
$a = rand(0, 1) ? makeA() : makeC();
|
|
|
|
if ($a instanceof B || $a instanceof D) { }',
|
|
],
|
|
'typeReconciliationAfterIfAndReturn' => [
|
|
'<?php
|
|
/**
|
|
* @param string|int $a
|
|
* @return string|int
|
|
*/
|
|
function foo($a) {
|
|
if (is_string($a)) {
|
|
return $a;
|
|
} elseif (is_int($a)) {
|
|
return $a;
|
|
}
|
|
|
|
throw new \LogicException("Runtime error");
|
|
}',
|
|
'assertions' => [],
|
|
'error_levels' => ['RedundantConditionGivenDocblockType'],
|
|
],
|
|
'ignoreNullCheckAndMaintainNullValue' => [
|
|
'<?php
|
|
$a = null;
|
|
if ($a !== null) { }
|
|
$b = $a;',
|
|
'assertions' => [
|
|
'$b' => 'null',
|
|
],
|
|
'error_levels' => ['TypeDoesNotContainType', 'RedundantCondition'],
|
|
],
|
|
'ignoreNullCheckAndMaintainNullableValue' => [
|
|
'<?php
|
|
$a = rand(0, 1) ? 5 : null;
|
|
if ($a !== null) { }
|
|
$b = $a;',
|
|
'assertions' => [
|
|
'$b' => 'int|null',
|
|
],
|
|
],
|
|
'ternaryByRefVar' => [
|
|
'<?php
|
|
function foo(): void {
|
|
$b = null;
|
|
$c = rand(0, 1) ? bar($b) : null;
|
|
if (is_int($b)) { }
|
|
}
|
|
function bar(?int &$a): void {
|
|
$a = 5;
|
|
}',
|
|
],
|
|
'ternaryByRefVarInConditional' => [
|
|
'<?php
|
|
function foo(): void {
|
|
$b = null;
|
|
if (rand(0, 1) || bar($b)) {
|
|
if (is_int($b)) { }
|
|
}
|
|
}
|
|
function bar(?int &$a): void {
|
|
$a = 5;
|
|
}',
|
|
],
|
|
'possibleInstanceof' => [
|
|
'<?php
|
|
interface I1 {}
|
|
interface I2 {}
|
|
|
|
class A
|
|
{
|
|
public function foo(): void {
|
|
if ($this instanceof I1 || $this instanceof I2) {}
|
|
}
|
|
}',
|
|
],
|
|
'intersection' => [
|
|
'<?php
|
|
interface I {
|
|
public function bat(): void;
|
|
}
|
|
|
|
function takesI(I $i): void {}
|
|
function takesA(A $a): void {}
|
|
/** @param A&I $a */
|
|
function takesAandI($a): void {}
|
|
/** @param I&A $a */
|
|
function takesIandA($a): void {}
|
|
|
|
class A {
|
|
/**
|
|
* @return A&I|null
|
|
*/
|
|
public function foo() {
|
|
if ($this instanceof I) {
|
|
$this->bar();
|
|
$this->bat();
|
|
|
|
takesA($this);
|
|
takesI($this);
|
|
takesAandI($this);
|
|
takesIandA($this);
|
|
}
|
|
}
|
|
|
|
protected function bar(): void {}
|
|
}
|
|
|
|
class B extends A implements I {
|
|
public function bat(): void {}
|
|
}',
|
|
],
|
|
'createIntersectionOfInterfaceAndClass' => [
|
|
'<?php
|
|
class A {
|
|
public function bat() : void {}
|
|
}
|
|
interface I {
|
|
public function baz() : void;
|
|
}
|
|
|
|
function foo(I $i) : void {
|
|
if ($i instanceof A) {
|
|
$i->bat();
|
|
$i->baz();
|
|
}
|
|
}
|
|
|
|
function bar(A $a) : void {
|
|
if ($a instanceof I) {
|
|
$a->bat();
|
|
$a->baz();
|
|
}
|
|
}
|
|
|
|
class B extends A implements I {
|
|
public function baz() : void {}
|
|
}
|
|
|
|
foo(new B);
|
|
bar(new B);',
|
|
],
|
|
'unionOfArrayOrTraversable' => [
|
|
'<?php
|
|
function foo(iterable $iterable) : void {
|
|
if (\is_array($iterable)) {}
|
|
if ($iterable instanceof \Traversable) {}
|
|
}',
|
|
],
|
|
'isTruthy' => [
|
|
'<?php
|
|
function f(string $s = null): string {
|
|
if ($s == true) {
|
|
return $s;
|
|
}
|
|
|
|
return "backup";
|
|
}',
|
|
],
|
|
'stringOrCallableArg' => [
|
|
'<?php
|
|
/**
|
|
* @param string|callable $param
|
|
*/
|
|
function f($param): void {}
|
|
f("is_array");',
|
|
],
|
|
'stringOrCallableOrObjectArg' => [
|
|
'<?php
|
|
/**
|
|
* @param string|callable|object $param
|
|
*/
|
|
function f($param): void {}
|
|
f("is_array");',
|
|
],
|
|
'intOrFloatArg' => [
|
|
'<?php
|
|
/**
|
|
* @param int|float $param
|
|
*/
|
|
function f($param): void {}
|
|
f(5.0);
|
|
f(5);',
|
|
],
|
|
'nullReplacement' => [
|
|
'<?php
|
|
/**
|
|
* @param string|null|false $a
|
|
* @return string|false $a
|
|
*/
|
|
function foo($a) {
|
|
if ($a === null) {
|
|
if (rand(0, 4) > 2) {
|
|
$a = "hello";
|
|
} else {
|
|
$a = false;
|
|
}
|
|
}
|
|
|
|
return $a;
|
|
}',
|
|
],
|
|
'nullableIntReplacement' => [
|
|
'<?php
|
|
$a = rand(0, 1) ? 5 : null;
|
|
|
|
$b = (bool)rand(0, 1);
|
|
|
|
if ($b || $a !== null) {
|
|
$a = 3;
|
|
}',
|
|
'assertions' => [
|
|
'$a' => 'int|null',
|
|
],
|
|
],
|
|
'eraseNullAfterInequalityCheck' => [
|
|
'<?php
|
|
$a = mt_rand(0, 1) ? mt_rand(-10, 10) : null;
|
|
|
|
if ($a > 0) {
|
|
echo $a + 3;
|
|
}
|
|
|
|
if (0 < $a) {
|
|
echo $a + 3;
|
|
}',
|
|
],
|
|
'twoWrongsDontMakeARight' => [
|
|
'<?php
|
|
if (rand(0, 1)) {
|
|
$a = false;
|
|
} else {
|
|
$a = false;
|
|
}',
|
|
'assertions' => [
|
|
'$a' => 'false',
|
|
],
|
|
],
|
|
'instanceofStatic' => [
|
|
'<?php
|
|
abstract class Foo {
|
|
/**
|
|
* @return static[]
|
|
*/
|
|
abstract public static function getArr() : array;
|
|
|
|
/**
|
|
* @return static|null
|
|
*/
|
|
public static function getOne() {
|
|
$one = current(static::getArr());
|
|
return $one instanceof static ? $one : null;
|
|
}
|
|
}',
|
|
],
|
|
'isaStaticClass' => [
|
|
'<?php
|
|
abstract class Foo {
|
|
/**
|
|
* @return static[]
|
|
*/
|
|
abstract public static function getArr() : array;
|
|
|
|
/**
|
|
* @return static|null
|
|
*/
|
|
public static function getOne() {
|
|
$one = current(static::getArr());
|
|
return is_a($one, static::class, false) ? $one : null;
|
|
}
|
|
}',
|
|
],
|
|
'isAClass' => [
|
|
'<?php
|
|
class A {}
|
|
$a_class = rand(0, 1) ? A::class : "blargle";
|
|
if (is_a($a_class, A::class, true)) {
|
|
echo "cool";
|
|
}',
|
|
],
|
|
'specificArrayFields' => [
|
|
'<?php
|
|
/**
|
|
* @param array{field:string} $array
|
|
*/
|
|
function print_field($array) : void {
|
|
echo $array["field"];
|
|
}
|
|
|
|
/**
|
|
* @param array{field:string,otherField:string} $array
|
|
*/
|
|
function has_mix_of_fields($array) : void {
|
|
print_field($array);
|
|
}',
|
|
],
|
|
'falsyScalar' => [
|
|
'<?php
|
|
/**
|
|
* @param scalar|null $value
|
|
*/
|
|
function Foo($value = null) : bool {
|
|
if (!$value) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}',
|
|
],
|
|
'numericStringAssertion' => [
|
|
'<?php
|
|
/**
|
|
* @param mixed $a
|
|
*/
|
|
function foo($a, string $b) : void {
|
|
if (is_numeric($b) && $a === $b) {
|
|
echo $a;
|
|
}
|
|
}',
|
|
],
|
|
'reconcileNullableStringWithWeakEquality' => [
|
|
'<?php
|
|
function foo(?string $s) : void {
|
|
if ($s == "hello" || $s == "goodbye") {
|
|
if ($s == "hello") {
|
|
echo "cool";
|
|
}
|
|
echo "cooler";
|
|
}
|
|
}',
|
|
],
|
|
'reconcileNullableStringWithStrictEqualityStrings' => [
|
|
'<?php
|
|
function foo(?string $s, string $a, string $b) : void {
|
|
if ($s === $a || $s === $b) {
|
|
if ($s === $a) {
|
|
echo "cool";
|
|
}
|
|
echo "cooler";
|
|
}
|
|
}',
|
|
],
|
|
'reconcileNullableStringWithWeakEqualityStrings' => [
|
|
'<?php
|
|
function foo(?string $s, string $a, string $b) : void {
|
|
if ($s == $a || $s == $b) {
|
|
if ($s == $a) {
|
|
echo "cool";
|
|
}
|
|
echo "cooler";
|
|
}
|
|
}',
|
|
],
|
|
'allowWeakEqualityScalarType' => [
|
|
'<?php
|
|
function foo(int $i) : void {
|
|
if ($i == "5") {}
|
|
if ("5" == $i) {}
|
|
if ($i == 5.0) {}
|
|
if (5.0 == $i) {}
|
|
if ($i == 0) {}
|
|
if (0 == $i) {}
|
|
if ($i == 0.0) {}
|
|
if (0.0 == $i) {}
|
|
}
|
|
function bar(float $i) : void {
|
|
$i = $i / 100.0;
|
|
if ($i == "5") {}
|
|
if ("5" == $i) {}
|
|
if ($i == 5) {}
|
|
if (5 == $i) {}
|
|
if ($i == "0") {}
|
|
if ("0" == $i) {}
|
|
if ($i == 0) {}
|
|
if (0 == $i) {}
|
|
}
|
|
function bat(string $i) : void {
|
|
if ($i == 5) {}
|
|
if (5 == $i) {}
|
|
if ($i == 5.0) {}
|
|
if (5.0 == $i) {}
|
|
if ($i == 0) {}
|
|
if (0 == $i) {}
|
|
if ($i == 0.0) {}
|
|
if (0.0 == $i) {}
|
|
}',
|
|
],
|
|
'filterSubclassBasedOnParentInstanceof' => [
|
|
'<?php
|
|
class A {}
|
|
class B extends A {
|
|
public function foo() : void {}
|
|
}
|
|
|
|
class C {}
|
|
class D extends C {}
|
|
|
|
$b_or_d = rand(0, 1) ? new B : new D;
|
|
|
|
if ($b_or_d instanceof A) {
|
|
$b_or_d->foo();
|
|
}',
|
|
],
|
|
'isArrayOnArrayKeyOffset' => [
|
|
'<?php
|
|
/** @var array{s:array<mixed, array<int, string>|string>} */
|
|
$doc = [];
|
|
|
|
if (!is_array($doc["s"]["t"])) {
|
|
$doc["s"]["t"] = [$doc["s"]["t"]];
|
|
}',
|
|
'assertions' => [
|
|
'$doc[\'s\'][\'t\']' => 'array<int, string>',
|
|
],
|
|
],
|
|
'removeTrue' => [
|
|
'<?php
|
|
$a = rand(0, 1) ? new stdClass : true;
|
|
|
|
if ($a === true) {
|
|
exit;
|
|
}
|
|
|
|
function takesStdClass(stdClass $s) : void {}
|
|
takesStdClass($a);',
|
|
],
|
|
'noReconciliationInElseIf' => [
|
|
'<?php
|
|
class A {}
|
|
$a = rand(0, 1) ? new A : null;
|
|
|
|
if (rand(0, 1)) {
|
|
// do nothing
|
|
} elseif (!$a) {
|
|
$a = new A();
|
|
}
|
|
|
|
if ($a) {}',
|
|
],
|
|
'removeStringWithIsScalar' => [
|
|
'<?php
|
|
$a = rand(0, 1) ? "hello" : null;
|
|
|
|
if (is_scalar($a)) {
|
|
exit;
|
|
}',
|
|
'assertions' => [
|
|
'$a' => 'null',
|
|
],
|
|
],
|
|
'removeNullWithIsScalar' => [
|
|
'<?php
|
|
$a = rand(0, 1) ? "hello" : null;
|
|
|
|
if (!is_scalar($a)) {
|
|
exit;
|
|
}',
|
|
'assertions' => [
|
|
'$a' => 'string',
|
|
],
|
|
],
|
|
'scalarToNumeric' => [
|
|
'<?php
|
|
/**
|
|
* @param scalar $thing
|
|
*/
|
|
function Foo($thing) : void {
|
|
if (is_numeric($thing)) {}
|
|
}',
|
|
],
|
|
'filterSubclassBasedOnParentNegativeInstanceof' => [
|
|
'<?php
|
|
class Obj {}
|
|
class A extends Obj {}
|
|
class B extends A {}
|
|
class C extends Obj {}
|
|
class D extends C {}
|
|
|
|
function takesD(D $d) : void {}
|
|
|
|
/** @param B|D $bar */
|
|
function foo(Obj $bar) : void {
|
|
if (!$bar instanceof A) {
|
|
takesD($bar);
|
|
}
|
|
}',
|
|
],
|
|
'dontEliminateAssignOp' => [
|
|
'<?php
|
|
class Obj {}
|
|
class A extends Obj {}
|
|
class B extends A {}
|
|
class C extends Obj {}
|
|
class D extends C {}
|
|
class E extends C {}
|
|
|
|
function bar(Obj $node) : void {
|
|
if ($node instanceof B
|
|
|| $node instanceof D
|
|
|| $node instanceof E
|
|
) {
|
|
if ($node instanceof C) {}
|
|
if ($node instanceof D) {}
|
|
}
|
|
}',
|
|
],
|
|
'eliminateNonArrays' => [
|
|
'<?php
|
|
interface I {}
|
|
|
|
function takesArray(array $_a): void {}
|
|
|
|
/** @param string|I|string[]|I[] $p */
|
|
function eliminatesNonArray($p): void {
|
|
if (is_array($p)) {
|
|
takesArray($p);
|
|
}
|
|
}',
|
|
],
|
|
'eliminateNonIterable' => [
|
|
'<?php
|
|
/**
|
|
* @param iterable<string>|null $foo
|
|
*/
|
|
function d(?iterable $foo): void {
|
|
if (is_iterable($foo)) {
|
|
foreach ($foo as $f) {}
|
|
}
|
|
|
|
if (!is_iterable($foo)) {
|
|
|
|
} else {
|
|
foreach ($foo as $f) {}
|
|
}
|
|
}',
|
|
],
|
|
'isStringServerVar' => [
|
|
'<?php
|
|
if (is_string($_SERVER["abc"])) {
|
|
echo substr($_SERVER["abc"], 1, 2);
|
|
}',
|
|
],
|
|
'notObject' => [
|
|
'<?php
|
|
function f(): ?object {
|
|
return rand(0,1) ? new stdClass : null;
|
|
}
|
|
|
|
$data = f();
|
|
if (!$data) {}
|
|
if ($data) {}',
|
|
],
|
|
'reconcileWithInstanceof' => [
|
|
'<?php
|
|
class A {}
|
|
class B extends A {
|
|
public function b() : bool {
|
|
return (bool) rand(0, 1);
|
|
}
|
|
}
|
|
|
|
function bar(?A $a) : void {
|
|
if (!$a || ($a instanceof B && $a->b())) {}
|
|
}',
|
|
],
|
|
'reconcileFloatToEmpty' => [
|
|
'<?php
|
|
function bar(float $f) : void {
|
|
if (!$f) {}
|
|
}',
|
|
],
|
|
'scalarToBool' => [
|
|
'<?php
|
|
/** @var scalar */
|
|
$s = 1;
|
|
|
|
if (is_bool($s)) {}
|
|
if (!is_bool($s)) {}',
|
|
[
|
|
'$s' => 'scalar'
|
|
]
|
|
],
|
|
'scalarToString' => [
|
|
'<?php
|
|
/** @var scalar */
|
|
$s = 1;
|
|
|
|
if (is_string($s)) {}
|
|
if (!is_string($s)) {}',
|
|
[
|
|
'$s' => 'scalar'
|
|
]
|
|
],
|
|
'scalarToInt' => [
|
|
'<?php
|
|
/** @var scalar */
|
|
$s = 1;
|
|
|
|
if (is_int($s)) {}
|
|
if (!is_int($s)) {}',
|
|
[
|
|
'$s' => 'scalar'
|
|
]
|
|
],
|
|
'scalarToFloat' => [
|
|
'<?php
|
|
/** @var scalar */
|
|
$s = 1;
|
|
|
|
if (is_float($s)) {}
|
|
if (!is_float($s)) {}',
|
|
[
|
|
'$s' => 'scalar'
|
|
]
|
|
],
|
|
'removeFromArray' => [
|
|
'<?php
|
|
/**
|
|
* @param array<string> $v
|
|
*/
|
|
function foo(array $v) : void {
|
|
if (!isset($v[0])) {
|
|
return;
|
|
}
|
|
|
|
if ($v[0] === " ") {
|
|
array_shift($v);
|
|
}
|
|
|
|
if (!isset($v[0])) {}
|
|
}',
|
|
],
|
|
'arrayEquality' => [
|
|
'<?php
|
|
/**
|
|
* @param array<string, array<array-key, string|int>> $haystack
|
|
* @param array<array-key, int|string> $needle
|
|
*/
|
|
function foo(array $haystack, array $needle) : void {
|
|
foreach ($haystack as $arr) {
|
|
if ($arr === $needle) {}
|
|
}
|
|
}',
|
|
],
|
|
'classResolvesBackToSelfAfterComparison' => [
|
|
'<?php
|
|
class A {}
|
|
class B extends A {}
|
|
function getA() : A {
|
|
return new A();
|
|
}
|
|
|
|
$a = getA();
|
|
if ($a instanceof B) {
|
|
$a = new B;
|
|
}',
|
|
'assertions' => [
|
|
'$a' => 'A',
|
|
],
|
|
],
|
|
'isNumericCanBeScalar' => [
|
|
'<?php
|
|
/** @param scalar $val */
|
|
function foo($val) : void {
|
|
if (!is_numeric($val)) {}
|
|
}',
|
|
],
|
|
'classStringCanBeFalsy' => [
|
|
'<?php
|
|
/** @param class-string<stdClass>|null $val */
|
|
function foo(?string $val) : void {
|
|
if (!$val) {}
|
|
if ($val) {}
|
|
}',
|
|
],
|
|
'allowStringToObjectReconciliation' => [
|
|
'<?php
|
|
/**
|
|
* @param string|object $maybe
|
|
*
|
|
* @throws InvalidArgumentException but it should not
|
|
*/
|
|
function foo($maybe) : string {
|
|
/** @psalm-suppress DocblockTypeContradiction */
|
|
if ( ! is_string($maybe) && ! is_object($maybe)) {
|
|
throw new InvalidArgumentException("bad");
|
|
}
|
|
|
|
return is_string($maybe) ? $maybe : get_class($maybe);
|
|
}',
|
|
],
|
|
'allowObjectToStringReconciliation' => [
|
|
'<?php
|
|
/**
|
|
* @param string|object $maybe
|
|
*
|
|
* @throws InvalidArgumentException but it should not
|
|
*/
|
|
function bar($maybe) : string {
|
|
/** @psalm-suppress DocblockTypeContradiction */
|
|
if ( ! is_string($maybe) && ! is_object($maybe)) {
|
|
throw new InvalidArgumentException("bad");
|
|
}
|
|
|
|
return is_object($maybe) ? get_class($maybe) : $maybe;
|
|
}',
|
|
],
|
|
'removeArrayWithIterableCheck' => [
|
|
'<?php
|
|
$s = rand(0,1) ? "foo" : [1];
|
|
if (!is_iterable($s)) {
|
|
strlen($s);
|
|
}',
|
|
],
|
|
'removeIterableWithIterableCheck' => [
|
|
'<?php
|
|
/** @var string|iterable */
|
|
$s = rand(0,1) ? "foo" : [1];
|
|
if (!is_iterable($s)) {
|
|
strlen($s);
|
|
}',
|
|
],
|
|
'removeArrayWithIterableCheckWithExit' => [
|
|
'<?php
|
|
$a = rand(0,1) ? "foo" : [1];
|
|
if (is_iterable($a)) {
|
|
return;
|
|
}
|
|
strlen($a);',
|
|
],
|
|
'removeIterableWithIterableCheckWithExit' => [
|
|
'<?php
|
|
/** @var string|iterable */
|
|
$a = rand(0,1) ? "foo" : [1];
|
|
if (is_iterable($a)) {
|
|
return;
|
|
}
|
|
strlen($a);',
|
|
],
|
|
'removeCallableString' => [
|
|
'<?php
|
|
$s = rand(0,1) ? "strlen" : [1];
|
|
if (!is_callable($s)) {
|
|
array_pop($s);
|
|
}',
|
|
],
|
|
'removeCallableClosure' => [
|
|
'<?php
|
|
$a = rand(0, 1) ? (function(): void {}) : 1;
|
|
if (!is_callable($a)) {
|
|
echo $a;
|
|
}',
|
|
],
|
|
'removeCallableWithAssertion' => [
|
|
'<?php
|
|
/**
|
|
* @param mixed $p
|
|
* @psalm-assert !callable $p
|
|
* @throws TypeError
|
|
*/
|
|
function assertIsNotCallable($p): void { if (!is_callable($p)) throw new TypeError; }
|
|
|
|
/** @return callable|float */
|
|
function f() { return rand(0,1) ? "f" : 1.1; }
|
|
|
|
$a = f();
|
|
assert(!is_callable($a));
|
|
|
|
$b = f();
|
|
assertIsNotCallable($b);
|
|
|
|
atan($a);
|
|
atan($b);',
|
|
],
|
|
'PHP71-removeNonCallable' => [
|
|
'<?php
|
|
$f = rand(0, 1) ? "strlen" : 1.1;
|
|
if (is_callable($f)) {
|
|
Closure::fromCallable($f);
|
|
}',
|
|
],
|
|
'dontChangeScalar' => [
|
|
'<?php
|
|
/**
|
|
* @param scalar|null $val
|
|
*/
|
|
function foo($val) : ? bool {
|
|
if ("1" === $val || 1 === $val) {
|
|
return true;
|
|
} elseif ("0" === $val || 0 === $val) {
|
|
return false;
|
|
}
|
|
|
|
return null;
|
|
}',
|
|
],
|
|
'emptyArrayCheck' => [
|
|
'<?php
|
|
/**
|
|
* @param non-empty-array $x
|
|
*/
|
|
function example(array $x): void {}
|
|
|
|
/** @var array */
|
|
$x = [];
|
|
if ($x !== []) {
|
|
example($x);
|
|
}',
|
|
],
|
|
'emptyArrayCheckInverse' => [
|
|
'<?php
|
|
/**
|
|
* @param non-empty-array $x
|
|
*/
|
|
function example(array $x): void {}
|
|
|
|
/** @var array */
|
|
$x = [];
|
|
if ($x === []) {
|
|
} else {
|
|
example($x);
|
|
}',
|
|
],
|
|
'allowNumericToFoldIntoType' => [
|
|
'<?php
|
|
/**
|
|
* @param mixed $width
|
|
* @param mixed $height
|
|
*
|
|
* @throws RuntimeException
|
|
*/
|
|
function Foo($width, $height) : void {
|
|
if (!is_numeric($width) || !is_numeric($height)) {
|
|
throw new RuntimeException("Width & Height were not numeric!");
|
|
}
|
|
|
|
echo sprintf("padding-top:%s%%;", 100 * ($height/$width));
|
|
}',
|
|
],
|
|
'notEmptyCheckOnMixedInTernary' => [
|
|
'<?php
|
|
$a = !empty($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] !== "off" ? true : false;',
|
|
],
|
|
'notEmptyCheckOnMixedInIf' => [
|
|
'<?php
|
|
if (!empty($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] !== "off") {
|
|
$a = true;
|
|
} else {
|
|
$a = false;
|
|
}',
|
|
],
|
|
'dontRewriteNullableArrayAfterEmptyCheck' => [
|
|
'<?php
|
|
/**
|
|
* @param array{x:int,y:int}|null $start_pos
|
|
* @return array{x:int,y:int}|null
|
|
*/
|
|
function foo(?array $start_pos) : ?array {
|
|
if ($start_pos) {}
|
|
|
|
return $start_pos;
|
|
}',
|
|
],
|
|
'falseEqualsBoolean' => [
|
|
'<?php
|
|
class A {}
|
|
class B extends A {
|
|
public function foo() : void {}
|
|
}
|
|
class C extends A {
|
|
public function foo() : void {}
|
|
}
|
|
function bar(A $a) : void {
|
|
if (false === ($a instanceof B || $a instanceof C)) {
|
|
return;
|
|
}
|
|
$a->foo();
|
|
}
|
|
function baz(A $a) : void {
|
|
if (($a instanceof B || $a instanceof C) === false) {
|
|
return;
|
|
}
|
|
$a->foo();
|
|
}',
|
|
],
|
|
'selfInstanceofStatic' => [
|
|
'<?php
|
|
class A {
|
|
public function foo(self $value): void {
|
|
if ($value instanceof static) {}
|
|
}
|
|
}',
|
|
],
|
|
'reconcileCallable' => [
|
|
'<?php
|
|
function reflectCallable(callable $callable): ReflectionFunctionAbstract {
|
|
if (\is_array($callable)) {
|
|
return new \ReflectionMethod($callable[0], $callable[1]);
|
|
} elseif ($callable instanceof \Closure || \is_string($callable)) {
|
|
return new \ReflectionFunction($callable);
|
|
} else {
|
|
return new \ReflectionMethod($callable, "__invoke");
|
|
}
|
|
}',
|
|
],
|
|
'noLeakyClassType' => [
|
|
'<?php
|
|
class A {
|
|
public array $foo = [];
|
|
public array $bar = [];
|
|
|
|
public function setter() : void {
|
|
if ($this->foo) {
|
|
$this->foo = [];
|
|
}
|
|
}
|
|
|
|
public function iffer() : bool {
|
|
return $this->foo || $this->bar;
|
|
}
|
|
}',
|
|
],
|
|
'noLeakyForeachType' => [
|
|
'<?php
|
|
|
|
class A {
|
|
/** @var mixed */
|
|
public $_array_value = null;
|
|
|
|
private function getArrayValue() : ?array {
|
|
return rand(0, 1) ? [] : null;
|
|
}
|
|
|
|
public function setValue(string $var) : void {
|
|
$this->_array_value = $this->getArrayValue();
|
|
|
|
if ($this->_array_value !== null && !count($this->_array_value)) {
|
|
return;
|
|
}
|
|
|
|
switch ($var) {
|
|
case "a":
|
|
foreach ($this->_array_value ?: [] as $v) {}
|
|
break;
|
|
|
|
case "b":
|
|
foreach ($this->_array_value ?: [] as $v) {}
|
|
break;
|
|
}
|
|
}
|
|
}',
|
|
[],
|
|
['MixedAssignment'],
|
|
],
|
|
'nonEmptyThing' => [
|
|
'<?php
|
|
/** @param mixed $clips */
|
|
function foo($clips, bool $found, int $id) : void {
|
|
if ($found === false) {
|
|
$clips = [];
|
|
}
|
|
|
|
$i = array_search($id, $clips);
|
|
|
|
if ($i !== false) {
|
|
unset($clips[$i]);
|
|
}
|
|
}',
|
|
[],
|
|
['MixedArgument', 'MixedArrayAccess', 'MixedAssignment', 'MixedArrayOffset'],
|
|
],
|
|
'allowNonEmptyArrayComparison' => [
|
|
'<?php
|
|
/**
|
|
* @param non-empty-array $a
|
|
* @param array<string> $b
|
|
*/
|
|
function foo(array $a, array $b) : void {
|
|
if ($a === $b) {}
|
|
}',
|
|
],
|
|
'preventCombinatorialExpansion' => [
|
|
'<?php
|
|
function gameOver(
|
|
int $b0,
|
|
int $b1,
|
|
int $b2,
|
|
int $b3,
|
|
int $b4,
|
|
int $b5,
|
|
int $b6,
|
|
int $b7,
|
|
int $b8
|
|
): bool {
|
|
if (($b0 === 1 && $b1 === 1 && $b2 === 1)
|
|
|| ($b3 === 1 && $b4 === 1 && $b5 === 1)
|
|
|| ($b6 === 1 && $b7 === 1 && $b8 === 1)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}'
|
|
],
|
|
'checkIterableType' => [
|
|
'<?php
|
|
/**
|
|
* @param array<int> $x
|
|
*/
|
|
function takesArray (array $x): void {}
|
|
|
|
/** @var iterable<int> */
|
|
$x = null;
|
|
assert(is_array($x));
|
|
takesArray($x);
|
|
|
|
/**
|
|
* @param Traversable<int> $x
|
|
*/
|
|
function takesTraversable (Traversable $x): void {}
|
|
|
|
/** @var iterable<int> */
|
|
$x = null;
|
|
assert($x instanceof Traversable);
|
|
takesTraversable($x);',
|
|
],
|
|
'dontReconcileArrayOffset' => [
|
|
'<?php
|
|
/** @psalm-suppress TypeDoesNotContainType */
|
|
function foo(array $a) : void {
|
|
if (!is_array($a)) {
|
|
return;
|
|
}
|
|
|
|
if ($a[0] === 5) {}
|
|
}'
|
|
],
|
|
'nullCoalesceTypedArrayValue' => [
|
|
'<?php
|
|
/** @param string[] $arr */
|
|
function foo(array $arr) : string {
|
|
return $arr["b"] ?? "bar";
|
|
}',
|
|
],
|
|
'nullCoalesceTypedValue' => [
|
|
'<?php
|
|
function foo(?string $s) : string {
|
|
return $s ?? "bar";
|
|
}',
|
|
],
|
|
'looseEqualityShouldNotConvertMixedToLiteralString' => [
|
|
'<?php
|
|
/** @var mixed */
|
|
$int = 0;
|
|
$string = "0";
|
|
|
|
function takes_string(string $string) : void {}
|
|
function takes_int(int $int) : void {}
|
|
|
|
if ($int == $string) {
|
|
/** @psalm-suppress MixedArgument */
|
|
takes_int($int);
|
|
}'
|
|
],
|
|
'looseEqualityShouldNotConverMixedToString' => [
|
|
'<?php
|
|
/** @var mixed */
|
|
$int = 0;
|
|
/** @var string */
|
|
$string = "0";
|
|
|
|
function takes_string(string $string) : void {}
|
|
function takes_int(int $int) : void {}
|
|
|
|
if ($int == $string) {
|
|
/** @psalm-suppress MixedArgument */
|
|
takes_int($int);
|
|
}'
|
|
],
|
|
'looseEqualityShouldNotConvertIntToString' => [
|
|
'<?php
|
|
/** @var int */
|
|
$int = 0;
|
|
/** @var string */
|
|
$string = "0";
|
|
|
|
function takes_string(string $string) : void {}
|
|
function takes_int(int $int) : void {}
|
|
|
|
if ($int == $string) {
|
|
/** @psalm-suppress MixedArgument */
|
|
takes_int($int);
|
|
}'
|
|
],
|
|
'removeAllObjects' => [
|
|
'<?php
|
|
class A {}
|
|
class B extends A {
|
|
public function foo() : void {}
|
|
}
|
|
class BChild extends B {}
|
|
class C extends A {}
|
|
class D extends A {}
|
|
|
|
/** @param B|C|D $a */
|
|
function foo(A $a) : B {
|
|
if ($a instanceof C) {
|
|
$a = new B();
|
|
} elseif ($a instanceof D) {
|
|
$a = new B();
|
|
} elseif (!$a instanceof BChild) {
|
|
// do something
|
|
}
|
|
|
|
return $a;
|
|
}'
|
|
],
|
|
'nullCoalescePossibleMixed' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-suppress MixedReturnStatement
|
|
* @psalm-suppress MixedInferredReturnType
|
|
*/
|
|
function foo() : array {
|
|
return filter_input_array(INPUT_POST) ?? [];
|
|
}',
|
|
],
|
|
'noCrashOnWeirdArrayKeys' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-suppress MixedPropertyFetch
|
|
* @psalm-suppress MixedArrayOffset
|
|
*/
|
|
function foo(array $a, array $b) : void {
|
|
if (isset($a[$b[0]->id])) {}
|
|
}',
|
|
],
|
|
'assertArrayReturnTypeNarrowed' => [
|
|
'<?php
|
|
/** @return array{0:Exception} */
|
|
function f(array $a): array {
|
|
if ($a[0] instanceof Exception) {
|
|
return $a;
|
|
}
|
|
|
|
return [new Exception("bad")];
|
|
}',
|
|
],
|
|
'assertTypeNarrowedByAssert' => [
|
|
'<?php
|
|
/** @return array{0:Exception,1:Exception} */
|
|
function f(array $ret): array {
|
|
assert($ret[0] instanceof Exception);
|
|
assert($ret[1] instanceof Exception);
|
|
return $ret;
|
|
}',
|
|
],
|
|
'assertTypeNarrowedByButOtherFetchesAreMixed' => [
|
|
'<?php
|
|
/**
|
|
* @return array{0:Exception}
|
|
* @psalm-suppress MixedArgument
|
|
*/
|
|
function f(array $ret): array {
|
|
assert($ret[0] instanceof Exception);
|
|
echo strlen($ret[1]);
|
|
return $ret;
|
|
}',
|
|
],
|
|
'assertCheckOnNonZeroArrayOffset' => [
|
|
'<?php
|
|
/**
|
|
* @param array{string,array|null} $a
|
|
* @return string
|
|
*/
|
|
function f(array $a) {
|
|
assert(is_array($a[1]));
|
|
return $a[0];
|
|
}',
|
|
],
|
|
'assertOnParseUrlOutput' => [
|
|
'<?php
|
|
/**
|
|
* @param array<"a"|"b"|"c", mixed> $arr
|
|
*/
|
|
function uriToPath(array $arr) : string {
|
|
if (!isset($arr["a"]) || $arr["b"] !== "foo") {
|
|
throw new \InvalidArgumentException("bad");
|
|
}
|
|
|
|
return (string) $arr["c"];
|
|
}',
|
|
],
|
|
'combineAfterLoopAssert' => [
|
|
'<?php
|
|
/** @param array<string, string> $array */
|
|
function foo(array $array) : void {
|
|
$c = 0;
|
|
|
|
if ($array["a"] === "a") {
|
|
foreach ([rand(0, 1), rand(0, 1)] as $i) {
|
|
if ($array["b"] === "c") {}
|
|
$c++;
|
|
}
|
|
}
|
|
}',
|
|
],
|
|
'assertOnArrayTwice' => [
|
|
'<?php
|
|
/** @param array<string, string> $array */
|
|
function f(array $array) : void {
|
|
if ($array["bar"] === "a") {}
|
|
if ($array["bar"] === "b") {}
|
|
}',
|
|
],
|
|
'assertOnArrayThrice' => [
|
|
'<?php
|
|
/** @param array<string, string> $array */
|
|
function f(array $array) : void {
|
|
if ($array["foo"] === "ok") {
|
|
if ($array["bar"] === "a") {}
|
|
if ($array["bar"] === "b") {}
|
|
}
|
|
}',
|
|
],
|
|
'assertOnBacktrace' => [
|
|
'<?php
|
|
function _validProperty(array $c, array $arr) : void {
|
|
if (empty($arr["a"])) {}
|
|
|
|
if ($c && $c["a"] !== "b") {}
|
|
}',
|
|
],
|
|
'notEmptyCheck' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-suppress MixedAssignment
|
|
*/
|
|
function load(string $objectName, array $config = []) : void {
|
|
if (isset($config["className"])) {
|
|
$name = $objectName;
|
|
$objectName = $config["className"];
|
|
}
|
|
if (!empty($config)) {}
|
|
}',
|
|
],
|
|
'unsetAfterIssetCheck' => [
|
|
'<?php
|
|
function checkbox(array $options = []) : void {
|
|
if ($options["a"]) {}
|
|
|
|
unset($options["a"], $options["b"]);
|
|
}',
|
|
],
|
|
'dontCrashWhenGettingEmptyCountAssertions' => [
|
|
'<?php
|
|
function foo() : bool {
|
|
/** @psalm-suppress TooFewArguments */
|
|
return count() > 0;
|
|
}',
|
|
],
|
|
'assertHasArrayAccessSimple' => [
|
|
'<?php
|
|
/**
|
|
* @return mixed
|
|
*/
|
|
function getBar(array $array) {
|
|
if (isset($array[\'foo\'][\'bar\'])) {
|
|
return $array[\'foo\'][\'baz\'];
|
|
}
|
|
|
|
return [];
|
|
}',
|
|
],
|
|
'assertHasArrayAccessWithType' => [
|
|
'<?php
|
|
/**
|
|
* @param array<string, array<string, string>> $array
|
|
* @return array<string, string>
|
|
*/
|
|
function getBar(array $array) : array {
|
|
if (isset($array[\'foo\'][\'bar\'])) {
|
|
return $array[\'foo\'];
|
|
}
|
|
|
|
return [];
|
|
}',
|
|
],
|
|
'assertHasArrayAccessOnSimpleXMLElement' => [
|
|
'<?php
|
|
function getBar(SimpleXMLElement $e, string $s) : void {
|
|
if (isset($e[$s])) {
|
|
echo (string) $e[$s];
|
|
}
|
|
|
|
if (isset($e[\'foo\'])) {
|
|
echo (string) $e[\'foo\'];
|
|
}
|
|
|
|
if (isset($e->bar)) {}
|
|
}',
|
|
],
|
|
'assertArrayOffsetToTraversable' => [
|
|
'<?php
|
|
function render(array $data): ?Traversable {
|
|
if ($data["o"] instanceof Traversable) {
|
|
return $data["o"];
|
|
}
|
|
|
|
return null;
|
|
}'
|
|
],
|
|
'assertOnArrayShouldNotChangeType' => [
|
|
'<?php
|
|
/** @return array|string|false */
|
|
function foo(string $a, string $b) {
|
|
$options = getopt($a, [$b]);
|
|
|
|
if (isset($options["config"])) {
|
|
$options["c"] = $options["config"];
|
|
}
|
|
|
|
if (isset($options["root"])) {
|
|
return $options["root"];
|
|
}
|
|
|
|
return false;
|
|
}'
|
|
],
|
|
'assertOnArrayInTernary' => [
|
|
'<?php
|
|
function foo(string $a, string $b) : void {
|
|
$o = getopt($a, [$b]);
|
|
|
|
$a = isset($o["a"]) && is_string($o["a"]) ? $o["a"] : "foo";
|
|
$a = isset($o["a"]) && is_string($o["a"]) ? $o["a"] : "foo";
|
|
echo $a;
|
|
}'
|
|
],
|
|
'nonEmptyArrayAfterIsset' => [
|
|
'<?php
|
|
/**
|
|
* @param array<string, int> $arr
|
|
* @return non-empty-array<string, int>
|
|
*/
|
|
function foo(array $arr) : array {
|
|
if (isset($arr["a"])) {
|
|
return $arr;
|
|
}
|
|
|
|
return ["b" => 1];
|
|
}'
|
|
],
|
|
'setArrayConstantOffset' => [
|
|
'<?php
|
|
class S {
|
|
const A = 0;
|
|
const B = 1;
|
|
const C = 2;
|
|
}
|
|
|
|
function foo(array $arr) : void {
|
|
switch ($arr[S::A]) {
|
|
case S::B:
|
|
case S::C:
|
|
break;
|
|
}
|
|
}',
|
|
],
|
|
'assertArrayWithPropertyOffset' => [
|
|
'<?php
|
|
class A {
|
|
public int $id = 0;
|
|
}
|
|
class B {
|
|
public function foo() : void {}
|
|
}
|
|
|
|
/**
|
|
* @param array<int, B> $arr
|
|
*/
|
|
function foo(A $a, array $arr): void {
|
|
if (!isset($arr[$a->id])) {
|
|
$arr[$a->id] = new B();
|
|
}
|
|
$arr[$a->id]->foo();
|
|
}'
|
|
],
|
|
'assertAfterNotEmptyArrayCheck' => [
|
|
'<?php
|
|
function foo(array $c): void {
|
|
if (!empty($c["d"])) {}
|
|
|
|
foreach (["a", "b", "c"] as $k) {
|
|
/** @psalm-suppress MixedAssignment */
|
|
foreach ($c[$k] as $d) {}
|
|
}
|
|
}',
|
|
],
|
|
'assertNotEmptyTwiceOnInstancePropertyArray' => [
|
|
'<?php
|
|
class A {
|
|
private array $c = [];
|
|
|
|
public function bar(string $s, string $t): void {
|
|
if (empty($this->c[$s]) && empty($this->c[$t])) {}
|
|
}
|
|
}'
|
|
],
|
|
'assertNotEmptyTwiceOnStaticPropertyArray' => [
|
|
'<?php
|
|
class A {
|
|
private static array $c = [];
|
|
|
|
public static function bar(string $s, string $t): void {
|
|
if (empty(self::$c[$s]) && empty(self::$c[$t])) {}
|
|
}
|
|
}'
|
|
],
|
|
'assertConstantArrayOffsetTwice' => [
|
|
'<?php
|
|
class A {
|
|
const FOO = "foo";
|
|
const BAR = "bar";
|
|
|
|
/** @psalm-suppress MixedArgument */
|
|
public function bar(array $args) : void {
|
|
if ($args[self::FOO]) {
|
|
echo $args[self::FOO];
|
|
}
|
|
if ($args[self::BAR]) {
|
|
echo $args[self::BAR];
|
|
}
|
|
}
|
|
}'
|
|
],
|
|
'assertNotEmptyOnArray' => [
|
|
'<?php
|
|
function foo(bool $c, array $arr) : void {
|
|
if ($c && !empty($arr["b"])) {
|
|
return;
|
|
}
|
|
|
|
if ($c && rand(0, 1)) {}
|
|
}'
|
|
],
|
|
'assertIssetOnArray' => [
|
|
'<?php
|
|
function foo(bool $c, array $arr) : void {
|
|
if ($c && $arr && isset($arr["b"]) && $arr["b"]) {
|
|
return;
|
|
}
|
|
|
|
if ($c && rand(0, 1)) {}
|
|
}'
|
|
],
|
|
'assertMixedOffsetExists' => [
|
|
'<?php
|
|
class A {
|
|
/** @var mixed */
|
|
private $arr;
|
|
|
|
/**
|
|
* @psalm-suppress MixedArrayAccess
|
|
* @psalm-suppress MixedReturnStatement
|
|
* @psalm-suppress MixedInferredReturnType
|
|
* @psalm-suppress MixedArrayAssignment
|
|
*/
|
|
public function foo() : stdClass {
|
|
if (isset($this->arr[0])) {
|
|
return $this->arr[0];
|
|
}
|
|
|
|
$this->arr[0] = new stdClass;
|
|
return $this->arr[0];
|
|
}
|
|
}'
|
|
],
|
|
'assertPropertiesOfElseStatement' => [
|
|
'<?php
|
|
class C {
|
|
public string $a = "";
|
|
public string $b = "";
|
|
}
|
|
|
|
function testElse(C $obj) : void {
|
|
if ($obj->a === "foo") {
|
|
} elseif ($obj->b === "bar") {
|
|
} else if ($obj->b === "baz") {}
|
|
|
|
if ($obj->b === "baz") {}
|
|
}'
|
|
],
|
|
'assertPropertiesOfElseifStatement' => [
|
|
'<?php
|
|
class C {
|
|
public string $a = "";
|
|
public string $b = "";
|
|
}
|
|
|
|
function testElseif(C $obj) : void {
|
|
if ($obj->a === "foo") {
|
|
} elseif ($obj->b === "bar") {
|
|
} elseif ($obj->b === "baz") {}
|
|
|
|
if ($obj->b === "baz") {}
|
|
}'
|
|
],
|
|
'assertArrayWithOffset' => [
|
|
'<?php
|
|
/**
|
|
* @param mixed $decoded
|
|
* @return array{icons:mixed}
|
|
*/
|
|
function assertArrayWithOffset($decoded): array {
|
|
if (!is_array($decoded)
|
|
|| !isset($decoded["icons"])
|
|
) {
|
|
throw new RuntimeException("Bad");
|
|
}
|
|
|
|
return $decoded;
|
|
}'
|
|
],
|
|
'avoidOOM' => [
|
|
'<?php
|
|
function gameOver(
|
|
int $b0,
|
|
int $b1,
|
|
int $b2,
|
|
int $b3,
|
|
int $b4,
|
|
int $b5,
|
|
int $b6,
|
|
int $b7,
|
|
int $b8
|
|
): bool {
|
|
if (($b0 === 1 && $b4 === 1 && $b8 === 1)
|
|
|| ($b0 === 1 && $b1 === 1 && $b2 === 1)
|
|
|| ($b0 === 1 && $b3 === 1 && $b6 === 1)
|
|
|| ($b1 === 1 && $b4 === 1 && $b7 === 1)
|
|
|| ($b2 === 1 && $b5 === 1 && $b8 === 1)
|
|
|| ($b2 === 1 && $b4 === 1 && $b6 === 1)
|
|
|| ($b3 === 1 && $b4 === 1 && $b5 === 1)
|
|
|| ($b6 === 1 && $b7 === 1 && $b8 === 1)
|
|
) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}'
|
|
],
|
|
'assertVarAfterNakedBinaryOp' => [
|
|
'<?php
|
|
class A {
|
|
public bool $b = false;
|
|
}
|
|
|
|
function foo(A $a, A $b): void {
|
|
$c = !$a->b && !$b->b;
|
|
echo $a->b ? 1 : 0;
|
|
}'
|
|
],
|
|
'literalStringComparisonInIf' => [
|
|
'<?php
|
|
function foo(string $t, bool $b) : void {
|
|
if ($t !== "a") {
|
|
if ($t === "b" && $b) {}
|
|
}
|
|
}
|
|
|
|
function bar(string $t, bool $b) : void {
|
|
if ($t !== "a") {
|
|
if ($t === "b" || $b) {}
|
|
}
|
|
}'
|
|
],
|
|
'literalStringComparisonInElseif' => [
|
|
'<?php
|
|
function foo(string $t, bool $b) : void {
|
|
if ($t === "a") {
|
|
} elseif ($t === "b" && $b) {}
|
|
}
|
|
|
|
function bar(string $t, bool $b) : void {
|
|
if ($t === "a") {
|
|
} elseif ($t === "b" || $b) {}
|
|
}'
|
|
],
|
|
'literalStringComparisonInElse' => [
|
|
'<?php
|
|
function foo(string $t, bool $b) : void {
|
|
if ($t === "a") {
|
|
} else {
|
|
if ($t === "b" && $b) {}
|
|
}
|
|
}
|
|
|
|
function bar(string $t, bool $b) : void {
|
|
if ($t === "a") {
|
|
} else {
|
|
if ($t === "b" || $b) {}
|
|
}
|
|
}'
|
|
],
|
|
'assertOnArrayThings' => [
|
|
'<?php
|
|
/** @var array<string, array<int, string>> */
|
|
$a = null;
|
|
|
|
if (isset($a["b"]) || isset($a["c"])) {
|
|
$all_params = ($a["b"] ?? []) + ($a["c"] ?? []);
|
|
}'
|
|
],
|
|
'assertOnNestedLogic' => [
|
|
'<?php
|
|
function foo(?string $a) : void {
|
|
if (($a && rand(0, 1)) || rand(0, 1)) {
|
|
if ($a && strlen($a) > 5) {}
|
|
}
|
|
}'
|
|
],
|
|
'arrayUnionTypeSwitching' => [
|
|
'<?php
|
|
/** @param array<string, int|string> $map */
|
|
function foo(array $map, string $o) : void {
|
|
if ($mapped_type = $map[$o] ?? null) {
|
|
if (is_int($mapped_type)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (($mapped_type = $map[""] ?? null) && is_string($mapped_type)) {
|
|
|
|
}
|
|
}'
|
|
],
|
|
'propertySetOnElementInConditional' => [
|
|
'<?php
|
|
class DiffElem {
|
|
/** @var scalar */
|
|
public $old = false;
|
|
/** @var scalar */
|
|
public $new = false;
|
|
}
|
|
|
|
function foo(DiffElem $diff_elem) : void {
|
|
if ((is_string($diff_elem->old) && is_string($diff_elem->new))
|
|
|| (is_int($diff_elem->old) && is_int($diff_elem->new))
|
|
) {
|
|
}
|
|
}'
|
|
],
|
|
'manyNestedAsserts' => [
|
|
'<?php
|
|
class A {}
|
|
class B extends A {}
|
|
function foo(A $left, A $right) : void {
|
|
if (($left instanceof B && rand(0, 1))
|
|
|| ($right instanceof B && rand(0, 1))
|
|
) {
|
|
if ($left instanceof B
|
|
&& rand(0, 1)
|
|
&& $right instanceof B
|
|
&& rand(0, 1)
|
|
) {}
|
|
}
|
|
}'
|
|
],
|
|
'manyNestedWedgeAssertions' => [
|
|
'<?php
|
|
if (rand(0, 1) && rand(0, 1)) {}'
|
|
],
|
|
'assertionAfterAssertionInsideBooleanNot' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
function foo(?A $a) : void {
|
|
if (rand(0, 1) && !($a && rand(0, 1))) {
|
|
if ($a !== null) {}
|
|
}
|
|
}'
|
|
],
|
|
'assertionAfterAssertionInsideExpandedBooleanNot' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
function bar(?A $a) : void {
|
|
if (rand(0, 1) && (!$a || rand(0, 1))) {
|
|
if ($a !== null) {}
|
|
}
|
|
}'
|
|
],
|
|
'byrefChangeNested' => [
|
|
'<?php
|
|
if (!preg_match("/hello/", "hello", $matches) || $matches[0] !== "hello") {}'
|
|
],
|
|
'checkBeforeUse' => [
|
|
'<?php
|
|
class A {
|
|
public function foo() : void {}
|
|
}
|
|
|
|
function takesA(A $a) : bool {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param mixed $a
|
|
*/
|
|
function takesMaybeA($a) : void {
|
|
/**
|
|
* @psalm-suppress MixedArgument
|
|
*/
|
|
if ($a !== null && takesA($a)) {}
|
|
}'
|
|
],
|
|
'nestedAssertInElse' => [
|
|
'<?php
|
|
function foo(string $type, bool $and) : void {
|
|
if ($type === "a") {
|
|
} elseif ($type === "b" && $and) {
|
|
} else {
|
|
if ($type === "c" && $and) {}
|
|
}
|
|
}'
|
|
],
|
|
'allowEmptyScalarAndNonEmptyScalarAssertions' => [
|
|
'<?php
|
|
/** @param mixed $value */
|
|
function foo($value) : void {
|
|
if (\is_scalar($value)) {
|
|
if ($value) {
|
|
echo $value;
|
|
} else {
|
|
echo $value;
|
|
}
|
|
}
|
|
}'
|
|
],
|
|
'ignoreRedundantAssertion' => [
|
|
'<?php
|
|
function gimmeAString(?string $v): string {
|
|
/** @psalm-suppress TypeDoesNotContainType */
|
|
assert(is_string($v) || is_object($v));
|
|
|
|
return $v;
|
|
}'
|
|
],
|
|
'assertOnVarStaticClassKey' => [
|
|
'<?php
|
|
abstract class Obj {
|
|
/**
|
|
* @param array<class-string, array<string, int>> $arr
|
|
* @return array<string, int>
|
|
*/
|
|
public static function getArr(array $arr) : array {
|
|
if (!isset($arr[static::class])) {
|
|
$arr[static::class] = ["hello" => 5];
|
|
}
|
|
|
|
return $arr[static::class];
|
|
}
|
|
}'
|
|
],
|
|
'assertOnVarVar' => [
|
|
'<?php
|
|
abstract class Obj {
|
|
/**
|
|
* @param array<class-string, array<string, int>> $arr
|
|
* @return array<string, int>
|
|
*/
|
|
function getArr(array $arr, string $s) : array {
|
|
if (!isset($arr[$s])) {
|
|
$arr[$s] = ["hello" => 5];
|
|
}
|
|
|
|
return $arr[$s];
|
|
}
|
|
}'
|
|
],
|
|
'assertOnPropertyStaticClassKey' => [
|
|
'<?php
|
|
abstract class Obj {
|
|
/** @var array<class-string, array<string, int>> */
|
|
private static $arr = [];
|
|
|
|
/** @return array<string, int> */
|
|
public static function getArr() : array {
|
|
$arr = self::$arr;
|
|
if (!isset($arr[static::class])) {
|
|
$arr[static::class] = ["hello" => 5];
|
|
}
|
|
|
|
return $arr[static::class];
|
|
}
|
|
}'
|
|
],
|
|
'assertOnStaticPropertyOffset' => [
|
|
'<?php
|
|
class C {
|
|
/** @var array<string, string>|null */
|
|
private static $map = [];
|
|
|
|
public static function foo(string $id) : ?string {
|
|
if (isset(self::$map[$id])) {
|
|
return self::$map[$id];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}',
|
|
],
|
|
'issetTwice' => [
|
|
'<?php
|
|
class B {
|
|
public function foo() : bool {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/** @param array<int, B> $p */
|
|
function foo(array $p, int $id) : void {
|
|
if ((isset($p[$id]) && rand(0, 1))
|
|
|| (!isset($p[$id]) && rand(0, 1))
|
|
) {
|
|
isset($p[$id]) ? $p[$id] : new B;
|
|
isset($p[$id]) ? $p[$id]->foo() : "bar";
|
|
}
|
|
}'
|
|
],
|
|
'reconcileEmptinessBetter' => [
|
|
'<?php
|
|
/**
|
|
* @param string|array $valuePath
|
|
*/
|
|
function combine($valuePath) : void {
|
|
if (!empty($valuePath) && is_array($valuePath)) {
|
|
|
|
} elseif (!empty($valuePath)) {
|
|
echo $valuePath;
|
|
}
|
|
}',
|
|
],
|
|
'issetAssertionOnStaticProperty' => [
|
|
'<?php
|
|
class C {
|
|
protected static array $cache = [];
|
|
|
|
/**
|
|
* @psalm-suppress MixedReturnStatement
|
|
* @psalm-suppress MixedInferredReturnType
|
|
*/
|
|
public static function get(string $k1, string $k2) : ?string {
|
|
if (!isset(static::$cache[$k1][$k2])) {
|
|
return null;
|
|
}
|
|
|
|
return static::$cache[$k1][$k2];
|
|
}
|
|
}'
|
|
],
|
|
'isNotTraversable' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-param iterable<string> $collection
|
|
* @psalm-return array<string>
|
|
*/
|
|
function order(iterable $collection): array {
|
|
if ($collection instanceof \Traversable) {
|
|
$collection = iterator_to_array($collection, false);
|
|
}
|
|
|
|
return $collection;
|
|
}'
|
|
],
|
|
'memoizeChainedImmutableCallsInside' => [
|
|
'<?php
|
|
class Assessment {
|
|
private ?string $root = null;
|
|
|
|
/** @psalm-mutation-free */
|
|
public function getRoot(): ?string {
|
|
return $this->root;
|
|
}
|
|
}
|
|
|
|
class Project {
|
|
private ?Assessment $assessment = null;
|
|
|
|
/** @psalm-mutation-free */
|
|
public function getAssessment(): ?Assessment {
|
|
return $this->assessment;
|
|
}
|
|
}
|
|
|
|
function f(Project $project): int {
|
|
if (($project->getAssessment() !== null)
|
|
&& ($project->getAssessment()->getRoot() !== null)
|
|
) {
|
|
return strlen($project->getAssessment()->getRoot());
|
|
}
|
|
|
|
throw new RuntimeException();
|
|
}',
|
|
],
|
|
'memoizeChainedImmutableCallsOutside' => [
|
|
'<?php
|
|
class Assessment {
|
|
private ?string $root = null;
|
|
|
|
/** @psalm-mutation-free */
|
|
public function getRoot(): ?string {
|
|
return $this->root;
|
|
}
|
|
}
|
|
|
|
class Project {
|
|
private ?Assessment $assessment = null;
|
|
|
|
/** @psalm-mutation-free */
|
|
public function getAssessment(): ?Assessment {
|
|
return $this->assessment;
|
|
}
|
|
}
|
|
|
|
function f(Project $project): int {
|
|
if (($project->getAssessment() === null)
|
|
|| ($project->getAssessment()->getRoot() === null)
|
|
) {
|
|
throw new RuntimeException();
|
|
}
|
|
|
|
return strlen($project->getAssessment()->getRoot());
|
|
}',
|
|
],
|
|
'propertyChainedOutside' => [
|
|
'<?php
|
|
class Assessment {
|
|
public ?string $root = null;
|
|
}
|
|
|
|
class Project {
|
|
public ?Assessment $assessment = null;
|
|
}
|
|
|
|
function f(Project $project): int {
|
|
if (($project->assessment === null)
|
|
|| ($project->assessment->root === null)
|
|
) {
|
|
throw new RuntimeException();
|
|
}
|
|
|
|
return strlen($project->assessment->root);
|
|
}'
|
|
],
|
|
'castIsType' => [
|
|
'<?php
|
|
/**
|
|
* @param string|int $s
|
|
*/
|
|
function foo($s, int $f = 1) : void {
|
|
if ($f === 1
|
|
&& (string) $s === $s
|
|
&& \strpos($s, "foo") !== false
|
|
) {}
|
|
}'
|
|
],
|
|
'assertNotFalseOnSameNamedVar' => [
|
|
'<?php
|
|
function foo(): int {
|
|
$a = rand(0, 1) ? 3 : false;
|
|
|
|
if ($a !== false && rand(0, 1)) {
|
|
$a = rand(0, 1) ? 3 : false;
|
|
if ($a !== false) {
|
|
return $a;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}',
|
|
],
|
|
'nonEmptyStringFromConcat' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-param non-empty-string $name
|
|
*/
|
|
function sayHello(string $name) : void {
|
|
echo "Hello " . $name;
|
|
}
|
|
|
|
function takeInput() : void {
|
|
if (isset($_GET["name"]) && is_string($_GET["name"])) {
|
|
$name = trim($_GET["name"]);
|
|
sayHello("a" . $name);
|
|
}
|
|
}',
|
|
],
|
|
'noCrashOnCountUndefined' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-suppress UndefinedGlobalVariable
|
|
* @psalm-suppress MixedArgument
|
|
*/
|
|
if(!(count($colonnes) == 37 || count($colonnes) == 40)) {}',
|
|
],
|
|
'reconcilePropertyInTrait' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
trait T {
|
|
private static ?A $one = null;
|
|
|
|
private static function maybeSetOne(): A {
|
|
if (null === self::$one) {
|
|
self::$one = new A();
|
|
}
|
|
|
|
return self::$one;
|
|
}
|
|
}
|
|
|
|
class Implementer {
|
|
use T;
|
|
}'
|
|
],
|
|
'smallConditional' => [
|
|
'<?php
|
|
class A {
|
|
public array $parts = [];
|
|
}
|
|
|
|
class FuncCall {
|
|
/** @var ?A */
|
|
public $name;
|
|
/** @var array<string> */
|
|
public $args = [];
|
|
}
|
|
|
|
function barr(FuncCall $function) : void {
|
|
if (!$function->name instanceof A) {
|
|
return;
|
|
}
|
|
|
|
if ($function->name->parts === ["function_exists"]
|
|
&& isset($function->args[0])
|
|
) {
|
|
// do something
|
|
} elseif ($function->name->parts === ["class_exists"]
|
|
&& isset($function->args[0])
|
|
) {
|
|
// do something else
|
|
}
|
|
}'
|
|
],
|
|
'largeConditional' => [
|
|
'<?php
|
|
/**
|
|
* @param string $return_block
|
|
*
|
|
* @return array<string>
|
|
*/
|
|
function splitDocLine($return_block)
|
|
{
|
|
$brackets = \'\';
|
|
|
|
$type = \'\';
|
|
|
|
$expects_callable_return = false;
|
|
|
|
$return_block = str_replace("\t", \' \', $return_block);
|
|
|
|
$quote_char = null;
|
|
$escaped = false;
|
|
|
|
for ($i = 0, $l = strlen($return_block); $i < $l; ++$i) {
|
|
$char = $return_block[$i];
|
|
$next_char = $i < $l - 1 ? $return_block[$i + 1] : null;
|
|
$last_char = $i > 0 ? $return_block[$i - 1] : null;
|
|
|
|
if ($quote_char) {
|
|
if ($char === $quote_char && $i > 1 && !$escaped) {
|
|
$quote_char = null;
|
|
|
|
$type .= $char;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (rand(0, 1)) {
|
|
$escaped = true;
|
|
|
|
$type .= $char;
|
|
|
|
continue;
|
|
}
|
|
|
|
$escaped = false;
|
|
|
|
$type .= $char;
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($char === \'"\' || $char === \'\\\\\') {
|
|
$quote_char = $char;
|
|
|
|
$type .= $char;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (rand(0, 1)) {
|
|
$expects_callable_return = true;
|
|
|
|
$type .= $char;
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($char === \'[\' || $char === \'{\' || $char === \'(\' || $char === \'<\') {
|
|
$brackets .= $char;
|
|
} elseif ($char === \']\' || $char === \'}\' || $char === \')\' || $char === \'>\') {
|
|
$last_bracket = substr($brackets, -1);
|
|
$brackets = substr($brackets, 0, -1);
|
|
|
|
if (($char === \']\' && $last_bracket !== \'[\')
|
|
|| ($char === \'}\' && $last_bracket !== \'{\')
|
|
|| ($char === \')\' && $last_bracket !== \'(\')
|
|
|| ($char === \'>\' && $last_bracket !== \'<\')
|
|
) {
|
|
return [];
|
|
}
|
|
} elseif ($char === \' \') {
|
|
if ($brackets) {
|
|
$expects_callable_return = false;
|
|
$type .= \' \';
|
|
continue;
|
|
}
|
|
|
|
if ($next_char === \'|\' || $next_char === \'&\') {
|
|
$nexter_char = $i < $l - 2 ? $return_block[$i + 2] : null;
|
|
|
|
if ($nexter_char === \' \') {
|
|
++$i;
|
|
$type .= $next_char . \' \';
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ($last_char === \'|\' || $last_char === \'&\') {
|
|
$type .= \' \';
|
|
continue;
|
|
}
|
|
|
|
if ($next_char === \':\') {
|
|
++$i;
|
|
$type .= \' :\';
|
|
$expects_callable_return = true;
|
|
continue;
|
|
}
|
|
|
|
if ($expects_callable_return) {
|
|
$type .= \' \';
|
|
$expects_callable_return = false;
|
|
continue;
|
|
}
|
|
|
|
$remaining = trim(preg_replace(\'@^[ \t]*\* *@m\', \' \', substr($return_block, $i + 1)));
|
|
|
|
if ($remaining) {
|
|
/** @var array<string> */
|
|
return array_merge([rtrim($type)], preg_split(\'/[ \s]+/\', $remaining));
|
|
}
|
|
|
|
return [$type];
|
|
}
|
|
|
|
$expects_callable_return = false;
|
|
|
|
$type .= $char;
|
|
}
|
|
|
|
return [$type];
|
|
}'
|
|
],
|
|
'nonEmptyStringAfterLiteralCheck' => [
|
|
'<?php
|
|
/**
|
|
* @param non-empty-string $greeting
|
|
*/
|
|
function sayHi(string $greeting): void {
|
|
echo $greeting;
|
|
}
|
|
|
|
/** @var string */
|
|
$hello = "foo";
|
|
|
|
if ($hello === "") {
|
|
throw new \Exception("an empty string is not a greeting");
|
|
}
|
|
|
|
sayHi($hello);',
|
|
],
|
|
'equalsTrueInIf' => [
|
|
'<?php
|
|
$a = rand(0,1) ? new DateTime() : null;
|
|
|
|
if (($a !== null && $a->format("Y") === "2020") == true) {
|
|
$a->format("d-m-Y");
|
|
}',
|
|
],
|
|
'getClassIsStatic' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
class AChild extends A {
|
|
public static function compare(A $other_type) : AChild {
|
|
if (get_class($other_type) !== static::class) {
|
|
throw new \Exception();
|
|
}
|
|
|
|
return $other_type;
|
|
}
|
|
}',
|
|
],
|
|
'getClassInterfaceCanBeClass' => [
|
|
'<?php
|
|
interface Id {}
|
|
|
|
class A {
|
|
public function is(Id $other): bool {
|
|
return get_class($this) === get_class($other);
|
|
}
|
|
}'
|
|
],
|
|
'nullsafePropertyAccess' => [
|
|
'<?php
|
|
class IntLinkedList {
|
|
public function __construct(
|
|
public int $value,
|
|
public ?self $next
|
|
) {}
|
|
}
|
|
|
|
function skipOne(IntLinkedList $l) : ?int {
|
|
return $l->next?->value;
|
|
}
|
|
|
|
function skipTwo(IntLinkedList $l) : ?int {
|
|
return $l->next?->next?->value;
|
|
}',
|
|
[],
|
|
[],
|
|
'8.0'
|
|
],
|
|
'nullsafeMethodCall' => [
|
|
'<?php
|
|
class IntLinkedList {
|
|
public function __construct(
|
|
public int $value,
|
|
private ?self $next
|
|
) {}
|
|
|
|
public function getNext() : ?self {
|
|
return $this->next;
|
|
}
|
|
}
|
|
|
|
function skipOne(IntLinkedList $l) : ?int {
|
|
return $l->getNext()?->value;
|
|
}
|
|
|
|
function skipTwo(IntLinkedList $l) : ?int {
|
|
return $l->getNext()?->getNext()?->value;
|
|
}',
|
|
[],
|
|
[],
|
|
'8.0'
|
|
],
|
|
'onlySingleErrorForEarlyExit' => [
|
|
'<?php
|
|
class App {
|
|
public function bar(int $i) : bool {
|
|
return $i === 5;
|
|
}
|
|
}
|
|
|
|
/** @psalm-suppress MixedArgument, MissingParamType */
|
|
function bar(App $foo, $arr) : void {
|
|
/** @psalm-suppress TypeDoesNotContainNull */
|
|
if ($foo === null || $foo->bar($arr)) {
|
|
return;
|
|
}
|
|
}'
|
|
],
|
|
'nonRedundantConditionAfterThing' => [
|
|
'<?php
|
|
class U {
|
|
public function takes(self $u) : bool {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function bar(?U $a, ?U $b) : void {
|
|
if ($a === null
|
|
|| ($b !== null && $a->takes($b))
|
|
|| $b === null
|
|
) {}
|
|
}'
|
|
],
|
|
'usedAssertedVarButNotWithStrongerTypeGuarantee' => [
|
|
'<?php
|
|
function broken(bool $b, ?User $u) : void {
|
|
if ($b || (rand(0, 1) && (!$u || takesUser($u)))) {
|
|
return;
|
|
}
|
|
|
|
if ($u) {}
|
|
}
|
|
|
|
class User {}
|
|
|
|
function takesUser(User $a) : bool {
|
|
return true;
|
|
}'
|
|
],
|
|
'negateIsNull' => [
|
|
'<?php
|
|
function scope(?string $str): string{
|
|
if (is_null($str) === false){
|
|
return $str;
|
|
}
|
|
|
|
return "";
|
|
}'
|
|
],
|
|
'strictIntFloatComparison' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-suppress InvalidReturnType
|
|
* @psalm-suppress MismatchingDocblockReturnType
|
|
* @return ($bar is int ? list<int> : list<float>)
|
|
*/
|
|
function foo($bar): string {}
|
|
|
|
/** @var int */
|
|
$baz = 1;
|
|
$a = foo($baz);
|
|
|
|
/** @var float */
|
|
$baz = 1.;
|
|
$b = foo($baz);
|
|
|
|
/** @var int|float */
|
|
$baz = 1;
|
|
$c = foo($baz);
|
|
',
|
|
'assertions' => [
|
|
'$a' => 'list<int>',
|
|
'$b' => 'list<float>',
|
|
'$c' => 'list<float|int>',
|
|
],
|
|
],
|
|
'negateTypeInGenericContext' => [
|
|
'<?php
|
|
|
|
/**
|
|
* @template T
|
|
*/
|
|
final class Valid {}
|
|
final class Invalid {}
|
|
|
|
/**
|
|
* @template T
|
|
*
|
|
* @param Valid<T>|Invalid $val
|
|
* @psalm-assert-if-true Valid<T> $val
|
|
*/
|
|
function isValid($val): bool
|
|
{
|
|
return $val instanceof Valid;
|
|
}
|
|
|
|
/**
|
|
* @template T
|
|
* @param Valid<T>|Invalid $val
|
|
*/
|
|
function genericContext($val): void
|
|
{
|
|
$takesValid =
|
|
/** @param Valid<T> $_valid */
|
|
function ($_valid): void {};
|
|
|
|
$takesInvalid =
|
|
/** @param Invalid $_invalid */
|
|
function ($_invalid): void {};
|
|
|
|
isValid($val) ? $takesValid($val) : $takesInvalid($val);
|
|
}'
|
|
],
|
|
'reconcileMoreThanOneGenericObject' => [
|
|
'<?php
|
|
|
|
final class Invalid {}
|
|
|
|
/**
|
|
* @template T
|
|
*/
|
|
final class Valid {}
|
|
|
|
/**
|
|
* @template T
|
|
*
|
|
* @param Invalid|Valid<T> $val
|
|
* @psalm-assert-if-true Valid<T> $val
|
|
*/
|
|
function isValid($val): bool
|
|
{
|
|
return $val instanceof Valid;
|
|
}
|
|
|
|
/**
|
|
* @template T
|
|
* @param Valid<T>|Invalid $val1
|
|
* @param Valid<T>|Invalid $val2
|
|
* @param Valid<T>|Invalid $val3
|
|
*/
|
|
function inGenericContext($val1, $val2, $val3): void
|
|
{
|
|
$takesValid =
|
|
/** @param Valid<T> $_valid */
|
|
function ($_valid): void {};
|
|
|
|
if (isValid($val1) && isValid($val2) && isValid($val3)) {
|
|
$takesValid($val1);
|
|
$takesValid($val2);
|
|
$takesValid($val3);
|
|
}
|
|
}'
|
|
],
|
|
'ternaryRedefineAllVars' => [
|
|
'<?php
|
|
$_a = null;
|
|
$b = rand(0,1) ? "" : "a";
|
|
$b === "a" ? $_a = "Y" : $_a = "N";',
|
|
'assertions' => [
|
|
'$_a===' => '"N"|"Y"',
|
|
]
|
|
],
|
|
'assertionsWorksBothWays' => [
|
|
'<?php
|
|
$a = 2;
|
|
$b = getPositiveInt();
|
|
|
|
assert($a === $b);
|
|
|
|
/** @return positive-int */
|
|
function getPositiveInt(): int{
|
|
return 2;
|
|
}',
|
|
'assertions' => [
|
|
'$a===' => '2',
|
|
'$b===' => '2',
|
|
]
|
|
],
|
|
'nullErasureWithSmallerAndGreater' => [
|
|
'<?php
|
|
function getIntOrNull(): ?int{return null;}
|
|
$a = getIntOrNull();
|
|
|
|
if ($a < 0) {
|
|
echo $a + 3;
|
|
}
|
|
|
|
if ($a <= 0) {
|
|
/** @psalm-suppress PossiblyNullOperand */
|
|
echo $a + 3;
|
|
}
|
|
|
|
if ($a > 0) {
|
|
echo $a + 3;
|
|
}
|
|
|
|
if ($a >= 0) {
|
|
/** @tmp-psalm-suppress PossiblyNullOperand this should be suppressed but assertions remove null for now */
|
|
echo $a + 3;
|
|
}
|
|
|
|
if (0 < $a) {
|
|
echo $a + 3;
|
|
}
|
|
|
|
if (0 <= $a) {
|
|
/** @tmp-psalm-suppress PossiblyNullOperand this should be suppressed but assertions remove null for now */
|
|
echo $a + 3;
|
|
}
|
|
|
|
if (0 > $a) {
|
|
echo $a + 3;
|
|
}
|
|
|
|
if (0 >= $a) {
|
|
/** @psalm-suppress PossiblyNullOperand */
|
|
echo $a + 3;
|
|
}
|
|
',
|
|
],
|
|
'SimpleXMLElementNotAlwaysTruthy' => [
|
|
'<?php
|
|
$lilstring = "";
|
|
|
|
$n = new SimpleXMLElement($lilstring);
|
|
/** @psalm-suppress MixedAssignment */
|
|
$n = $n->b;
|
|
|
|
if (!$n instanceof SimpleXMLElement) {
|
|
return;
|
|
}
|
|
|
|
if (!$n) {
|
|
echo "false";
|
|
}',
|
|
],
|
|
'nullIsFalsyEvenInTemplate' => [
|
|
'<?php
|
|
|
|
abstract class Animal {
|
|
public function foo(): void {}
|
|
}
|
|
|
|
class Dog extends Animal {}
|
|
class Cat extends Animal {}
|
|
|
|
/**
|
|
* @template T
|
|
*/
|
|
interface RepositoryInterface
|
|
{
|
|
/**
|
|
* @return ?T
|
|
*/
|
|
public function find(int $id);
|
|
}
|
|
|
|
/**
|
|
* @template T
|
|
* @implements RepositoryInterface<T>
|
|
*/
|
|
class AbstarctRepository implements RepositoryInterface
|
|
{
|
|
public function find(int $id) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @template T as Animal
|
|
* @extends AbstarctRepository<T>
|
|
*/
|
|
class AnimalRepository extends AbstarctRepository {}
|
|
|
|
/**
|
|
* @extends AnimalRepository<Cat>
|
|
*/
|
|
class CatRepository extends AnimalRepository {}
|
|
|
|
/**
|
|
* @extends AnimalRepository<Dog>
|
|
*/
|
|
class DogRepository extends AnimalRepository {}
|
|
|
|
function doSomething(AnimalRepository $repository) : void {
|
|
$foo = $repository->find(1);
|
|
|
|
if (!$foo) {
|
|
return;
|
|
}
|
|
|
|
$foo->foo();
|
|
}',
|
|
],
|
|
'variable::classAssertion' => [
|
|
'<?php
|
|
abstract class A {}
|
|
class B extends A {}
|
|
|
|
function a(A $a): void {
|
|
if($a::class == B::class) {
|
|
b($a);
|
|
}
|
|
}
|
|
|
|
function b(B $_b): void {
|
|
}',
|
|
],
|
|
'SimpleXMLIteratorNotAlwaysTruthy' => [
|
|
'<?php
|
|
$lilstring = "";
|
|
|
|
$n = new SimpleXMLElement($lilstring);
|
|
$n = $n->children();
|
|
|
|
if (!$n) {
|
|
echo "false";
|
|
}',
|
|
],
|
|
'#7771: non-UTF8 binary data is passed' => [
|
|
'<?php
|
|
function matches(string $value): bool {
|
|
if ("\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" !== $value) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}'
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return iterable<string,array{string,error_message:string,1?:string[],2?:bool,3?:string}>
|
|
*/
|
|
public function providerInvalidCodeParse(): iterable
|
|
{
|
|
return [
|
|
'makeNonNullableNull' => [
|
|
'<?php
|
|
class A { }
|
|
$a = new A();
|
|
if ($a === null) {
|
|
}',
|
|
'error_message' => 'TypeDoesNotContainNull',
|
|
],
|
|
'makeInstanceOfThingInElseif' => [
|
|
'<?php
|
|
class A { }
|
|
class B { }
|
|
class C { }
|
|
$a = rand(0, 10) > 5 ? new A() : new B();
|
|
if ($a instanceof A) {
|
|
} elseif ($a instanceof C) {
|
|
}',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'functionValueIsNotType' => [
|
|
'<?php
|
|
if (json_last_error() === "5") { }',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'stringIsNotTnt' => [
|
|
'<?php
|
|
if (5 === "5") { }',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'stringIsNotNull' => [
|
|
'<?php
|
|
if (5 === null) { }',
|
|
'error_message' => 'TypeDoesNotContainNull',
|
|
],
|
|
'stringIsNotFalse' => [
|
|
'<?php
|
|
if (5 === false) { }',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'typeTransformation' => [
|
|
'<?php
|
|
$a = "5";
|
|
|
|
if (is_numeric($a)) {
|
|
if (is_int($a)) {
|
|
echo $a;
|
|
}
|
|
}',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'nonRedundantConditionGivenDocblockType' => [
|
|
'<?php
|
|
/** @param array[] $arr */
|
|
function foo(array $arr) : void {
|
|
if ($arr === "hello") {}
|
|
}',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'lessSpecificArrayFields' => [
|
|
'<?php
|
|
/**
|
|
* @param array{field:string, otherField:string} $array
|
|
*/
|
|
function print_field($array) : void {
|
|
echo $array["field"] . " " . $array["otherField"];
|
|
}
|
|
|
|
print_field(["field" => "name"]);',
|
|
'error_message' => 'InvalidArgument',
|
|
],
|
|
'intersectionIncorrect' => [
|
|
'<?php
|
|
interface I {
|
|
public function bat(): void;
|
|
}
|
|
|
|
interface C {}
|
|
|
|
/** @param I&C $a */
|
|
function takesIandC($a): void {}
|
|
|
|
class A {
|
|
public function foo(): void {
|
|
if ($this instanceof I) {
|
|
takesIandC($this);
|
|
}
|
|
}
|
|
}',
|
|
'error_message' => 'InvalidArgument',
|
|
],
|
|
'catchTypeMismatchInBinaryOp' => [
|
|
'<?php
|
|
/** @return array<int, string|int> */
|
|
function getStrings(): array {
|
|
return ["hello", "world", 50];
|
|
}
|
|
|
|
$a = getStrings();
|
|
|
|
if (is_bool($a[0]) && $a[0]) {}',
|
|
'error_message' => 'DocblockTypeContradiction',
|
|
],
|
|
'preventWeakEqualityToObject' => [
|
|
'<?php
|
|
function foo(int $i, stdClass $s) : void {
|
|
if ($i == $s) {}
|
|
}',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'properReconciliationInElseIf' => [
|
|
'<?php
|
|
class A {}
|
|
$a = rand(0, 1) ? new A : null;
|
|
|
|
if (rand(0, 1)) {
|
|
$a = new A();
|
|
} elseif (!$a) {
|
|
$a = new A();
|
|
}
|
|
|
|
if ($a) {}',
|
|
'error_message' => 'RedundantCondition',
|
|
],
|
|
'allRemovalOfStringWithIsScalar' => [
|
|
'<?php
|
|
$a = rand(0, 1) ? "hello" : "goodbye";
|
|
|
|
if (is_scalar($a)) {
|
|
exit;
|
|
}',
|
|
'error_message' => 'RedundantCondition',
|
|
],
|
|
'noRemovalOfStringWithIsScalar' => [
|
|
'<?php
|
|
$a = rand(0, 1) ? "hello" : "goodbye";
|
|
|
|
if (!is_scalar($a)) {
|
|
exit;
|
|
}',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'impossibleNullEquality' => [
|
|
'<?php
|
|
$i = 5;
|
|
echo $i === null;',
|
|
'error_message' => 'TypeDoesNotContainNull',
|
|
],
|
|
'impossibleTrueEquality' => [
|
|
'<?php
|
|
$i = 5;
|
|
echo $i === true;',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'impossibleFalseEquality' => [
|
|
'<?php
|
|
$i = 5;
|
|
echo $i === false;',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'impossibleNumberEquality' => [
|
|
'<?php
|
|
$i = 5;
|
|
echo $i === 3;',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'noIntersectionOfArrayOrTraversable' => [
|
|
'<?php
|
|
function foo(iterable $iterable) : void {
|
|
if (\is_array($iterable) && $iterable instanceof \Traversable) {}
|
|
}',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'scalarToBoolContradiction' => [
|
|
'<?php
|
|
/** @param mixed $s */
|
|
function foo($s) : void {
|
|
if (!is_scalar($s)) {
|
|
return;
|
|
}
|
|
|
|
if (!is_bool($s)) {
|
|
if (is_bool($s)) {}
|
|
}
|
|
}',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'noCrashWhenCastingArray' => [
|
|
'<?php
|
|
function foo() : string {
|
|
return (object) ["a" => 1, "b" => 2];
|
|
}',
|
|
'error_message' => 'InvalidReturnStatement',
|
|
],
|
|
'preventStrongEqualityScalarType' => [
|
|
'<?php
|
|
function bar(float $f) : void {
|
|
if ($f === 0) {}
|
|
}',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'preventYodaStrongEqualityScalarType' => [
|
|
'<?php
|
|
function bar(float $f) : void {
|
|
if (0 === $f) {}
|
|
}',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'classCannotNotBeSelf' => [
|
|
'<?php
|
|
class A {}
|
|
class B extends A {}
|
|
function getA() : A {
|
|
return new A();
|
|
}
|
|
|
|
$a = getA();
|
|
if ($a instanceof B) {
|
|
$a = new B;
|
|
}
|
|
|
|
if ($a instanceof A) {}',
|
|
'error_message' => 'RedundantCondition',
|
|
],
|
|
'preventImpossibleComparisonToTrue' => [
|
|
'<?php
|
|
/** @return false|string */
|
|
function firstChar(string $s) {
|
|
return empty($s) ? false : $s[0];
|
|
}
|
|
|
|
if (true === firstChar("sdf")) {}',
|
|
'error_message' => 'DocblockTypeContradiction',
|
|
],
|
|
'preventAlwaysPossibleComparisonToTrue' => [
|
|
'<?php
|
|
/** @return false|string */
|
|
function firstChar(string $s) {
|
|
return empty($s) ? false : $s[0];
|
|
}
|
|
|
|
if (true !== firstChar("sdf")) {}',
|
|
'error_message' => 'RedundantConditionGivenDocblockType',
|
|
],
|
|
'preventAlwaysImpossibleComparisonToFalse' => [
|
|
'<?php
|
|
function firstChar(string $s) : string { return $s; }
|
|
|
|
if (false === firstChar("sdf")) {}',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'preventAlwaysPossibleComparisonToFalse' => [
|
|
'<?php
|
|
function firstChar(string $s) : string { return $s; }
|
|
|
|
if (false !== firstChar("sdf")) {}',
|
|
'error_message' => 'RedundantCondition',
|
|
],
|
|
'nullCoalesceImpossible' => [
|
|
'<?php
|
|
function foo(?string $s) : string {
|
|
return ((string) $s) ?? "bar";
|
|
}',
|
|
'error_message' => 'RedundantCondition'
|
|
],
|
|
'allowEmptyScalarAndNonEmptyScalarAssertions1' => [
|
|
'<?php
|
|
/** @param mixed $value */
|
|
function foo($value) : void {
|
|
if (\is_scalar($value)) {
|
|
if ($value) {
|
|
if (\is_scalar($value)) {}
|
|
} else {
|
|
echo $value;
|
|
}
|
|
}
|
|
}',
|
|
'error_message' => 'RedundantCondition',
|
|
],
|
|
'allowEmptyScalarAndNonEmptyScalarAssertions2' => [
|
|
'<?php
|
|
/** @param mixed $value */
|
|
function foo($value) : void {
|
|
if (\is_scalar($value)) {
|
|
if ($value) {
|
|
echo $value;
|
|
} else {
|
|
if (\is_scalar($value)) {}
|
|
}
|
|
}
|
|
}',
|
|
'error_message' => 'RedundantCondition',
|
|
],
|
|
'allowEmptyScalarAndNonEmptyScalarAssertions3' => [
|
|
'<?php
|
|
/** @param mixed $value */
|
|
function foo($value) : void {
|
|
if (\is_scalar($value)) {
|
|
if ($value) {
|
|
if ($value) {}
|
|
} else {
|
|
echo $value;
|
|
}
|
|
}
|
|
}',
|
|
'error_message' => 'RedundantCondition',
|
|
],
|
|
'allowEmptyScalarAndNonEmptyScalarAssertions4' => [
|
|
'<?php
|
|
/** @param mixed $value */
|
|
function foo($value) : void {
|
|
if (\is_scalar($value)) {
|
|
if ($value) {
|
|
echo $value;
|
|
} else {
|
|
if (!$value) {}
|
|
}
|
|
}
|
|
}',
|
|
'error_message' => 'RedundantCondition',
|
|
],
|
|
'catchRedundantConditionOnBinaryOpForwards' => [
|
|
'<?php
|
|
class App {}
|
|
|
|
function test(App $app) : void {
|
|
if ($app || rand(0, 1)) {}
|
|
}',
|
|
'error_message' => 'RedundantCondition',
|
|
],
|
|
'nonEmptyString' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-param non-empty-string $name
|
|
*/
|
|
function sayHello(string $name) : void {
|
|
echo "Hello " . $name;
|
|
}
|
|
|
|
function takeInput() : void {
|
|
if (isset($_GET["name"]) && is_string($_GET["name"])) {
|
|
$name = trim($_GET["name"]);
|
|
sayHello($name);
|
|
}
|
|
}',
|
|
'error_message' => 'ArgumentTypeCoercion',
|
|
],
|
|
'getClassCannotBeStringEquals' => [
|
|
'<?php
|
|
function foo(Exception $e) : void {
|
|
if (get_class($e) == "InvalidArgumentException") {}
|
|
}',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'falsyValuesInIf' => [
|
|
'<?php
|
|
if (0) {
|
|
echo 123;
|
|
}',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'BooleanNotOfAlwaysTruthyisFalse' => [
|
|
'<?php
|
|
class a
|
|
{
|
|
public function fluent(): self
|
|
{
|
|
return $this;
|
|
}
|
|
}
|
|
|
|
$a = new a();
|
|
if (!$a->fluent()) {
|
|
echo "always";
|
|
}
|
|
',
|
|
'error_message' => 'TypeDoesNotContainType',
|
|
],
|
|
'redundantConditionForNonEmptyString' => [
|
|
'<?php
|
|
|
|
/**
|
|
* @param non-empty-string $c
|
|
*/
|
|
function c(string $c): void {
|
|
if ($c) {
|
|
if ($c) {
|
|
echo "hello";
|
|
}
|
|
}
|
|
}
|
|
',
|
|
'error_message' => 'RedundantCondition',
|
|
],
|
|
];
|
|
}
|
|
}
|