1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00
psalm/tests/ClosureTest.php

1434 lines
51 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
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' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'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' => 'list{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' => [],
2022-12-18 17:15:15 +01:00
'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}[]
2019-11-30 05:46:21 +01:00
*/
function f(array $ret) : array
{
return array_map(
/**
* @param array{0:string,1:string} $row
2019-11-30 05:46:21 +01:00
*/
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}[]
2019-11-30 05:46:21 +01:00
*/
function f(array $ret): array
{
return array_map(
/**
* @param array{0:string,1:string} $row
2019-11-30 05:46:21 +01:00
*/
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>',
2022-12-18 17:15:15 +01:00
],
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
);
2022-12-18 17:15:15 +01:00
}',
2019-11-30 05:46:21 +01:00
],
'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
);
2022-12-18 17:15:15 +01:00
}',
2019-11-30 05:46:21 +01:00
],
'inferGeneratorReturnType' => [
'code' => '<?php
function accept(Generator $gen): void {}
accept(
(function() {
yield;
return 42;
})()
2022-12-18 17:15:15 +01:00
);',
],
'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' => [],
2022-12-18 17:15:15 +01:00
'php_version' => '7.4',
],
'annotateShortClosureReturn' => [
'code' => '<?php
/** @psalm-suppress MissingReturnType */
function returnsBool() { return true; }
$a = fn() : bool => /** @var bool */ returnsBool();',
'assertions' => [],
'ignored_issues' => [],
2022-12-18 17:15:15 +01:00
'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();
};
}
2022-12-18 17:15:15 +01:00
}',
],
'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' => [
2022-12-18 17:15:15 +01:00
'$result' => 'list{stdClass}',
],
],
'CallableWithArrayReduce' => [
2022-10-16 13:59:15 +02:00
'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' => [
2022-12-18 17:15:15 +01:00
'$result' => 'int',
],
],
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' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'php_version' => '8.1',
],
'FirstClassCallable:InstanceMethod:BuiltIn' => [
'code' => '<?php
$queue = new \SplQueue;
$closure = $queue->count(...);
$count = $closure();
',
'assertions' => [
'$count' => 'int',
],
'ignored_issues' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'php_version' => '8.1',
],
'FirstClassCallable:InheritedStaticMethod' => [
2022-10-16 13:59:15 +02:00
'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' => [],
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
'ignored_issues' => [],
'php_version' => '8.1',
],
'FirstClassCallable:InheritedStaticMethodWithStaticTypeParameter' => [
2022-10-16 13:59:15 +02:00
'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 {}
2022-12-18 17:15:15 +01:00
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' => 'list{null, null, null}',
'$result2' => 'list{string, string, string}',
'$result3' => 'list{int, int, int}',
],
'ignored_issues' => [],
2022-12-18 17:15:15 +01:00
'php_version' => '8.1',
],
'FirstClassCallable:array_map' => [
'code' => '<?php call_user_func(array_map(...), intval(...), ["1"]);',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1',
],
2022-02-22 01:37:20 +01:00
'FirstClassCallable:AssignmentVisitorMap' => [
2022-02-26 21:28:15 +01:00
'code' => '<?php
2022-02-22 01:37:20 +01:00
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',
],
'FirstClassCallable:Method:Asserted' => [
'code' => '<?php
$r = false;
/** @var object $o */;
/** @var string $m */;
if (method_exists($o, $m)) {
$r = $o->$m(...);
}
',
'assertions' => [
'$r===' => 'Closure|false',
],
'ignored_issues' => [],
2022-02-22 01:37:20 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'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(...);',
],
'reconcileClosure' => [
'code' => '<?php
/**
* @param Closure|callable-string $callable
*/
function use_callable($callable) : void
{
}
/**
* @param Closure|string $var
*/
function test($var) : void
{
if (is_callable($var))
use_callable($var);
else
echo $var; // $var should be string, instead it\'s considered to be Closure|string.
}',
],
'classExistsInOuterScopeOfArrowFunction' => [
'code' => <<<'PHP'
<?php
if (class_exists(Foo::class)) {
/** @return mixed */
fn() => Foo::bar(23, []);
}
PHP,
'assertions' => [],
'ignored_issues' => [],
'php_version' => '7.4',
],
'classExistsInOuterScopeOfAClosure' => [
'code' => <<<'PHP'
<?php
if (class_exists(Foo::class)) {
/** @return mixed */
function () {
return Foo::bar(23, []);
};
}
PHP,
],
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());',
2022-08-03 19:56:38 +02:00
'error_message' => 'ArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:28 - Argument 1 of takesB expects B, but 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;
}',
2022-12-18 17:15:15 +01:00
'error_message' => 'MixedReturnStatement',
2019-11-30 05:46:21 +01:00
],
'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";
}
);',
2022-12-18 17:15:15 +01:00
'error_message' => 'InvalidArgument',
],
'undefinedVariableInEncapsedString' => [
'code' => '<?php
fn(): string => "$a";
',
'error_message' => 'UndefinedVariable',
'ignored_issues' => [],
2022-12-18 17:15:15 +01:00
'php_version' => '7.4',
],
'undefinedVariableInStringCast' => [
'code' => '<?php
fn(): string => (string) $a;
',
'error_message' => 'UndefinedVariable',
'ignored_issues' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'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' => [],
2022-12-18 17:15:15 +01:00
'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',
],
'thisInStaticClosure' => [
'code' => '<?php
class C {
public string $a = "zzz";
public function f(): void {
$f = static function (): void {
echo $this->a;
};
$f();
}
}
',
'error_message' => 'InvalidScope',
],
'thisInStaticArrowFunction' => [
'code' => '<?php
class C {
public int $a = 1;
public function f(): int {
$f = static fn(): int => $this->a;
return $f();;
}
}
',
'error_message' => 'InvalidScope',
'ignored_issues' => [],
'php_version' => '7.4',
],
'FirstClassCallable:WithNew' => [
'code' => <<<'PHP'
<?php
new stdClass(...);
PHP,
'error_message' => 'ParseError',
'ignored_issues' => [],
'php_version' => '8.1',
],
2019-11-30 05:46:21 +01:00
];
}
}