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

1455 lines
48 KiB
PHP
Raw Normal View History

<?php
namespace Psalm\Tests;
2021-12-04 21:55:53 +01:00
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
class CallableTest extends TestCase
{
2021-12-04 21:55:53 +01:00
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
2017-01-13 20:07:23 +01:00
/**
2019-03-01 21:55:20 +01:00
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
2017-01-13 20:07:23 +01:00
*/
public function providerValidCodeParse(): iterable
{
return [
'byRefUseVar' => [
'<?php
/** @return void */
function run_function(\Closure $fnc) {
$fnc();
}
2017-06-29 16:22:49 +02:00
/**
* @return void
* @psalm-suppress MixedArgument
*/
function f() {
run_function(
/**
* @return void
*/
function() use(&$data) {
$data = 1;
}
);
echo $data;
}
2017-06-29 16:22:49 +02:00
f();',
],
'inferredArg' => [
'<?php
$bar = ["foo", "bar"];
2017-06-29 16:22:49 +02:00
$bam = array_map(
function(string $a) {
return $a . "blah";
},
$bar
2017-05-27 02:05:57 +02:00
);',
],
'inferredArgArrowFunction' => [
'<?php
$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' => [],
'error_levels' => [],
'7.4',
],
2021-12-11 20:51:18 +01:00
'inferArgFromClassContext' => [
'<?php
final class Calc
{
/**
* @param Closure(int, int): int $_fn
*/
2021-12-11 20:51:18 +01:00
public function __invoke(Closure $_fn): int
2021-12-11 20:51:18 +01:00
{
return $_fn(42, 42);
}
}
$calc = new Calc();
$a = $calc(fn($a, $b) => $a + $b);',
'assertions' => [
'$a' => 'int',
],
'error_levels' => [],
'7.4',
],
'inferArgFromClassContextWithNamedArguments' => [
'<?php
final class Calc
{
/**
* @param Closure(int, int): int ...$_fn
*/
public function __invoke(Closure ...$_fn): int
{
throw new RuntimeException("???");
}
}
$calc = new Calc();
$a = $calc(
foo: fn($_a, $_b) => $_a + $_b,
bar: fn($_a, $_b) => $_a + $_b,
);',
'assertions' => [
'$a' => 'int',
],
'error_levels' => [],
'7.4',
],
'inferArgFromClassContextInGenericContext' => [
'<?php
/**
* @template A
*/
final class ArrayList
{
/**
* @template B
* @param Closure(A): B $ab
* @return ArrayList<B>
*/
public function map(Closure $ab): ArrayList
{
throw new RuntimeException("???");
}
}
/**
* @template T
* @param ArrayList<T> $list
* @return ArrayList<array{T}>
*/
function asTupled(ArrayList $list): ArrayList
{
return $list->map(function ($_a) {
return [$_a];
});
}
/** @var ArrayList<int> $a */
$a = new ArrayList();
2021-12-22 13:10:56 +01:00
$b = asTupled($a);',
'assertions' => [
'$b' => 'ArrayList<array{int}>',
],
],
'inferArgByPreviousFunctionArg' => [
'<?php
/**
* @template A
* @template B
*
* @param iterable<array-key, A> $_collection
* @param callable(A): B $_ab
* @return list<B>
*/
function map(iterable $_collection, callable $_ab) { return []; }
/** @template T */
final class Foo
{
/** @return Foo<int> */
public function toInt() { throw new RuntimeException("???"); }
}
/** @var list<Foo<string>> */
$items = [];
$inferred = map($items, function ($i) {
return $i->toInt();
});',
'assertions' => [
'$inferred' => 'list<Foo<int>>',
],
],
'inferTemplateForExplicitlyTypedArgByPreviousFunctionArg' => [
'<?php
/**
* @template A
* @template B
*
* @param iterable<array-key, A> $_collection
* @param callable(A): B $_ab
* @return list<B>
*/
function map(iterable $_collection, callable $_ab) { return []; }
/** @template T */
final class Foo
{
/** @return Foo<int> */
public function toInt() { throw new RuntimeException("???"); }
}
/** @var list<Foo<string>> */
$items = [];
$inferred = map($items, function (Foo $i) {
return $i->toInt();
});',
'assertions' => [
'$inferred' => 'list<Foo<int>>',
],
],
'doNotInferTemplateForExplicitlyTypedWithPhpdocArgByPreviousFunctionArg' => [
'<?php
/**
* @template A
* @template B
*
* @param iterable<array-key, A> $_collection
* @param callable(A): B $_ab
* @return list<B>
*/
function map(iterable $_collection, callable $_ab) { return []; }
/** @template T */
2021-12-27 17:43:06 +01:00
final class Foo { }
/** @var list<Foo<string>> */
$items = [];
$inferred = map($items,
/** @param Foo $i */
function ($i) {
return $i;
}
);',
'assertions' => [
'$inferred' => 'list<Foo>',
],
],
'varReturnType' => [
'<?php
$add_one = function(int $a) : int {
return $a + 1;
};
2017-06-29 16:22:49 +02:00
$a = $add_one(1);',
'assertions' => [
2017-06-29 16:22:49 +02:00
'$a' => 'int',
2017-05-27 02:05:57 +02:00
],
],
'varReturnTypeArray' => [
'<?php
$add_one = fn(int $a) : int => $a + 1;
$a = $add_one(1);',
'assertions' => [
'$a' => 'int',
],
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
'error_levels' => [],
'7.4',
],
2018-03-27 07:05:11 +02:00
'varCallableParamReturnType' => [
'<?php
$add_one = function(int $a): int {
return $a + 1;
};
/**
* @param callable(int) : int $c
*/
function bar(callable $c) : int {
return $c(1);
}
bar($add_one);',
],
'callableToClosure' => [
'<?php
/**
* @return callable
*/
function foo() {
2018-01-11 21:50:45 +01:00
return function(string $a): string {
return $a . "blah";
};
2017-05-27 02:05:57 +02:00
}',
],
'callableToClosureArrow' => [
'<?php
/**
* @return callable
*/
function foo() {
return fn(string $a): string => $a . "blah";
}',
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' => [],
'error_levels' => [],
'7.4',
],
'callable' => [
'<?php
2018-01-11 21:50:45 +01:00
function foo(callable $c): void {
echo (string)$c();
2017-05-27 02:05:57 +02:00
}',
],
'callableClass' => [
'<?php
class C {
2018-01-11 21:50:45 +01:00
public function __invoke(): string {
return "You ran?";
}
}
2017-06-29 16:22:49 +02:00
2018-01-11 21:50:45 +01:00
function foo(callable $c): void {
echo (string)$c();
}
2017-06-29 16:22:49 +02:00
foo(new C());
2017-06-29 16:22:49 +02:00
$c2 = new C();
2017-05-27 02:05:57 +02:00
$c2();',
],
2022-01-06 21:22:18 +01:00
'invokeMethodExists' => [
'<?php
function call(object $obj): void {
if (!method_exists($obj, "__invoke")) {
return;
}
$obj();
}',
],
'correctParamType' => [
'<?php
2018-01-11 21:50:45 +01:00
$take_string = function(string $s): string { return $s; };
2017-05-27 02:05:57 +02:00
$take_string("string");',
],
'callableMethodStringCallable' => [
'<?php
class A {
2018-01-11 21:50:45 +01:00
public static function bar(string $a): string {
return $a . "b";
}
}
2018-01-11 21:50:45 +01:00
function foo(callable $c): void {}
foo("A::bar");
foo(A::class . "::bar");',
],
'callableMethodArrayCallable' => [
'<?php
class A {
public static function bar(string $a): string {
return $a . "b";
}
}
function foo(callable $c): void {}
foo(["A", "bar"]);
foo([A::class, "bar"]);
$a = new A();
foo([$a, "bar"]);',
],
'callableMethodArrayCallableMissingTypes' => [
'<?php
function foo(callable $c): void {}
/** @psalm-suppress MissingParamType */
function bar($a, $b) : void {
foo([$a, $b]);
}',
],
'arrayMapCallableMethod' => [
'<?php
class A {
2018-01-11 21:50:45 +01:00
public static function bar(string $a): string {
return $a . "b";
}
}
2018-01-11 21:50:45 +01:00
function baz(string $a): string {
return $a . "b";
}
$a = array_map("A::bar", ["one", "two"]);
$b = array_map(["A", "bar"], ["one", "two"]);
$c = array_map([A::class, "bar"], ["one", "two"]);
$d = array_map([new A(), "bar"], ["one", "two"]);
$a_instance = new A();
$e = array_map([$a_instance, "bar"], ["one", "two"]);
$f = array_map("baz", ["one", "two"]);',
'assertions' => [
2020-07-07 16:41:46 +02:00
'$a' => 'array{string, string}',
'$b' => 'array{string, string}',
'$c' => 'array{string, string}',
'$d' => 'array{string, string}',
'$e' => 'array{string, string}',
'$f' => 'array{string, string}',
],
],
'arrayCallableMethod' => [
'<?php
class A {
2018-01-11 21:50:45 +01:00
public static function bar(string $a): string {
return $a . "b";
}
}
2018-01-11 21:50:45 +01:00
function foo(callable $c): void {}
foo(["A", "bar"]);',
],
'callableFunction' => [
'<?php
2018-01-11 21:50:45 +01:00
function foo(callable $c): void {}
foo("trim");',
],
'inlineCallableFunction' => [
'<?php
class A {
2018-01-11 21:50:45 +01:00
function bar(): void {
function foobar(int $a, int $b): int {
return $a > $b ? 1 : 0;
}
$arr = [5, 4, 3, 1, 2];
2017-08-18 23:23:12 +02:00
usort($arr, "fooBar");
}
}',
],
'closureSelf' => [
'<?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]);',
],
'possiblyUndefinedFunction' => [
'<?php
/**
* @param string|callable $middlewareOrPath
*/
function pipe($middlewareOrPath, ?callable $middleware = null): void { }
pipe("zzzz", function() : void {});',
],
'callableWithNonInvokable' => [
'<?php
function asd(): void {}
class B {}
/**
* @param callable|B $p
*/
function passes($p): void {}
passes("asd");',
],
'callableWithInvokable' => [
'<?php
function asd(): void {}
class A { public function __invoke(): void {} }
/**
* @param callable|A $p
*/
function fails($p): void {}
fails("asd");',
],
'isCallableArray' => [
'<?php
class A
{
public function callMeMaybe(string $method): void
{
$handleMethod = [$this, $method];
if (is_callable($handleMethod)) {
$handleMethod();
}
}
public function foo(): void {}
}
$a = new A();
$a->callMeMaybe("foo");',
],
'isCallableString' => [
'<?php
function foo(): void {}
function callMeMaybe(string $method): void {
if (is_callable($method)) {
$method();
}
}
callMeMaybe("foo");',
],
'allowVoidCallable' => [
'<?php
/**
* @param callable():void $p
*/
function doSomething($p): void {}
doSomething(function(): bool { return false; });',
],
2018-05-09 01:49:25 +02:00
'callableProperties' => [
'<?php
class C {
/** @psalm-var callable():bool */
private $callable;
/**
* @psalm-param callable():bool $callable
*/
public function __construct(callable $callable) {
$this->callable = $callable;
}
public function callTheCallableDirectly(): bool {
return ($this->callable)();
}
public function callTheCallableIndirectly(): bool {
$r = $this->callable;
return $r();
}
}',
],
'invokableProperties' => [
'<?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();
}
}',
],
2018-06-09 05:54:07 +02:00
'nullableReturnTypeShorthand' => [
'<?php
2019-02-11 07:57:28 +01:00
class A {}
2018-06-09 05:54:07 +02:00
/** @param callable(mixed):?A $a */
function foo(callable $a): void {}',
],
'callablesCanBeObjects' => [
'<?php
function foo(callable $c) : void {
if (is_object($c)) {
$c();
}
2019-03-23 19:27:54 +01:00
}',
],
'objectsCanBeCallable' => [
'<?php
function foo(object $c) : void {
if (is_callable($c)) {
$c();
}
2019-03-23 19:27:54 +01:00
}',
],
'unionCanBeCallable' => [
'<?php
class A {}
class B {
public function __invoke() : string {
return "hello";
}
}
/**
* @param A|B $c
*/
function foo($c) : void {
if (is_callable($c)) {
$c();
}
2019-03-23 19:27:54 +01:00
}',
],
'goodCallableArgs' => [
'<?php
/**
* @param callable(string,string):int $_p
*/
function f(callable $_p): void {}
class C {
public static function m(string $a, string $b): int { return $a <=> $b; }
}
f("strcmp");
f([new C, "m"]);
2019-03-23 19:27:54 +01:00
f([C::class, "m"]);',
],
'callableWithSpaces' => [
'<?php
/**
* @param callable(string, string) : int $p
*/
2019-03-23 19:27:54 +01:00
function f(callable $p): void {}',
],
'fileExistsCallable' => [
'<?php
/** @return string[] */
function foo(string $prospective_file_path) : array {
return array_filter(
glob($prospective_file_path),
"file_exists"
);
2019-03-23 19:27:54 +01:00
}',
],
'callableSelfArg' => [
'<?php
class C extends B {}
$b = new B();
$c = new C();
$b->func2(function(B $x): void {});
$c->func2(function(B $x): void {});
2019-03-08 00:02:31 +01:00
class A {}
class B extends A {
/**
* @param callable(self) $f
*/
function func2(callable $f): void {
$f($this);
}
}',
],
'callableParentArg' => [
'<?php
class C extends B {}
$b = new B();
$c = new C();
$b->func3(function(A $x): void {});
$c->func3(function(A $x): void {});
class A {}
class B extends A {
/**
* @param callable(parent) $f
*/
function func3(callable $f): void {
$f($this);
}
}',
],
'callableStaticArg' => [
'<?php
2019-03-08 00:02:31 +01:00
class C extends B {}
$b = new B();
$c = new C();
$b->func1(function(B $x): void {});
$c->func1(function(C $x): void {});
class A {}
class B extends A {
/**
* @param callable(static) $f
*/
function func1(callable $f): void {
$f($this);
}
}',
2019-03-08 00:02:31 +01:00
],
2020-03-25 14:18:49 +01:00
'callableStaticReturn' => [
2019-03-08 00:02:31 +01:00
'<?php
class A {}
class B extends A {
/**
* @param callable():static $f
*/
function func1(callable $f): void {}
2020-03-25 14:18:49 +01:00
}
2019-03-08 00:02:31 +01:00
2020-03-25 14:18:49 +01:00
final class C extends B {}
$c = new C();
$c->func1(function(): C { return new C(); });',
],
'callableSelfReturn' => [
'<?php
class A {}
class B extends A {
2019-03-08 00:02:31 +01:00
/**
* @param callable():self $f
*/
function func2(callable $f): void {}
2020-03-25 14:18:49 +01:00
}
2019-03-08 00:02:31 +01:00
2020-03-25 14:18:49 +01:00
final class C extends B {}
$b = new B();
$c = new C();
$b->func2(function() { return new B(); });
$c->func2(function() { return new C(); });',
],
'callableParentReturn' => [
'<?php
class A {}
class B extends A {
2019-03-08 00:02:31 +01:00
/**
* @param callable():parent $f
*/
function func3(callable $f): void {}
}
$b = new B();
2020-03-25 14:18:49 +01:00
$b->func3(function() { return new A(); });',
],
'selfArrayMapCallableWrongClass' => [
'<?php
class Foo {
public function __construct(int $param) {}
public static function foo(int $param): Foo {
return new self($param);
}
public static function baz(int $param): self {
return new self($param);
}
}
class Bar {
/**
* @return array<int, Foo>
*/
public function bar() {
return array_map([Foo::class, "foo"], [1,2,3]);
}
/** @return array<int, Foo> */
public function bat() {
return array_map([Foo::class, "baz"], [1]);
}
2019-03-23 19:27:54 +01:00
}',
],
'dynamicCallableArray' => [
'<?php
class A {
/** @var string */
private $value = "default";
private function modify(string $name, string $value): void {
call_user_func([$this, "modify" . $name], $value);
}
public function modifyFoo(string $value): void {
$this->value = $value;
}
2019-03-23 19:27:54 +01:00
}',
],
'callableIsArrayAssertion' => [
'<?php
function foo(callable $c) : void {
if (is_array($c)) {
echo $c[1];
}
2019-03-23 19:27:54 +01:00
}',
],
'callableOrArrayIsArrayAssertion' => [
'<?php
/**
* @param callable|array $c
*/
function foo($c) : void {
2020-04-09 15:27:14 +02:00
if (is_array($c) && isset($c[1]) && is_string($c[1])) {
echo $c[1];
}
2019-03-23 19:27:54 +01:00
}',
],
2019-08-12 21:04:43 +02:00
'dontInferMethodIdWhenFormatDoesntFit' => [
'<?php
/** @param string|callable $p */
function f($p): array {
return [];
}
f("#b::a");'
],
'removeCallableAssertionAfterReassignment' => [
'<?php
function foo(string $key) : void {
$setter = "a" . $key;
if (is_callable($setter)) {
return;
}
$setter = "b" . $key;
if (is_callable($setter)) {}
}'
],
'noExceptionOnSelfString' => [
'<?php
class Fish {
public static function example(array $vals): void {
usort($vals, ["self", "compare"]);
}
/**
* @param mixed $a
* @param mixed $b
*/
public static function compare($a, $b): int {
return -1;
}
}',
],
'noFatalErrorOnClassWithSlash' => [
'<?php
class Func {
public function __construct(string $name, callable $callable) {}
}
class Foo {
public static function bar(): string { return "asd"; }
}
new Func("f", ["\Foo", "bar"]);',
],
'staticReturningCallable' => [
'<?php
abstract class Id
{
/**
* @var string
*/
private $id;
final protected function __construct(string $id)
{
$this->id = $id;
}
/**
* @return static
*/
final public static function fromString(string $id): self
{
return new static($id);
}
}
final class CriterionId extends Id
{
}
final class CriterionIds
{
/**
* @psalm-var non-empty-list<CriterionId>
*/
private $ids;
/**
* @psalm-param non-empty-list<CriterionId> $ids
*/
private function __construct(array $ids)
{
$this->ids = $ids;
}
/**
* @psalm-param non-empty-list<string> $ids
*/
public static function fromStrings(array $ids): self
{
return new self(array_map([CriterionId::class, "fromString"], $ids));
}
}'
],
'offsetOnCallable' => [
'<?php
function c(callable $c) : void {
if (is_array($c)) {
new ReflectionClass($c[0]);
}
}'
],
'destructureCallableArray' => [
'<?php
function getCallable(): callable {
return [DateTimeImmutable::class, "createFromFormat"];
}
$callable = getCallable();
if (!is_array($callable)) {
exit;
}
[$classOrObject, $method] = $callable;',
[
'$classOrObject' => 'class-string|object',
'$method' => 'string'
]
],
'callableInterface' => [
'<?php
interface CallableInterface{
public function __invoke(): bool;
}
function takesInvokableInterface(CallableInterface $c): void{
takesCallable($c);
}
function takesCallable(callable $c): void {
$c();
}'
],
'notCallableArrayNoUndefinedClass' => [
'<?php
/**
* @psalm-param array|callable $_fields
*/
function f($_fields): void {}
f(["instance_date" => "ASC", "start_time" => "ASC"]);'
],
'callOnInvokableOrCallable' => [
'<?php
interface Callback {
public function __invoke(): void;
}
/** @var Callback|callable */
$test = function (): void {};
$test();'
],
'resolveTraitClosureReturn' => [
'<?php
class B {
/**
* @psalm-param callable(mixed...):static $i
*/
function takesACall(callable $i) : void {}
public function call() : void {
$this->takesACall(function() {return $this;});
}
}'
],
'returnClosureReturningStatic' => [
'<?php
/**
* @psalm-consistent-constructor
*/
class C {
/**
* @return Closure():static
*/
public static function foo() {
return function() {
return new static();
};
}
}',
],
'returnsVoidAcceptableForNullable' => [
'<?php
/** @param callable():?bool $c */
function takesCallable(callable $c) : void {}
takesCallable(function() { return; });',
],
'byRefUsesAlwaysMixed' => [
'<?php
$callback = function() use (&$isCalled) : void {
$isCalled = true;
};
$isCalled = false;
$callback();
if ($isCalled === true) {}'
],
'notCallableListNoUndefinedClass' => [
'<?php
/**
* @param array|callable $arg
*/
function foo($arg): void {}
foo(["a", "b"]);'
],
'abstractInvokeInTrait' => [
'<?php
function testFunc(callable $func) : void {}
trait TestTrait {
abstract public function __invoke() : void;
public function apply() : void {
testFunc($this);
}
}
abstract class TestClass {
use TestTrait;
}'
],
];
}
/**
* @return iterable<string,array{string,error_message:string,1?:string[],2?:bool,3?:string}>
*/
public function providerInvalidCodeParse(): iterable
{
return [
'undefinedCallableClass' => [
'<?php
class A {
2018-01-11 21:50:45 +01:00
public function getFoo(): Foo
{
return new Foo([]);
}
2017-06-29 16:22:49 +02:00
/**
* @param mixed $argOne
* @param mixed $argTwo
* @return void
*/
public function bar($argOne, $argTwo)
{
$this->getFoo()($argOne, $argTwo);
}
}',
'error_message' => 'InvalidFunctionCall',
'error_levels' => ['UndefinedClass', 'MixedInferredReturnType'],
],
'undefinedCallableMethodFullString' => [
'<?php
class A {
2018-01-11 21:50:45 +01:00
public static function bar(string $a): string {
return $a . "b";
}
}
2018-01-11 21:50:45 +01:00
function foo(callable $c): void {}
foo("A::barr");',
'error_message' => 'UndefinedMethod',
],
'undefinedCallableMethodClassConcat' => [
'<?php
class A {
public static function bar(string $a): string {
return $a . "b";
}
}
function foo(callable $c): void {}
foo(A::class . "::barr");',
'error_message' => 'UndefinedMethod',
],
'undefinedCallableMethodArray' => [
'<?php
class A {
public static function bar(string $a): string {
return $a . "b";
}
}
function foo(callable $c): void {}
foo([A::class, "::barr"]);',
'error_message' => 'InvalidArgument',
],
'undefinedCallableMethodArrayWithoutClass' => [
'<?php
class A {
public static function bar(string $a): string {
return $a . "b";
}
}
function foo(callable $c): void {}
foo(["A", "::barr"]);',
'error_message' => 'InvalidArgument',
],
'undefinedCallableMethodClass' => [
'<?php
class A {
2018-01-11 21:50:45 +01:00
public static function bar(string $a): string {
return $a . "b";
}
}
2018-01-11 21:50:45 +01:00
function foo(callable $c): void {}
foo("B::bar");',
'error_message' => 'UndefinedClass',
],
'undefinedCallableFunction' => [
'<?php
2018-01-11 21:50:45 +01:00
function foo(callable $c): void {}
foo("trime");',
'error_message' => 'UndefinedFunction',
],
'stringFunctionCall' => [
'<?php
$bad_one = "hello";
$a = $bad_one(1);',
'error_message' => 'MixedAssignment',
],
'wrongCallableReturnType' => [
'<?php
$add_one = function(int $a): int {
return $a + 1;
};
/**
* @param callable(int) : int $c
*/
function bar(callable $c) : string {
return $c(1);
}
bar($add_one);',
'error_message' => 'InvalidReturnStatement',
],
'checkCallableTypeString' => [
'<?php
/**
* @param callable(int,int):int $_p
*/
function f(callable $_p): void {}
f("strcmp");',
'error_message' => 'InvalidScalarArgument',
],
'checkCallableTypeArrayInstanceFirstArg' => [
'<?php
/**
* @param callable(int,int):int $_p
*/
function f(callable $_p): void {}
class C {
public static function m(string $a, string $b): int { return $a <=> $b; }
}
f([new C, "m"]);',
'error_message' => 'InvalidScalarArgument',
],
'checkCallableTypeArrayClassStringFirstArg' => [
'<?php
/**
* @param callable(int,int):int $_p
*/
function f(callable $_p): void {}
class C {
public static function m(string $a, string $b): int { return $a <=> $b; }
}
f([C::class, "m"]);',
'error_message' => 'InvalidScalarArgument',
],
'callableWithSpaceAfterColonBadVarArg' => [
'<?php
class C {
/**
* @var callable(string, string): bool $p
*/
public $p;
public function __construct() {
$this->p = function (string $s, string $t): stdClass {
return new stdClass;
};
}
}',
'error_message' => 'InvalidPropertyAssignmentValue',
],
'callableWithSpaceBeforeColonBadVarArg' => [
'<?php
class C {
/**
* @var callable(string, string) :bool $p
*/
public $p;
public function __construct() {
$this->p = function (string $s, string $t): stdClass {
return new stdClass;
};
}
}',
'error_message' => 'InvalidPropertyAssignmentValue',
],
'callableWithSpacesEitherSideOfColonBadVarArg' => [
'<?php
class C {
/**
* @var callable(string, string) : bool $p
*/
public $p;
public function __construct() {
$this->p = function (string $s, string $t): stdClass {
return new stdClass;
};
}
}',
'error_message' => 'InvalidPropertyAssignmentValue',
],
'badArrayMapArrayCallable' => [
'<?php
class one { public function two(string $_p): void {} }
array_map(["two", "three"], ["one", "two"]);',
2019-03-23 19:27:54 +01:00
'error_message' => 'InvalidArgument',
],
'noFatalErrorOnMissingClassWithSlash' => [
'<?php
class Func {
public function __construct(string $name, callable $callable) {}
}
new Func("f", ["\Foo", "bar"]);',
'error_message' => 'InvalidArgument'
],
'noFatalErrorOnMissingClassWithoutSlash' => [
'<?php
class Func {
public function __construct(string $name, callable $callable) {}
}
new Func("f", ["Foo", "bar"]);',
'error_message' => 'InvalidArgument'
],
'preventStringDocblockType' => [
'<?php
/**
* @param string $mapper
*/
function map2(callable $mapper): void {}
map2("foo");',
'error_message' => 'MismatchingDocblockParamType',
],
2019-12-14 17:55:26 +01:00
'moreSpecificCallable' => [
'<?php
/** @param callable(string):void $c */
function takesSpecificCallable(callable $c) : void {
$c("foo");
}
function takesCallable(callable $c) : void {
takesSpecificCallable($c);
}',
'error_message' => 'MixedArgumentTypeCoercion'
],
'undefinedVarInBareCallable' => [
'<?php
$fn = function(int $a): void{};
function a(callable $fn): void{
$fn(++$a);
}
a($fn);',
'error_message' => 'UndefinedVariable',
],
'dontQualifyStringCallables' => [
'<?php
namespace NS;
function ff() : void {}
function run(callable $f) : void {
$f();
}
run("ff");',
'error_message' => 'UndefinedFunction',
],
'badCustomFunction' => [
'<?php
/**
* @param callable(int):bool $func
*/
function takesFunction(callable $func) : void {}
function myFunction( string $foo ) : bool {
return false;
}
takesFunction("myFunction");',
'error_message' => 'InvalidScalarArgument',
],
'emptyCallable' => [
'<?php
$a = "";
$a();',
'error_message' => 'InvalidFunctionCall',
],
2020-08-26 22:51:22 +02:00
'ImpureFunctionCall' => [
'<?php
/**
* @psalm-template T
*
* @psalm-param array<int, T> $values
* @psalm-param (callable(T): numeric) $num_func
*
* @psalm-return null|T
*
* @psalm-pure
*/
function max_by(array $values, callable $num_func)
{
$max = null;
$max_num = null;
foreach ($values as $value) {
$value_num = $num_func($value);
if (null === $max_num || $value_num >= $max_num) {
$max = $value;
$max_num = $value_num;
}
}
return $max;
}
$c = max_by([1, 2, 3], static function(int $a): int {
return $a + mt_rand(0, $a);
});
echo $c;
',
'error_message' => 'ImpureFunctionCall',
'error_levels' => [],
2020-08-30 04:02:58 +02:00
],
'constructCallableFromClassStringArray' => [
'<?php
interface Foo {
public function bar() : int;
}
/**
* @param callable():string $c
*/
function takesCallableReturningString(callable $c) : void {
$c();
}
/**
* @param class-string<Foo> $c
*/
function foo(string $c) : void {
takesCallableReturningString([$c, "bar"]);
}',
'error_message' => 'InvalidScalarArgument',
],
2021-11-23 20:04:52 +01:00
'inexistantCallableinCallableString' => [
'<?php
/**
* @param callable-string $c
*/
function c(string $c): void {
$c();
}
c("hii");',
'error_message' => 'InvalidArgument',
],
'mismatchParamTypeFromDocblock' => [
'<?php
/**
* @template A
*/
final class ArrayList
{
/**
* @template B
* @param Closure(A): B $effect
* @return ArrayList<B>
*/
public function map(Closure $effect): ArrayList
{
throw new RuntimeException("???");
}
}
/**
* @template T
* @template B
*
* @param ArrayList<T> $list
* @return ArrayList<array{T}>
*/
function genericContext(ArrayList $list): ArrayList
{
return $list->map(
/** @param B $_a */
function ($_a) {
return [$_a];
}
);
}',
'error_message' => 'InvalidArgument',
]
];
}
}