mirror of
https://github.com/danog/psalm.git
synced 2024-12-13 09:47:29 +01:00
3422 lines
111 KiB
PHP
3422 lines
111 KiB
PHP
<?php
|
|
|
|
namespace Psalm\Tests;
|
|
|
|
use Psalm\Config;
|
|
use Psalm\Context;
|
|
use Psalm\Exception\CodeException;
|
|
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
|
use Psalm\Internal\Provider\FakeFileProvider;
|
|
use Psalm\Internal\Provider\Providers;
|
|
use Psalm\Internal\RuntimeCaches;
|
|
use Psalm\Tests\Internal\Provider\FakeParserCacheProvider;
|
|
|
|
use function preg_quote;
|
|
use function strpos;
|
|
|
|
use const DIRECTORY_SEPARATOR;
|
|
|
|
class UnusedVariableTest extends TestCase
|
|
{
|
|
/** @var ProjectAnalyzer */
|
|
protected $project_analyzer;
|
|
|
|
public function setUp(): void
|
|
{
|
|
RuntimeCaches::clearAll();
|
|
|
|
$this->file_provider = new FakeFileProvider();
|
|
|
|
$this->project_analyzer = new ProjectAnalyzer(
|
|
new TestConfig(),
|
|
new Providers(
|
|
$this->file_provider,
|
|
new FakeParserCacheProvider()
|
|
)
|
|
);
|
|
|
|
$this->project_analyzer->setPhpVersion('7.4', 'tests');
|
|
$this->project_analyzer->getCodebase()->reportUnusedVariables();
|
|
}
|
|
|
|
/**
|
|
* @dataProvider providerValidCodeParse
|
|
*
|
|
* @param string $code
|
|
* @param array<string> $error_levels
|
|
*
|
|
*/
|
|
public function testValidCode($code, array $error_levels = []): void
|
|
{
|
|
$test_name = $this->getTestName();
|
|
if (strpos($test_name, 'SKIPPED-') !== false) {
|
|
$this->markTestSkipped('Skipped due to a bug.');
|
|
}
|
|
|
|
$file_path = self::$src_dir_path . 'somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
$code
|
|
);
|
|
|
|
foreach ($error_levels as $error_level) {
|
|
$this->project_analyzer->getCodebase()->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS);
|
|
}
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
/**
|
|
* @dataProvider providerInvalidCodeParse
|
|
*
|
|
* @param string $code
|
|
* @param string $error_message
|
|
* @param array<string> $error_levels
|
|
*
|
|
*/
|
|
public function testInvalidCode($code, $error_message, $error_levels = []): void
|
|
{
|
|
if (strpos($this->getTestName(), 'SKIPPED-') !== false) {
|
|
$this->markTestSkipped();
|
|
}
|
|
|
|
$this->expectException(CodeException::class);
|
|
$this->expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/');
|
|
|
|
$file_path = self::$src_dir_path . 'somefile.php';
|
|
|
|
foreach ($error_levels as $error_level) {
|
|
$this->project_analyzer->getCodebase()->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS);
|
|
}
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
$code
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
/**
|
|
* @return array<string, array{string,error_levels?:string[]}>
|
|
*/
|
|
public function providerValidCodeParse(): array
|
|
{
|
|
return [
|
|
'arrayOffset' => [
|
|
'<?php
|
|
$a = 0;
|
|
|
|
$arr = ["hello"];
|
|
|
|
echo $arr[$a];',
|
|
],
|
|
'unset' => [
|
|
'<?php
|
|
$a = 0;
|
|
|
|
$arr = ["hello"];
|
|
|
|
unset($arr[$a]);',
|
|
],
|
|
'usedVariables' => [
|
|
'<?php
|
|
/** @return string */
|
|
function foo() {
|
|
$a = 5;
|
|
$b = [];
|
|
$c[] = "hello";
|
|
class Foo {
|
|
public function __construct(string $_i) {}
|
|
}
|
|
$d = "Foo";
|
|
$e = "arg";
|
|
$f = new $d($e);
|
|
return $a . implode(",", $b) . $c[0] . get_class($f);
|
|
}',
|
|
'error_levels' => [
|
|
'PossiblyUndefinedVariable',
|
|
'MixedArrayAccess',
|
|
'MixedOperand',
|
|
'MixedAssignment',
|
|
'InvalidStringClass',
|
|
],
|
|
],
|
|
'varDefinedInIfWithReference' => [
|
|
'<?php
|
|
$a = 5;
|
|
if (rand(0, 1)) {
|
|
$b = "hello";
|
|
} else {
|
|
$b = "goodbye";
|
|
}
|
|
echo $a . $b;',
|
|
],
|
|
'varRedefinedInIfWithReference' => [
|
|
'<?php
|
|
$a = (string) 5;
|
|
|
|
if (rand(0, 1)) {
|
|
$a = (string) 6;
|
|
}
|
|
|
|
echo $a;',
|
|
],
|
|
'byrefInForeachLoopWithReference' => [
|
|
'<?php
|
|
$a = [1, 2, 3];
|
|
foreach ($a as &$b) {
|
|
$b = $b + 1;
|
|
}
|
|
echo $a[0];',
|
|
],
|
|
'foreachVarSetInValue' => [
|
|
'<?php
|
|
/** @param string[] $arr */
|
|
function foo(array $arr) : void {
|
|
$a = null;
|
|
foreach ($arr as $a) { }
|
|
if ($a) {}
|
|
}',
|
|
],
|
|
'definedInSecondBranchOfCondition' => [
|
|
'<?php
|
|
if (rand(0, 1) && $a = rand(0, 1)) {
|
|
echo $a;
|
|
}',
|
|
],
|
|
'booleanOr' => [
|
|
'<?php
|
|
function foo(int $a, int $b): bool {
|
|
return $a || $b;
|
|
}',
|
|
],
|
|
'paramUsedInIf' => [
|
|
'<?php
|
|
function foo(string $a): void {
|
|
if (rand(0, 1)) {
|
|
echo $a;
|
|
}
|
|
}',
|
|
],
|
|
'dummyByRefVar' => [
|
|
'<?php
|
|
function foo(string &$a = null, string $b = null): void {
|
|
if ($a) {
|
|
echo $a;
|
|
}
|
|
if ($b) {
|
|
echo $b;
|
|
}
|
|
}
|
|
|
|
function bar(): void {
|
|
foo($dummy_byref_var, "hello");
|
|
}
|
|
|
|
bar();',
|
|
],
|
|
'foreachReassigned' => [
|
|
'<?php
|
|
/**
|
|
* @param list<int> $arr
|
|
*/
|
|
function foo(array $arr) : void {
|
|
$a = false;
|
|
|
|
foreach ($arr as $b) {
|
|
$a = true;
|
|
echo $b;
|
|
}
|
|
|
|
echo $a;
|
|
}',
|
|
],
|
|
'doWhileReassigned' => [
|
|
'<?php
|
|
$a = 5;
|
|
|
|
do {
|
|
echo $a;
|
|
$a = $a - rand(-3, 3);
|
|
} while ($a > 3);',
|
|
],
|
|
'loopTypeChangedInIfAndContinueWithReference' => [
|
|
'<?php
|
|
$a = false;
|
|
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = true;
|
|
continue;
|
|
}
|
|
|
|
$a = false;
|
|
}
|
|
|
|
echo $a;',
|
|
],
|
|
'loopReassignedInIfAndContinueWithReferenceAfter' => [
|
|
'<?php
|
|
$a = 5;
|
|
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = 7;
|
|
continue;
|
|
}
|
|
|
|
$a = 3;
|
|
}
|
|
|
|
echo $a;',
|
|
],
|
|
'loopReassignedInIfAndContinueWithReferenceBeforeAndAfter' => [
|
|
'<?php
|
|
$a = 5;
|
|
|
|
echo $a;
|
|
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = 7;
|
|
continue;
|
|
}
|
|
|
|
$a = 3;
|
|
}
|
|
|
|
echo $a;',
|
|
],
|
|
'loopReassigned' => [
|
|
'<?php
|
|
$a = false;
|
|
|
|
while(rand(0, 1)) {
|
|
$a = true;
|
|
}
|
|
|
|
echo $a;',
|
|
],
|
|
'ifVarReassignedInBranchWithUse' => [
|
|
'<?php
|
|
$a = true;
|
|
|
|
if (rand(0, 1)) {
|
|
$a = false;
|
|
}
|
|
|
|
if ($a) {
|
|
echo "cool";
|
|
}',
|
|
],
|
|
'elseVarReassignedInBranchAndReference' => [
|
|
'<?php
|
|
$a = false;
|
|
|
|
if (rand(0, 1)) {
|
|
// do nothing
|
|
} else {
|
|
$a = true;
|
|
//echo $a;
|
|
}
|
|
|
|
if ($a) {
|
|
echo "cool";
|
|
}',
|
|
],
|
|
'switchVarReassignedInBranch' => [
|
|
'<?php
|
|
$a = false;
|
|
|
|
switch (rand(0, 2)) {
|
|
case 0:
|
|
$a = true;
|
|
}
|
|
|
|
if ($a) {
|
|
echo "cool";
|
|
}',
|
|
],
|
|
'switchVarDefinedInAllBranches' => [
|
|
'<?php
|
|
switch (rand(0, 2)) {
|
|
case 0:
|
|
$a = true;
|
|
break;
|
|
|
|
default:
|
|
$a = false;
|
|
}
|
|
|
|
if ($a) {
|
|
echo "cool";
|
|
}',
|
|
],
|
|
'switchVarConditionalAssignmentWithReference' => [
|
|
'<?php
|
|
switch (rand(0, 4)) {
|
|
case 0:
|
|
if (rand(0, 1)) {
|
|
$a = 0;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
$a = 1;
|
|
}
|
|
|
|
echo $a;',
|
|
],
|
|
'throwWithMessageCall' => [
|
|
'<?php
|
|
function dangerous(): void {
|
|
throw new \Exception("bad");
|
|
}
|
|
|
|
function callDangerous(): void {
|
|
try {
|
|
dangerous();
|
|
} catch (Exception $e) {
|
|
echo $e->getMessage();
|
|
}
|
|
}',
|
|
],
|
|
'throwWithMessageCallAndAssignmentAndReference' => [
|
|
'<?php
|
|
function dangerous(): string {
|
|
if (rand(0, 1)) {
|
|
throw new \Exception("bad");
|
|
}
|
|
|
|
return "hello";
|
|
}
|
|
|
|
function callDangerous(): void {
|
|
$s = null;
|
|
|
|
try {
|
|
$s = dangerous();
|
|
} catch (Exception $e) {
|
|
echo $e->getMessage();
|
|
}
|
|
|
|
if ($s) {}
|
|
}',
|
|
],
|
|
'throwWithMessageCallAndAssignmentInCatchAndReference' => [
|
|
'<?php
|
|
function dangerous(): string {
|
|
if (rand(0, 1)) {
|
|
throw new \Exception("bad");
|
|
}
|
|
|
|
return "hello";
|
|
}
|
|
|
|
function callDangerous(): void {
|
|
$s = null;
|
|
|
|
try {
|
|
dangerous();
|
|
} catch (Exception $e) {
|
|
echo $e->getMessage();
|
|
$s = "hello";
|
|
}
|
|
|
|
if ($s) {}
|
|
}',
|
|
],
|
|
'throwWithMessageCallAndAssignmentInTryAndCatchAndReference' => [
|
|
'<?php
|
|
function dangerous(): string {
|
|
if (rand(0, 1)) {
|
|
throw new \Exception("bad");
|
|
}
|
|
|
|
return "hello";
|
|
}
|
|
|
|
function callDangerous(): void {
|
|
try {
|
|
$s = dangerous();
|
|
} catch (Exception $e) {
|
|
echo $e->getMessage();
|
|
$s = "hello";
|
|
}
|
|
|
|
if ($s) {}
|
|
}',
|
|
],
|
|
'throwWithMessageCallAndNestedAssignmentInTryAndCatchAndReference' => [
|
|
'<?php
|
|
function dangerous(): string {
|
|
if (rand(0, 1)) {
|
|
throw new \Exception("bad");
|
|
}
|
|
|
|
return "hello";
|
|
}
|
|
|
|
function callDangerous(): void {
|
|
$s = null;
|
|
|
|
if (rand(0, 1)) {
|
|
$s = "hello";
|
|
} else {
|
|
try {
|
|
$t = dangerous();
|
|
} catch (Exception $e) {
|
|
echo $e->getMessage();
|
|
$t = "hello";
|
|
}
|
|
|
|
if ($t) {
|
|
$s = $t;
|
|
}
|
|
}
|
|
|
|
if ($s) {}
|
|
}',
|
|
],
|
|
'throwWithReturnInOneCatch' => [
|
|
'<?php
|
|
class E1 extends Exception {}
|
|
|
|
function dangerous(): void {
|
|
if (rand(0, 1)) {
|
|
throw new \Exception("bad");
|
|
}
|
|
}
|
|
|
|
function callDangerous(): void {
|
|
try {
|
|
dangerous();
|
|
$s = true;
|
|
} catch (E1 $e) {
|
|
echo $e->getMessage();
|
|
$s = false;
|
|
} catch (Exception $e) {
|
|
return;
|
|
}
|
|
|
|
if ($s) {}
|
|
}',
|
|
],
|
|
'loopWithIfRedefinition' => [
|
|
'<?php
|
|
$i = false;
|
|
|
|
foreach ([1, 2, 3] as $a) {
|
|
if (rand(0, 1)) {
|
|
$i = true;
|
|
}
|
|
|
|
echo $a;
|
|
}
|
|
|
|
if ($i) {}',
|
|
],
|
|
'unknownMethodCallWithVar' => [
|
|
'<?php
|
|
/** @psalm-suppress MixedMethodCall */
|
|
function passesByRef(object $a): void {
|
|
/** @psalm-suppress PossiblyUndefinedVariable */
|
|
$a->passedByRef($b);
|
|
}',
|
|
],
|
|
'usedMethodCallVariable' => [
|
|
'<?php
|
|
function reindex(array $arr, string $methodName): array {
|
|
$ret = [];
|
|
|
|
foreach ($arr as $element) {
|
|
$ret[$element->$methodName()] = true;
|
|
}
|
|
|
|
return $ret;
|
|
}',
|
|
'error_levels' => [
|
|
'MixedAssignment',
|
|
'MixedMethodCall',
|
|
'MixedArrayOffset',
|
|
],
|
|
],
|
|
'globalVariableUsage' => [
|
|
'<?php
|
|
$a = "hello";
|
|
function example() : void {
|
|
global $a;
|
|
echo $a;
|
|
$a = "hello";
|
|
}
|
|
example();',
|
|
],
|
|
'staticVar' => [
|
|
'<?php
|
|
function use_static() : void {
|
|
static $token;
|
|
if (!$token) {
|
|
$token = rand(1, 10);
|
|
}
|
|
echo "token is $token\n";
|
|
}',
|
|
],
|
|
'staticVarUsedLater' => [
|
|
'<?php
|
|
function use_static() : int {
|
|
static $x = null;
|
|
if ($x) {
|
|
return (int) $x;
|
|
}
|
|
$x = rand(0, 1);
|
|
return -1;
|
|
}',
|
|
],
|
|
'tryCatchWithUseInIf' => [
|
|
'<?php
|
|
function example_string() : string {
|
|
if (rand(0, 1) > 0) {
|
|
return "value";
|
|
}
|
|
throw new Exception("fail");
|
|
}
|
|
|
|
function main() : void {
|
|
try {
|
|
$s = example_string();
|
|
if (!$s) {
|
|
echo "Failed to get string\n";
|
|
}
|
|
} catch (Exception $e) {
|
|
$s = "fallback";
|
|
}
|
|
printf("s is %s\n", $s);
|
|
}',
|
|
],
|
|
'loopTypeChangedInIfAndBreakWithReference' => [
|
|
'<?php
|
|
$a = 1;
|
|
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = 2;
|
|
break;
|
|
}
|
|
|
|
$a = 3;
|
|
}
|
|
|
|
echo $a;',
|
|
],
|
|
'loopReassignedInIfAndBreakWithReferenceAfter' => [
|
|
'<?php
|
|
$a = 5;
|
|
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = 7;
|
|
break;
|
|
}
|
|
|
|
$a = 3;
|
|
}
|
|
|
|
echo $a;',
|
|
],
|
|
'loopSetIfNullWithBreakAndReference' => [
|
|
'<?php
|
|
$a = null;
|
|
|
|
while (rand(0, 1)) {
|
|
if ($a !== null) {
|
|
$a = 4;
|
|
break;
|
|
}
|
|
|
|
$a = 5;
|
|
}
|
|
|
|
echo $a;',
|
|
],
|
|
'loopSetIfNullWithContinueAndReference' => [
|
|
'<?php
|
|
$a = null;
|
|
|
|
while (rand(0, 1)) {
|
|
if ($a !== null) {
|
|
$a = 4;
|
|
continue;
|
|
}
|
|
|
|
$a = 5;
|
|
}
|
|
|
|
echo $a;',
|
|
],
|
|
'loopAssignmentAfterReferenceSimple' => [
|
|
'<?php
|
|
$a = 0;
|
|
while (rand(0, 1)) {
|
|
echo $a;
|
|
$a = 1;
|
|
}',
|
|
],
|
|
'loopAssignmentAfterReferenceWithContinue' => [
|
|
'<?php
|
|
$a = 0;
|
|
while (rand(0, 1)) {
|
|
echo $a;
|
|
$a = 1;
|
|
continue;
|
|
}',
|
|
],
|
|
'loopAssignmentAfterReferenceWithConditionalAssignmentWithContinue' => [
|
|
'<?php
|
|
$a = 0;
|
|
while (rand(0, 1)) {
|
|
echo $a;
|
|
|
|
if (rand(0, 1)) {
|
|
$a = 1;
|
|
}
|
|
|
|
continue;
|
|
}',
|
|
],
|
|
'loopAssignmentAfterReferenceWithContinueInIf' => [
|
|
'<?php
|
|
$a = 0;
|
|
while (rand(0, 1)) {
|
|
echo $a;
|
|
|
|
if (rand(0, 1)) {
|
|
$a = 1;
|
|
continue;
|
|
}
|
|
}',
|
|
],
|
|
'loopAssignmentAfterReferenceWithContinueInSwitch' => [
|
|
'<?php
|
|
$a = 0;
|
|
while (rand(0, 1)) {
|
|
switch (rand(0, 1)) {
|
|
case 0:
|
|
$a = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
echo $a;',
|
|
],
|
|
'loopAssignmentAfterReferenceWithContinueInSwitch2' => [
|
|
'<?php
|
|
$a = 0;
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
switch (rand(0, 1)) {
|
|
case 0:
|
|
$a = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
echo $a;',
|
|
],
|
|
'listVarAssignmentInIf' => [
|
|
'<?php
|
|
$a = "a";
|
|
$b = "b";
|
|
|
|
if (rand(0, 1)) {
|
|
list($a, $b) = explode(".", "c.d");
|
|
}
|
|
|
|
echo $a;
|
|
echo $b;',
|
|
],
|
|
'arrayVarAssignmentInFunctionAndReturned' => [
|
|
'<?php
|
|
/**
|
|
* @param array{string} $arr
|
|
*/
|
|
function far(array $arr): string {
|
|
[$a] = $arr;
|
|
|
|
return $a;
|
|
}',
|
|
],
|
|
'arrayUnpackInForeach' => [
|
|
'<?php
|
|
/**
|
|
* @param list<array{string, string}> $arr
|
|
*/
|
|
function far(array $arr): void {
|
|
foreach ($arr as [$a, $b]) {
|
|
echo $a;
|
|
echo $b;
|
|
}
|
|
}',
|
|
],
|
|
'arrayAssignmentInFunctionCoerced' => [
|
|
'<?php
|
|
class A {
|
|
public int $a = 0;
|
|
public int $b = 1;
|
|
|
|
function setPhpVersion(string $version): void {
|
|
[$a, $b] = explode(".", $version);
|
|
|
|
$this->a = (int) $a;
|
|
$this->b = (int) $b;
|
|
}
|
|
}
|
|
'
|
|
],
|
|
'varCheckAfterNestedAssignmentAndBreak' => [
|
|
'<?php
|
|
$a = false;
|
|
|
|
if (rand(0, 1)) {
|
|
while (rand(0, 1)) {
|
|
$a = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($a) {}',
|
|
],
|
|
'varCheckAfterNestedAssignmentAndBreakInIf' => [
|
|
'<?php
|
|
$a = false;
|
|
|
|
if (rand(0, 1)) {
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($a) {}',
|
|
],
|
|
'breakInSwitchStatementIf' => [
|
|
'<?php
|
|
$a = 0;
|
|
|
|
while (rand(0, 1)) {
|
|
switch (rand(0, 1)) {
|
|
default:
|
|
echo $a;
|
|
|
|
if (rand(0, 1)) {
|
|
$a = 5;
|
|
break;
|
|
}
|
|
}
|
|
}',
|
|
],
|
|
'breakInSwitchStatementIfWithSecondCase' => [
|
|
'<?php
|
|
$a = 0;
|
|
|
|
while (rand(0, 1)) {
|
|
switch (rand(0, 1)) {
|
|
case 0:
|
|
$a = 1;
|
|
break;
|
|
|
|
default:
|
|
echo $a;
|
|
|
|
if (rand(0, 1)) {
|
|
$a = 5;
|
|
break;
|
|
}
|
|
}
|
|
}',
|
|
],
|
|
'echoVarWithAdditionOp' => [
|
|
'<?php
|
|
$a = 5;
|
|
|
|
while (rand(0, 1)) {
|
|
echo($a += 1);
|
|
}',
|
|
],
|
|
'echoVarWithIncrement' => [
|
|
'<?php
|
|
function foo(int $i) : void {
|
|
echo $i;
|
|
}
|
|
|
|
$a = 5;
|
|
|
|
while (rand(0, 1)) {
|
|
foo(++$a);
|
|
}',
|
|
],
|
|
'afterMethodExistsCheck' => [
|
|
'<?php
|
|
class A {
|
|
/**
|
|
* @param array<string, string> $options
|
|
*/
|
|
public function __construct(array $options) {
|
|
$this->setOptions($options);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $options
|
|
*/
|
|
protected function setOptions(array $options): void
|
|
{
|
|
foreach ($options as $key => $value) {
|
|
$normalized = ucfirst($key);
|
|
$method = "set" . $normalized;
|
|
|
|
if (method_exists($this, $method)) {
|
|
$this->$method($value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
new A(["bar" => "bat"]);',
|
|
],
|
|
'instanceofVarUse' => [
|
|
'<?php
|
|
interface Foo { }
|
|
|
|
function returnFoo(): Foo {
|
|
return new class implements Foo { };
|
|
}
|
|
|
|
$interface = Foo::class;
|
|
|
|
if (returnFoo() instanceof $interface) {
|
|
exit;
|
|
}',
|
|
],
|
|
'usedVariableInDoWhile' => [
|
|
'<?php
|
|
$i = 5;
|
|
do {
|
|
echo "hello";
|
|
} while (--$i > 0);
|
|
echo $i;',
|
|
],
|
|
'callableReferencesItself' => [
|
|
'<?php
|
|
/** @psalm-suppress UnusedParam */
|
|
function foo(callable $c) : void {}
|
|
$listener = function () use (&$listener) : void {
|
|
/** @psalm-suppress MixedArgument */
|
|
foo($listener);
|
|
};
|
|
foo($listener);',
|
|
],
|
|
'newVariableConstructor' => [
|
|
'<?php
|
|
/**
|
|
* @param class-string<ArrayObject> $type
|
|
*/
|
|
function bar(string $type) : ArrayObject {
|
|
$data = [["foo"], ["bar"]];
|
|
|
|
/** @psalm-suppress UnsafeInstantiation */
|
|
return new $type($data[0]);
|
|
}',
|
|
],
|
|
'byRefVariableUsedInAddition' => [
|
|
'<?php
|
|
$i = 0;
|
|
$a = function () use (&$i) : void {
|
|
$i = 1;
|
|
};
|
|
$a();
|
|
/** @psalm-suppress MixedArgument */
|
|
echo $i;',
|
|
],
|
|
'regularVariableClosureUseInAddition' => [
|
|
'<?php
|
|
$i = 0;
|
|
$a = function () use ($i) : int {
|
|
return $i + 1;
|
|
};
|
|
$a();',
|
|
],
|
|
'superGlobalInFunction' => [
|
|
'<?php
|
|
function example1() : void {
|
|
$_SESSION = [];
|
|
}
|
|
function example2() : int {
|
|
return (int) $_SESSION["str"];
|
|
}',
|
|
],
|
|
'usedInArray' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-suppress MixedMethodCall
|
|
* @psalm-suppress MissingParamType
|
|
*/
|
|
function foo($a) : void {
|
|
$b = "b";
|
|
$a->bar([$b]);
|
|
}',
|
|
],
|
|
'paramUsedInsideLoop' => [
|
|
'<?php
|
|
function foo(int $counter) : void {
|
|
foreach ([1, 2, 3] as $_) {
|
|
echo ($counter = $counter + 1);
|
|
echo rand(0, 1) ? 1 : 0;
|
|
}
|
|
}',
|
|
],
|
|
'useParamInsideIfLoop' => [
|
|
'<?php
|
|
function foo() : void {
|
|
$a = 1;
|
|
|
|
if (rand(0, 1)) {
|
|
while (rand(0, 1)) {
|
|
$a = 2;
|
|
}
|
|
}
|
|
|
|
echo $a;
|
|
}',
|
|
],
|
|
'useVariableInsideTry' => [
|
|
'<?php
|
|
$foo = false;
|
|
|
|
try {
|
|
if (rand(0, 1)) {
|
|
throw new \Exception("bad");
|
|
}
|
|
|
|
$foo = rand(0, 1);
|
|
|
|
if ($foo) {}
|
|
} catch (Exception $e) {}
|
|
|
|
if ($foo) {}',
|
|
],
|
|
'useTryAssignedVariableInsideFinally' => [
|
|
'<?php
|
|
$var = "";
|
|
try {
|
|
if (rand(0, 1)) {
|
|
throw new \Exception();
|
|
}
|
|
$var = "hello";
|
|
} finally {
|
|
if ($var !== "") {
|
|
echo $var;
|
|
}
|
|
}',
|
|
],
|
|
'useTryAssignedVariableInFinallyWhenCatchExits' => [
|
|
'<?php
|
|
/**
|
|
* @return resource
|
|
*/
|
|
function getStream() {
|
|
throw new \Exception();
|
|
}
|
|
|
|
$stream = null;
|
|
|
|
try {
|
|
$stream = getStream();
|
|
\file_put_contents("./foobar", $stream);
|
|
} catch (\Exception $e) {
|
|
throw new \Exception("Something went wrong");
|
|
} finally {
|
|
if ($stream) {
|
|
\fclose($stream);
|
|
}
|
|
}',
|
|
],
|
|
'varUsedInloop' => [
|
|
'<?php
|
|
class A {
|
|
public static function getA() : ?A {
|
|
return rand(0, 1) ? new A : null;
|
|
}
|
|
}
|
|
|
|
function foo(?A $a) : void {
|
|
while ($a) {
|
|
echo get_class($a);
|
|
$a = A::getA();
|
|
}
|
|
}',
|
|
],
|
|
'varPassedByRef' => [
|
|
'<?php
|
|
function foo(array $returned) : array {
|
|
$ancillary = &$returned;
|
|
$ancillary["foo"] = 5;
|
|
return $returned;
|
|
}',
|
|
],
|
|
'usedAsMethodName' => [
|
|
'<?php
|
|
class A {
|
|
public static function foo() : void {}
|
|
}
|
|
|
|
function foo() : void {
|
|
$method = "foo";
|
|
A::$method();
|
|
}',
|
|
],
|
|
'usedAsStaticPropertyName' => [
|
|
'<?php
|
|
class A {
|
|
private static bool $something = false;
|
|
|
|
public function foo() : void {
|
|
$var = "something";
|
|
|
|
if (rand(0, 1)) {
|
|
static::${$var} = true;
|
|
}
|
|
}
|
|
}'
|
|
],
|
|
'setInLoopThatsAlwaysEntered' => [
|
|
'<?php
|
|
/**
|
|
* @param non-empty-array<int> $a
|
|
*/
|
|
function getLastNum(array $a): int {
|
|
foreach ($a as $num) {
|
|
$last = $num;
|
|
}
|
|
return $last;
|
|
}'
|
|
],
|
|
'usedStrtolowerInArray' => [
|
|
'<?php
|
|
/**
|
|
* @param array<string, int> $row
|
|
*/
|
|
function foo(array $row, string $s) : array {
|
|
$row["a" . strtolower($s)] += 1;
|
|
return $row;
|
|
}',
|
|
],
|
|
'pureWithReflectionMethodSetValue' => [
|
|
'<?php
|
|
function foo(object $mock) : void {
|
|
$m = new \ReflectionProperty($mock, "bar");
|
|
$m->setValue([get_class($mock) => "hello"]);
|
|
}'
|
|
],
|
|
'defineBeforeAssignmentInConditional' => [
|
|
'<?php
|
|
$i = null;
|
|
|
|
if (rand(0, 1) || ($i = rand(0, 1))) {
|
|
echo $i;
|
|
}',
|
|
],
|
|
'definedInFirstAssignmentInConditional' => [
|
|
'<?php
|
|
if (($b = rand(0, 1)) || rand(0, 1)) {
|
|
echo $b;
|
|
}',
|
|
],
|
|
'noUnusedVariableWhenUndefinedMethod' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
function foo(A $a) : void {
|
|
$i = 0;
|
|
|
|
/** @psalm-suppress UndefinedMethod */
|
|
$a->bar($i);
|
|
}',
|
|
],
|
|
'noUnusedVariableAfterRedeclaredInCatch' => [
|
|
'<?php
|
|
$path = "";
|
|
|
|
echo $path;
|
|
|
|
try {
|
|
// do nothing
|
|
} catch (\Exception $exception) {
|
|
$path = "hello";
|
|
}
|
|
|
|
echo $path;'
|
|
],
|
|
'assignedInElseif' => [
|
|
'<?php
|
|
function bar(): int {
|
|
if (rand(0, 1) === 0) {
|
|
$foo = 0;
|
|
} elseif ($foo = rand(0, 10)) {
|
|
return 5;
|
|
}
|
|
|
|
return $foo;
|
|
}',
|
|
],
|
|
'refineForeachVarType' => [
|
|
'<?php
|
|
function foo() : array {
|
|
return ["hello"];
|
|
}
|
|
|
|
/** @var string $s */
|
|
foreach (foo() as $s) {
|
|
echo $s;
|
|
}',
|
|
],
|
|
'doWhileReassignedInConditional' => [
|
|
'<?php
|
|
$index = 0;
|
|
|
|
do {
|
|
echo $index;
|
|
} while (($index = $index + 1) < 10);'
|
|
],
|
|
'tryCatchInsaneRepro' => [
|
|
'<?php
|
|
function maybeThrows() : string {
|
|
return "hello";
|
|
}
|
|
|
|
function b(bool $a): void {
|
|
if (!$a) {
|
|
return;
|
|
}
|
|
|
|
$b = "";
|
|
|
|
try {
|
|
$b = maybeThrows();
|
|
echo $b;
|
|
} catch (\Exception $e) {}
|
|
|
|
echo $b;
|
|
}'
|
|
],
|
|
'tryCatchInsaneReproNoFirstBoolCheck' => [
|
|
'<?php
|
|
function maybeThrows() : string {
|
|
return "hello";
|
|
}
|
|
|
|
function b(): void {
|
|
$b = "";
|
|
|
|
try {
|
|
$b = maybeThrows();
|
|
echo $b;
|
|
} catch (\Exception $e) {}
|
|
|
|
echo $b;
|
|
}'
|
|
],
|
|
'tryWithWhile' => [
|
|
'<?php
|
|
function foo(): void {
|
|
$done = false;
|
|
|
|
while (!$done) {
|
|
try {
|
|
$done = true;
|
|
} catch (\Exception $e) {
|
|
}
|
|
}
|
|
}',
|
|
],
|
|
'tryWithWhileWithoutTry' => [
|
|
'<?php
|
|
function foo(): void {
|
|
$done = false;
|
|
|
|
while (!$done) {
|
|
$done = true;
|
|
}
|
|
}',
|
|
],
|
|
'usedInCatchAndTryWithReturnInTry' => [
|
|
'<?php
|
|
function foo() : ?string {
|
|
$a = null;
|
|
|
|
try {
|
|
$a = "hello";
|
|
echo $a;
|
|
} catch (Exception $e) {
|
|
return $a;
|
|
}
|
|
|
|
return $a;
|
|
}
|
|
|
|
function dangerous() : string {
|
|
if (rand(0, 1)) {
|
|
throw new \Exception("bad");
|
|
}
|
|
return "hello";
|
|
}',
|
|
],
|
|
'useTryAndCatchAssignedVariableInsideFinally' => [
|
|
'<?php
|
|
function foo() : void {
|
|
try {
|
|
// do something dangerous
|
|
$a = 5;
|
|
} catch (Exception $e) {
|
|
$a = 4;
|
|
throw new Exception("bad");
|
|
} finally {
|
|
/** @psalm-suppress PossiblyUndefinedVariable */
|
|
echo $a;
|
|
}
|
|
}'
|
|
],
|
|
'usedVarInCatchAndAfter' => [
|
|
'<?php
|
|
function foo() : void {
|
|
if (rand(0, 1)) {
|
|
throw new \Exception("bad");
|
|
}
|
|
}
|
|
|
|
$a = null;
|
|
|
|
try {
|
|
foo();
|
|
$a = "hello";
|
|
} catch (\Exception $e) {
|
|
echo $a;
|
|
}
|
|
|
|
echo $a;'
|
|
],
|
|
'unusedForeach' => [
|
|
'<?php
|
|
/**
|
|
* @param array<int, string> $test
|
|
*/
|
|
function foo(array $test) : void {
|
|
foreach($test as $key => $_testValue) {
|
|
echo $key;
|
|
}
|
|
}'
|
|
],
|
|
'usedAfterMixedVariableAssignment' => [
|
|
'<?php
|
|
function foo(array $arr): array {
|
|
$c = "c";
|
|
/** @psalm-suppress MixedArrayAssignment */
|
|
$arr["a"]["b"][$c] = 1;
|
|
return $arr;
|
|
}',
|
|
],
|
|
'binaryOpIncrementInElse' => [
|
|
'<?php
|
|
function foo(int $i, string $alias) : void {
|
|
echo $alias ?: $i++;
|
|
echo $i;
|
|
}'
|
|
],
|
|
'binaryOpIncrementInCond' => [
|
|
'<?php
|
|
function foo(int $i, string $alias) : void {
|
|
echo $i++ ?: $alias;
|
|
echo $i;
|
|
}'
|
|
],
|
|
'binaryOpIncrementInIf' => [
|
|
'<?php
|
|
function foo(int $i, string $alias) : void {
|
|
echo rand(0, 1) ? $i++ : $alias;
|
|
echo $i;
|
|
}'
|
|
],
|
|
'usedInNewCall' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-suppress MixedMethodCall
|
|
* @psalm-suppress MixedArgument
|
|
* @psalm-suppress PossiblyNullArgument
|
|
* @param mixed $mixed
|
|
* @param mixed|null $mixed_or_null
|
|
*/
|
|
function foo($mixed, $mixed_or_null): void {
|
|
$mixed->foo(new Exception($mixed_or_null));
|
|
}',
|
|
],
|
|
'validMixedAnnotation' => [
|
|
'<?php
|
|
function keys(): array {
|
|
return ["foo", "bar"];
|
|
}
|
|
|
|
/** @var mixed $k */
|
|
foreach (keys() as $k) {
|
|
echo gettype($k);
|
|
}'
|
|
],
|
|
'byRefVariableAfterAssignmentToArray' => [
|
|
'<?php
|
|
$a = [1, 2, 3];
|
|
$b = &$a[1];
|
|
$b = 5;
|
|
print_r($a);'
|
|
],
|
|
'byRefVariableAfterAssignmentToProperty' => [
|
|
'<?php
|
|
class A {
|
|
public string $value = "";
|
|
public function writeByRef(string $value): void {
|
|
$update =& $this->value;
|
|
$update = $value;
|
|
}
|
|
}'
|
|
],
|
|
'createdAndUsedInCondition' => [
|
|
'<?php
|
|
class A {
|
|
public function foo() : bool {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function getA() : ?A {
|
|
return rand(0, 1) ? new A() : null;
|
|
}
|
|
|
|
if (rand(0, 1)) {
|
|
if (!($a = getA()) || $a->foo()) {}
|
|
return;
|
|
}
|
|
|
|
if (!($a = getA()) || $a->foo()) {}'
|
|
],
|
|
'usedInUndefinedFunction' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-suppress MixedInferredReturnType
|
|
* @psalm-suppress MixedReturnStatement
|
|
*/
|
|
function test(): string {
|
|
$s = "a";
|
|
/** @psalm-suppress UndefinedFunction */
|
|
return undefined_function($s);
|
|
}'
|
|
],
|
|
'useVariableVariable' => [
|
|
'<?php
|
|
$variables = ["a" => "b", "c" => "d"];
|
|
|
|
foreach ($variables as $name => $value) {
|
|
${$name} = $value;
|
|
}'
|
|
],
|
|
'usedLoopVariable' => [
|
|
'<?php
|
|
$a = 0;
|
|
while (rand(0, 1)) {
|
|
if ($a < 20) {
|
|
$a = $a + 1;
|
|
echo "hello";
|
|
continue;
|
|
}
|
|
echo "goodbye";
|
|
break;
|
|
}'
|
|
],
|
|
'usedForVariable' => [
|
|
'<?php
|
|
$a = 0;
|
|
for ($i = 0; $i < 1000; $i++) {
|
|
if (rand(0, 1)) {
|
|
$a = $a + 1;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
echo $a;'
|
|
],
|
|
'usedForVariableMinusString' => [
|
|
'<?php
|
|
function foo(string $limit) : void {
|
|
/**
|
|
* @psalm-suppress InvalidOperand
|
|
*/
|
|
for ($i = $limit; $i > 0; $i--) {
|
|
echo $i . "\n";
|
|
}
|
|
}'
|
|
],
|
|
'usedForVariablePlusString' => [
|
|
'<?php
|
|
function foo(string $limit) : void {
|
|
/**
|
|
* @psalm-suppress InvalidOperand
|
|
*/
|
|
for ($i = $limit; $i < 50; $i++) {
|
|
echo $i . "\n";
|
|
}
|
|
}'
|
|
],
|
|
'breakInForeachInsideSwitch' => [
|
|
'<?php
|
|
function foo(string $b) : void {
|
|
switch ($b){
|
|
case "foo":
|
|
$a = null;
|
|
foreach ([1,2,3] as $f){
|
|
if ($f == 2) {
|
|
$a = $f;
|
|
break;
|
|
}
|
|
}
|
|
echo $a;
|
|
}
|
|
}'
|
|
],
|
|
'passedByRefSimpleUndefinedBefore' => [
|
|
'<?php
|
|
takes_ref($a);
|
|
|
|
function takes_ref(?array &$p): void {
|
|
$p = [0];
|
|
}'
|
|
],
|
|
'passedByRefSimpleDefinedBefore' => [
|
|
'<?php
|
|
$a = [];
|
|
takes_ref($a);
|
|
|
|
function takes_ref(?array &$p): void {
|
|
$p = [0];
|
|
}'
|
|
],
|
|
'passedByRefSimpleDefinedBeforeWithExtract' => [
|
|
'<?php
|
|
function foo(array $arr) : void {
|
|
while (rand(0, 1)) {
|
|
/** @psalm-suppress MixedArgument */
|
|
extract($arr);
|
|
$a = [];
|
|
takes_ref($a);
|
|
}
|
|
}
|
|
|
|
/** @param mixed $p */
|
|
function takes_ref(&$p): void {}'
|
|
],
|
|
'passedByRefArrayOffset' => [
|
|
'<?php
|
|
$a = [
|
|
"a" => [1],
|
|
"b" => [2]
|
|
];
|
|
|
|
foreach (["a"] as $e){
|
|
takes_ref($a[$e]);
|
|
}
|
|
|
|
/** @param array<string|int> $p */
|
|
function takes_ref(array &$p): void {
|
|
echo implode(",", $p);
|
|
}'
|
|
],
|
|
'doWhileWithBreak' => [
|
|
'<?php
|
|
function foo(): void {
|
|
$f = false;
|
|
|
|
do {
|
|
if (rand(0,1)) {
|
|
$f = true;
|
|
break;
|
|
}
|
|
} while (rand(0,1));
|
|
|
|
if ($f) {}
|
|
}'
|
|
],
|
|
'usedParamInWhileAddition' => [
|
|
'<?php
|
|
function foo(int $index): void {
|
|
while ($index++ <= 100) {
|
|
//
|
|
}
|
|
}'
|
|
],
|
|
'usedParamInWhileDirectly' => [
|
|
'<?php
|
|
function foo(int $index): void {
|
|
while (100 >= $index = nextNumber($index)) {
|
|
// ...
|
|
}
|
|
}
|
|
|
|
function nextNumber(int $eee): int {
|
|
return $eee + 1;
|
|
}'
|
|
],
|
|
'usedParamInWhileIndirectly' => [
|
|
'<?php
|
|
function foo(int $i): void {
|
|
$index = $i;
|
|
while (100 >= $index = nextNumber($index)) {
|
|
// ...
|
|
}
|
|
}
|
|
|
|
function nextNumber(int $i): int {
|
|
return $i + 1;
|
|
}'
|
|
],
|
|
'doArrayIncrement' => [
|
|
'<?php
|
|
/**
|
|
* @param list<int> $keys
|
|
* @param int $key
|
|
*/
|
|
function error2(array $keys, int $key): int
|
|
{
|
|
if ($key === 1) {}
|
|
|
|
do {
|
|
$nextKey = $keys[++$key] ?? null;
|
|
} while ($nextKey === null);
|
|
|
|
return $nextKey;
|
|
}'
|
|
],
|
|
'variableUsedIndirectly' => [
|
|
'<?php
|
|
$a = 0;
|
|
|
|
while (rand(0,1)){
|
|
$b = $a + 1;
|
|
echo $b;
|
|
$a = $b;
|
|
}',
|
|
],
|
|
'arrayMapClosureWithParamType' => [
|
|
'<?php
|
|
$a = [1, 2, 3];
|
|
|
|
$b = array_map(
|
|
function(int $i) {
|
|
return $i * 3;
|
|
},
|
|
$a
|
|
);
|
|
|
|
foreach ($b as $c) {
|
|
echo $c;
|
|
}',
|
|
],
|
|
'arrayMapClosureWithoutParamType' => [
|
|
'<?php
|
|
$a = [1, 2, 3];
|
|
|
|
$b = array_map(
|
|
function($i) {
|
|
return $i * 3;
|
|
},
|
|
$a
|
|
);
|
|
|
|
foreach ($b as $c) {
|
|
echo $c;
|
|
}',
|
|
],
|
|
'unusedArrayAdditionWithArrayChecked' => [
|
|
'<?php
|
|
$a = [];
|
|
|
|
while (rand(0,1)) {
|
|
$a[] = 1;
|
|
}
|
|
|
|
if ($a) {}',
|
|
],
|
|
'usedArrayRecursiveAddition' => [
|
|
'<?php
|
|
$a = [];
|
|
|
|
while (rand(0,1)) {
|
|
$a[] = $a;
|
|
}
|
|
|
|
print_r($a);',
|
|
],
|
|
'usedImmutableProperty' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-immutable
|
|
*/
|
|
class Clause {
|
|
/**
|
|
* @var array<int, int>
|
|
*/
|
|
public $b = [];
|
|
}
|
|
|
|
function foo(Clause $c, int $var): void {
|
|
$new_b = $c->b;
|
|
|
|
if (isset($c->b[0])) {
|
|
$new_b[$var] = 0;
|
|
}
|
|
|
|
if ($new_b) {}
|
|
}',
|
|
],
|
|
'arrayAssignOpAdditionInsideLoop' => [
|
|
'<?php
|
|
/**
|
|
* @param array<string, string> $arr0
|
|
* @param array<string, string> $arr1
|
|
* @param array<string, string> $arr2
|
|
* @return void
|
|
*/
|
|
function parp(array $arr0, array $arr1, array $arr2) {
|
|
$arr3 = $arr0;
|
|
|
|
foreach ($arr1 as $a) {
|
|
echo $a;
|
|
$arr3 += $arr2;
|
|
}
|
|
|
|
if ($arr3) {}
|
|
}',
|
|
],
|
|
'arrayAdditionInsideLoop' => [
|
|
'<?php
|
|
/**
|
|
* @param array<string, string> $arr0
|
|
* @param array<string, string> $arr1
|
|
* @param array<string, string> $arr2
|
|
* @return void
|
|
*/
|
|
function parp(array $arr0, array $arr1, array $arr2) {
|
|
$arr3 = $arr0;
|
|
|
|
foreach ($arr1 as $a) {
|
|
echo $a;
|
|
$arr3 = $arr3 + $arr2;
|
|
}
|
|
|
|
if ($arr3) {}
|
|
}',
|
|
],
|
|
'checkValueBeforeAdding' => [
|
|
'<?php
|
|
class T {
|
|
public bool $b = false;
|
|
}
|
|
|
|
function foo(
|
|
?T $t
|
|
): void {
|
|
if (!$t) {
|
|
$t = new T();
|
|
} elseif (rand(0, 1)) {
|
|
//
|
|
}
|
|
|
|
if ($t->b) {}
|
|
}'
|
|
],
|
|
'loopOverUnknown' => [
|
|
'<?php
|
|
/** @psalm-suppress MixedAssignment */
|
|
function foo(Traversable $t) : void {
|
|
foreach ($t as $u) {
|
|
if ($u instanceof stdClass) {}
|
|
}
|
|
}'
|
|
],
|
|
'loopWithRequire' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-suppress UnresolvableInclude
|
|
*/
|
|
function foo(string $delta_file) : void {
|
|
while (rand(0, 1)) {
|
|
/**
|
|
* @var array<string, mixed>
|
|
*/
|
|
$diff_call_map = require($delta_file);
|
|
|
|
foreach ($diff_call_map as $key => $_) {
|
|
$cased_key = strtolower($key);
|
|
echo $cased_key;
|
|
}
|
|
}
|
|
}',
|
|
],
|
|
'loopAgain' => [
|
|
'<?php
|
|
/** @param non-empty-list<string> $lines */
|
|
function parse(array $lines) : array {
|
|
$last = 0;
|
|
foreach ($lines as $k => $line) {
|
|
if (rand(0, 1)) {
|
|
$last = $k;
|
|
} elseif (rand(0, 1)) {
|
|
$last = 0;
|
|
} elseif ($last !== 0) {
|
|
$lines[$last] .= $line;
|
|
}
|
|
}
|
|
|
|
return $lines;
|
|
}'
|
|
],
|
|
'necessaryVarAnnotation' => [
|
|
'<?php
|
|
function foo(array $arr) : void {
|
|
/** @var int $key */
|
|
foreach ($arr as $key => $_) {
|
|
echo $key;
|
|
}
|
|
}'
|
|
],
|
|
'continuingEducation' => [
|
|
'<?php
|
|
function breakUpPathIntoParts(): void {
|
|
$b = false;
|
|
|
|
while (rand(0, 1)) {
|
|
if ($b) {
|
|
if (rand(0, 1)) {
|
|
$b = 0;
|
|
}
|
|
|
|
echo "hello";
|
|
|
|
continue;
|
|
}
|
|
|
|
$b = true;
|
|
}
|
|
}'
|
|
],
|
|
'usedInBinaryOp' => [
|
|
'<?php
|
|
function foo(int $a, int $b) : int {
|
|
$a |= $b;
|
|
return $a;
|
|
}'
|
|
],
|
|
'reassignedInFinally' => [
|
|
'<?php
|
|
function getRows(int $s) : void {
|
|
try {}
|
|
finally {
|
|
$s = $s + 3;
|
|
}
|
|
|
|
echo $s;
|
|
}'
|
|
],
|
|
'divAssignOp' => [
|
|
'<?php
|
|
function hslToRgb(float $hue): float {
|
|
$hue /= 360;
|
|
|
|
return $hue;
|
|
}'
|
|
],
|
|
'concatAssignOp' => [
|
|
'<?php
|
|
function hslToRgb(string $hue): string {
|
|
$hue .= "hello";
|
|
|
|
return $hue;
|
|
}'
|
|
],
|
|
'possiblyUndefinedVariableUsed' => [
|
|
'<?php
|
|
function foo(string $a): void {
|
|
if ($a === "a") {
|
|
$hue = "hello";
|
|
} elseif ($a === "b") {
|
|
$hue = "goodbye";
|
|
}
|
|
|
|
/**
|
|
* @psalm-suppress PossiblyUndefinedVariable
|
|
* @psalm-suppress MixedArgument
|
|
*/
|
|
echo $hue;
|
|
}'
|
|
],
|
|
'possiblyUndefinedVariableUsedInUnknownMethod' => [
|
|
'<?php
|
|
function foo(string $a, object $b): void {
|
|
if ($a === "a") {
|
|
$hue = "hello";
|
|
} elseif ($a === "b") {
|
|
$hue = "goodbye";
|
|
}
|
|
|
|
/**
|
|
* @psalm-suppress PossiblyUndefinedVariable
|
|
* @psalm-suppress MixedMethodCall
|
|
*/
|
|
$b->foo($hue);
|
|
}'
|
|
],
|
|
'usedAsArrayKey' => [
|
|
'<?php
|
|
function hslToRgb(string $hue, string $lightness): array {
|
|
$arr = [$hue => $lightness];
|
|
return $arr;
|
|
}'
|
|
],
|
|
'assignToGlobalVar' => [
|
|
'<?php
|
|
/** @psalm-suppress MixedAssignment */
|
|
function foo(array $args) : void {
|
|
foreach ($args as $key => $value) {
|
|
$_GET[$key] = $value;
|
|
}
|
|
}'
|
|
],
|
|
'assignToArrayTwice' => [
|
|
'<?php
|
|
function foo(string $c): void {
|
|
$arr = [$c];
|
|
$arr[] = 1;
|
|
|
|
foreach ($arr as $e) {
|
|
echo $e;
|
|
}
|
|
}'
|
|
],
|
|
'classPropertyThing' => [
|
|
'<?php
|
|
function foo(): string {
|
|
$notice = "i";
|
|
$notice .= "j";
|
|
$notice .= "k";
|
|
$notice .= "l";
|
|
$notice .= "m";
|
|
$notice .= "n";
|
|
$notice .= "o";
|
|
$notice .= "p";
|
|
$notice .= "q";
|
|
$notice .= "r";
|
|
$notice .= "s";
|
|
|
|
return $notice;
|
|
}'
|
|
],
|
|
'usedInIsset' => [
|
|
'<?php
|
|
function foo(int $i): void {
|
|
if ($i === 0) {
|
|
$j = "hello";
|
|
} elseif ($i === 1) {
|
|
$j = "goodbye";
|
|
}
|
|
|
|
if (isset($j)) {
|
|
/** @psalm-suppress MixedArgument */
|
|
echo $j;
|
|
}
|
|
}'
|
|
],
|
|
'byRefNestedArrayParam' => [
|
|
'<?php
|
|
function foo(array &$arr): void {
|
|
$b = 5;
|
|
$arr[0] = $b;
|
|
}'
|
|
],
|
|
'byRefNestedArrayInForeach' => [
|
|
'<?php
|
|
function foo(array $arr): array {
|
|
/**
|
|
* @psalm-suppress MixedAssignment
|
|
* @psalm-suppress MixedArrayAssignment
|
|
*/
|
|
foreach ($arr as &$element) {
|
|
$b = 5;
|
|
$element[0] = $b;
|
|
}
|
|
|
|
return $arr;
|
|
}'
|
|
],
|
|
'instantArrayAssignment' => [
|
|
'<?php
|
|
function foo(string $b) : array {
|
|
/** @psalm-suppress PossiblyUndefinedVariable */
|
|
$arr["foo"] = $b;
|
|
|
|
return $arr;
|
|
}',
|
|
],
|
|
'explodeSource' => [
|
|
'<?php
|
|
$start = microtime();
|
|
$start = explode(" ", $start);
|
|
/**
|
|
* @psalm-suppress InvalidOperand
|
|
*/
|
|
$start = $start[1] + $start[0];
|
|
echo $start;'
|
|
],
|
|
'csvByRefForeach' => [
|
|
'<?php
|
|
function foo(string $value) : array {
|
|
$arr = str_getcsv($value);
|
|
|
|
foreach ($arr as &$element) {
|
|
$element = $element ?: "foo";
|
|
}
|
|
|
|
return $arr;
|
|
}'
|
|
],
|
|
'memoryFree' => [
|
|
'<?php
|
|
function verifyLoad(string $free) : void {
|
|
$free = explode("\n", $free);
|
|
|
|
$parts_mem = preg_split("/\s+/", $free[1]);
|
|
|
|
$free_mem = $parts_mem[3];
|
|
$total_mem = $parts_mem[1];
|
|
|
|
/** @psalm-suppress InvalidOperand */
|
|
$used_mem = ($total_mem - $free_mem) / $total_mem;
|
|
|
|
echo $used_mem;
|
|
}'
|
|
],
|
|
'returnNotBool' => [
|
|
'<?php
|
|
function verifyLoad(bool $b) : bool {
|
|
$c = !$b;
|
|
return $c;
|
|
}'
|
|
],
|
|
'sourcemaps' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-suppress MixedAssignment
|
|
* @psalm-suppress MixedArgument
|
|
* @param iterable<mixed, int> $keys
|
|
*/
|
|
function foo(iterable $keys, int $colno) : void {
|
|
$i = 0;
|
|
$key = 0;
|
|
$index = 0;
|
|
|
|
foreach ($keys as $index => $key) {
|
|
if ($key === $colno) {
|
|
$i = $index;
|
|
break;
|
|
} elseif ($key > $colno) {
|
|
$i = $index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
echo $i;
|
|
echo $index;
|
|
echo $key;
|
|
}'
|
|
],
|
|
'whileLoopVarUpdatedInWhileLoop' => [
|
|
'<?php
|
|
/** @param non-empty-list<int> $arr */
|
|
function foo(array $arr) : void {
|
|
while ($a = array_pop($arr)) {
|
|
if ($a === 4) {
|
|
$arr = array_merge($arr, ["a", "b", "c"]);
|
|
continue;
|
|
}
|
|
|
|
echo "here";
|
|
}
|
|
}'
|
|
],
|
|
'usedThroughParamByRef' => [
|
|
'<?php
|
|
$arr = [];
|
|
|
|
$populator = function(array &$arr): void {
|
|
$arr[] = 5;
|
|
};
|
|
|
|
$populator($arr);
|
|
|
|
print_r($arr);'
|
|
],
|
|
'maybeUndefinedCheckedWithEmpty' => [
|
|
'<?php
|
|
function foo(array $arr) : void {
|
|
if (rand(0, 1)) {
|
|
$maybe_undefined = $arr;
|
|
}
|
|
|
|
if (empty($maybe_undefined)) {
|
|
$maybe_undefined = [0];
|
|
}
|
|
|
|
print_r($maybe_undefined);
|
|
}'
|
|
],
|
|
'maybeUndefinedCheckedWithEmptyOrRand' => [
|
|
'<?php
|
|
function foo(array $arr) : void {
|
|
if (rand(0, 1)) {
|
|
$maybe_undefined = $arr;
|
|
}
|
|
|
|
if (empty($maybe_undefined) || rand(0, 1)) {
|
|
$maybe_undefined = [0];
|
|
}
|
|
|
|
print_r($maybe_undefined);
|
|
}'
|
|
],
|
|
'maybeUndefinedCheckedWithNotIsset' => [
|
|
'<?php
|
|
function foo(array $arr) : void {
|
|
if (rand(0, 1)) {
|
|
$maybe_undefined = $arr;
|
|
}
|
|
|
|
if (!isset($maybe_undefined)) {
|
|
$maybe_undefined = [0];
|
|
}
|
|
|
|
print_r($maybe_undefined);
|
|
}'
|
|
],
|
|
'maybeUndefinedCheckedWithImplicitIsset' => [
|
|
'<?php
|
|
function foo(array $arr) : void {
|
|
if (rand(0, 1)) {
|
|
$maybe_undefined = $arr;
|
|
}
|
|
|
|
/** @psalm-suppress MixedAssignment */
|
|
$maybe_undefined = $maybe_undefined ?? [0];
|
|
|
|
print_r($maybe_undefined);
|
|
}'
|
|
],
|
|
'usedInGlobalAfterAssignOp' => [
|
|
'<?php
|
|
$total = 0;
|
|
$foo = &$total;
|
|
|
|
$total = 5;
|
|
|
|
echo $foo;'
|
|
],
|
|
'takesByRefThing' => [
|
|
'<?php
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$c = 5;
|
|
}
|
|
|
|
takesByRef($c);
|
|
echo $c;
|
|
}
|
|
|
|
/**
|
|
* @psalm-param-out int $c
|
|
*/
|
|
function takesByRef(?int &$c) : void {
|
|
$c = 7;
|
|
}'
|
|
],
|
|
'clips' => [
|
|
'<?php declare(strict_types=1);
|
|
function foo(array $clips) : void {
|
|
/** @psalm-suppress MixedAssignment */
|
|
foreach ($clips as &$clip) {
|
|
/** @psalm-suppress MixedArgument */
|
|
if (!empty($clip)) {
|
|
$legs = explode("/", $clip);
|
|
$clip_id = $clip = $legs[1];
|
|
|
|
if ((is_numeric($clip_id) || $clip = (new \Exception($clip_id)))) {}
|
|
|
|
print_r($clips);
|
|
}
|
|
}
|
|
}'
|
|
],
|
|
'validator' => [
|
|
'<?php
|
|
/**
|
|
* @param bool $b
|
|
*/
|
|
function validate($b, string $source) : void {
|
|
/**
|
|
* @psalm-suppress DocblockTypeContradiction
|
|
* @psalm-suppress MixedAssignment
|
|
*/
|
|
if (!is_bool($b)) {
|
|
$source = $b;
|
|
}
|
|
|
|
print_r($source);
|
|
}'
|
|
],
|
|
'implicitSpread' => [
|
|
'<?php
|
|
function validate(bool $b, bool $c) : void {
|
|
$d = [$b, $c];
|
|
print_r(...$d);
|
|
}'
|
|
],
|
|
'explicitSpread' => [
|
|
'<?php
|
|
function f(): array {
|
|
$s = [1, 2, 3];
|
|
$b = ["a", "b", "c"];
|
|
|
|
$r = [...$s, ...$b];
|
|
return $r;
|
|
}'
|
|
],
|
|
'funcGetArgs' => [
|
|
'<?php
|
|
function validate(bool $b, bool $c) : void {
|
|
/** @psalm-suppress MixedArgument */
|
|
print_r(...func_get_args());
|
|
}'
|
|
],
|
|
'nullCoalesce' => [
|
|
'<?php
|
|
function foo (?bool $b, int $c): void {
|
|
$b ??= $c;
|
|
|
|
echo $b;
|
|
}'
|
|
],
|
|
'arrowFunctionImplicitlyUsedVar' => [
|
|
'<?php
|
|
function test(Exception $e): callable {
|
|
return fn() => $e->getMessage();
|
|
}'
|
|
],
|
|
'useImmutableGetIteratorInForeach' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-immutable
|
|
*/
|
|
class A implements IteratorAggregate
|
|
{
|
|
/**
|
|
* @return Iterator<int>
|
|
*/
|
|
public function getIterator() {
|
|
yield from [1, 2, 3];
|
|
}
|
|
}
|
|
|
|
$a = new A();
|
|
|
|
foreach ($a as $v) {
|
|
echo $v;
|
|
}'
|
|
],
|
|
'castToBoolAndDouble' => [
|
|
'<?php
|
|
function string_to_bool(string $a): bool {
|
|
$b = (bool)$a;
|
|
return $b;
|
|
}
|
|
|
|
function string_to_float(string $a): float {
|
|
$b = (float)$a;
|
|
return $b;
|
|
}'
|
|
],
|
|
'allowUseByRef' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-suppress MixedReturnStatement
|
|
* @psalm-suppress MixedInferredReturnType
|
|
*/
|
|
function foo(array $data) : array {
|
|
$output = [];
|
|
|
|
array_map(
|
|
function (array $row) use (&$output) {
|
|
$output = $row;
|
|
},
|
|
$data
|
|
);
|
|
|
|
return $output;
|
|
}'
|
|
],
|
|
'allowedUseByRefArrayAssignment' => [
|
|
'<?php
|
|
$output_rows = [];
|
|
|
|
$a = function() use (&$output_rows) : void {
|
|
$output_row = 5;
|
|
/** @psalm-suppress MixedArrayAssignment */
|
|
$output_rows[] = $output_row;
|
|
};
|
|
$a();
|
|
|
|
print_r($output_rows);'
|
|
],
|
|
'usedInAssignOpToByRef' => [
|
|
'<?php
|
|
function foo(int &$d): void {
|
|
$l = 4;
|
|
$d += $l;
|
|
}',
|
|
],
|
|
'mixedArrayAccessMighBeObject' => [
|
|
'<?php
|
|
function takesResults(array $arr) : void {
|
|
/**
|
|
* @psalm-suppress MixedAssignment
|
|
*/
|
|
foreach ($arr as $item) {
|
|
/**
|
|
* @psalm-suppress MixedArrayAccess
|
|
* @psalm-suppress MixedArrayAssignment
|
|
*/
|
|
$item[0] = $item[1];
|
|
}
|
|
}'
|
|
],
|
|
'usedThrow' => [
|
|
'<?php
|
|
function f(Exception $e): void {
|
|
throw $e;
|
|
}
|
|
',
|
|
],
|
|
'usedThrowInReturnedCallable' => [
|
|
'<?php
|
|
function createFailingFunction(RuntimeException $exception): Closure
|
|
{
|
|
return static function () use ($exception): void {
|
|
throw $exception;
|
|
};
|
|
}
|
|
',
|
|
],
|
|
'usedInIntCastInAssignment' => [
|
|
'<?php
|
|
/** @return mixed */
|
|
function f() {
|
|
$a = random_int(0, 10) >= 5 ? true : false;
|
|
|
|
$b = (int) $a;
|
|
|
|
return $b;
|
|
}
|
|
'
|
|
],
|
|
'promotedPropertiesAreNeverMarkedAsUnusedParams' => [
|
|
'<?php
|
|
class Container {
|
|
private function __construct(
|
|
public float $value
|
|
) {}
|
|
|
|
public static function fromValue(float $value): self {
|
|
return new self($value);
|
|
}
|
|
}'
|
|
],
|
|
'noUnusedVariableDefinedInBranchOfIf' => [
|
|
'<?php
|
|
abstract class Foo {
|
|
abstract function validate(): bool|string;
|
|
abstract function save(): bool|string;
|
|
|
|
function bar(): int {
|
|
if (($result = $this->validate()) && ($result = $this->save())) {
|
|
return 0;
|
|
} elseif (is_string($result)) {
|
|
return 1;
|
|
} else {
|
|
return 2;
|
|
}
|
|
}
|
|
}'
|
|
],
|
|
'concatWithUnknownProperty' => [
|
|
'<?php
|
|
/** @param array<string> $key */
|
|
function foo(object $a, string $k) : string {
|
|
$sortA = "";
|
|
|
|
/** @psalm-suppress MixedOperand */
|
|
$sortA .= $a->$k;
|
|
|
|
return $sortA;
|
|
}'
|
|
],
|
|
'varDocblockVariableIsUsedByRef' => [
|
|
'<?php
|
|
/** @param array<string|int> $arr */
|
|
function foo(array $arr) : string {
|
|
/** @var string $val */
|
|
foreach ($arr as &$val) {
|
|
$val = urlencode($val);
|
|
}
|
|
return implode("/", $arr);
|
|
}'
|
|
],
|
|
'initVariableInOffset' => [
|
|
'<?php
|
|
$a = [
|
|
$b = "b" => $b,
|
|
];
|
|
|
|
foreach ($a as $key => $value) {
|
|
echo $key . " " . $value;
|
|
}',
|
|
],
|
|
'intAndBitwiseNotOperator' => [
|
|
'<?php
|
|
function foo() : int
|
|
{
|
|
$bitmask = 0x1;
|
|
$bytes = 2;
|
|
$ret = $bytes | ~$bitmask;
|
|
return $ret;
|
|
}'
|
|
],
|
|
'stringAndBitwiseAndOperator' => [
|
|
'<?php
|
|
function randomBits() : string
|
|
{
|
|
$bitmask = \chr(0xFF >> 1);
|
|
|
|
$randomBytes = random_bytes(1);
|
|
$randomBytes[0] = $randomBytes[0] & $bitmask;
|
|
|
|
return $randomBytes;
|
|
}'
|
|
],
|
|
'globalChangeValue' => [
|
|
'<?php
|
|
function setProxySettingsFromEnv(): void {
|
|
global $a;
|
|
|
|
$a = false;
|
|
}'
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string,array{string,error_message:string}>
|
|
*/
|
|
public function providerInvalidCodeParse(): array
|
|
{
|
|
return [
|
|
'simpleUnusedVariable' => [
|
|
'<?php
|
|
$a = 5;
|
|
$b = [];
|
|
echo $a;',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'unusedVarWithAdditionOp' => [
|
|
'<?php
|
|
$a = 5;
|
|
$a += 1;',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'unusedVarWithConditionalAdditionOp' => [
|
|
'<?php
|
|
$a = 5;
|
|
if (rand(0, 1)) {
|
|
$a += 1;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'unusedVarWithConditionalAddition' => [
|
|
'<?php
|
|
$a = 5;
|
|
if (rand(0, 1)) {
|
|
$a = $a + 1;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'unusedVarWithIncrement' => [
|
|
'<?php
|
|
$a = 5;
|
|
$a++;',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'unusedVarWithConditionalIncrement' => [
|
|
'<?php
|
|
$a = 5;
|
|
if (rand(0, 1)) {
|
|
$a++;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'ifInBothBranchesWithoutReference' => [
|
|
'<?php
|
|
$a = 5;
|
|
if (rand(0, 1)) {
|
|
$b = "hello";
|
|
} else {
|
|
$b = "goodbye";
|
|
}
|
|
echo $a;',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'varInNestedAssignmentWithoutReference' => [
|
|
'<?php
|
|
if (rand(0, 1)) {
|
|
$a = "foo";
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'varInSecondNestedAssignmentWithoutReference' => [
|
|
'<?php
|
|
if (rand(0, 1)) {
|
|
$a = "foo";
|
|
echo $a;
|
|
}
|
|
|
|
if (rand(0, 1)) {
|
|
$a = "foo";
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'varReassignedInBothBranchesOfIf' => [
|
|
'<?php
|
|
$a = "foo";
|
|
|
|
if (rand(0, 1)) {
|
|
$a = "bar";
|
|
} else {
|
|
$a = "bat";
|
|
}
|
|
|
|
echo $a;',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'varReassignedInNestedBranchesOfIf' => [
|
|
'<?php
|
|
$a = "foo";
|
|
|
|
if (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = "bar";
|
|
} else {
|
|
$a = "bat";
|
|
}
|
|
} else {
|
|
$a = "bang";
|
|
}
|
|
|
|
echo $a;',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'ifVarReassignedInBranchWithNoUse' => [
|
|
'<?php
|
|
$a = true;
|
|
|
|
if (rand(0, 1)) {
|
|
$a = false;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'elseVarReassignedInBranchAndNoReference' => [
|
|
'<?php
|
|
$a = true;
|
|
|
|
if (rand(0, 1)) {
|
|
// do nothing
|
|
} else {
|
|
$a = false;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'switchVarReassignedInBranch' => [
|
|
'<?php
|
|
$a = false;
|
|
|
|
switch (rand(0, 2)) {
|
|
case 0:
|
|
$a = true;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'switchVarReassignedInBranchWithDefault' => [
|
|
'<?php
|
|
$a = false;
|
|
|
|
switch (rand(0, 2)) {
|
|
case 0:
|
|
$a = true;
|
|
break;
|
|
|
|
default:
|
|
$a = false;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'switchVarReassignedInAllBranches' => [
|
|
'<?php
|
|
$a = false;
|
|
|
|
switch (rand(0, 2)) {
|
|
case 0:
|
|
$a = true;
|
|
break;
|
|
|
|
default:
|
|
$a = false;
|
|
}
|
|
|
|
if ($a) {
|
|
echo "cool";
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'unusedListVar' => [
|
|
'<?php
|
|
list($a, $b) = explode(" ", "hello world");
|
|
echo $a;',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'unusedPreForVar' => [
|
|
'<?php
|
|
$i = 0;
|
|
|
|
for ($i = 0; $i < 10; $i++) {
|
|
echo $i;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'unusedIfInReturnBlock' => [
|
|
'<?php
|
|
$i = rand(0, 1);
|
|
|
|
foreach ([1, 2, 3] as $a) {
|
|
if ($a % 2) {
|
|
$i = 7;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ($i) {}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'unusedIfVarInBranch' => [
|
|
'<?php
|
|
if (rand(0, 1)) {
|
|
|
|
} elseif (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = "foo";
|
|
} else {
|
|
$a = "bar";
|
|
echo $a;
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'throwWithMessageCallAndAssignmentAndNoReference' => [
|
|
'<?php
|
|
function dangerous(): string {
|
|
if (rand(0, 1)) {
|
|
throw new \Exception("bad");
|
|
}
|
|
|
|
return "hello";
|
|
}
|
|
|
|
function callDangerous(): void {
|
|
$s = null;
|
|
|
|
try {
|
|
$s = dangerous();
|
|
} catch (Exception $e) {
|
|
echo $e->getMessage();
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'throwWithMessageCallAndAssignmentInCatchAndNoReference' => [
|
|
'<?php
|
|
function dangerous(): string {
|
|
if (rand(0, 1)) {
|
|
throw new \Exception("bad");
|
|
}
|
|
|
|
return "hello";
|
|
}
|
|
|
|
function callDangerous(): void {
|
|
$s = null;
|
|
|
|
try {
|
|
dangerous();
|
|
} catch (Exception $e) {
|
|
echo $e->getMessage();
|
|
$s = "hello";
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'throwWithMessageCallAndNestedAssignmentInTryAndCatchAndNoReference' => [
|
|
'<?php
|
|
function dangerous(): string {
|
|
if (rand(0, 1)) {
|
|
throw new \Exception("bad");
|
|
}
|
|
|
|
return "hello";
|
|
}
|
|
|
|
function callDangerous(): void {
|
|
$s = null;
|
|
|
|
if (rand(0, 1)) {
|
|
$s = "hello";
|
|
} else {
|
|
try {
|
|
$t = dangerous();
|
|
} catch (Exception $e) {
|
|
echo $e->getMessage();
|
|
$t = "hello";
|
|
}
|
|
|
|
if ($t) {
|
|
$s = $t;
|
|
}
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'throwWithReturnInOneCatchAndNoReference' => [
|
|
'<?php
|
|
class E1 extends Exception {}
|
|
|
|
function dangerous(): void {
|
|
if (rand(0, 1)) {
|
|
throw new \Exception("bad");
|
|
}
|
|
}
|
|
|
|
function callDangerous(): void {
|
|
try {
|
|
dangerous();
|
|
$s = true;
|
|
} catch (E1 $e) {
|
|
echo $e->getMessage();
|
|
$s = false;
|
|
} catch (Exception $e) {
|
|
return;
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'loopTypeChangedInIfWithoutReference' => [
|
|
'<?php
|
|
$a = false;
|
|
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = true;
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'loopTypeChangedInIfAndContinueWithoutReference' => [
|
|
'<?php
|
|
$a = false;
|
|
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = true;
|
|
continue;
|
|
}
|
|
|
|
$a = false;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'loopReassignedInIfAndContinueWithoutReferenceAfter' => [
|
|
'<?php
|
|
$a = 5;
|
|
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = 7;
|
|
continue;
|
|
}
|
|
|
|
$a = 3;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'loopReassignedInIfAndContinueWithoutReference' => [
|
|
'<?php
|
|
$a = 3;
|
|
|
|
echo $a;
|
|
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = 5;
|
|
continue;
|
|
}
|
|
|
|
$a = 3;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'unusedConditionalCode' => [
|
|
'<?php
|
|
$a = 5;
|
|
if (rand(0, 1)) {
|
|
$a = $a + 5;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'varDefinedInIfWithoutReference' => [
|
|
'<?php
|
|
$a = 5;
|
|
if (rand(0, 1)) {
|
|
$b = "hello";
|
|
} else {
|
|
$b = "goodbye";
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'SKIPPED-byrefInForeachLoopWithoutReference' => [
|
|
'<?php
|
|
$a = [1, 2, 3];
|
|
foreach ($a as &$b) {
|
|
$b = $b + 1;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'loopSetIfNullWithBreakWithoutReference' => [
|
|
'<?php
|
|
$a = null;
|
|
|
|
while (rand(0, 1)) {
|
|
if ($a !== null) {
|
|
$a = 4;
|
|
break;
|
|
}
|
|
|
|
$a = 5;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'loopSetIfNullWithBreakWithoutReference2' => [
|
|
'<?php
|
|
$a = null;
|
|
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = 4;
|
|
break;
|
|
}
|
|
|
|
$a = 5;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'loopSetIfNullWithContinueWithoutReference' => [
|
|
'<?php
|
|
$a = null;
|
|
|
|
while (rand(0, 1)) {
|
|
if (rand(0, 1)) {
|
|
$a = 4;
|
|
continue;
|
|
}
|
|
|
|
$a = 5;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'loopAssignmentAfterReferenceWithBreak' => [
|
|
'<?php
|
|
$a = 0;
|
|
while (rand(0, 1)) {
|
|
echo $a;
|
|
$a = 1;
|
|
break;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'loopAssignmentAfterReferenceWithBreakInIf' => [
|
|
'<?php
|
|
$a = 0;
|
|
while (rand(0, 1)) {
|
|
echo $a;
|
|
|
|
if (rand(0, 1)) {
|
|
$a = 1;
|
|
break;
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'switchVarConditionalAssignmentWithoutReference' => [
|
|
'<?php
|
|
switch (rand(0, 4)) {
|
|
case 0:
|
|
if (rand(0, 1)) {
|
|
$a = 0;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
$a = 1;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'switchInIf' => [
|
|
'<?php
|
|
$a = 0;
|
|
|
|
if (rand(0, 1)) {
|
|
switch (rand(0, 4)) {
|
|
case 0:
|
|
$a = 3;
|
|
break;
|
|
|
|
default:
|
|
$a = 3;
|
|
}
|
|
} else {
|
|
$a = 6;
|
|
}
|
|
|
|
echo $a;',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'reusedKeyVar' => [
|
|
'<?php
|
|
$key = "a";
|
|
echo $key;
|
|
|
|
$arr = ["foo" => "foo.foo"];
|
|
|
|
foreach ($arr as $key => $v) {
|
|
list($key) = explode(".", $v);
|
|
echo $key;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'detectUnusedVarBeforeTryInsideForeach' => [
|
|
'<?php
|
|
function foo() : void {
|
|
$unused = 1;
|
|
|
|
while (rand(0, 1)) {
|
|
try {} catch (\Exception $e) {}
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'detectUnusedVariableInsideIfLoop' => [
|
|
'<?php
|
|
function foo() : void {
|
|
$a = 1;
|
|
|
|
if (rand(0, 1)) {
|
|
while (rand(0, 1)) {
|
|
$a = 2;
|
|
}
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'detectUnusedVariableInsideIfElseLoop' => [
|
|
'<?php
|
|
function foo() : void {
|
|
$a = 1;
|
|
|
|
if (rand(0, 1)) {
|
|
} else {
|
|
while (rand(0, 1)) {
|
|
$a = 2;
|
|
}
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'detectUnusedVariableInsideIfElseifLoop' => [
|
|
'<?php
|
|
function foo() : void {
|
|
$a = 1;
|
|
|
|
if (rand(0, 1)) {
|
|
} elseif (rand(0, 1)) {
|
|
while (rand(0, 1)) {
|
|
$a = 2;
|
|
}
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'detectUnusedVariableInsideIfLoopWithEchoInside' => [
|
|
'<?php
|
|
function foo() : void {
|
|
$a = 1;
|
|
|
|
if (rand(0, 1)) {
|
|
while (rand(0, 1)) {
|
|
$a = 2;
|
|
echo $a;
|
|
}
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'detectUnusedVariableInsideLoopAfterAssignment' => [
|
|
'<?php
|
|
function foo() : void {
|
|
foreach ([1, 2, 3] as $i) {
|
|
$i = $i;
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedForeachValue',
|
|
],
|
|
'detectUnusedVariableInsideLoopAfterAssignmentWithAddition' => [
|
|
'<?php
|
|
function foo() : void {
|
|
foreach ([1, 2, 3] as $i) {
|
|
$i = $i + 1;
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedForeachValue',
|
|
],
|
|
'detectUnusedVariableInsideLoopCalledInFunction' => [
|
|
'<?php
|
|
function foo(int $s) : int {
|
|
return $s;
|
|
}
|
|
|
|
function bar() : void {
|
|
foreach ([1, 2, 3] as $i) {
|
|
$i = foo($i);
|
|
}
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'detectUnusedVariableReassignedInIfFollowedByTryInsideForLoop' => [
|
|
'<?php
|
|
$user_id = 0;
|
|
$user = null;
|
|
|
|
if (rand(0, 1)) {
|
|
$user_id = rand(0, 1);
|
|
$user = $user_id;
|
|
}
|
|
|
|
if ($user) {
|
|
$a = 0;
|
|
for ($i = 1; $i <= 10; $i++) {
|
|
$a += $i;
|
|
try {} catch (\Exception $e) {}
|
|
}
|
|
echo $i;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'detectUnusedVariableReassignedInIfFollowedByTryInsideForeachLoop' => [
|
|
'<?php
|
|
$user_id = 0;
|
|
$user = null;
|
|
|
|
if (rand(0, 1)) {
|
|
$user_id = rand(0, 1);
|
|
$user = $user_id;
|
|
}
|
|
|
|
if ($user) {
|
|
$a = 0;
|
|
foreach ([1, 2, 3] as $i) {
|
|
$a += $i;
|
|
try {} catch (\Exception $e) {}
|
|
}
|
|
echo $i;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'detectUselessArrayAssignment' => [
|
|
'<?php
|
|
function foo() : void {
|
|
$a = [];
|
|
$a[0] = 1;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'detectUnusedSecondAssignmentBeforeTry' => [
|
|
'<?php
|
|
$a = [1, 2, 3];
|
|
echo($a[0]);
|
|
$a = [4, 5, 6];
|
|
|
|
try {
|
|
// something
|
|
} catch (\Throwable $t) {
|
|
// something else
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'detectRedundancyAfterLoopWithContinue' => [
|
|
'<?php
|
|
$gap = null;
|
|
|
|
foreach ([1, 2, 3] as $_) {
|
|
if (rand(0, 1)) {
|
|
continue;
|
|
}
|
|
|
|
$gap = "asa";
|
|
throw new \Exception($gap);
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'setInLoopThatsAlwaysEnteredButNotReferenced' => [
|
|
'<?php
|
|
/**
|
|
* @param non-empty-array<int> $a
|
|
*/
|
|
function getLastNum(array $a): int {
|
|
foreach ($a as $num) {
|
|
$last = $num;
|
|
}
|
|
return 4;
|
|
}',
|
|
'error_message' => 'UnusedForeachValue',
|
|
],
|
|
'conditionalForeachWithUnusedValue' => [
|
|
'<?php
|
|
if (rand(0, 1) > 0) {
|
|
foreach ([1, 2, 3] as $val) {}
|
|
}
|
|
',
|
|
'error_message' => 'UnusedForeachValue',
|
|
],
|
|
'doubleForeachWithInnerUnusedValue' => [
|
|
'<?php
|
|
/**
|
|
* @param non-empty-list<list<int>> $arr
|
|
* @return list<int>
|
|
*/
|
|
function f(array $arr): array {
|
|
foreach ($arr as $elt) {
|
|
foreach ($elt as $subelt) {}
|
|
}
|
|
return $elt;
|
|
}
|
|
',
|
|
'error_message' => 'UnusedForeachValue'
|
|
],
|
|
'defineInBothBranchesOfConditional' => [
|
|
'<?php
|
|
$i = null;
|
|
|
|
if (($i = rand(0, 5)) || ($i = rand(0, 3))) {
|
|
echo $i;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'knownVarType' => [
|
|
'<?php
|
|
function foo() : string {
|
|
return "hello";
|
|
}
|
|
|
|
/** @var string */
|
|
$a = foo();
|
|
|
|
echo $a;',
|
|
'error_message' => 'UnnecessaryVarAnnotation',
|
|
],
|
|
'knownVarTypeWithName' => [
|
|
'<?php
|
|
function foo() : string {
|
|
return "hello";
|
|
}
|
|
|
|
/** @var string $a */
|
|
$a = foo();
|
|
|
|
echo $a;',
|
|
'error_message' => 'UnnecessaryVarAnnotation',
|
|
],
|
|
'knownForeachVarType' => [
|
|
'<?php
|
|
/** @return string[] */
|
|
function foo() : array {
|
|
return ["hello"];
|
|
}
|
|
|
|
/** @var string $s */
|
|
foreach (foo() as $s) {
|
|
echo $s;
|
|
}',
|
|
'error_message' => 'UnnecessaryVarAnnotation',
|
|
],
|
|
'arrowFunctionUnusedVariable' => [
|
|
'<?php
|
|
function f(callable $c): void {
|
|
$c(22);
|
|
}
|
|
|
|
f(
|
|
fn(int $p)
|
|
=>
|
|
++$p
|
|
);',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'arrowFunctionUnusedParam' => [
|
|
'<?php
|
|
function f(callable $c): void {
|
|
$c(22);
|
|
}
|
|
|
|
f(
|
|
fn(int $p)
|
|
=>
|
|
0
|
|
);',
|
|
'error_message' => 'UnusedClosureParam',
|
|
],
|
|
'unusedFunctionParamWithDefault' => [
|
|
'<?php
|
|
function foo(bool $b = false) : void {}',
|
|
'error_message' => 'UnusedParam',
|
|
],
|
|
'arrayMapClosureWithParamTypeNoUse' => [
|
|
'<?php
|
|
$a = [1, 2, 3];
|
|
|
|
$b = array_map(
|
|
function(int $i) {
|
|
return rand(0, 5);
|
|
},
|
|
$a
|
|
);
|
|
|
|
foreach ($b as $c) {
|
|
echo $c;
|
|
}',
|
|
'error_message' => 'UnusedClosureParam',
|
|
],
|
|
'noUseOfInstantArrayAssignment' => [
|
|
'<?php
|
|
function foo() : void {
|
|
/** @psalm-suppress PossiblyUndefinedVariable */
|
|
$arr["foo"] = 1;
|
|
}',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'expectsNonNullAndPassedPossiblyNull' => [
|
|
'<?php
|
|
/**
|
|
* @param mixed|null $mixed_or_null
|
|
*/
|
|
function foo($mixed_or_null): Exception {
|
|
/**
|
|
* @psalm-suppress MixedArgument
|
|
*/
|
|
return new Exception($mixed_or_null);
|
|
}',
|
|
'error_message' => 'PossiblyNullArgument'
|
|
],
|
|
'useArrayAssignmentNeverUsed' => [
|
|
'<?php
|
|
$data = [];
|
|
|
|
return function () use ($data) {
|
|
$data[] = 1;
|
|
};',
|
|
'error_message' => 'UnusedVariable',
|
|
],
|
|
'warnAboutOriginalBadArray' => [
|
|
'<?php
|
|
function takesArray(array $arr) : void {
|
|
foreach ($arr as $a) {}
|
|
}',
|
|
'error_message' => 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:42 - Unable to determine the type that $a is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:47'
|
|
],
|
|
'warnAboutOriginalBadFunctionCall' => [
|
|
'<?php
|
|
function makeArray() : array {
|
|
return ["hello"];
|
|
}
|
|
|
|
$arr = makeArray();
|
|
|
|
foreach ($arr as $a) {
|
|
echo $a;
|
|
}',
|
|
'error_message' => 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:38 - Unable to determine the type that $a is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:44'
|
|
],
|
|
'warnAboutOriginalBadStaticCall' => [
|
|
'<?php
|
|
class A {
|
|
public static function makeArray() : array {
|
|
return ["hello"];
|
|
}
|
|
}
|
|
|
|
$arr = A::makeArray();
|
|
|
|
foreach ($arr as $a) {
|
|
echo $a;
|
|
}',
|
|
'error_message' => 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:38 - Unable to determine the type that $a is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:3:62'
|
|
],
|
|
'warnAboutOriginalBadInstanceCall' => [
|
|
'<?php
|
|
class A {
|
|
public function makeArray() : array {
|
|
return ["hello"];
|
|
}
|
|
}
|
|
|
|
$arr = (new A)->makeArray();
|
|
|
|
foreach ($arr as $a) {
|
|
echo $a;
|
|
}',
|
|
'error_message' => 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:38 - Unable to determine the type that $a is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:3:55'
|
|
],
|
|
'warnAboutDocblockReturnType' => [
|
|
'<?php
|
|
/** @return array[] */
|
|
function makeArray() : array {
|
|
return [["hello"]];
|
|
}
|
|
|
|
$arr = makeArray();
|
|
|
|
foreach ($arr as $some_arr) {
|
|
foreach ($some_arr as $a) {
|
|
echo $a;
|
|
}
|
|
}',
|
|
'error_message' => 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:47 - Unable to determine the type that $a is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:33'
|
|
],
|
|
'warnAboutMixedArgument' => [
|
|
'<?php
|
|
function makeArray() : array {
|
|
return ["hello"];
|
|
}
|
|
|
|
$arr = makeArray();
|
|
|
|
/** @psalm-suppress MixedAssignment */
|
|
foreach ($arr as $a) {
|
|
echo $a;
|
|
}',
|
|
'error_message' => 'MixedArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:30 - Argument 1 of echo cannot be mixed, expecting string. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:44'
|
|
],
|
|
'warnAboutMixedMethodCall' => [
|
|
'<?php
|
|
function makeArray() : array {
|
|
return ["hello"];
|
|
}
|
|
|
|
$arr = makeArray();
|
|
|
|
/** @psalm-suppress MixedAssignment */
|
|
foreach ($arr as $a) {
|
|
$a->foo();
|
|
}',
|
|
'error_message' => 'MixedMethodCall - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:29 - Cannot determine the type of $a when calling method foo. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:44'
|
|
],
|
|
'warnAboutMixedReturnStatement' => [
|
|
'<?php
|
|
function makeArray() : array {
|
|
return ["hello"];
|
|
}
|
|
|
|
function foo() : string {
|
|
$arr = makeArray();
|
|
|
|
/** @psalm-suppress MixedAssignment */
|
|
foreach ($arr as $a) {
|
|
return $a;
|
|
}
|
|
|
|
return "";
|
|
}',
|
|
'error_message' => 'MixedReturnStatement - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:36 - Could not infer a return type. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:44'
|
|
],
|
|
'warnAboutIterableKeySource' => [
|
|
'<?php
|
|
function foo(iterable $arr) : void {
|
|
foreach ($arr as $key => $_) {}
|
|
}',
|
|
'error_message' => 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:42 - Unable to determine the type that $key is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:43'
|
|
],
|
|
'warnAboutMixedKeySource' => [
|
|
'<?php
|
|
/** @param mixed $arr */
|
|
function foo($arr) : void {
|
|
foreach ($arr as $key => $_) {}
|
|
}',
|
|
'error_message' => 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:42 - Unable to determine the type that $key is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:3:34'
|
|
],
|
|
'warnAboutMixedArgumentTypeCoercionSource' => [
|
|
'<?php
|
|
/** @param array<string> $arr */
|
|
function takesArrayOfString(array $arr) : void {
|
|
foreach ($arr as $a) {
|
|
echo $a;
|
|
}
|
|
}
|
|
|
|
/** @param mixed $a */
|
|
function takesArray($a) : void {
|
|
$arr = [$a];
|
|
takesArrayOfString($arr);
|
|
}',
|
|
'error_message' => 'MixedArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:44 - Argument 1 of takesArrayOfString expects array<array-key, string>, parent type array{mixed} provided. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:10:41'
|
|
],
|
|
];
|
|
}
|
|
}
|