1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00
psalm/tests/ClosureTest.php
Bruce Weirdan dabfb16e34
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
2021-01-29 11:38:04 +01:00

825 lines
28 KiB
PHP

<?php
namespace Psalm\Tests;
use const DIRECTORY_SEPARATOR;
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()
{
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
);',
'assertions' => [],
'error_levels' => [],
'7.4'
],
'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',
],
'error_levels' => [],
'7.4'
],
'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' => [
'$a' => 'array{int, int, int}',
],
],
'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));
}',
'assertions' => [],
'error_levels' => [],
'7.4'
],
'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' => 'Closure():Closure():string(hello)',
'$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;
})()
);'
],
];
}
/**
* @return iterable<string,array{string,error_message:string,2?:string[],3?:bool,4?:string}>
*/
public function providerInvalidCodeParse()
{
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',
],
'closureByRefUseToMixed' => [
'<?php
function assertInt(int $int): int {
$s = static function() use(&$int): void {
$int = "42";
};
$s();
return $int;
}',
'error_message' => 'MixedReturnStatement'
],
];
}
}