1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00
psalm/tests/AnnotationTest.php

1511 lines
47 KiB
PHP

<?php
namespace Psalm\Tests;
use Psalm\Config;
use Psalm\Context;
class AnnotationTest extends TestCase
{
use Traits\InvalidCodeAnalysisTestTrait;
use Traits\ValidCodeAnalysisTestTrait;
/**
* @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 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 testDocumentedThrowInFunctionCallWithoutThrow()
{
Config::getInstance()->check_for_throws_docblock = true;
$this->addFile(
'somefile.php',
'<?php
class Foo
{
/**
* @throws \TypeError
*/
public static function notReallyThrowing(int $a): string
{
if ($a > 0) {
return "";
}
return (string) $a;
}
public function test(): string
{
try {
return self::notReallyThrowing(2);
} catch (\Throwable $E) {
return "";
}
}
}'
);
$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 providerValidCodeParse()
{
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',
],
],
'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 A_OR_B = A|B
* @psalm-type CoolType = A_OR_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());'
],
'typeAliasInSeparateBlockBeforeFunction' => [
'<?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());'
],
'typeAliasUsedTwice' => [
'<?php
/** @psalm-type TA = array<int, string> */
class Bar {
public function foo() : void {
$bar =
/** @return TA */
function() {
return ["hello"];
};
/** @var array<int, TA> */
$bat = [$bar(), $bar()];
foreach ($bat as $b) {
echo $b[0];
}
}
}
/**
* @psalm-type _A=array{elt:int}
* @param _A $p
* @return _A
*/
function f($p) {
/** @var _A */
$r = $p;
return $r;
}',
],
'listUnpackWithDocblock' => [
'<?php
interface I {}
class A implements I {
public function bar() : void {}
}
/** @return I[] */
function foo() : array {
return [new A()];
}
/** @var A $a1 */
[$a1, $a2] = foo();
$a1->bar();',
],
'spaceInType' => [
'<?php
/** @return string | null */
function foo(string $s = null) {
return $s;
}',
],
'missingReturnTypeWithBadDocblockIgnoreBoth' => [
'<?php
/**
* @return [bad]
*/
function fooBar() {
}',
[],
[
'InvalidDocblock' => \Psalm\Config::REPORT_INFO,
'MissingReturnType' => \Psalm\Config::REPORT_INFO,
]
],
];
}
/**
* @return array
*/
public function providerInvalidCodeParse()
{
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',
],
'missingReturnTypeWithBadDocblock' => [
'<?php
/**
* @return [bad]
*/
function fooBar() {
}',
'error_message' => 'MissingReturnType',
[
'InvalidDocblock' => \Psalm\Config::REPORT_INFO,
]
],
'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',
],
'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',
],
'typeAliasInObjectLike' => [
'<?php
/**
* @psalm-type aType null|"a"|"b"|"c"|"d"
*/
/** @psalm-return array{0:bool,1:aType} */
function f(): array {
return [(bool)rand(0,1), rand(0,1) ? "z" : null];
}',
'error_message' => 'InvalidReturnStatement',
],
];
}
}