1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00
psalm/tests/ClosureTest.php
Daniil Gentili 1986c8b4a8
Add support for strict arrays, fix type alias intersection, fix array_is_list assertion on non-lists (#8395)
* Immutable CodeLocation

* Remove excess clones

* Remove external clones

* Remove leftover clones

* Fix final clone issue

* Immutable storages

* Refactoring

* Fixes

* Fixes

* Fix

* Fix

* Fixes

* Simplify

* Fixes

* Fix

* Fixes

* Update

* Fix

* Cache global types

* Fix

* Update

* Update

* Fixes

* Fixes

* Refactor

* Fixes

* Fix

* Fix

* More caching

* Fix

* Fix

* Update

* Update

* Fix

* Fixes

* Update

* Refactor

* Update

* Fixes

* Break one more test

* Fix

* FIx

* Fix

* Fix

* Fix

* Fix

* Improve performance and readability

* Equivalent logic

* Fixes

* Revert

* Revert "Revert"

This reverts commit f9175100c8452c80559234200663fd4c4f4dd889.

* Fix

* Fix reference bug

* Make default TypeVisitor immutable

* Bugfix

* Remove clones

* Partial refactoring

* Refactoring

* Fixes

* Fix

* Fixes

* Fixes

* cs-fix

* Fix final bugs

* Add test

* Misc fixes

* Update

* Fixes

* Experiment with removing different property

* revert "Experiment with removing different property"

This reverts commit ac1156e077fc4ea633530d51096d27b6e88bfdf9.

* Uniform naming

* Uniform naming

* Hack hotfix

* Clean up $_FILES ref #8621

* Undo hack, try fixing properly

* Helper method

* Remove redundant call

* Partially fix bugs

* Cleanup

* Change defaults

* Fix bug

* Fix (?, hope this doesn't break anything else)

* cs-fix

* Review fixes

* Bugfix

* Bugfix

* Improve logic

* Add support for list{} and callable-list{} types, properly implement array_is_list assertions (fixes #8389)

* Default to sealed arrays

* Fix array_merge bug

* Fixes

* Fix

* Sealed type checks

* Properly infer properties-of and get_object_vars on final classes

* Fix array_map zipping

* Fix tests

* Fixes

* Fixes

* Fix more stuff

* Recursively resolve type aliases

* Fix typo

* Fixes

* Fix array_is_list assertion on keyed array

* Add BC docs

* Fixes

* fix

* Update

* Update

* Update

* Update

* Seal arrays with count assertions

* Fix #8528

* Fix

* Update

* Improve sealed array foreach logic

* get_object_vars on template properties

* Fix sealed array assertion reconciler logic

* Improved reconciler

* Add tests

* Single source of truth for test types

* Fix tests

* Fixup tests

* Fixup tests

* Fixup tests

* Update

* Fix tests

* Fix tests

* Final fixes

* Fixes

* Use list syntax only when needed

* Fix tests

* Cs-fix

* Update docs

* Update docs

* Update docs

* Update docs

* Update docs

* Document missing types

* Update docs

* Improve class-string-map docs

* Update

* Update

* I love working on psalm :)

* Keep arrays unsealed by default

* Fixup tests

* Fix syntax mistake

* cs-fix

* Fix typo

* Re-import missing types

* Keep strict types only in return types

* argc/argv fixes

* argc/argv fixes

* Fix test

* Comment-out valinor code, pinging @romm pls merge https://github.com/CuyZ/Valinor/pull/246 so we can add valinor to the psalm docs :)
2022-11-05 22:34:42 +01:00

1345 lines
48 KiB
PHP

<?php
namespace Psalm\Tests;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
use const DIRECTORY_SEPARATOR;
class ClosureTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
/**
*
*/
public function providerValidCodeParse(): iterable
{
return [
'byRefUseVar' => [
'code' => '<?php
/** @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
$bar = ["foo", "bar"];
$bam = array_map(
function(string $a) {
return $a . "blah";
},
$bar
);',
],
'inferredArgArrowFunction' => [
'code' => '<?php
$bar = ["foo", "bar"];
$bam = array_map(
fn(string $a) => $a . "blah",
$bar
);',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '7.4'
],
'varReturnType' => [
'code' => '<?php
$add_one = function(int $a) : int {
return $a + 1;
};
$a = $add_one(1);',
'assertions' => [
'$a' => 'int',
],
],
'varReturnTypeArray' => [
'code' => '<?php
$add_one = fn(int $a) : int => $a + 1;
$a = $add_one(1);',
'assertions' => [
'$a' => 'int',
],
'ignored_issues' => [],
'php_version' => '7.4'
],
'correctParamType' => [
'code' => '<?php
$take_string = function(string $s): string { return $s; };
$take_string("string");',
],
'arrayMapClosureVar' => [
'code' => '<?php
$mirror = function(int $i) : int { return $i; };
$a = array_map($mirror, [1, 2, 3]);',
'assertions' => [
'$a' => 'strict-list{int, int, int}',
],
],
'inlineCallableFunction' => [
'code' => '<?php
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
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
$a = array_map(
function(int $type, string ...$args):string {
return "hello";
},
[1, 2, 3]
);',
],
'returnsTypedClosure' => [
'code' => '<?php
/**
* @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
/**
* @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));
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '7.4'
],
'returnsTypedClosureWithClasses' => [
'code' => '<?php
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
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
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
/**
* @param strict-array{0:string,1:string}[] $ret
* @return strict-array{0:string,1:int}[]
*/
function f(array $ret) : array
{
return array_map(
/**
* @param strict-array{0:string,1:string} $row
*/
function (array $row) {
return [
strval($row[0]),
intval($row[1]),
];
},
$ret
);
}',
'assertions' => [],
'ignored_issues' => ['MissingClosureReturnType'],
],
'inferArrayMapReturnTypeWithTypehints' => [
'code' => '<?php
/**
* @param strict-array{0:string,1:string}[] $ret
* @return strict-array{0:string,1:int}[]
*/
function f(array $ret): array
{
return array_map(
/**
* @param strict-array{0:string,1:string} $row
*/
function (array $row): array {
return [
strval($row[0]),
intval($row[1]),
];
},
$ret
);
}',
],
'invokableProperties' => [
'code' => '<?php
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
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
$a = function() : Closure { return function() : string { return "hello"; }; };
$b = $a()();',
'assertions' => [
'$a' => 'pure-Closure():pure-Closure():string',
'$b' => 'string',
],
],
'voidReturningArrayMap' => [
'code' => '<?php
array_map(
function(int $i) : void {
echo $i;
},
[1, 2, 3]
);',
],
'closureFromCallableInvokableNamedClass' => [
'code' => '<?php
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
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
class Base {
public function publicMethod() : void {}
}
class Example extends Base {
public function test() : Closure {
return Closure::fromCallable([$this, "publicMethod"]);
}
}',
],
'protectedCallableFromInside' => [
'code' => '<?php
class Base {
protected function protectedMethod() : void {}
}
class Example extends Base {
public function test() : Closure {
return Closure::fromCallable([$this, "protectedMethod"]);
}
}',
],
'closureFromCallableNamedFunction' => [
'code' => '<?php
$closure = Closure::fromCallable("strlen");
',
'assertions' => [
'$closure' => 'pure-Closure(string):int<0, max>',
]
],
'allowClosureWithNarrowerReturn' => [
'code' => '<?php
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
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
/**
* @param Closure():int $x
*/
function accept_closure($x) : void {
$x();
}
accept_closure(
function (int $x = 5) : int {
return $x;
}
);',
],
'refineCallableTypeWithTypehint' => [
'code' => '<?php
/** @param string[][] $arr */
function foo(array $arr) : void {
array_map(
function(array $a) {
return reset($a);
},
$arr
);
}'
],
'refineCallableTypeWithoutTypehint' => [
'code' => '<?php
/** @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' => 'strict-list{stdClass}'
],
],
'CallableWithArrayReduce' => [
'code' => '<?php
/**
* @return callable(int, int): int
*/
function maker() {
return function(int $sum, int $e) {
return $sum + $e;
};
}
$maker = maker();
$result = array_reduce([1, 2, 3], $maker, 0);',
'assertions' => [
'$result' => 'int'
],
],
'FirstClassCallable:NamedFunction:is_int' => [
'code' => '<?php
$closure = is_int(...);
$result = $closure(1);
',
'assertions' => [
'$closure' => 'pure-Closure(mixed):bool',
'$result' => 'bool',
],
'ignored_issues' => [],
'php_version' => '8.1'
],
'FirstClassCallable:NamedFunction:strlen' => [
'code' => '<?php
$closure = strlen(...);
$result = $closure("test");
',
'assertions' => [
'$closure' => 'pure-Closure(string):int<0, max>',
'$result' => 'int<0, max>',
],
'ignored_issues' => [],
'php_version' => '8.1'
],
'FirstClassCallable:InstanceMethod:UserDefined' => [
'code' => '<?php
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'
],
'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'
],
'FirstClassCallable:StaticMethod' => [
'code' => '<?php
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'
],
'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");
',
'assertions' => [
'$length' => 'int',
],
'ignored_issues' => [],
'php_version' => '8.1'
],
'FirstClassCallable:InvokableObject' => [
'code' => '<?php
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'
],
'FirstClassCallable:FromClosure' => [
'code' => '<?php
$closure = fn (string $string): int => strlen($string);
$closure = $closure(...);
',
'assertions' => [
'$closure' => 'pure-Closure(string):int<0, max>',
],
'ignored_issues' => [],
'php_version' => '8.1'
],
'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:InheritedStaticMethod' => [
'code' => '<?php
abstract class A
{
public function foo(int $i): string
{
return (string) $i;
}
}
class C extends A {}
/** @param \Closure(int):string $_ */
function takesIntToString(\Closure $_): void {}
takesIntToString(C::foo(...));',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1',
],
'FirstClassCallable:InheritedStaticMethodWithStaticTypeParameter' => [
'code' => '<?php
/** @template T */
class Holder
{
/** @param T $value */
public function __construct(public $value) {}
}
abstract class A
{
final public function __construct(public int $i) {}
/** @return Holder<static> */
public static function create(int $i): Holder
{
return new Holder(new static($i));
}
}
class C extends A {}
/** @param \Closure(int):Holder<C> $_ */
function takesIntToHolder(\Closure $_): void {}
takesIntToHolder(C::create(...));'
],
'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' => 'strict-list{null, null, null}',
'$result2' => 'strict-list{string, string, string}',
'$result3' => 'strict-list{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',
],
'FirstClassCallable:AssignmentVisitorMap' => [
'code' => '<?php
class Test {
/** @var list<\Closure():void> */
public array $handlers = [];
public function register(): void {
foreach ([1, 2, 3] as $index) {
$this->push($this->handler(...));
}
}
/**
* @param Closure():void $closure
* @return void
*/
private function push(\Closure $closure): void {
$this->handlers[] = $closure;
}
private function handler(): void {
}
}
$test = new Test();
$test->register();
$handlers = $test->handlers;
',
'assertions' => [
'$handlers' => 'list<Closure():void>',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
'arrowFunctionReturnsNeverImplictly' => [
'code' => '<?php
$bar = ["foo", "bar"];
$bam = array_map(
fn(string $a) => throw new Exception($a),
$bar
);',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1'
],
'arrowFunctionReturnsNeverExplictly' => [
'code' => '<?php
$bar = ["foo", "bar"];
$bam = array_map(
/** @return never */
fn(string $a) => die(),
$bar
);',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1'
],
'unknownFirstClassCallable' => [
'code' => '<?php
/** @psalm-suppress UndefinedFunction */
unknown(...);',
],
];
}
/**
*
*/
public function providerInvalidCodeParse(): iterable
{
return [
'wrongArg' => [
'code' => '<?php
$bar = ["foo", "bar"];
$bam = array_map(
function(int $a): int {
return $a + 1;
},
$bar
);',
'error_message' => 'InvalidScalarArgument',
],
'noReturn' => [
'code' => '<?php
$bar = ["foo", "bar"];
$bam = array_map(
function(string $a): string {
},
$bar
);',
'error_message' => 'InvalidReturnType',
],
'possiblyNullFunctionCall' => [
'code' => '<?php
/**
* @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
$take_string = function(string $s): string { return $s; };
$take_string(42);',
'error_message' => 'InvalidScalarArgument',
],
'missingClosureReturnType' => [
'code' => '<?php
$a = function() {
return "foo";
};',
'error_message' => 'MissingClosureReturnType',
],
'returnsTypedClosureWithBadReturnType' => [
'code' => '<?php
/**
* @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
/**
* @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
/**
* @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
/**
* @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
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
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
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
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
$a = function() use ($i) {};',
'error_message' => 'UndefinedVariable',
],
'voidReturningArrayMap' => [
'code' => '<?php
$arr = array_map(
function(int $i) : void {
echo $i;
},
[1, 2, 3]
);
foreach ($arr as $a) {
if ($a) {}
}',
'error_message' => 'TypeDoesNotContainType',
],
'closureFromCallableInvokableNamedClassWrongArgs' => [
'code' => '<?php
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
class Foo {
public function __construct(UndefinedClass $o) {}
}
new Foo(function() : void {});',
'error_message' => 'UndefinedClass',
],
'useDuplicateName' => [
'code' => '<?php
$foo = "bar";
$a = function (string $foo) use ($foo) : string {
return $foo;
};',
'error_message' => 'DuplicateParam',
],
'privateCallable' => [
'code' => '<?php
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
/**
* @param Closure():int $x
*/
function accept_closure($x) : void {
$x();
}
accept_closure(
function (int $x) : int {
return $x;
}
);',
'error_message' => 'InvalidArgument',
],
'useClosureDocblockType' => [
'code' => '<?php
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, but parent type A provided',
],
'closureByRefUseToMixed' => [
'code' => '<?php
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',
],
];
}
}