mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
1601 lines
50 KiB
PHP
1601 lines
50 KiB
PHP
<?php
|
|
namespace Psalm\Tests;
|
|
|
|
use Psalm\Config;
|
|
use Psalm\Context;
|
|
|
|
class AnnotationTest extends TestCase
|
|
{
|
|
use Traits\FileCheckerInvalidCodeParseTestTrait;
|
|
use Traits\FileCheckerValidCodeParseTestTrait;
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function testPhpStormGenericsWithValidArgument()
|
|
{
|
|
Config::getInstance()->allow_phpstorm_generics = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
function takesString(string $s): void {}
|
|
|
|
/** @param ArrayIterator|string[] $i */
|
|
function takesArrayIteratorOfString(ArrayIterator $i): void {
|
|
$s = $i->offsetGet("a");
|
|
takesString($s);
|
|
|
|
foreach ($i as $s2) {
|
|
takesString($s2);
|
|
}
|
|
}'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new Context());
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function testPhpStormGenericsWithClassProperty()
|
|
{
|
|
Config::getInstance()->allow_phpstorm_generics = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
/** @psalm-suppress MissingConstructor */
|
|
class Foo {
|
|
/** @var \stdClass[]|\ArrayObject */
|
|
public $bar;
|
|
|
|
/**
|
|
* @return \stdClass[]|\ArrayObject
|
|
*/
|
|
public function getBar(): \ArrayObject
|
|
{
|
|
return $this->bar;
|
|
}
|
|
}'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new Context());
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function testPhpStormGenericsWithValidIterableArgument()
|
|
{
|
|
Config::getInstance()->allow_phpstorm_generics = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
function takesString(string $s): void {}
|
|
|
|
/** @param iterable|string[] $i */
|
|
function takesArrayIteratorOfString(iterable $i): void {
|
|
foreach ($i as $s2) {
|
|
takesString($s2);
|
|
}
|
|
}'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new Context());
|
|
}
|
|
|
|
/**
|
|
* @expectedException \Psalm\Exception\CodeException
|
|
* @expectedExceptionMessage InvalidScalarArgument
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testPhpStormGenericsInvalidArgument()
|
|
{
|
|
Config::getInstance()->allow_phpstorm_generics = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
function takesInt(int $s): void {}
|
|
|
|
/** @param ArrayIterator|string[] $i */
|
|
function takesArrayIteratorOfString(ArrayIterator $i): void {
|
|
$s = $i->offsetGet("a");
|
|
takesInt($s);
|
|
}'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new Context());
|
|
}
|
|
|
|
/**
|
|
* @expectedException \Psalm\Exception\CodeException
|
|
* @expectedExceptionMessage PossiblyInvalidMethodCall
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testPhpStormGenericsNoTypehint()
|
|
{
|
|
Config::getInstance()->allow_phpstorm_generics = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
/** @param ArrayIterator|string[] $i */
|
|
function takesArrayIteratorOfString($i): void {
|
|
$s = $i->offsetGet("a");
|
|
}'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new Context());
|
|
}
|
|
|
|
/**
|
|
* @expectedException \Psalm\Exception\CodeException
|
|
* @expectedExceptionMessage InvalidArgument
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testDontAllowStringConstCoercion()
|
|
{
|
|
Config::getInstance()->allow_coercion_from_string_to_class_const = false;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
/**
|
|
* @param class-string $s
|
|
*/
|
|
function takesClassConstants(string $s) : void {}
|
|
|
|
class A {}
|
|
|
|
takesClassConstants("A");'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new Context());
|
|
}
|
|
|
|
/**
|
|
* @expectedException \Psalm\Exception\CodeException
|
|
* @expectedExceptionMessage InvalidStringClass
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testDontAllowStringStandInForNewClass()
|
|
{
|
|
Config::getInstance()->allow_string_standin_for_class = false;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
class A {}
|
|
|
|
$a = "A";
|
|
|
|
new $a();'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new Context());
|
|
}
|
|
|
|
/**
|
|
* @expectedException \Psalm\Exception\CodeException
|
|
* @expectedExceptionMessage InvalidStringClass
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testDontAllowStringStandInForStaticMethodCall()
|
|
{
|
|
Config::getInstance()->allow_string_standin_for_class = false;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
class A {
|
|
public static function foo() : void {}
|
|
}
|
|
|
|
$a = "A";
|
|
|
|
$a::foo();'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new Context());
|
|
}
|
|
|
|
/**
|
|
* @expectedException \Psalm\Exception\CodeException
|
|
* @expectedExceptionMessage InvalidParamDefault
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testInvalidParamDefault()
|
|
{
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
/**
|
|
* @param array $arr
|
|
* @return void
|
|
*/
|
|
function foo($arr = false) {}'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new Context());
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function testInvalidParamDefaultButAllowedInConfig()
|
|
{
|
|
Config::getInstance()->add_param_default_to_docblock_type = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
/**
|
|
* @param array $arr
|
|
* @return void
|
|
*/
|
|
function foo($arr = false) {}
|
|
foo(false);
|
|
foo(["hello"]);'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new Context());
|
|
}
|
|
|
|
/**
|
|
* @expectedException \Psalm\Exception\CodeException
|
|
* @expectedExceptionMessage InvalidParamDefault
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testInvalidTypehintParamDefaultButAllowedInConfig()
|
|
{
|
|
Config::getInstance()->add_param_default_to_docblock_type = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
function foo(array $arr = false) : void {}'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new Context());
|
|
}
|
|
|
|
/**
|
|
* @expectedException \Psalm\Exception\CodeException
|
|
* @expectedExceptionMessage MissingThrowsDocblock
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testUndocumentedThrow()
|
|
{
|
|
Config::getInstance()->check_for_throws_docblock = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
function foo(int $x, int $y) : int {
|
|
if ($y === 0) {
|
|
throw new \RangeException("Cannot divide by zero");
|
|
}
|
|
|
|
if ($y < 0) {
|
|
throw new \InvalidArgumentException("This is also bad");
|
|
}
|
|
|
|
return intdiv($x, $y);
|
|
}'
|
|
);
|
|
|
|
$context = new Context();
|
|
|
|
$this->analyzeFile('somefile.php', $context);
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function testDocumentedThrow()
|
|
{
|
|
Config::getInstance()->check_for_throws_docblock = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
/**
|
|
* @throws RangeException
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
function foo(int $x, int $y) : int {
|
|
if ($y === 0) {
|
|
throw new \RangeException("Cannot divide by zero");
|
|
}
|
|
|
|
if ($y < 0) {
|
|
throw new \InvalidArgumentException("This is also bad");
|
|
}
|
|
|
|
return intdiv($x, $y);
|
|
}'
|
|
);
|
|
|
|
$context = new Context();
|
|
|
|
$this->analyzeFile('somefile.php', $context);
|
|
}
|
|
|
|
/**
|
|
* @expectedException \Psalm\Exception\CodeException
|
|
* @expectedExceptionMessage MissingThrowsDocblock
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testUndocumentedThrowInFunctionCall()
|
|
{
|
|
Config::getInstance()->check_for_throws_docblock = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
/**
|
|
* @throws RangeException
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
function foo(int $x, int $y) : int {
|
|
if ($y === 0) {
|
|
throw new \RangeException("Cannot divide by zero");
|
|
}
|
|
|
|
if ($y < 0) {
|
|
throw new \InvalidArgumentException("This is also bad");
|
|
}
|
|
|
|
return intdiv($x, $y);
|
|
}
|
|
|
|
function bar(int $x, int $y) : void {
|
|
foo($x, $y);
|
|
}'
|
|
);
|
|
|
|
$context = new Context();
|
|
|
|
$this->analyzeFile('somefile.php', $context);
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function testDocumentedThrowInFunctionCall()
|
|
{
|
|
Config::getInstance()->check_for_throws_docblock = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
/**
|
|
* @throws RangeException
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
function foo(int $x, int $y) : int {
|
|
if ($y === 0) {
|
|
throw new \RangeException("Cannot divide by zero");
|
|
}
|
|
|
|
if ($y < 0) {
|
|
throw new \InvalidArgumentException("This is also bad");
|
|
}
|
|
|
|
return intdiv($x, $y);
|
|
}
|
|
|
|
/**
|
|
* @throws RangeException
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
function bar(int $x, int $y) : void {
|
|
foo($x, $y);
|
|
}'
|
|
);
|
|
|
|
$context = new Context();
|
|
|
|
$this->analyzeFile('somefile.php', $context);
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function testCaughtThrowInFunctionCall()
|
|
{
|
|
Config::getInstance()->check_for_throws_docblock = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
/**
|
|
* @throws RangeException
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
function foo(int $x, int $y) : int {
|
|
if ($y === 0) {
|
|
throw new \RangeException("Cannot divide by zero");
|
|
}
|
|
|
|
if ($y < 0) {
|
|
throw new \InvalidArgumentException("This is also bad");
|
|
}
|
|
|
|
return intdiv($x, $y);
|
|
}
|
|
|
|
function bar(int $x, int $y) : void {
|
|
try {
|
|
foo($x, $y);
|
|
} catch (RangeException $e) {
|
|
|
|
} catch (InvalidArgumentException $e) {}
|
|
}'
|
|
);
|
|
|
|
$context = new Context();
|
|
|
|
$this->analyzeFile('somefile.php', $context);
|
|
}
|
|
|
|
/**
|
|
* @expectedException \Psalm\Exception\CodeException
|
|
* @expectedExceptionMessage MissingThrowsDocblock
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testUncaughtThrowInFunctionCall()
|
|
{
|
|
Config::getInstance()->check_for_throws_docblock = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
/**
|
|
* @throws RangeException
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
function foo(int $x, int $y) : int {
|
|
if ($y === 0) {
|
|
throw new \RangeException("Cannot divide by zero");
|
|
}
|
|
|
|
if ($y < 0) {
|
|
throw new \InvalidArgumentException("This is also bad");
|
|
}
|
|
|
|
return intdiv($x, $y);
|
|
}
|
|
|
|
function bar(int $x, int $y) : void {
|
|
try {
|
|
foo($x, $y);
|
|
} catch (\RangeException $e) {
|
|
|
|
}
|
|
}'
|
|
);
|
|
|
|
$context = new Context();
|
|
|
|
$this->analyzeFile('somefile.php', $context);
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function testCaughtAllThrowInFunctionCall()
|
|
{
|
|
Config::getInstance()->check_for_throws_docblock = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
/**
|
|
* @throws RangeException
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
function foo(int $x, int $y) : int {
|
|
if ($y === 0) {
|
|
throw new \RangeException("Cannot divide by zero");
|
|
}
|
|
|
|
if ($y < 0) {
|
|
throw new \InvalidArgumentException("This is also bad");
|
|
}
|
|
|
|
return intdiv($x, $y);
|
|
}
|
|
|
|
function bar(int $x, int $y) : void {
|
|
try {
|
|
foo($x, $y);
|
|
} catch (Exception $e) {}
|
|
}'
|
|
);
|
|
|
|
$context = new Context();
|
|
|
|
$this->analyzeFile('somefile.php', $context);
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function providerFileCheckerValidCodeParse()
|
|
{
|
|
return [
|
|
'nopType' => [
|
|
'<?php
|
|
$a = "hello";
|
|
|
|
/** @var int $a */',
|
|
'assertions' => [
|
|
'$a' => 'int',
|
|
],
|
|
],
|
|
'deprecatedMethod' => [
|
|
'<?php
|
|
class Foo {
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
public static function barBar(): void {
|
|
}
|
|
}',
|
|
],
|
|
'validDocblockReturn' => [
|
|
'<?php
|
|
/**
|
|
* @return string
|
|
*/
|
|
function fooFoo(): string {
|
|
return "boop";
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
function foo2(): array {
|
|
return ["hello"];
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
function foo3(): array {
|
|
return ["hello"];
|
|
}',
|
|
],
|
|
'reassertWithIs' => [
|
|
'<?php
|
|
/** @param array $a */
|
|
function foo($a): void {
|
|
if (is_array($a)) {
|
|
// do something
|
|
}
|
|
}',
|
|
'assertions' => [],
|
|
'error_level' => ['RedundantConditionGivenDocblockType'],
|
|
],
|
|
'checkArrayWithIs' => [
|
|
'<?php
|
|
/** @param mixed $b */
|
|
function foo($b): void {
|
|
/** @var array */
|
|
$a = (array)$b;
|
|
if (is_array($a)) {
|
|
// do something
|
|
}
|
|
}',
|
|
'assertions' => [],
|
|
'error_level' => ['RedundantConditionGivenDocblockType'],
|
|
],
|
|
'checkArrayWithIsInsideLoop' => [
|
|
'<?php
|
|
/** @param array<mixed, array<mixed, mixed>> $data */
|
|
function foo($data): void {
|
|
foreach ($data as $key => $val) {
|
|
if (!\is_array($data)) {
|
|
$data = [$key => null];
|
|
} else {
|
|
$data[$key] = !empty($val);
|
|
}
|
|
}
|
|
}',
|
|
'assertions' => [],
|
|
'error_level' => ['LoopInvalidation', 'MixedArrayOffset', 'RedundantConditionGivenDocblockType'],
|
|
],
|
|
'goodDocblock' => [
|
|
'<?php
|
|
class A {
|
|
/**
|
|
* @param A $a
|
|
* @param bool $b
|
|
*/
|
|
public function g(A $a, $b): void {
|
|
}
|
|
}',
|
|
],
|
|
'goodDocblockInNamespace' => [
|
|
'<?php
|
|
namespace Foo;
|
|
|
|
class A {
|
|
/**
|
|
* @param \Foo\A $a
|
|
* @param bool $b
|
|
*/
|
|
public function g(A $a, $b): void {
|
|
}
|
|
}',
|
|
],
|
|
|
|
'ignoreNullableReturn' => [
|
|
'<?php
|
|
class A {
|
|
/** @var int */
|
|
public $bar = 5;
|
|
public function foo(): void {}
|
|
}
|
|
|
|
/**
|
|
* @return ?A
|
|
* @psalm-ignore-nullable-return
|
|
*/
|
|
function makeA() {
|
|
return rand(0, 1) ? new A(): null;
|
|
}
|
|
|
|
function takeA(A $a): void { }
|
|
|
|
$a = makeA();
|
|
$a->foo();
|
|
$a->bar = 7;
|
|
takeA($a);',
|
|
],
|
|
'invalidDocblockParamSuppress' => [
|
|
'<?php
|
|
/**
|
|
* @param int $bar
|
|
* @psalm-suppress MismatchingDocblockParamType
|
|
*/
|
|
function fooFoo(array $bar): void {
|
|
}',
|
|
],
|
|
'differentDocblockParamClassSuppress' => [
|
|
'<?php
|
|
class A {}
|
|
class B {}
|
|
|
|
/**
|
|
* @param B $bar
|
|
* @psalm-suppress MismatchingDocblockParamType
|
|
*/
|
|
function fooFoo(A $bar): void {
|
|
}',
|
|
],
|
|
'varDocblock' => [
|
|
'<?php
|
|
/** @var array<Exception> */
|
|
$a = [];
|
|
|
|
$a[0]->getMessage();',
|
|
],
|
|
'mixedDocblockParamTypeDefinedInParent' => [
|
|
'<?php
|
|
class A {
|
|
/** @param mixed $a */
|
|
public function foo($a): void {}
|
|
}
|
|
|
|
class B extends A {
|
|
public function foo($a): void {}
|
|
}',
|
|
],
|
|
'intDocblockParamTypeDefinedInParent' => [
|
|
'<?php
|
|
class A {
|
|
/** @param int $a */
|
|
public function foo($a): void {}
|
|
}
|
|
|
|
class B extends A {
|
|
public function foo($a): void {}
|
|
}',
|
|
],
|
|
'varSelf' => [
|
|
'<?php
|
|
class A
|
|
{
|
|
public function foo(): void {}
|
|
|
|
public function getMeAgain(): void {
|
|
/** @var self */
|
|
$me = $this;
|
|
$me->foo();
|
|
}
|
|
}',
|
|
],
|
|
'psalmVar' => [
|
|
'<?php
|
|
class A
|
|
{
|
|
/** @psalm-var array<int, string> */
|
|
public $foo = [];
|
|
|
|
public function updateFoo(): void {
|
|
$this->foo[5] = "hello";
|
|
}
|
|
}',
|
|
],
|
|
'psalmParam' => [
|
|
'<?php
|
|
function takesInt(int $a): void {}
|
|
|
|
/**
|
|
* @psalm-param array<int, string> $a
|
|
* @param string[] $a
|
|
*/
|
|
function foo(array $a): void {
|
|
foreach ($a as $key => $value) {
|
|
takesInt($key);
|
|
}
|
|
}',
|
|
],
|
|
'returnDocblock' => [
|
|
'<?php
|
|
function foo(int $i): int {
|
|
/** @var int */
|
|
return $i;
|
|
}',
|
|
],
|
|
'doubleVar' => [
|
|
'<?php
|
|
function foo() : array {
|
|
return ["hello" => new stdClass, "goodbye" => new stdClass];
|
|
}
|
|
|
|
$a = null;
|
|
$b = null;
|
|
|
|
/**
|
|
* @var string $key
|
|
* @var stdClass $value
|
|
*/
|
|
foreach (foo() as $key => $value) {
|
|
$a = $key;
|
|
$b = $value;
|
|
}',
|
|
'assertions' => [
|
|
'$a' => 'null|string',
|
|
'$b' => 'null|stdClass',
|
|
],
|
|
],
|
|
'arrayOfClassConstants' => [
|
|
'<?php
|
|
/**
|
|
* @param array<class-string> $arr
|
|
*/
|
|
function takesClassConstants(array $arr) : void {}
|
|
|
|
class A {}
|
|
class B {}
|
|
|
|
takesClassConstants([A::class, B::class]);',
|
|
],
|
|
'arrayOfStringClasses' => [
|
|
'<?php
|
|
/**
|
|
* @param array<class-string> $arr
|
|
*/
|
|
function takesClassConstants(array $arr) : void {}
|
|
|
|
class A {}
|
|
class B {}
|
|
|
|
takesClassConstants(["A", "B"]);',
|
|
'annotations' => [],
|
|
'error_levels' => ['TypeCoercion'],
|
|
],
|
|
'singleClassConstantAsConstant' => [
|
|
'<?php
|
|
/**
|
|
* @param class-string $s
|
|
*/
|
|
function takesClassConstants(string $s) : void {}
|
|
|
|
class A {}
|
|
|
|
takesClassConstants(A::class);',
|
|
],
|
|
'singleClassConstantWithString' => [
|
|
'<?php
|
|
/**
|
|
* @param class-string $s
|
|
*/
|
|
function takesClassConstants(string $s) : void {}
|
|
|
|
class A {}
|
|
|
|
takesClassConstants("A");',
|
|
'annotations' => [],
|
|
'error_levels' => ['TypeCoercion'],
|
|
],
|
|
'returnClassConstant' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
/**
|
|
* @return class-string
|
|
*/
|
|
function takesClassConstants() : string {
|
|
return A::class;
|
|
}',
|
|
],
|
|
'returnClassConstantAllowCoercion' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
/**
|
|
* @return class-string
|
|
*/
|
|
function takesClassConstants() : string {
|
|
return "A";
|
|
}',
|
|
'annotations' => [],
|
|
'error_levels' => ['LessSpecificReturnStatement', 'MoreSpecificReturnType'],
|
|
],
|
|
'returnClassConstantArray' => [
|
|
'<?php
|
|
class A {}
|
|
class B {}
|
|
|
|
/**
|
|
* @return array<class-string>
|
|
*/
|
|
function takesClassConstants() : array {
|
|
return [A::class, B::class];
|
|
}',
|
|
],
|
|
'returnClassConstantArrayAllowCoercion' => [
|
|
'<?php
|
|
class A {}
|
|
class B {}
|
|
|
|
/**
|
|
* @return array<class-string>
|
|
*/
|
|
function takesClassConstants() : array {
|
|
return ["A", "B"];
|
|
}',
|
|
'annotations' => [],
|
|
'error_levels' => ['LessSpecificReturnStatement', 'MoreSpecificReturnType'],
|
|
],
|
|
'ifClassStringEquals' => [
|
|
'<?php
|
|
class A {}
|
|
class B {}
|
|
|
|
/** @param class-string $class */
|
|
function foo(string $class) : void {
|
|
if ($class === A::class) {}
|
|
if ($class === A::class || $class === B::class) {}
|
|
}',
|
|
],
|
|
'allowOptionalParamsToBeEmptyArray' => [
|
|
'<?php
|
|
/** @param array{b?: int, c?: string} $a */
|
|
function foo(array $a = []) : void {}',
|
|
],
|
|
'allowEmptyVarAnnotation' => [
|
|
'<?php
|
|
/**
|
|
* @param $x
|
|
*/
|
|
function example(array $x) : void {}',
|
|
],
|
|
'allowCapitalisedNamespacedString' => [
|
|
'<?php
|
|
namespace Foo;
|
|
|
|
/**
|
|
* @param String $x
|
|
*/
|
|
function example(string $x) : void {}',
|
|
],
|
|
'megaClosureAnnotationWithoutSpacing' => [
|
|
'<?php
|
|
/** @var array{a:Closure():(array<mixed, mixed>|null), b?:Closure():array<mixed, mixed>, c?:Closure():array<mixed, mixed>, d?:Closure():array<mixed, mixed>, e?:Closure():(array{f:null|string, g:null|string, h:null|string, i:string, j:mixed, k:mixed, l:mixed, m:mixed, n:bool, o?:array{0:string}}|null), p?:Closure():(array{f:null|string, g:null|string, h:null|string, q:string, i:string, j:mixed, k:mixed, l:mixed, m:mixed, n:bool, o?:array{0:string}}|null), r?:Closure():(array<mixed, mixed>|null), s:array<mixed, mixed>} */
|
|
$arr = [];
|
|
|
|
$arr["a"]()',
|
|
],
|
|
'megaClosureAnnotationWithSpacing' => [
|
|
'<?php
|
|
/** @var array{
|
|
a: Closure() : (array<mixed, mixed>|null),
|
|
b?: Closure() : array<mixed, mixed>,
|
|
c?: Closure() : array<mixed, mixed>,
|
|
d?: Closure() : array<mixed, mixed>,
|
|
e?: Closure() : (array{
|
|
f: null|string,
|
|
g: null|string,
|
|
h: null|string,
|
|
i: string,
|
|
j: mixed,
|
|
k: mixed,
|
|
l: mixed,
|
|
m: mixed,
|
|
n: bool,
|
|
o?: array{0:string}
|
|
}|null),
|
|
p?: Closure() : (array{
|
|
f: null|string,
|
|
g: null|string,
|
|
h: null|string,
|
|
q: string,
|
|
i: string,
|
|
j: mixed,
|
|
k: mixed,
|
|
l: mixed,
|
|
m: mixed,
|
|
n: bool,
|
|
o?: array{0:string}
|
|
}|null),
|
|
r?: Closure() : (array<mixed, mixed>|null),
|
|
s: array<mixed, mixed>
|
|
} */
|
|
$arr = [];
|
|
|
|
$arr["a"]()',
|
|
],
|
|
'slashAfter?' => [
|
|
'<?php
|
|
namespace ns;
|
|
|
|
/** @param ?\stdClass $s */
|
|
function foo($s) : void {
|
|
}
|
|
|
|
foo(null);
|
|
foo(new \stdClass);',
|
|
],
|
|
'generatorReturnType' => [
|
|
'<?php
|
|
/** @return Generator<int, stdClass> */
|
|
function g():Generator { yield new stdClass; }
|
|
|
|
$g = g();',
|
|
'assertions' => [
|
|
'$g' => 'Generator<int, stdClass>',
|
|
],
|
|
],
|
|
'returnTypeShouldBeNullable' => [
|
|
'<?php
|
|
/**
|
|
* @return stdClass
|
|
*/
|
|
function foo() : ?stdClass {
|
|
return rand(0, 1) ? new stdClass : null;
|
|
}
|
|
|
|
$f = foo();
|
|
if ($f) {}'
|
|
],
|
|
'spreadOperatorArrayAnnotation' => [
|
|
'<?php
|
|
/** @param string[] $s */
|
|
function foo(string ...$s) : void {}',
|
|
],
|
|
'valueReturnType' => [
|
|
'<?php
|
|
/**
|
|
* @param "a"|"b" $_p
|
|
*/
|
|
function acceptsLiteral($_p): void {}
|
|
|
|
/**
|
|
* @return "a"|"b"
|
|
*/
|
|
function returnsLiteral(): string {
|
|
return rand(0,1) ? "a" : "b";
|
|
}
|
|
|
|
acceptsLiteral(returnsLiteral());'
|
|
],
|
|
'typeAliasBeforeClass' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-type CoolType = A|B|null
|
|
*/
|
|
|
|
class A {}
|
|
class B {}
|
|
|
|
/** @return CoolType */
|
|
function foo() {
|
|
if (rand(0, 1)) {
|
|
return new A();
|
|
}
|
|
|
|
if (rand(0, 1)) {
|
|
return new B();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** @param CoolType $a **/
|
|
function bar ($a) : void { }
|
|
|
|
bar(foo());'
|
|
],
|
|
'typeAliasBeforeFunction' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-type CoolType = A|B|null
|
|
* @return CoolType
|
|
*/
|
|
function foo() {
|
|
if (rand(0, 1)) {
|
|
return new A();
|
|
}
|
|
|
|
if (rand(0, 1)) {
|
|
return new B();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
class A {}
|
|
class B {}
|
|
|
|
/** @param CoolType $a **/
|
|
function bar ($a) : void { }
|
|
|
|
bar(foo());'
|
|
],
|
|
'almostFreeStandingTypeAlias' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-type CoolType = A|B|null
|
|
*/
|
|
|
|
// this breaks up the line
|
|
|
|
class A {}
|
|
class B {}
|
|
|
|
/** @return CoolType */
|
|
function foo() {
|
|
if (rand(0, 1)) {
|
|
return new A();
|
|
}
|
|
|
|
if (rand(0, 1)) {
|
|
return new B();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** @param CoolType $a **/
|
|
function bar ($a) : void { }
|
|
|
|
bar(foo());'
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function providerFileCheckerInvalidCodeParse()
|
|
{
|
|
return [
|
|
'invalidReturn' => [
|
|
'<?php
|
|
interface I {
|
|
/**
|
|
* @return $thus
|
|
*/
|
|
public static function barBar();
|
|
}',
|
|
'error_message' => 'MissingDocblockType',
|
|
],
|
|
'invalidReturnClass' => [
|
|
'<?php
|
|
interface I {
|
|
/**
|
|
* @return 1
|
|
*/
|
|
public static function barBar();
|
|
}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'invalidReturnBrackets' => [
|
|
'<?php
|
|
interface I {
|
|
/**
|
|
* @return []
|
|
*/
|
|
public static function barBar();
|
|
}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'invalidPropertyClass' => [
|
|
'<?php
|
|
class A {
|
|
/**
|
|
* @var 1
|
|
*/
|
|
public $bar;
|
|
}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'invalidPropertyBrackets' => [
|
|
'<?php
|
|
class A {
|
|
/**
|
|
* @var []
|
|
*/
|
|
public $bar;
|
|
}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'invalidReturnClassWithComma' => [
|
|
'<?php
|
|
interface I {
|
|
/**
|
|
* @return 1,
|
|
*/
|
|
public static function barBar();
|
|
}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'returnClassWithComma' => [
|
|
'<?php
|
|
interface I {
|
|
/**
|
|
* @return a,
|
|
*/
|
|
public static function barBar();
|
|
}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'deprecatedMethodWithCall' => [
|
|
'<?php
|
|
class Foo {
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
public static function barBar(): void {
|
|
}
|
|
}
|
|
|
|
Foo::barBar();',
|
|
'error_message' => 'DeprecatedMethod',
|
|
],
|
|
'deprecatedClassWithStaticCall' => [
|
|
'<?php
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
class Foo {
|
|
public static function barBar(): void {
|
|
}
|
|
}
|
|
|
|
Foo::barBar();',
|
|
'error_message' => 'DeprecatedClass',
|
|
],
|
|
'deprecatedClassWithNew' => [
|
|
'<?php
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
class Foo { }
|
|
|
|
$a = new Foo();',
|
|
'error_message' => 'DeprecatedClass',
|
|
],
|
|
'deprecatedClassWithExtends' => [
|
|
'<?php
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
class Foo { }
|
|
|
|
class Bar extends Foo {}',
|
|
'error_message' => 'DeprecatedClass',
|
|
],
|
|
'deprecatedPropertyGet' => [
|
|
'<?php
|
|
class A{
|
|
/**
|
|
* @deprecated
|
|
* @var ?int
|
|
*/
|
|
public $foo;
|
|
}
|
|
echo (new A)->foo;',
|
|
'error_message' => 'DeprecatedProperty',
|
|
],
|
|
'deprecatedPropertySet' => [
|
|
'<?php
|
|
class A{
|
|
/**
|
|
* @deprecated
|
|
* @var ?int
|
|
*/
|
|
public $foo;
|
|
}
|
|
$a = new A;
|
|
$a->foo = 5;',
|
|
'error_message' => 'DeprecatedProperty',
|
|
],
|
|
'missingParamType' => [
|
|
'<?php
|
|
/**
|
|
* @param string $bar
|
|
*/
|
|
function fooBar(): void {
|
|
}
|
|
|
|
fooBar("hello");',
|
|
'error_message' => 'TooManyArguments - src' . DIRECTORY_SEPARATOR . 'somefile.php:8 - Too many arguments for method fooBar '
|
|
. '- expecting 0 but saw 1',
|
|
],
|
|
'missingParamVar' => [
|
|
'<?php
|
|
/**
|
|
* @param string
|
|
*/
|
|
function fooBar(): void {
|
|
}',
|
|
'error_message' => 'InvalidDocblock - src' . DIRECTORY_SEPARATOR . 'somefile.php:5 - Badly-formatted @param',
|
|
],
|
|
'invalidDocblockReturn' => [
|
|
'<?php
|
|
/**
|
|
* @return string
|
|
*/
|
|
function fooFoo(): int {
|
|
return 5;
|
|
}',
|
|
'error_message' => 'MismatchingDocblockReturnType',
|
|
],
|
|
'noStringParamType' => [
|
|
'<?php
|
|
function fooFoo($a): void {
|
|
echo substr($a, 4, 2);
|
|
}',
|
|
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:2 - Parameter $a has no provided type,'
|
|
. ' should be string',
|
|
'error_levels' => ['MixedArgument'],
|
|
],
|
|
'noParamTypeButConcat' => [
|
|
'<?php
|
|
function fooFoo($a): void {
|
|
echo $a . "foo";
|
|
}',
|
|
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:2 - Parameter $a has no provided type,'
|
|
. ' should be string',
|
|
'error_levels' => ['MixedOperand'],
|
|
],
|
|
'noParamTypeButAddition' => [
|
|
'<?php
|
|
function fooFoo($a): void {
|
|
echo $a + 5;
|
|
}',
|
|
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:2 - Parameter $a has no provided type,'
|
|
. ' should be int|float',
|
|
'error_levels' => ['MixedOperand', 'MixedArgument'],
|
|
],
|
|
'noParamTypeButDivision' => [
|
|
'<?php
|
|
function fooFoo($a): void {
|
|
echo $a / 5;
|
|
}',
|
|
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:2 - Parameter $a has no provided type,'
|
|
. ' should be int|float',
|
|
'error_levels' => ['MixedOperand', 'MixedArgument'],
|
|
],
|
|
'noParamTypeButTemplatedString' => [
|
|
'<?php
|
|
function fooFoo($a): void {
|
|
echo "$a";
|
|
}',
|
|
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:2 - Parameter $a has no provided type,'
|
|
. ' should be string',
|
|
'error_levels' => ['MixedOperand'],
|
|
],
|
|
'noStringIntParamType' => [
|
|
'<?php
|
|
function fooFoo($a): void {
|
|
if (is_string($a)) {
|
|
echo substr($a, 4, 2);
|
|
} else {
|
|
echo substr("hello", $a, 2);
|
|
}
|
|
}',
|
|
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:2 - Parameter $a has no provided type,'
|
|
. ' should be int|string',
|
|
'error_levels' => ['MixedArgument'],
|
|
],
|
|
'intParamTypeDefinedInParent' => [
|
|
'<?php
|
|
class A {
|
|
public function foo(int $a): void {}
|
|
}
|
|
|
|
class B extends A {
|
|
public function foo($a): void {}
|
|
}',
|
|
'error_message' => 'MissingParamType',
|
|
'error_levels' => ['MethodSignatureMismatch'],
|
|
],
|
|
'alreadyHasCheck' => [
|
|
'<?php
|
|
function takesString(string $s): void {}
|
|
|
|
function shouldTakeString($s): void {
|
|
if (is_string($s)) takesString($s);
|
|
}',
|
|
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:4 - Parameter $s has no provided type,'
|
|
. ' could not infer',
|
|
'error_levels' => ['MixedArgument'],
|
|
],
|
|
'isSetBeforeInferrence' => [
|
|
'input' => '<?php
|
|
function takesString(string $s): void {}
|
|
|
|
/** @return mixed */
|
|
function returnsMixed() {}
|
|
|
|
function shouldTakeString($s): void {
|
|
$s = returnsMixed();
|
|
takesString($s);
|
|
}',
|
|
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:7 - Parameter $s has no provided type,'
|
|
. ' could not infer',
|
|
'error_levels' => ['MixedArgument', 'InvalidReturnType', 'MixedAssignment'],
|
|
],
|
|
'psalmInvalidVar' => [
|
|
'<?php
|
|
class A
|
|
{
|
|
/** @psalm-var array<int, string> */
|
|
public $foo = [];
|
|
|
|
public function updateFoo(): void {
|
|
$this->foo["boof"] = "hello";
|
|
}
|
|
}',
|
|
'error_message' => 'InvalidPropertyAssignmentValue',
|
|
],
|
|
'incorrectDocblockOrder' => [
|
|
'<?php
|
|
class MyClass {
|
|
/**
|
|
* Comment
|
|
* @var $fooPropTypo string
|
|
*/
|
|
public $fooProp = "/tmp/file.txt";
|
|
}',
|
|
'error_message' => 'MissingDocblockType',
|
|
],
|
|
'badlyFormattedVar' => [
|
|
'<?php
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
function returns_strings() {
|
|
/** @var array(string) $result */
|
|
$result = ["example"];
|
|
return $result;
|
|
}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'badlyWrittenVar' => [
|
|
'<?php
|
|
/** @param mixed $x */
|
|
function myvalue($x): void {
|
|
/** @var $myVar MyNS\OtherClass */
|
|
$myVar = $x->conn()->method();
|
|
$myVar->otherMethod();
|
|
}',
|
|
'error_message' => 'MissingDocblockType',
|
|
],
|
|
'dontOverrideSameType' => [
|
|
'<?php
|
|
class A {
|
|
/** @return ?int */
|
|
public function foo(): ?int {
|
|
if (rand(0, 1)) return 5;
|
|
}
|
|
}',
|
|
'error_message' => 'InvalidReturnType',
|
|
],
|
|
'alwaysCheckReturnType' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
/**
|
|
* @return A
|
|
* @psalm-suppress MismatchingDocblockReturnType
|
|
*/
|
|
function foo(): B {
|
|
return new A;
|
|
}',
|
|
'error_message' => 'UndefinedClass',
|
|
],
|
|
'preventBadBoolean' => [
|
|
'<?php
|
|
function foo(): boolean {
|
|
return true;
|
|
}',
|
|
'error_message' => 'UndefinedClass',
|
|
],
|
|
'preventBadObjectLikeFormat' => [
|
|
'<?php
|
|
/**
|
|
* @param array{} $arr
|
|
*/
|
|
function bar(array $arr): void {}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'noPhpStormAnnotationsThankYou' => [
|
|
'<?php
|
|
/** @param ArrayIterator|string[] $i */
|
|
function takesArrayIteratorOfString(ArrayIterator $i): void {}',
|
|
'error_message' => 'MismatchingDocblockParamType',
|
|
],
|
|
'noPhpStormAnnotationsPossiblyInvalid' => [
|
|
'<?php
|
|
/** @param ArrayIterator|string[] $i */
|
|
function takesArrayIteratorOfString($i): void {
|
|
$s = $i->offsetGet("a");
|
|
}',
|
|
'error_message' => 'PossiblyInvalidMethodCall',
|
|
],
|
|
'arrayOfStringClasses' => [
|
|
'<?php
|
|
/**
|
|
* @param array<class-string> $arr
|
|
*/
|
|
function takesClassConstants(array $arr) : void {}
|
|
|
|
class A {}
|
|
class B {}
|
|
|
|
takesClassConstants(["A", "B"]);',
|
|
'error_message' => 'TypeCoercion',
|
|
],
|
|
'arrayOfNonExistentStringClasses' => [
|
|
'<?php
|
|
/**
|
|
* @param array<class-string> $arr
|
|
*/
|
|
function takesClassConstants(array $arr) : void {}
|
|
takesClassConstants(["A", "B"]);',
|
|
'error_message' => 'UndefinedClass',
|
|
'error_levels' => ['TypeCoercion'],
|
|
],
|
|
'singleClassConstantWithInvalidDocblock' => [
|
|
'<?php
|
|
/**
|
|
* @param clas-string $s
|
|
*/
|
|
function takesClassConstants(string $s) : void {}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'returnClassConstantDisallowCoercion' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
/**
|
|
* @return class-string
|
|
*/
|
|
function takesClassConstants() : string {
|
|
return "A";
|
|
}',
|
|
'error_message' => 'LessSpecificReturnStatement',
|
|
],
|
|
'returnClassConstantArrayDisallowCoercion' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
/**
|
|
* @return array<class-string>
|
|
*/
|
|
function takesClassConstants() : array {
|
|
return ["A", "B"];
|
|
}',
|
|
'error_message' => 'LessSpecificReturnStatement',
|
|
],
|
|
'returnClassConstantArrayAllowCoercionWithUndefinedClass' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
/**
|
|
* @return array<class-string>
|
|
*/
|
|
function takesClassConstants() : array {
|
|
return ["A", "B"];
|
|
}',
|
|
'error_message' => 'UndefinedClass',
|
|
'error_levels' => ['LessSpecificReturnStatement', 'MoreSpecificReturnType'],
|
|
],
|
|
'badStaticVar' => [
|
|
'<?php
|
|
/** @var static */
|
|
$a = new stdClass();',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'doubleBar' => [
|
|
'<?php
|
|
/** @param PDO||Closure|numeric $a */
|
|
function foo($a) : void {}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'badStringVar' => [
|
|
'<?php
|
|
/** @var string; */
|
|
$a = "hello";',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'badCallableVar' => [
|
|
'<?php
|
|
/** @return Closure(int): */
|
|
function foo() : callable {
|
|
return function () : void {}
|
|
}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'hyphenInType' => [
|
|
'<?php
|
|
/**
|
|
* @return - Description
|
|
*/
|
|
function example() {
|
|
return "placeholder";
|
|
}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'badAmpersand' => [
|
|
'<?php
|
|
/** @return &array */
|
|
function foo() : array {
|
|
return [];
|
|
}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
'invalidTypeAlias' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-type CoolType = A|B>
|
|
*/
|
|
|
|
class A {}',
|
|
'error_message' => 'InvalidDocblock',
|
|
],
|
|
];
|
|
}
|
|
}
|