mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 12:55:26 +01:00
726 lines
26 KiB
PHP
726 lines
26 KiB
PHP
<?php
|
|
|
|
namespace Psalm\Tests\TypeReconciliation;
|
|
|
|
use Psalm\Tests\TestCase;
|
|
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
|
|
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
|
|
|
|
class EmptyTest extends TestCase
|
|
{
|
|
use InvalidCodeAnalysisTestTrait;
|
|
use ValidCodeAnalysisTestTrait;
|
|
|
|
public function providerValidCodeParse(): iterable
|
|
{
|
|
return [
|
|
'ifNotUndefinedAndEmpty' => [
|
|
'code' => '<?php
|
|
$a = !empty($b) ? $b : null;',
|
|
'assertions' => [
|
|
'$a' => 'mixed|null',
|
|
],
|
|
'ignored_issues' => ['MixedAssignment'],
|
|
],
|
|
'emptyArrayVar' => [
|
|
'code' => '<?php
|
|
function a(array $in): void
|
|
{
|
|
$r = [];
|
|
foreach ($in as $entry) {
|
|
if (!empty($entry["a"])) {
|
|
$r[] = [];
|
|
}
|
|
if (empty($entry["a"])) {
|
|
$r[] = [];
|
|
}
|
|
}
|
|
}
|
|
|
|
function b(array $in): void
|
|
{
|
|
$i = 0;
|
|
foreach ($in as $entry) {
|
|
if (!empty($entry["a"])) {
|
|
$i--;
|
|
}
|
|
if (empty($entry["a"])) {
|
|
$i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
function c(array $in): void
|
|
{
|
|
foreach ($in as $entry) {
|
|
if (!empty($entry["a"])) {}
|
|
}
|
|
foreach ($in as $entry) {
|
|
if (empty($entry["a"])) {}
|
|
}
|
|
}',
|
|
'assertions' => [],
|
|
'ignored_issues' => ['MixedAssignment', 'MixedArrayAccess'],
|
|
],
|
|
'removeEmptyArray' => [
|
|
'code' => '<?php
|
|
$arr_or_string = [];
|
|
|
|
if (rand(0, 1)) {
|
|
$arr_or_string = "hello";
|
|
}
|
|
|
|
/** @return void **/
|
|
function foo(string $s) {}
|
|
|
|
if (!empty($arr_or_string)) {
|
|
foo($arr_or_string);
|
|
}',
|
|
],
|
|
'emptyArrayReconciliationThenIf' => [
|
|
'code' => '<?php
|
|
/**
|
|
* @param string|string[] $a
|
|
*/
|
|
function foo($a): string {
|
|
if (is_string($a)) {
|
|
return $a;
|
|
} elseif (empty($a)) {
|
|
return "goodbye";
|
|
}
|
|
|
|
if (isset($a[0])) {
|
|
return $a[0];
|
|
};
|
|
|
|
return "not found";
|
|
}',
|
|
],
|
|
'emptyStringReconciliationThenIf' => [
|
|
'code' => '<?php
|
|
/**
|
|
* @param Exception|string|string[] $a
|
|
*/
|
|
function foo($a): string {
|
|
if (is_array($a)) {
|
|
return "hello";
|
|
} elseif (empty($a)) {
|
|
return "goodbye";
|
|
}
|
|
|
|
if (is_string($a)) {
|
|
return $a;
|
|
};
|
|
|
|
return "an exception";
|
|
}',
|
|
],
|
|
'emptyExceptionReconciliationAfterIf' => [
|
|
'code' => '<?php
|
|
/**
|
|
* @param Exception|null $a
|
|
*/
|
|
function foo($a): string {
|
|
if ($a && $a->getMessage() === "hello") {
|
|
return "hello";
|
|
} elseif (empty($a)) {
|
|
return "goodbye";
|
|
}
|
|
|
|
return $a->getMessage();
|
|
}',
|
|
],
|
|
'noFalsyLeak' => [
|
|
'code' => '<?php
|
|
function foo(string $s): void {
|
|
if (empty($s) || $s === "hello") {}
|
|
}',
|
|
],
|
|
'noRedundantConditionOnMixed' => [
|
|
'code' => '<?php
|
|
function testarray(array $data): void {
|
|
foreach ($data as $item) {
|
|
if (!empty($item["a"]) && !empty($item["b"]["c"])) {
|
|
echo "Found\n";
|
|
}
|
|
}
|
|
}',
|
|
'assertions' => [],
|
|
'ignored_issues' => ['MixedAssignment', 'MixedArrayAccess'],
|
|
],
|
|
'dontBleedEmptyAfterExtract' => [
|
|
'code' => '<?php
|
|
function foo(array $args): void {
|
|
extract($args);
|
|
if ((empty($arr) && empty($a)) || $c === 0) {
|
|
} else {
|
|
foreach ($arr as $b) {}
|
|
}
|
|
}',
|
|
'assertions' => [],
|
|
'ignored_issues' => ['MixedAssignment', 'MixedArgument'],
|
|
],
|
|
'emptyTKeyedArray' => [
|
|
'code' => '<?php
|
|
$arr = [
|
|
"profile" => [
|
|
"foo" => "bar",
|
|
],
|
|
"groups" => [
|
|
"foo" => "bar",
|
|
"hide" => rand(0, 5),
|
|
],
|
|
];
|
|
|
|
foreach ($arr as $item) {
|
|
if (empty($item["hide"]) || $item["hide"] === 3) {}
|
|
}',
|
|
],
|
|
'alwaysBoolResult' => [
|
|
'code' => '<?php
|
|
function takesBool(bool $p): void {}
|
|
takesBool(empty($q));',
|
|
],
|
|
'noRedundantConditionAfterFalsyIntChecks' => [
|
|
'code' => '<?php
|
|
function foo(int $t) : void {
|
|
if (!$t) {
|
|
foreach ([0, 1, 2] as $a) {
|
|
if (!$t) {
|
|
$t = $a;
|
|
}
|
|
}
|
|
}
|
|
}',
|
|
],
|
|
'noRedundantConditionAfterEmptyMixedChecks' => [
|
|
'code' => '<?php
|
|
function foo($t) : void {
|
|
if (empty($t)) {
|
|
foreach ($GLOBALS["u"] as $a) {
|
|
if (empty($t)) {
|
|
$t = $a;
|
|
}
|
|
}
|
|
}
|
|
}',
|
|
'assertions' => [],
|
|
'ignored_issues' => ['MixedAssignment', 'MissingParamType'],
|
|
],
|
|
'canBeNonEmptyArray' => [
|
|
'code' => '<?php
|
|
function _processScopes($scopes) : void {
|
|
if (!is_array($scopes) && !empty($scopes)) {
|
|
$scopes = explode(" ", trim($scopes));
|
|
} else {
|
|
// false is allowed here
|
|
}
|
|
|
|
if (empty($scopes)){}
|
|
}',
|
|
'assertions' => [],
|
|
'ignored_issues' => ['MixedAssignment', 'MissingParamType', 'MixedArgument'],
|
|
],
|
|
'multipleEmptiesInCondition' => [
|
|
'code' => '<?php
|
|
/** @param array<int, int> $o */
|
|
function foo(array $o) : void {
|
|
if (empty($o[0]) && empty($o[1])) {}
|
|
}',
|
|
],
|
|
'multipleEmptiesInConditionWithMixedOffset' => [
|
|
'code' => '<?php
|
|
/** @param array $o */
|
|
function foo(array $o) : void {
|
|
if (empty($o[0]) && empty($o[1])) {}
|
|
}',
|
|
],
|
|
'unsetChangesArrayEmptiness' => [
|
|
'code' => '<?php
|
|
function foo(array $n): void {
|
|
if (empty($n)) {
|
|
return;
|
|
}
|
|
while (!empty($n)) {
|
|
unset($n[rand(0, 10)]);
|
|
}
|
|
}',
|
|
],
|
|
'unsetChangesComplicatedArrayEmptiness' => [
|
|
'code' => '<?php
|
|
function contains(array $data, array $needle): bool {
|
|
if (empty($data) || empty($needle)) {
|
|
return false;
|
|
}
|
|
$stack = [];
|
|
|
|
while (!empty($needle)) {
|
|
$key = key($needle);
|
|
$val = $needle[$key];
|
|
unset($needle[$key]);
|
|
|
|
if (array_key_exists($key, $data) && is_array($val)) {
|
|
$next = $data[$key];
|
|
unset($data[$key]);
|
|
|
|
if (!empty($val)) {
|
|
$stack[] = [$val, $next];
|
|
}
|
|
} elseif (!array_key_exists($key, $data) || $data[$key] != $val) {
|
|
return false;
|
|
}
|
|
|
|
if (empty($needle) && !empty($stack)) {
|
|
list($needle, $data) = array_pop($stack);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}',
|
|
'assertions' => [],
|
|
'ignored_issues' => ['MixedAssignment', 'MissingParamType', 'MixedArgument', 'MixedArrayOffset', 'MixedArrayAccess'],
|
|
],
|
|
'possiblyEmptyIterable' => [
|
|
'code' => '<?php
|
|
function foo(iterable $i) : void {
|
|
if (empty($i)) {}
|
|
if (!empty($i)) {}
|
|
}',
|
|
],
|
|
'allowEmptyClassConstantOffsetCheck' => [
|
|
'code' => '<?php
|
|
class Foo {
|
|
const BAR = "bar";
|
|
const ONE = 1;
|
|
}
|
|
|
|
/**
|
|
* @param array<string,string> $data
|
|
*/
|
|
function bat(array $data) : void {
|
|
if (!empty($data["foo"])) {
|
|
if (empty($data[Foo::BAR])) {}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<int,string> $data
|
|
*/
|
|
function baz(array $data) : void {
|
|
if (!empty($data[0])) {
|
|
if (empty($data[Foo::ONE])) {}
|
|
}
|
|
}',
|
|
],
|
|
'doubleEmptyCheckTwoArrays' => [
|
|
'code' => '<?php
|
|
function foo(array $a, array $b) : void {
|
|
if (empty($a) && empty($b)) {}
|
|
}',
|
|
],
|
|
'doubleEmptyCheckOnTKeyedArray' => [
|
|
'code' => '<?php
|
|
/**
|
|
* @param array{a: array, b: array} $arr
|
|
*/
|
|
function foo(array $arr) : void {
|
|
if (empty($arr["a"]) && empty($arr["b"])) {}
|
|
}',
|
|
],
|
|
'doubleEmptyCheckOnTKeyedArrayVariableOffsets' => [
|
|
'code' => '<?php
|
|
function foo(int $i, int $j) : void {
|
|
$arr = [];
|
|
$arr[0] = rand(0, 1);
|
|
$arr[1] = rand(0, 1);
|
|
|
|
if (empty($arr[$i]) && empty($arr[$j])) {}
|
|
}',
|
|
],
|
|
'checkArrayEmptyUnknownRoot' => [
|
|
'code' => '<?php
|
|
function foo(array $arr) : void {
|
|
if (empty($arr[rand(0, 1)])) {
|
|
if ($arr) {}
|
|
}
|
|
}',
|
|
],
|
|
'allowEmptyCheckOnPossiblyNullPropertyFetch' => [
|
|
'code' => '<?php
|
|
class A {
|
|
public bool $b = false;
|
|
}
|
|
|
|
function foo(?A $a) : void {
|
|
if (!empty($a->b)) {}
|
|
}',
|
|
],
|
|
'allowNumericEmpty' => [
|
|
'code' => '<?php
|
|
/**
|
|
* @param numeric $p
|
|
*/
|
|
function f($p): bool {
|
|
if (empty($p)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}',
|
|
],
|
|
'possiblyUndefinedArrayOffset' => [
|
|
'code' => '<?php
|
|
$d = [];
|
|
if (!rand(0,1)) {
|
|
$d[0] = "a";
|
|
}
|
|
|
|
if (empty($d[0])) {}',
|
|
],
|
|
'reconcileNonEmptyArrayKey' => [
|
|
'code' => '<?php
|
|
/**
|
|
* @param array{a?: string, b: string} $arr
|
|
*/
|
|
function createFromString(array $arr): void
|
|
{
|
|
if (empty($arr["a"])) {
|
|
return;
|
|
}
|
|
|
|
echo $arr["a"];
|
|
}',
|
|
],
|
|
'reconcileEmptyTwiceWithoutReturn' => [
|
|
'code' => '<?php
|
|
function foo(array $arr): void {
|
|
if (!empty($arr["a"])) {
|
|
} else {
|
|
if (empty($arr["dontcare"])) {}
|
|
}
|
|
|
|
if (empty($arr["a"])) {}
|
|
}',
|
|
],
|
|
'reconcileEmptyTwiceWithReturn' => [
|
|
'code' => '<?php
|
|
function foo(array $arr): void {
|
|
if (!empty($arr["a"])) {
|
|
} else {
|
|
if (empty($arr["dontcare"])) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (empty($arr["a"])) {}
|
|
}',
|
|
],
|
|
'SKIPPED-strlenWithGreaterZero' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return strlen($str) > 0 ? $str : "string";
|
|
}',
|
|
],
|
|
'SKIPPED-strlenRighthandWithGreaterZero' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return 0 < strlen($str) ? $str : "string";
|
|
}',
|
|
],
|
|
'SKIPPED-strlenWithGreaterEqualsOne' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return strlen($str) >= 1 ? $str : "string";
|
|
}',
|
|
],
|
|
'SKIPPED-strlenRighthandWithGreaterEqualsOne' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return 1 <= strlen($str) ? $str : "string";
|
|
}',
|
|
],
|
|
'SKIPPED-strlenWithInequalZero' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return strlen($str) !== 0 ? $str : "string";
|
|
}',
|
|
],
|
|
'SKIPPED-strlenRighthandWithInequalZero' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return 0 !== strlen($str) ? $str : "string";
|
|
}',
|
|
],
|
|
'SKIPPED-strlenWithEqualOne' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return strlen($str) === 1 ? $str : "string";
|
|
}',
|
|
],
|
|
'SKIPPED-strlenRighthandWithEqualOne' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return 1 === strlen($str) ? $str : "string";
|
|
}',
|
|
],
|
|
'SKIPPED-mb_strlen' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return mb_strlen($str) === 1 ? $str : "string";
|
|
}',
|
|
],
|
|
'SKIPPED-countWithLiteralIntVariable' => [ // #8163
|
|
'code' => '<?php
|
|
$c = 1;
|
|
/** @var list<int> */
|
|
$arr = [1];
|
|
assert(count($arr) === $c);
|
|
',
|
|
'assertions' => ['$arr===' => 'non-empty-list<int>'],
|
|
],
|
|
'SKIPPED-countWithIntRange' => [ // #8163
|
|
'code' => '<?php
|
|
/** @var int<1, max> */
|
|
$c = 1;
|
|
/** @var list<int> */
|
|
$arr = [1];
|
|
assert(count($arr) === $c);
|
|
',
|
|
'assertions' => ['$arr===' => 'non-empty-list<int>'],
|
|
],
|
|
'SKIPPED-countEmptyWithIntRange' => [ // #8163
|
|
'code' => '<?php
|
|
/** @var int<0, max> */
|
|
$c = 1;
|
|
/** @var list<int> */
|
|
$arr = [1];
|
|
assert(count($arr) === $c);
|
|
',
|
|
'assertions' => ['$arr===' => 'list<int>'],
|
|
],
|
|
'issue-9205-1' => [
|
|
'code' => <<<'PHP'
|
|
<?php
|
|
/** @var string $domainCandidate */;
|
|
|
|
$candidateLabels = explode('.', $domainCandidate);
|
|
|
|
$lastLabel = $candidateLabels[0];
|
|
|
|
if (strlen($lastLabel) === 2) {
|
|
exit;
|
|
}
|
|
PHP,
|
|
'assertions' => [
|
|
'$lastLabel===' => 'string',
|
|
],
|
|
],
|
|
'issue-9205-2' => [
|
|
'code' => <<<'PHP'
|
|
<?php
|
|
/** @var string $x */
|
|
if (strlen($x) > 0) {
|
|
exit;
|
|
}
|
|
PHP,
|
|
'assertions' => [
|
|
'$x===' => 'string', // perhaps this should be improved in future
|
|
],
|
|
],
|
|
'issue-9205-3' => [
|
|
'code' => <<<'PHP'
|
|
<?php
|
|
/** @var string $x */
|
|
if (strlen($x) === 2) {
|
|
exit;
|
|
}
|
|
PHP,
|
|
'assertions' => [
|
|
'$x===' => 'string', // can't be improved really
|
|
],
|
|
],
|
|
'issue-9205-4' => [
|
|
'code' => <<<'PHP'
|
|
<?php
|
|
/** @var string $x */
|
|
if (strlen($x) < 2 ) {
|
|
exit;
|
|
}
|
|
PHP,
|
|
'assertions' => [
|
|
'$x===' => 'string', // can be improved
|
|
],
|
|
],
|
|
'issue-9349' => [
|
|
'code' => <<<'PHP'
|
|
<?php
|
|
|
|
$str = $argv[1] ?? '';
|
|
if (empty($str) || strlen($str) < 3) {
|
|
exit(1);
|
|
}
|
|
|
|
echo $str;
|
|
PHP,
|
|
'assertions' => [
|
|
'$str===' => 'non-falsy-string', // can't be improved
|
|
],
|
|
],
|
|
'issue-9349-2' => [
|
|
'code' => <<<'PHP'
|
|
<?php
|
|
function test(string $s): void {
|
|
if (!$s || strlen($s) !== 9) {
|
|
throw new Exception();
|
|
}
|
|
}
|
|
PHP,
|
|
],
|
|
'issue-9349-3' => [
|
|
'code' => <<<'PHP'
|
|
<?php
|
|
/** @var string $a */;
|
|
if (strlen($a) === 7) {
|
|
return $a;
|
|
} elseif (strlen($a) === 10) {
|
|
return $a;
|
|
}
|
|
PHP,
|
|
'assertions' => [
|
|
'$a===' => 'string', // can't be improved
|
|
],
|
|
],
|
|
'issue-9341-1' => [
|
|
'code' => <<<'PHP'
|
|
<?php
|
|
/** @var string */
|
|
$GLOBALS['sql_query'] = rand(0,1) ? 'asd' : null;
|
|
if(!empty($GLOBALS['sql_query']) && mb_strlen($GLOBALS['sql_query']) > 2)
|
|
{
|
|
exit;
|
|
}
|
|
PHP,
|
|
'assertions' => [
|
|
'$GLOBALS[\'sql_query\']===' => 'string',
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
public function providerInvalidCodeParse(): iterable
|
|
{
|
|
return [
|
|
'preventImpossibleEmpty' => [
|
|
'code' => '<?php
|
|
function foo(array $arr) : void {
|
|
if (empty($ar)) {
|
|
// do something
|
|
}
|
|
}',
|
|
'error_message' => 'UndefinedVariable',
|
|
],
|
|
'reconciliationForMixed' => [
|
|
'code' => '<?php
|
|
function foo(array $arr) : void {
|
|
$a = empty($arr["a"]) ? "" : $arr["a"];
|
|
|
|
if ($a) {
|
|
if ($a) {}
|
|
}
|
|
}',
|
|
'error_message' => 'RedundantCondition',
|
|
'ignored_issues' => ['MixedAssignment', 'MissingParamType'],
|
|
],
|
|
'preventEmptyOnBool' => [
|
|
'code' => '<?php
|
|
function foo(bool $b) : void {
|
|
if (!empty($b)) {}
|
|
}',
|
|
'error_message' => 'InvalidArgument',
|
|
],
|
|
'preventEmptyCreatingArray' => [
|
|
'code' => '<?php
|
|
/** @return array{a:mixed} */
|
|
function foo(array $r) {
|
|
if (!empty($r["a"])) {}
|
|
return $r;
|
|
}',
|
|
'error_message' => 'MixedReturnTypeCoercion',
|
|
],
|
|
'preventEmptyEquivalentCreatingArray' => [
|
|
'code' => '<?php
|
|
/** @return array{a:mixed} */
|
|
function foo(array $r) {
|
|
if (isset($r["a"]) && $r["a"]) {}
|
|
return $r;
|
|
}',
|
|
'error_message' => 'MixedReturnTypeCoercion',
|
|
],
|
|
'SKIPPED-preventStrlenGreaterMinusOne' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return strlen($str) > -1 ? $str : "string";
|
|
}',
|
|
'error_message' => 'LessSpecificReturnStatement',
|
|
],
|
|
'SKIPPED-preventRighthandStrlenGreaterMinusOne' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return -1 < strlen($str) ? $str : "string";
|
|
}',
|
|
'error_message' => 'LessSpecificReturnStatement',
|
|
],
|
|
'SKIPPED-preventStrlenGreaterEqualsZero' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return strlen($str) >= 0 ? $str : "string";
|
|
}',
|
|
'error_message' => 'LessSpecificReturnStatement',
|
|
],
|
|
'SKIPPED-preventStrlenEqualsZero' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return strlen($str) === 0 ? $str : "string";
|
|
}',
|
|
'error_message' => 'InvalidReturnStatement',
|
|
],
|
|
'SKIPPED-preventStrlenLessThanOne' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return strlen($str) < 1 ? $str : "string";
|
|
}',
|
|
'error_message' => 'InvalidReturnStatement',
|
|
],
|
|
'SKIPPED-preventStrlenLessEqualsZero' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return strlen($str) <= 0 ? $str : "string";
|
|
}',
|
|
'error_message' => 'InvalidReturnStatement',
|
|
],
|
|
'SKIPPED-preventStrlenWithConcatenatedString' => [
|
|
'code' => '<?php
|
|
/** @return non-empty-string */
|
|
function nonEmptyString(string $str): string {
|
|
return strlen("a" . $str . "b") > 2 ? $str : "string";
|
|
}',
|
|
'error_message' => 'LessSpecificReturnStatement',
|
|
],
|
|
];
|
|
}
|
|
}
|