1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00
psalm/tests/ClosureTest.php

895 lines
31 KiB
PHP
Raw Normal View History

2019-11-30 05:46:21 +01:00
<?php
namespace Psalm\Tests;
use const DIRECTORY_SEPARATOR;
2019-11-30 05:46:21 +01:00
class ClosureTest extends TestCase
{
use Traits\InvalidCodeAnalysisTestTrait;
use Traits\ValidCodeAnalysisTestTrait;
/**
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
*/
public function providerValidCodeParse(): iterable
2019-11-30 05:46:21 +01:00
{
return [
'byRefUseVar' => [
'<?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' => [
'<?php
$bar = ["foo", "bar"];
$bam = array_map(
function(string $a) {
return $a . "blah";
},
$bar
);',
],
'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'
2019-11-30 05:46:21 +01:00
],
'varReturnType' => [
'<?php
$add_one = function(int $a) : int {
return $a + 1;
};
$a = $add_one(1);',
'assertions' => [
'$a' => 'int',
],
],
'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'
2019-11-30 05:46:21 +01:00
],
'correctParamType' => [
'<?php
$take_string = function(string $s): string { return $s; };
$take_string("string");',
],
'arrayMapClosureVar' => [
'<?php
$mirror = function(int $i) : int { return $i; };
$a = array_map($mirror, [1, 2, 3]);',
'assertions' => [
2020-07-07 16:41:46 +02:00
'$a' => 'array{int, int, int}',
2019-11-30 05:46:21 +01:00
],
],
'inlineCallableFunction' => [
'<?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' => [
'<?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' => [
'<?php
$a = array_map(
function(int $type, string ...$args):string {
return "hello";
},
[1, 2, 3]
);',
],
'returnsTypedClosure' => [
'<?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' => [
'<?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));
}',
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'
2019-11-30 05:46:21 +01:00
],
'returnsTypedClosureWithClasses' => [
'<?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' => [
'<?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' => [
'<?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' => [
'<?php
/**
* @param array{0:string,1:string}[] $ret
* @return array{0:string,1:int}[]
*/
function f(array $ret) : array
{
return array_map(
/**
* @param array{0:string,1:string} $row
*/
function (array $row) {
return [
strval($row[0]),
intval($row[1]),
];
},
$ret
);
}',
'assertions' => [],
'error_levels' => ['MissingClosureReturnType'],
],
'inferArrayMapReturnTypeWithTypehints' => [
'<?php
/**
* @param array{0:string,1:string}[] $ret
* @return array{0:string,1:int}[]
*/
function f(array $ret): array
{
return array_map(
/**
* @param array{0:string,1:string} $row
*/
function (array $row): array {
return [
strval($row[0]),
intval($row[1]),
];
},
$ret
);
}',
],
'invokableProperties' => [
'<?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();
}
}',
],
'PHP71-mirrorCallableParams' => [
'<?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' => [
'<?php
$a = function() : Closure { return function() : string { return "hello"; }; };
$b = $a()();',
'assertions' => [
'$a' => 'pure-Closure():pure-Closure():string(hello)',
2019-11-30 05:46:21 +01:00
'$b' => 'string',
],
],
'voidReturningArrayMap' => [
'<?php
array_map(
function(int $i) : void {
echo $i;
},
[1, 2, 3]
);',
],
'PHP71-closureFromCallableInvokableNamedClass' => [
'<?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));',
],
'PHP71-closureFromCallableInvokableAnonymousClass' => [
'<?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));',
],
'PHP71-publicCallableFromInside' => [
'<?php
class Base {
public function publicMethod() : void {}
}
class Example extends Base {
public function test() : Closure {
return Closure::fromCallable([$this, "publicMethod"]);
}
}',
],
'PHP71-protectedCallableFromInside' => [
'<?php
class Base {
protected function protectedMethod() : void {}
}
class Example extends Base {
public function test() : Closure {
return Closure::fromCallable([$this, "protectedMethod"]);
}
}',
],
'allowClosureWithNarrowerReturn' => [
'<?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' => [
'<?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' => [
'<?php
/**
* @param Closure():int $x
*/
function accept_closure($x) : void {
$x();
}
accept_closure(
function (int $x = 5) : int {
return $x;
}
);',
],
'refineCallableTypeWithTypehint' => [
'<?php
/** @param string[][] $arr */
function foo(array $arr) : void {
array_map(
function(array $a) {
return reset($a);
},
$arr
);
}'
],
'refineCallableTypeWithoutTypehint' => [
'<?php
/** @param string[][] $arr */
function foo(array $arr) : void {
array_map(
function($a) {
return reset($a);
},
$arr
);
}'
],
'inferGeneratorReturnType' => [
'<?php
function accept(Generator $gen): void {}
accept(
(function() {
yield;
return 42;
})()
);'
],
'callingInvokeOnClosureIsSameAsCallingDirectly' => [
'<?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' => [],
'error_levels' => [],
'7.4'
],
'annotateShortClosureReturn' => [
'<?php
/** @psalm-suppress MissingReturnType */
function returnsBool() { return true; }
$a = fn() : bool => /** @var bool */ returnsBool();',
[],
[],
'7.4'
],
'rememberParentAssertions' => [
'<?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();
};
}
}'
],
2019-11-30 05:46:21 +01:00
];
}
/**
* @return iterable<string,array{string,error_message:string,2?:string[],3?:bool,4?:string}>
*/
public function providerInvalidCodeParse(): iterable
2019-11-30 05:46:21 +01:00
{
return [
'wrongArg' => [
'<?php
$bar = ["foo", "bar"];
$bam = array_map(
function(int $a): int {
return $a + 1;
},
$bar
);',
'error_message' => 'InvalidScalarArgument',
],
'noReturn' => [
'<?php
$bar = ["foo", "bar"];
$bam = array_map(
function(string $a): string {
},
$bar
);',
'error_message' => 'InvalidReturnType',
],
'possiblyNullFunctionCall' => [
'<?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' => [
'<?php
$take_string = function(string $s): string { return $s; };
$take_string(42);',
'error_message' => 'InvalidScalarArgument',
],
'missingClosureReturnType' => [
'<?php
$a = function() {
return "foo";
};',
'error_message' => 'MissingClosureReturnType',
],
'returnsTypedClosureWithBadReturnType' => [
'<?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' => [
'<?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' => [
'<?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' => [
'<?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' => [
'<?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' => [
'<?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' => [
'<?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' => [
'<?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' => [
'<?php
$a = function() use ($i) {};',
'error_message' => 'UndefinedVariable',
],
'voidReturningArrayMap' => [
'<?php
$arr = array_map(
function(int $i) : void {
echo $i;
},
[1, 2, 3]
);
foreach ($arr as $a) {
if ($a) {}
}',
'error_message' => 'TypeDoesNotContainType',
],
'PHP71-closureFromCallableInvokableNamedClassWrongArgs' => [
'<?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' => [
'<?php
class Foo {
public function __construct(UndefinedClass $o) {}
}
new Foo(function() : void {});',
'error_message' => 'UndefinedClass',
],
'useDuplicateName' => [
'<?php
$foo = "bar";
$a = function (string $foo) use ($foo) : string {
return $foo;
};',
'error_message' => 'DuplicateParam',
],
'PHP71-privateCallable' => [
'<?php
class Base {
private function privateMethod() : void {}
}
class Example extends Base {
public function test() : Closure {
return Closure::fromCallable([$this, "privateMethod"]);
}
}',
'error_message' => 'InvalidArgument',
],
'prohibitCallableWithRequiredArg' => [
'<?php
/**
* @param Closure():int $x
*/
function accept_closure($x) : void {
$x();
}
accept_closure(
function (int $x) : int {
return $x;
}
);',
'error_message' => 'InvalidArgument',
],
'useClosureDocblockType' => [
'<?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, parent type A provided',
2019-11-30 05:46:21 +01:00
],
'closureByRefUseToMixed' => [
'<?php
function assertInt(int $int): int {
$s = static function() use(&$int): void {
$int = "42";
};
$s();
return $int;
}',
'error_message' => 'MixedReturnStatement'
],
'noCrashWhenComparingIllegitimateCallable' => [
'<?php
class C {}
function foo() : C {
return fn (int $i) => "";
}',
'error_message' => 'InvalidReturnStatement',
[],
false,
'7.4',
],
'detectImplicitVoidReturn' => [
'<?php
/**
* @param Closure():Exception $c
*/
function takesClosureReturningException(Closure $c) : void {
echo $c()->getMessage();
}
takesClosureReturningException(
function () {
echo "hello";
}
);',
'error_message' => 'InvalidArgument'
],
2019-11-30 05:46:21 +01:00
];
}
}