1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-12 09:19:40 +01:00
psalm/tests/ClosureTest.php

1246 lines
45 KiB
PHP
Raw Normal View History

2019-11-30 05:46:21 +01:00
<?php
2019-11-30 05:46:21 +01:00
namespace Psalm\Tests;
2021-12-04 21:55:53 +01:00
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
use const DIRECTORY_SEPARATOR;
2019-11-30 05:46:21 +01:00
class ClosureTest extends TestCase
{
2021-12-04 21:55:53 +01:00
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
2019-11-30 05:46:21 +01:00
/**
2022-01-13 20:38:17 +01:00
* @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>}>
2019-11-30 05:46:21 +01:00
*/
public function providerValidCodeParse(): iterable
2019-11-30 05:46:21 +01:00
{
return [
'byRefUseVar' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/** @return void */
function run_function(\Closure $fnc) {
$fnc();
}
/**
* @return void
* @psalm-suppress MixedArgument
*/
function f() {
run_function(
/**
* @return void
*/
function() use(&$data) {
$data = 1;
}
);
echo $data;
}
f();',
],
'inferredArg' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$bar = ["foo", "bar"];
$bam = array_map(
function(string $a) {
return $a . "blah";
},
$bar
);',
],
'inferredArgArrowFunction' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$bar = ["foo", "bar"];
$bam = array_map(
fn(string $a) => $a . "blah",
$bar
);',
Test parallelization (#4045) * Run tests in random order Being able to run tests in any order is a pre-requisite for being able to run them in parallel. * Reset type coverage between tests, fix affected tests * Reset parser and lexer between test runs and on php version change Previously lexer was reset, but parser kept the reference to the old one, and reference to the parser was kept by StatementsProvider. This resulted in order-dependent tests - if the parser was first initialized with phpVersion set to 7.4 then arrow functions worked fine, but were failing when the parser was initially constructed with settings for 7.3 This can be demonstrated on current master by upgrading to nikic/php-parser:4.9 and running: ``` vendor/bin/phpunit --no-coverage --filter="inferredArgArrowFunction" tests/ClosureTest.php ``` Now all tests using PHP 7.4 features must set the PHP version accordingly. * Marked more tests using 7.4 syntax * Reset newline-between-annotation flag between tests * Resolve real paths before passing them to checkPaths When checkPaths is called from psalm.php the paths are resolved, so we just mimicking SUT behaviour here. * Restore newline-between-annotations in DocCommentTest * Tweak Appveyor caches * Tweak TravisCI caches * Tweak CircleCI caches * Run tests in parallel Use `vendor/bin/paratest` instead of `vendor/bin/phpunit` * Use default paratest runner on Windows WrapperRunner is not supported on Windows. * TRAVIS_TAG could be empty * Restore appveyor conditional caching
2020-08-23 16:32:07 +02:00
'assertions' => [],
'ignored_issues' => [],
'php_version' => '7.4'
2019-11-30 05:46:21 +01:00
],
'varReturnType' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$add_one = function(int $a) : int {
return $a + 1;
};
$a = $add_one(1);',
'assertions' => [
'$a' => 'int',
],
],
'varReturnTypeArray' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$add_one = fn(int $a) : int => $a + 1;
$a = $add_one(1);',
'assertions' => [
'$a' => 'int',
],
'ignored_issues' => [],
'php_version' => '7.4'
2019-11-30 05:46:21 +01:00
],
'correctParamType' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$take_string = function(string $s): string { return $s; };
$take_string("string");',
],
'arrayMapClosureVar' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$mirror = function(int $i) : int { return $i; };
$a = array_map($mirror, [1, 2, 3]);',
'assertions' => [
'$a' => 'array{int, int, int}<int>',
2019-11-30 05:46:21 +01:00
],
],
'inlineCallableFunction' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class A {
function bar(): void {
function foobar(int $a, int $b): int {
return $a > $b ? 1 : 0;
}
$arr = [5, 4, 3, 1, 2];
usort($arr, "fooBar");
}
}',
],
'closureSelf' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class A
{
/**
* @var self[]
*/
private $subitems;
/**
* @param self[] $in
*/
public function __construct(array $in = [])
{
array_map(function(self $i): self { return $i; }, $in);
$this->subitems = array_map(
function(self $i): self {
return $i;
},
$in
);
}
}
new A([new A, new A]);',
],
'arrayMapVariadicClosureArg' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$a = array_map(
function(int $type, string ...$args):string {
return "hello";
},
[1, 2, 3]
);',
],
'returnsTypedClosure' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/**
* @param Closure(int):int $f
* @param Closure(int):int $g
*
* @return Closure(int):int
*/
function foo(Closure $f, Closure $g) : Closure {
return function (int $x) use ($f, $g) : int {
return $f($g($x));
};
}',
],
'returnsTypedClosureArrow' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/**
* @param Closure(int):int $f
* @param Closure(int):int $g
*
* @return Closure(int):int
*/
function foo(Closure $f, Closure $g) : Closure {
return fn(int $x):int => $f($g($x));
}',
Test parallelization (#4045) * Run tests in random order Being able to run tests in any order is a pre-requisite for being able to run them in parallel. * Reset type coverage between tests, fix affected tests * Reset parser and lexer between test runs and on php version change Previously lexer was reset, but parser kept the reference to the old one, and reference to the parser was kept by StatementsProvider. This resulted in order-dependent tests - if the parser was first initialized with phpVersion set to 7.4 then arrow functions worked fine, but were failing when the parser was initially constructed with settings for 7.3 This can be demonstrated on current master by upgrading to nikic/php-parser:4.9 and running: ``` vendor/bin/phpunit --no-coverage --filter="inferredArgArrowFunction" tests/ClosureTest.php ``` Now all tests using PHP 7.4 features must set the PHP version accordingly. * Marked more tests using 7.4 syntax * Reset newline-between-annotation flag between tests * Resolve real paths before passing them to checkPaths When checkPaths is called from psalm.php the paths are resolved, so we just mimicking SUT behaviour here. * Restore newline-between-annotations in DocCommentTest * Tweak Appveyor caches * Tweak TravisCI caches * Tweak CircleCI caches * Run tests in parallel Use `vendor/bin/paratest` instead of `vendor/bin/phpunit` * Use default paratest runner on Windows WrapperRunner is not supported on Windows. * TRAVIS_TAG could be empty * Restore appveyor conditional caching
2020-08-23 16:32:07 +02:00
'assertions' => [],
'ignored_issues' => [],
'php_version' => '7.4'
2019-11-30 05:46:21 +01:00
],
'returnsTypedClosureWithClasses' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class A {}
class B {}
class C {}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return Closure(C):A
*/
function foo(Closure $f, Closure $g) : Closure {
return function (C $x) use ($f, $g) : A {
return $f($g($x));
};
}
$a = foo(
function(B $b) : A { return new A;},
function(C $c) : B { return new B;}
)(new C);',
'assertions' => [
'$a' => 'A',
],
],
'returnsTypedClosureWithSubclassParam' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class A {}
class B {}
class C {}
class C2 extends C {}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return Closure(C2):A
*/
function foo(Closure $f, Closure $g) : Closure {
return function (C $x) use ($f, $g) : A {
return $f($g($x));
};
}
$a = foo(
function(B $b) : A { return new A;},
function(C $c) : B { return new B;}
)(new C2);',
'assertions' => [
'$a' => 'A',
],
],
'returnsTypedClosureWithParentReturn' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class A {}
class B {}
class C {}
class A2 extends A {}
/**
* @param Closure(B):A2 $f
* @param Closure(C):B $g
*
* @return Closure(C):A
*/
function foo(Closure $f, Closure $g) : Closure {
return function (C $x) use ($f, $g) : A2 {
return $f($g($x));
};
}
$a = foo(
function(B $b) : A2 { return new A2;},
function(C $c) : B { return new B;}
)(new C);',
'assertions' => [
'$a' => 'A',
],
],
'inferArrayMapReturnTypeWithoutTypehints' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/**
* @param array{0:string,1:string}[] $ret
* @return array{0:string,1:int}[]
*/
function f(array $ret) : array
{
return array_map(
/**
* @param array{0:string,1:string} $row
*/
function (array $row) {
return [
strval($row[0]),
intval($row[1]),
];
},
$ret
);
}',
'assertions' => [],
'ignored_issues' => ['MissingClosureReturnType'],
2019-11-30 05:46:21 +01:00
],
'inferArrayMapReturnTypeWithTypehints' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/**
* @param array{0:string,1:string}[] $ret
* @return array{0:string,1:int}[]
*/
function f(array $ret): array
{
return array_map(
/**
* @param array{0:string,1:string} $row
*/
function (array $row): array {
return [
strval($row[0]),
intval($row[1]),
];
},
$ret
);
}',
],
'invokableProperties' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class A {
public function __invoke(): bool { return true; }
}
class C {
/** @var A $invokable */
private $invokable;
public function __construct(A $invokable) {
$this->invokable = $invokable;
}
public function callTheInvokableDirectly(): bool {
return ($this->invokable)();
}
public function callTheInvokableIndirectly(): bool {
$r = $this->invokable;
return $r();
}
}',
],
'mirrorCallableParams' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
namespace NS;
use Closure;
/** @param Closure(int):bool $c */
function acceptsIntToBool(Closure $c): void {}
acceptsIntToBool(Closure::fromCallable(function(int $n): bool { return $n > 0; }));',
],
'singleLineClosures' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$a = function() : Closure { return function() : string { return "hello"; }; };
$b = $a()();',
'assertions' => [
'$a' => 'pure-Closure():pure-Closure():string',
2019-11-30 05:46:21 +01:00
'$b' => 'string',
],
],
'voidReturningArrayMap' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
array_map(
function(int $i) : void {
echo $i;
},
[1, 2, 3]
);',
],
'closureFromCallableInvokableNamedClass' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
namespace NS;
use Closure;
/** @param Closure(int):bool $c */
function acceptsIntToBool(Closure $c): void {}
class NamedInvokable {
public function __invoke(int $p): bool {
return $p > 0;
}
}
acceptsIntToBool(Closure::fromCallable(new NamedInvokable));',
],
'closureFromCallableInvokableAnonymousClass' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
namespace NS;
use Closure;
/** @param Closure(int):bool $c */
function acceptsIntToBool(Closure $c): void {}
$anonInvokable = new class {
public function __invoke(int $p):bool {
return $p > 0;
}
};
acceptsIntToBool(Closure::fromCallable($anonInvokable));',
],
'publicCallableFromInside' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class Base {
public function publicMethod() : void {}
}
class Example extends Base {
public function test() : Closure {
return Closure::fromCallable([$this, "publicMethod"]);
}
}',
],
'protectedCallableFromInside' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class Base {
protected function protectedMethod() : void {}
}
class Example extends Base {
public function test() : Closure {
return Closure::fromCallable([$this, "protectedMethod"]);
}
}',
],
'closureFromCallableNamedFunction' => [
'code' => '<?php
2021-12-09 04:26:31 +01:00
$closure = Closure::fromCallable("strlen");
',
'assertions' => [
'$closure' => 'pure-Closure(string):int<0, max>',
2021-12-09 04:26:31 +01:00
]
],
2019-11-30 05:46:21 +01:00
'allowClosureWithNarrowerReturn' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class A {}
class B extends A {}
/**
* @param Closure():A $x
*/
function accept_closure($x) : void {
$x();
}
accept_closure(
function () : B {
return new B();
}
);',
],
'allowCallableWithWiderParam' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class A {}
class B extends A {}
/**
* @param Closure(B $a):A $x
*/
function accept_closure($x) : void {
$x(new B());
}
accept_closure(
function (A $a) : A {
return $a;
}
);',
],
'allowCallableWithOptionalArg' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/**
* @param Closure():int $x
*/
function accept_closure($x) : void {
$x();
}
accept_closure(
function (int $x = 5) : int {
return $x;
}
);',
],
'refineCallableTypeWithTypehint' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/** @param string[][] $arr */
function foo(array $arr) : void {
array_map(
function(array $a) {
return reset($a);
},
$arr
);
}'
],
'refineCallableTypeWithoutTypehint' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/** @param string[][] $arr */
function foo(array $arr) : void {
array_map(
function($a) {
return reset($a);
},
$arr
);
}'
],
'inferGeneratorReturnType' => [
'code' => '<?php
function accept(Generator $gen): void {}
accept(
(function() {
yield;
return 42;
})()
);'
],
'callingInvokeOnClosureIsSameAsCallingDirectly' => [
'code' => '<?php
class A {
/** @var Closure(int):int */
private Closure $a;
public function __construct() {
$this->a = fn(int $a) : int => $a + 5;
}
public function invoker(int $b) : int {
return $this->a->__invoke($b);
}
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '7.4'
],
'annotateShortClosureReturn' => [
'code' => '<?php
/** @psalm-suppress MissingReturnType */
function returnsBool() { return true; }
$a = fn() : bool => /** @var bool */ returnsBool();',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '7.4'
],
'rememberParentAssertions' => [
'code' => '<?php
class A {
public ?A $a = null;
public function foo() : void {}
}
function doFoo(A $a): void {
if ($a->a instanceof A) {
function () use ($a): void {
$a->a->foo();
};
}
}'
],
'CallableWithArrayMap' => [
'code' => '<?php
/**
* @psalm-template T
* @param class-string<T> $className
* @return callable(...mixed):T
*/
function maker(string $className) {
return function(...$args) use ($className) {
/** @psalm-suppress MixedMethodCall */
return new $className(...$args);
};
}
$maker = maker(stdClass::class);
$result = array_map($maker, ["abc"]);',
'assertions' => [
'$result' => 'array{stdClass}<stdClass>'
],
],
2021-12-09 04:26:31 +01:00
'FirstClassCallable:NamedFunction:is_int' => [
'code' => '<?php
2021-12-09 04:26:31 +01:00
$closure = is_int(...);
$result = $closure(1);
',
'assertions' => [
'$closure' => 'pure-Closure(mixed):bool',
'$result' => 'bool',
],
'ignored_issues' => [],
'php_version' => '8.1'
2021-12-09 04:26:31 +01:00
],
'FirstClassCallable:NamedFunction:strlen' => [
'code' => '<?php
2021-12-09 04:26:31 +01:00
$closure = strlen(...);
$result = $closure("test");
',
'assertions' => [
'$closure' => 'pure-Closure(string):int<0, max>',
'$result' => 'int<0, max>',
2021-12-09 04:26:31 +01:00
],
'ignored_issues' => [],
'php_version' => '8.1'
2021-12-09 04:26:31 +01:00
],
'FirstClassCallable:InstanceMethod:UserDefined' => [
'code' => '<?php
2021-12-09 04:26:31 +01:00
class Test {
public function __construct(private readonly string $string) {
}
public function length(): int {
return strlen($this->string);
}
}
$test = new Test("test");
$closure = $test->length(...);
$length = $closure();
',
'assertions' => [
'$length' => 'int',
],
'ignored_issues' => [],
'php_version' => '8.1'
2021-12-09 04:26:31 +01:00
],
'FirstClassCallable:InstanceMethod:Expr' => [
'code' => '<?php
class Test {
public function __construct(private readonly string $string) {
}
public function length(): int {
return strlen($this->string);
}
}
$test = new Test("test");
$method_name = "length";
$closure = $test->$method_name(...);
$length = $closure();
',
'assertions' => [
'$length' => 'int',
],
'ignored_issues' => [],
'php_version' => '8.1'
],
'FirstClassCallable:InstanceMethod:BuiltIn' => [
'code' => '<?php
$queue = new \SplQueue;
$closure = $queue->count(...);
$count = $closure();
',
'assertions' => [
'$count' => 'int',
],
'ignored_issues' => [],
'php_version' => '8.1'
],
2021-12-09 04:26:31 +01:00
'FirstClassCallable:StaticMethod' => [
'code' => '<?php
2021-12-09 04:26:31 +01:00
class Test {
public static function length(string $param): int {
return strlen($param);
}
}
$closure = Test::length(...);
$length = $closure("test");
',
'assertions' => [
'$length' => 'int',
],
'ignored_issues' => [],
'php_version' => '8.1'
2021-12-09 04:26:31 +01:00
],
'FirstClassCallable:StaticMethod:Expr' => [
'code' => '<?php
class Test {
public static function length(string $param): int {
return strlen($param);
}
}
$method_name = "length";
$closure = Test::$method_name(...);
$length = $closure("test");
',
2021-12-09 04:26:31 +01:00
'assertions' => [
'$length' => 'int',
],
'ignored_issues' => [],
'php_version' => '8.1'
2021-12-09 04:26:31 +01:00
],
'FirstClassCallable:InvokableObject' => [
'code' => '<?php
2021-12-09 04:26:31 +01:00
class Test {
public function __invoke(string $param): int {
return strlen($param);
}
}
$test = new Test();
$closure = $test(...);
$length = $closure("test");
',
'assertions' => [
'$length' => 'int',
],
'ignored_issues' => [],
'php_version' => '8.1'
2021-12-09 04:26:31 +01:00
],
'FirstClassCallable:FromClosure' => [
'code' => '<?php
2021-12-09 04:26:31 +01:00
$closure = fn (string $string): int => strlen($string);
$closure = $closure(...);
',
'assertions' => [
'$closure' => 'pure-Closure(string):int<0, max>',
2021-12-09 04:26:31 +01:00
],
'ignored_issues' => [],
'php_version' => '8.1'
2021-12-09 04:26:31 +01:00
],
'FirstClassCallable:MagicInstanceMethod' => [
'code' => '<?php
/**
* @method int length()
*/
class Test {
public function __construct(private readonly string $string) {
}
public function __call(string $name, array $args): mixed {
return match ($name) {
"length" => strlen($this->string),
default => throw new \Error("Undefined method"),
};
}
}
$test = new Test("test");
$closure = $test->length(...);
$length = $closure();
',
'assertions' => [
'$length' => 'int',
],
'ignored_issues' => [],
'php_version' => '8.1'
],
'FirstClassCallable:MagicStaticMethod' => [
'code' => '<?php
/**
* @method static int length(string $length)
*/
class Test {
public static function __callStatic(string $name, array $args): mixed {
return match ($name) {
"length" => strlen((string) $args[0]),
default => throw new \Error("Undefined method"),
};
}
}
$closure = Test::length(...);
$length = $closure("test");
',
'assertions' => [
'$length' => 'int',
],
'ignored_issues' => [],
'php_version' => '8.1'
],
'FirstClassCallable:WithArrayMap' => [
'code' => '<?php
$array = [1, 2, 3];
$closure = fn (int $value): int => $value * $value;
$result1 = array_map((new \SplQueue())->enqueue(...), $array);
$result2 = array_map(strval(...), $array);
$result3 = array_map($closure(...), $array);
',
'assertions' => [
'$result1' => 'array{null, null, null}<null>',
'$result2' => 'array{string, string, string}<string>',
'$result3' => 'array{int, int, int}<int>',
],
'ignored_issues' => [],
'php_version' => '8.1'
],
'FirstClassCallable:array_map' => [
'code' => '<?php call_user_func(array_map(...), intval(...), ["1"]);',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1',
],
2022-01-06 21:12:51 +01:00
'arrowFunctionReturnsNeverImplictly' => [
2022-01-14 21:13:34 +01:00
'code' => '<?php
2022-01-06 21:12:51 +01:00
$bar = ["foo", "bar"];
$bam = array_map(
fn(string $a) => throw new Exception($a),
$bar
);',
'assertions' => [],
2022-01-14 21:13:34 +01:00
'ignored_issues' => [],
'php_version' => '8.1'
2022-01-06 21:12:51 +01:00
],
'arrowFunctionReturnsNeverExplictly' => [
2022-01-14 21:13:34 +01:00
'code' => '<?php
2022-01-06 21:12:51 +01:00
$bar = ["foo", "bar"];
$bam = array_map(
/** @return never */
fn(string $a) => die(),
$bar
);',
'assertions' => [],
2022-01-14 21:13:34 +01:00
'ignored_issues' => [],
'php_version' => '8.1'
2022-01-06 21:12:51 +01:00
],
'unknownFirstClassCallable' => [
'code' => '<?php
2022-01-31 21:51:31 +01:00
/** @psalm-suppress UndefinedFunction */
unknown(...);',
],
2019-11-30 05:46:21 +01:00
];
}
/**
2022-01-13 20:38:17 +01:00
* @return iterable<string,array{code:string,error_message:string,ignored_issues?:list<string>,php_version?:string}>
2019-11-30 05:46:21 +01:00
*/
public function providerInvalidCodeParse(): iterable
2019-11-30 05:46:21 +01:00
{
return [
'wrongArg' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$bar = ["foo", "bar"];
$bam = array_map(
function(int $a): int {
return $a + 1;
},
$bar
);',
'error_message' => 'InvalidScalarArgument',
],
'noReturn' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$bar = ["foo", "bar"];
$bam = array_map(
function(string $a): string {
},
$bar
);',
'error_message' => 'InvalidReturnType',
],
'possiblyNullFunctionCall' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/**
* @var Closure|null $foo
*/
$foo = null;
$foo =
/**
* @param mixed $bar
* @psalm-suppress MixedFunctionCall
*/
function ($bar) use (&$foo): string
{
if (is_array($bar)) {
return $foo($bar);
}
return $bar;
};',
'error_message' => 'MixedReturnStatement',
],
'wrongParamType' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$take_string = function(string $s): string { return $s; };
$take_string(42);',
'error_message' => 'InvalidScalarArgument',
],
'missingClosureReturnType' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$a = function() {
return "foo";
};',
'error_message' => 'MissingClosureReturnType',
],
'returnsTypedClosureWithBadReturnType' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/**
* @param Closure(int):int $f
* @param Closure(int):int $g
*
* @return Closure(int):string
*/
function foo(Closure $f, Closure $g) : Closure {
return function (int $x) use ($f, $g) : int {
return $f($g($x));
};
}',
'error_message' => 'InvalidReturnStatement',
],
'returnsTypedCallableWithBadReturnType' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/**
* @param Closure(int):int $f
* @param Closure(int):int $g
*
* @return callable(int):string
*/
function foo(Closure $f, Closure $g) : callable {
return function (int $x) use ($f, $g) : int {
return $f($g($x));
};
}',
'error_message' => 'InvalidReturnStatement',
],
'returnsTypedClosureWithBadParamType' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/**
* @param Closure(int):int $f
* @param Closure(int):int $g
*
* @return Closure(string):int
*/
function foo(Closure $f, Closure $g) : Closure {
return function (int $x) use ($f, $g) : int {
return $f($g($x));
};
}',
'error_message' => 'InvalidReturnStatement',
],
'returnsTypedCallableWithBadParamType' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/**
* @param Closure(int):int $f
* @param Closure(int):int $g
*
* @return callable(string):int
*/
function foo(Closure $f, Closure $g) : callable {
return function (int $x) use ($f, $g) : int {
return $f($g($x));
};
}',
'error_message' => 'InvalidReturnStatement',
],
'returnsTypedClosureWithBadCall' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class A {}
class B {}
class C {}
class D {}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return Closure(C):A
*/
function foo(Closure $f, Closure $g) : Closure {
return function (int $x) use ($f, $g) : int {
return $f($g($x));
};
}',
'error_message' => 'InvalidArgument',
],
'returnsTypedClosureWithSubclassParam' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class A {}
class B {}
class C {}
class C2 extends C {}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return Closure(C):A
*/
function foo(Closure $f, Closure $g) : Closure {
return function (C2 $x) use ($f, $g) : A {
return $f($g($x));
};
}',
'error_message' => 'LessSpecificReturnStatement',
],
'returnsTypedClosureWithSubclassReturn' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class A {}
class B {}
class C {}
class A2 extends A {}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return Closure(C):A2
*/
function foo(Closure $f, Closure $g) : Closure {
return function (C $x) use ($f, $g) : A {
return $f($g($x));
};
}',
'error_message' => 'LessSpecificReturnStatement',
],
'returnsTypedClosureFromCallable' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class A {}
class B {}
class C {}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return callable(C):A
*/
function foo(Closure $f, Closure $g) : callable {
return function (C $x) use ($f, $g) : A {
return $f($g($x));
};
}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return Closure(C):A
*/
function bar(Closure $f, Closure $g) : Closure {
return foo($f, $g);
}',
'error_message' => 'LessSpecificReturnStatement',
],
'undefinedVariable' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$a = function() use ($i) {};',
'error_message' => 'UndefinedVariable',
],
'voidReturningArrayMap' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$arr = array_map(
function(int $i) : void {
echo $i;
},
[1, 2, 3]
);
foreach ($arr as $a) {
if ($a) {}
}',
'error_message' => 'TypeDoesNotContainType',
],
'closureFromCallableInvokableNamedClassWrongArgs' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
namespace NS;
use Closure;
/** @param Closure(string):bool $c */
function acceptsIntToBool(Closure $c): void {}
class NamedInvokable {
public function __invoke(int $p): bool {
return $p > 0;
}
}
acceptsIntToBool(Closure::fromCallable(new NamedInvokable));',
'error_message' => 'InvalidScalarArgument',
],
'undefinedClassForCallable' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class Foo {
public function __construct(UndefinedClass $o) {}
}
new Foo(function() : void {});',
'error_message' => 'UndefinedClass',
],
'useDuplicateName' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
$foo = "bar";
$a = function (string $foo) use ($foo) : string {
return $foo;
};',
'error_message' => 'DuplicateParam',
],
'privateCallable' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class Base {
private function privateMethod() : void {}
}
class Example extends Base {
public function test() : Closure {
return Closure::fromCallable([$this, "privateMethod"]);
}
}',
'error_message' => 'InvalidArgument',
],
'prohibitCallableWithRequiredArg' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
/**
* @param Closure():int $x
*/
function accept_closure($x) : void {
$x();
}
accept_closure(
function (int $x) : int {
return $x;
}
);',
'error_message' => 'InvalidArgument',
],
'useClosureDocblockType' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
class A {}
class B extends A {}
function takesA(A $_a) : void {}
function takesB(B $_b) : void {}
$getAButReallyB = /** @return A */ function() {
return new B;
};
takesA($getAButReallyB());
takesB($getAButReallyB());',
'error_message' => 'ArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:28 - Argument 1 of takesB expects B, parent type A provided',
2019-11-30 05:46:21 +01:00
],
'closureByRefUseToMixed' => [
'code' => '<?php
2019-11-30 05:46:21 +01:00
function assertInt(int $int): int {
$s = static function() use(&$int): void {
$int = "42";
};
$s();
return $int;
}',
'error_message' => 'MixedReturnStatement'
],
'noCrashWhenComparingIllegitimateCallable' => [
'code' => '<?php
class C {}
function foo() : C {
return fn (int $i) => "";
}',
'error_message' => 'InvalidReturnStatement',
'ignored_issues' => [],
'php_version' => '7.4',
],
'detectImplicitVoidReturn' => [
'code' => '<?php
/**
* @param Closure():Exception $c
*/
function takesClosureReturningException(Closure $c) : void {
echo $c()->getMessage();
}
takesClosureReturningException(
function () {
echo "hello";
}
);',
'error_message' => 'InvalidArgument'
],
'undefinedVariableInEncapsedString' => [
'code' => '<?php
fn(): string => "$a";
',
'error_message' => 'UndefinedVariable',
'ignored_issues' => [],
'php_version' => '7.4'
],
'undefinedVariableInStringCast' => [
'code' => '<?php
fn(): string => (string) $a;
',
'error_message' => 'UndefinedVariable',
'ignored_issues' => [],
'php_version' => '7.4'
],
'forbidTemplateAnnotationOnClosure' => [
'code' => '<?php
/** @template T */
function (): void {};
',
'error_message' => 'InvalidDocblock',
],
'forbidTemplateAnnotationOnShortClosure' => [
'code' => '<?php
/** @template T */
fn(): bool => false;
',
'error_message' => 'InvalidDocblock',
'ignored_issues' => [],
'php_version' => '7.4'
],
'closureInvalidArg' => [
'code' => '<?php
/** @param Closure(int): string $c */
function takesClosure(Closure $c): void {}
takesClosure(5);',
'error_message' => 'InvalidArgument',
],
'FirstClassCallable:UndefinedMethod' => [
'code' => '<?php
$queue = new \SplQueue;
$closure = $queue->undefined(...);
$count = $closure();
',
'error_message' => 'UndefinedMethod',
'ignored_issues' => [],
'php_version' => '8.1'
],
'FirstClassCallable:UndefinedMagicInstanceMethod' => [
'code' => '<?php
class Test {
public function __call(string $name, array $args): mixed {
return match ($name) {
default => throw new \Error("Undefined method"),
};
}
}
$test = new Test();
$closure = $test->length(...);
$length = $closure();
',
'error_message' => 'UndefinedMagicMethod',
'ignored_issues' => [],
'php_version' => '8.1'
],
'FirstClassCallable:UndefinedMagicStaticMethod' => [
'code' => '<?php
class Test {
public static function __callStatic(string $name, array $args): mixed {
return match ($name) {
default => throw new \Error("Undefined method"),
};
}
}
$closure = Test::length(...);
$length = $closure();
',
'error_message' => 'MixedAssignment',
'ignored_issues' => [],
'php_version' => '8.1',
],
2019-11-30 05:46:21 +01:00
];
}
}