1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-11 16:59:45 +01:00
psalm/tests/Template/FunctionTemplateTest.php
2024-01-22 13:36:08 +03:00

2383 lines
81 KiB
PHP

<?php
namespace Psalm\Tests\Template;
use Psalm\Tests\TestCase;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
class FunctionTemplateTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
public function providerValidCodeParse(): iterable
{
return [
'extractTypeParameterValue' => [
'code' => '<?php
/**
* @template T
*/
interface Type {}
/**
* @implements Type<int>
*/
final readonly class IntType implements Type {}
/**
* @template T
* @implements Type<list<T>>
*/
final readonly class ListType implements Type
{
/**
* @param Type<T> $type
*/
public function __construct(
public Type $type,
) {
}
}
/**
* @template T
* @param Type<T> $type
* @return T
*/
function extractType(Type $type): mixed
{
throw new \RuntimeException("Should never be called at runtime");
}
/**
* @template T
* @param Type<T> $t
* @return ListType<T>
*/
function listType(Type $t): ListType
{
return new ListType($t);
}
function intType(): IntType
{
return new IntType();
}
$listType = listType(intType());
$list = extractType($listType);
',
'assertions' => [
'$listType===' => 'ListType<int>',
'$list' => 'list<int>',
],
'ignored_issues' => [],
'php_version' => '8.2',
],
'validTemplatedType' => [
'code' => '<?php
namespace FooFoo;
/**
* @template T
* @param T $x
* @return T
*/
function foo($x) {
return $x;
}
function bar(string $a): void { }
bar(foo("string"));',
],
'validPsalmTemplatedFunctionType' => [
'code' => '<?php
namespace FooFoo;
/**
* @psalm-template T
* @psalm-param T $x
* @psalm-return T
*/
function foo($x) {
return $x;
}
function bar(string $a): void { }
bar(foo("string"));',
],
'validTemplatedStaticMethodType' => [
'code' => '<?php
namespace FooFoo;
class A {
/**
* @template T
* @param T $x
* @return T
*/
public static function foo($x) {
return $x;
}
}
function bar(string $a): void { }
bar(A::foo("string"));',
],
'validTemplatedInstanceMethodType' => [
'code' => '<?php
namespace FooFoo;
class A {
/**
* @template T
* @param T $x
* @return T
*/
public function foo($x) {
return $x;
}
}
function bar(string $a): void { }
bar((new A())->foo("string"));',
],
'genericArrayKeys' => [
'code' => '<?php
/**
* @template T as array-key
*
* @param array<T, mixed> $arr
* @return list<T>
*/
function my_array_keys($arr) {
return array_keys($arr);
}
$a = my_array_keys(["hello" => 5, "goodbye" => new \Exception()]);',
'assertions' => [
'$a' => 'list<string>',
],
],
'genericNonEmptyArrayKeys' => [
'code' => '<?php
/**
* @template T as array-key
*
* @param non-empty-array<T, mixed> $arr
* @return non-empty-list<T>
*/
function my_array_keys($arr) {
return array_keys($arr);
}
$a = my_array_keys(["hello" => 5, "goodbye" => new \Exception()]);',
'assertions' => [
'$a' => 'non-empty-list<string>',
],
],
'genericArrayFlip' => [
'code' => '<?php
/**
* @template TKey as array-key
* @template TValue as array-key
*
* @param array<TKey, TValue> $arr
* @return array<TValue, TKey>
*/
function my_array_flip($arr) {
return array_flip($arr);
}
$b = my_array_flip(["hello" => 5, "goodbye" => 6]);',
'assertions' => [
'$b' => 'array<int, string>',
],
],
'byRefKeyValueArray' => [
'code' => '<?php
/**
* @template TValue
* @template TKey as array-key
*
* @param array<TKey, TValue> $arr
*/
function byRef(array &$arr) : void {}
$b = ["a" => 5, "c" => 6];
byRef($b);',
'assertions' => [
'$b' => 'array<string, int>',
],
],
'byRefMixedKeyArray' => [
'code' => '<?php
/**
* @template TValue
*
* @param array<TValue> $arr
*/
function byRef(array &$arr) : void {}
$b = ["a" => 5, "c" => 6];
byRef($b);',
'assertions' => [
'$b' => 'array<array-key, int>',
],
],
'mixedArrayPop' => [
'code' => '<?php
/**
* @template TValue
*
* @param array<array-key, TValue> $arr
* @return TValue|null
*/
function my_array_pop(array &$arr) {
return array_pop($arr);
}
/** @var mixed */
$b = ["a" => 5, "c" => 6];
$a = my_array_pop($b);',
'assertions' => [
'$a' => 'mixed',
'$b' => 'array<array-key, mixed>',
],
'ignored_issues' => ['MixedAssignment', 'MixedArgument'],
],
'genericArrayPop' => [
'code' => '<?php
/**
* @template TValue
* @template TKey as array-key
*
* @param array<TKey, TValue> $arr
* @return TValue|null
*/
function my_array_pop(array &$arr) {
return array_pop($arr);
}
$b = ["a" => 5, "c" => 6];
$a = my_array_pop($b);',
'assertions' => [
'$a' => 'int|null',
'$b' => 'array<string, int>',
],
],
'templateCallableReturnType' => [
'code' => '<?php
namespace NS;
/**
* @template T
* @psalm-param callable():T $action
* @psalm-return T
*/
function retry(int $maxRetries, callable $action) {
return $action();
}
function takesInt(int $p): void{};
takesInt(retry(1, function(): int { return 1; }));',
],
'templateClosureReturnType' => [
'code' => '<?php
namespace NS;
/**
* @template T
* @psalm-param \Closure():T $action
* @psalm-return T
*/
function retry(int $maxRetries, callable $action) {
return $action();
}
function takesInt(int $p): void{};
takesInt(retry(1, function(): int { return 1; }));',
],
'replaceChildTypeWithGenerator' => [
'code' => '<?php
/**
* @template TKey as array-key
* @template TValue
* @param Traversable<TKey, TValue> $t
* @return array<TKey, TValue>
*/
function f(Traversable $t): array {
$ret = [];
foreach ($t as $k => $v) $ret[$k] = $v;
return $ret;
}
/** @return Generator<int, stdClass> */
function g():Generator { yield new stdClass; }
takesArrayOfStdClass(f(g()));
/** @param array<stdClass> $p */
function takesArrayOfStdClass(array $p): void {}',
],
'splatTemplateParam' => [
'code' => '<?php
/**
* @template TKey as array-key
* @template TValue
*
* @param array<TKey, TValue> $arr
* @param array $arr2
* @return array<TKey, TValue>
*/
function splat_proof(array $arr, array $arr2) {
return $arr;
}
$foo = [
[1, 2, 3],
[1, 2],
];
$a = splat_proof(...$foo);',
'assertions' => [
'$a' => 'array<int<0, 2>, int>',
],
],
'passArrayByRef' => [
'code' => '<?php
function acceptsStdClass(stdClass $_p): void {}
$q = [new stdClass];
acceptsStdClass(fNoRef($q));
acceptsStdClass(fRef($q));
acceptsStdClass(fNoRef($q));
/**
* @template TKey as array-key
* @template TValue
*
* @param array<TKey, TValue> $_arr
* @return null|TValue
* @psalm-ignore-nullable-return
*/
function fRef(array &$_arr) {
return array_shift($_arr);
}
/**
* @template TKey as array-key
* @template TValue
*
* @param array<TKey, TValue> $_arr
* @return null|TValue
* @psalm-ignore-nullable-return
*/
function fNoRef(array $_arr) {
return array_shift($_arr);
}',
],
'classTemplateAsCorrect' => [
'code' => '<?php
class Foo {}
class FooChild extends Foo {}
/**
* @template T as Foo
* @param T $x
* @return T
*/
function bar($x) {
return $x;
}
bar(new Foo());
bar(new FooChild());',
],
'classTemplateOfCorrect' => [
'code' => '<?php
class Foo {}
class FooChild extends Foo {}
/**
* @template T of Foo
* @param T $x
* @return T
*/
function bar($x) {
return $x;
}
bar(new Foo());
bar(new FooChild());',
],
'classTemplateAsInterface' => [
'code' => '<?php
interface Foo {}
interface FooChild extends Foo {}
class FooImplementer implements Foo {}
/**
* @template T as Foo
* @param T $x
* @return T
*/
function bar($x) {
return $x;
}
function takesFoo(Foo $f) : void {
bar($f);
}
function takesFooChild(FooChild $f) : void {
bar($f);
}
function takesFooImplementer(FooImplementer $f) : void {
bar($f);
}',
],
'templateFunctionVar' => [
'code' => '<?php
namespace A\B;
class C {
public function bar() : void {}
}
interface D {}
/**
* @template T as C
* @return T
*/
function foo($some_t) : C {
/** @var T */
$a = $some_t;
$a->bar();
/** @var T&D */
$b = $some_t;
$b->bar();
/** @var D&T */
$b = $some_t;
$b->bar();
return $a;
}',
'assertions' => [],
'ignored_issues' => ['MixedAssignment', 'MissingParamType'],
],
'bindFirstTemplatedClosureParameterValid' => [
'code' => '<?php
/**
* @template T
*
* @param Closure(T):void $t1
* @param T $t2
*/
function apply(Closure $t1, $t2) : void {}
apply(function(int $_i) : void {}, 5);
apply(function(string $_i) : void {}, "hello");
apply(function(stdClass $_i) : void {}, new stdClass);
class A {}
class AChild extends A {}
apply(function(A $_i) : void {}, new AChild());',
],
'callableReturnsItself' => [
'code' => '<?php
$a =
/**
* @param callable():string $s
* @return string
*/
function(callable $s) {
return $s();
};
/**
* @template T1
* @param callable(callable():T1):T1 $s
* @return void
*/
function takesReturnTCallable(callable $s) {}
takesReturnTCallable($a);',
],
'nonBindingParamReturn' => [
'code' => '<?php
/**
* @template T
*
* @param Closure():T $t1
* @param T $t2
*/
function foo(Closure $t1, $t2) : void {}
foo(
function () : int {
return 5;
},
"hello"
);',
],
'templatedInterfaceMethodInheritReturnType' => [
'code' => '<?php
class Foo {}
/**
* @psalm-suppress MissingTemplateParam
*/
class SomeIterator implements IteratorAggregate
{
public function getIterator() {
yield new Foo;
}
}
$i = (new SomeIterator())->getIterator();',
'assertions' => [
'$i' => 'Traversable<mixed, mixed>',
],
],
'upcastArrayToIterable' => [
'code' => '<?php
/**
* @template K
* @template V
* @param iterable<K,V> $collection
* @return V
* @psalm-suppress InvalidReturnType
*/
function first($collection) {}
$one = first([1,2,3]);',
'assertions' => [
'$one' => 'int',
],
],
'templateIntersectionLeft' => [
'code' => '<?php
interface I1 {}
interface I2 {}
/**
* @template T as I1&I2
* @param T $a
*/
function templatedBar(I1 $a) : void {}',
],
'templateIntersectionRight' => [
'code' => '<?php
interface I1 {}
interface I2 {}
/**
* @template T as I1&I2
* @param T $b
*/
function templatedBar(I2 $b) : void {}',
],
'matchMostSpecificTemplate' => [
'code' => '<?php
/**
* @template TReturn
* @param callable():(\Generator<mixed, mixed, mixed, TReturn>|TReturn) $gen
* @return array<int, TReturn>
*/
function call(callable $gen) : array {
$return = $gen();
if ($return instanceof Generator) {
return [$return->getReturn()];
}
/** @var array<int, TReturn> */
$wrapped_gen = [$gen];
return $wrapped_gen;
}
$arr = call(
function() {
yield 1;
return "hello";
}
);',
'assertions' => [
'$arr' => 'array<int, string>',
],
],
'templateOfWithSpace' => [
'code' => '<?php
/**
* @template T of array<int, mixed>
*/
class Foo
{
}
/**
* @param Foo<array<int, DateTime>> $a
*/
function bar(Foo $a) : void {}',
],
'allowUnionTypeParam' => [
'code' => '<?php
/**
* @template T
* @param callable(T) $x
* @param array<T> $y
*/
function example($x, $y): void {}
example(
/**
* @param int|false $x
*/
function($x): void {},
[rand(0, 1) ? 5 : false]
);',
],
'functionTemplateUnionType' => [
'code' => '<?php
/**
* @template T0 as int|string
* @param T0 $t
* @return T0
*/
function foo($t) {
return $t;
}
$s = foo("hello");
$i = foo(5);',
'assertions' => [
'$s' => 'string',
'$i' => 'int',
],
],
'reconcileTraversableTemplatedAndNormal' => [
'code' => '<?php
function foo(Traversable $t): void {
if ($t instanceof IteratorAggregate) {
$a = $t->getIterator();
$t = $a;
}
if (!$t instanceof Iterator) {
return;
}
if (rand(0, 1) && rand(0, 1)) {
$t->next();
}
}',
],
'keyOfTemplate' => [
'code' => '<?php
/**
* @template T as array
* @template K as key-of<T>
*
* @param T $o
* @param K $name
*
* @return T[K]
*/
function getOffset(array $o, $name) {
return $o[$name];
}
$a = ["foo" => "hello", "bar" => 2];
$b = getOffset($a, "foo");
$c = getOffset($a, "bar");',
'assertions' => [
'$b' => 'string',
'$c' => 'int',
],
],
'dontGeneraliseBoundParamWithWiderCallable' => [
'code' => '<?php
class C {
public function foo() : void {}
}
/**
* @psalm-template T
* @psalm-param T $t
* @psalm-param callable(?T):void $callable
* @return T
*/
function makeConcrete($t, callable $callable) {
$callable(rand(0, 1) ? $t : null);
return $t;
}
$c = makeConcrete(new C(), function (?C $c) : void {});',
'assertions' => [
'$c' => 'C',
],
],
'allowTemplateTypeBeingUsedInsideFunction' => [
'code' => '<?php
/**
* @template T of DateTime
* @param callable(T) $callable
* @param T $value
*/
function foo(callable $callable, DateTime $value) : void {
$callable($value);
}',
],
'callFindAnother' => [
'code' => '<?php
/**
* @template T as Foo
* @param T $foo
* @return T
*/
function loader($foo) {
return $foo::getAnother();
}
/**
* @psalm-consistent-constructor
*/
class Foo {
/** @return static */
public static function getAnother() {
return new static();
}
}',
],
'templatedVarOnReturn' => [
'code' => '<?php
namespace Ns;
class A {}
class B {}
/**
* @template T
* @param T $t
* @return T
*/
function getAOrB($t) {
if ($t instanceof A) {
/** @var T */
return new A();
}
/** @var T */
return new B();
}',
],
'assertOnTemplatedValue' => [
'code' => '<?php
/**
* @template I
* @param I $foo
*/
function bar($foo): void {
if (is_string($foo)) {}
if (!is_string($foo)) {}
if (is_int($foo)) {}
if (!is_int($foo)) {}
if (is_numeric($foo)) {}
if (!is_numeric($foo)) {}
if (is_scalar($foo)) {}
if (!is_scalar($foo)) {}
if (is_bool($foo)) {}
if (!is_bool($foo)) {}
if (is_object($foo)) {}
if (!is_object($foo)) {}
if (is_callable($foo)) {}
if (!is_callable($foo)) {}
}',
],
'interpretFunctionCallableReturnValue' => [
'code' => '<?php
final class Id
{
/**
* @var string
*/
private $id;
private function __construct(string $id)
{
$this->id = $id;
}
public static function fromString(string $id): self
{
return new self($id);
}
}
/**
* @template T
* @psalm-param callable(string): T $generator
* @psalm-return callable(): T
*/
function idGenerator(callable $generator)
{
return static function () use ($generator) {
return $generator("random id");
};
}
function client(Id $id): void
{
}
$staticIdGenerator = idGenerator([Id::class, "fromString"]);
client($staticIdGenerator());',
],
'noCrashWhenTemplatedClassIsStatic' => [
'code' => '<?php
/**
* @psalm-consistent-constructor
*/
abstract class Model {
/** @return static */
public function newInstance() {
return new static();
}
}
/**
* @template T of Model
* @param T $m
* @return T
*/
function foo(Model $m) : Model {
return $m->newInstance();
}',
],
'unboundVariableIsEmpty' => [
'code' => '<?php
/**
* @template TE
* @template TR
*
* @param TE $elt
* @param TR ...$elts
*
* @return TE|TR
*/
function collect($elt, ...$elts) {
$ret = $elt;
foreach ($elts as $item) {
if (rand(0, 1)) {
$ret = $item;
}
}
return $ret;
}
echo collect("a");',
],
'paramOutDontLeak' => [
'code' => '<?php
/**
* @template TKey as array-key
* @template TValue
*
* @param array<TKey, TValue> $arr
* @param-out list<TValue> $arr
*/
function example_sort_by_ref(array &$arr): bool {
$arr = array_values($arr);
return true;
}
/**
* @param array<int, array{0: int, 1: string}> $array
* @return list<array{0: int, 1: string}>
*/
function example(array $array): array {
example_sort_by_ref($array);
return $array;
}',
],
'narrowTemplateTypeWithIsObject' => [
'code' => '<?php
function takesObject(object $object): void {}
/**
* @template T as mixed
* @param T $value
*/
function example($value): void {
if (is_object($value)) {
takesObject($value);
}
}',
],
'falseDefault' => [
'code' => '<?php
/**
* @template T
* @param T $v
* @return T
*/
function exampleWithNullDefault($v = false) {
return $v;
}',
],
'nullDefault' => [
'code' => '<?php
/**
* @template T
* @param T $v
* @return T
*/
function exampleWithNullDefault($v = null) {
return $v;
}',
],
'uasortCallable' => [
'code' => '<?php
/**
* @template T of object
* @psalm-param array<T> $collection
* @psalm-param callable(T, T): int $sorter
* @psalm-return array<T>
*/
function order(array $collection, callable $sorter): array {
usort($collection, $sorter);
return $collection;
}',
],
'callableInference' => [
'code' => '<?php
class Foo {}
class FooChild extends Foo {}
class Bar {}
/**
* @psalm-template ThingType of Foo
* @psalm-param ThingType $t
* @return ThingType|Bar
*/
function from_other(Foo $t) {
if (rand(0, 1)) {
return new Bar();
}
return $t;
}
/**
* @param list<FooChild> $a
* @return list<Bar|FooChild>
*/
function baz(array $a) {
return array_map("from_other", $a);
}',
],
'templateFlipIntersection' => [
'code' => '<?php
/**
* @template T as object
* @template S as object
* @param S&T $item
* @return T&S
*/
function filter(object $item) {
return $item;
}',
],
'splatIntoTemplatedArray' => [
'code' => '<?php
/**
* @template T
* @param array<T> ...$iterators
* @return Generator<T>
*/
function joinBySplat(array ...$iterators): iterable {
foreach ($iterators as $iter) {
foreach ($iter as $value) {
yield $value;
}
}
}
/**
* @return Generator<int, array<int>>
*/
function genIters(): Generator {
yield [1,2,3];
yield [4,5,6];
}
$values = joinBySplat(...genIters());
foreach ($values as $value) {
echo $value;
}',
],
'allowTemplatedCast' => [
'code' => '<?php
/**
* @psalm-template Tk of array-key
* @psalm-param Tk $key
*/
function at($key) : void {
echo (string) $key;
}',
],
'uksortNoNamespace' => [
'code' => '<?php
/**
* @template Tk of array-key
* @template Tv
*
* @param array<Tk, Tv> $result
* @param (callable(Tk, Tk): int) $comparator
*
* @preturn array<Tk, Tv>
*/
function sort_by_key(array $result, callable $comparator): array
{
\uksort($result, $comparator);
return $result;
}',
],
'uksortNamespaced' => [
'code' => '<?php
namespace Psl\Arr;
/**
* @template Tk of array-key
* @template Tv
*
* @param array<Tk, Tv> $result
* @param (callable(Tk, Tk): int) $comparator
*
* @preturn array<Tk, Tv>
*/
function sort_by_key(array $result, callable $comparator): array
{
\uksort($result, $comparator);
return $result;
}',
],
'mockObject' => [
'code' => '<?php
class MockObject {}
/**
* @psalm-template T1 of object
* @psalm-param class-string<T1> $originalClassName
* @psalm-return MockObject&T1
*/
function foo(string $originalClassName): MockObject {
return createMock($originalClassName);
}
/**
* @psalm-suppress InvalidReturnType
* @psalm-suppress InvalidReturnStatement
*
* @psalm-template T2 of object
* @psalm-param class-string<T2> $originalClassName
* @psalm-return MockObject&T2
*/
function createMock(string $originalClassName): MockObject {
return new MockObject;
}',
],
'testStringCallableInference' => [
'code' => '<?php
class A {
public static function dup(string $a): string {
return $a . $a;
}
}
/**
* @template T
* @param iterable<T> $iter
* @return list<T>
*/
function toArray(iterable $iter): array {
$data = [];
foreach ($iter as $val) {
$data[] = $val;
}
return $data;
}
/**
* @template T
* @template U
* @param callable(T): U $predicate
* @return callable(iterable<int, T>): iterable<int, U>
*/
function map(callable $predicate): callable {
return
/** @param iterable<int, T> $iter */
function(iterable $iter) use ($predicate): iterable {
foreach ($iter as $key => $value) {
yield $key => $predicate($value);
}
};
}
/** @param list<string> $strings */
function _test(array $strings): void {}
$a = map([A::class, "dup"])(["a", "b", "c"]);',
'assertions' => [
'$a' => 'iterable<int, string>',
],
],
'testClosureCallableInference' => [
'code' => '<?php
/**
* @template T
* @param iterable<T> $iter
* @return list<T>
*/
function toArray(iterable $iter): array {
$data = [];
foreach ($iter as $val) {
$data[] = $val;
}
return $data;
}
/**
* @template T
* @template U
* @param callable(T): U $predicate
* @return callable(iterable<int, T>): iterable<int, U>
*/
function map(callable $predicate): callable {
return
/** @param iterable<int, T> $iter */
function(iterable $iter) use ($predicate): iterable {
foreach ($iter as $key => $value) {
yield $key => $predicate($value);
}
};
}
/** @param list<string> $strings */
function _test(array $strings): void {}
$a = map(
function (string $a) {
return $a . $a;
}
)(["a", "b", "c"]);',
'assertions' => [
'$a' => 'iterable<int, string>',
],
],
'possiblyNullMatchesTemplateType' => [
'code' => '<?php
/**
* @template T as object
* @param T $o
* @return T
*/
function takesObject(object $o) : object {
return $o;
}
class A {}
/** @psalm-suppress PossiblyNullArgument */
$a = takesObject(rand(0, 1) ? new A() : null);',
'assertions' => [
'$a' => 'A',
],
],
'possiblyNullMatchesAnotherTemplateType' => [
'code' => '<?php
/**
* @psalm-template RealObjectType of object
*
* @psalm-param class-string<RealObjectType> $className
* @psalm-param Closure(
* RealObjectType|null
* ) : void $initializer
*/
function createProxy(
string $className,
Closure $initializer
) : void {}
class Foo {}
createProxy(Foo::class, function (?Foo $f) : void {});',
],
'assertIntersectionsOnTemplatedTypes' => [
'code' => '<?php
interface Input {}
interface HasFoo {}
interface HasBar {}
/**
* @psalm-template InputType of Input
* @psalm-param InputType $input
* @psalm-return InputType&HasFoo
*/
function decorateWithFoo(Input $input): Input
{
assert($input instanceof HasFoo);
return $input;
}
/**
* @psalm-template InputType of Input
* @psalm-param InputType $input
* @psalm-return InputType&HasBar
*/
function decorateWithBar(Input $input): Input
{
assert($input instanceof HasBar);
return $input;
}
/** @param HasFoo&HasBar $input */
function useFooAndBar(object $input): void {}
function consume(Input $input): void {
useFooAndBar(decorateWithFoo(decorateWithBar($input)));
}',
],
'bottomTypeInClosureShouldNotBind' => [
'code' => '<?php
/**
* @template T
* @param class-string<T> $className
* @param Closure(T):void $outmaker
* @return T
*/
function createProxy(
string $className,
Closure $outmaker
) : object {
/** @psalm-suppress MixedMethodCall */
$t = new $className();
$outmaker($t);
return $t;
}
class A {
public function bar() : void {}
}
createProxy(A::class, function(object $o):void {})->bar();',
],
'bottomTypeInNamespacedCallableShouldMatch' => [
'code' => '<?php
namespace Ns;
/**
* @template T
* @param class-string<T> $className
* @param callable(T):void $outmaker
* @return T
*/
function createProxy(
string $className,
callable $outmaker
) : object {
/** @psalm-suppress MixedMethodCall */
$t = new $className();
$outmaker($t);
return $t;
}
class A {
public function bar() : void {}
}
function foo(A $o):void {}
createProxy(A::class, \'Ns\foo\')->bar();',
],
'compareToEmptyArray' => [
'code' => '<?php
/**
* @template T
*
* @param T $a
* @return T
*/
function ex($a) {
if($a === []) {}
return $a;
}',
],
'compareToFalse' => [
'code' => '<?php
/**
* @template T as int|false
* @param T $value
* @return int
*/
function foo($value) {
if ($value === false) {
return -1;
}
return $value;
}',
],
'refineTemplateTypeNotEmpty' => [
'code' => '<?php
/**
* @template T of Iterator|null
* @param T $iterator
*/
function toArray($iterator): array
{
if ($iterator) {
return iterator_to_array($iterator);
}
return [];
}',
],
'manyGenericParams' => [
'code' => '<?php
/**
* @template TArg1
* @template TArg2
* @template TRes
*
* @psalm-param Closure(TArg1, TArg2): TRes $func
* @psalm-param TArg1 $arg1
*
* @psalm-return Closure(TArg2): TRes
*/
function partial(Closure $func, $arg1): Closure {
return fn($arg2) => $func($arg1, $arg2);
}
/**
* @template TArg1
* @template TArg2
* @template TRes
*
* @template T as (Closure(): TRes | Closure(TArg1): TRes | Closure(TArg1, TArg2): TRes)
*
* @psalm-param T $fn
* @psalm-param TArg1 $arg
*/
function foo(Closure $fn, $arg): void {
$a = partial($fn, $arg);
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '7.4',
],
'mixedDoesntSwallowNull' => [
'code' => '<?php
/**
* @template E
* @param E $e
* @param mixed $d
* @return ?E
* @psalm-suppress MixedInferredReturnType
*/
function reduce_values($e, $d) {
if (rand(0, 1)) {
$c = $e;
} elseif (rand(0, 1)) {
/** @psalm-suppress MixedAssignment */
$c = $d;
} else {
$c = null;
}
/** @psalm-suppress MixedReturnStatement */
return $c;
}',
],
'mixedDoesntSwallowNullProgressive' => [
'code' => '<?php
/**
* @template E
* @param E $e
* @param mixed $d
* @return ?E
* @psalm-suppress MixedInferredReturnType
*/
function reduce_values($e, $d)
{
if (rand(0, 1)) {
$d = $e;
}
if (rand(0, 1)) {
/** @psalm-suppress MixedReturnStatement */
return $d;
}
return null;
}',
],
'inferIterableArrayKeyAfterIsArrayCheck' => [
'code' => '<?php
/**
* @template Key
* @template Element
* @psalm-param iterable<Key, Element> $input
* @psalm-return Iterator<Key, Element>
*/
function to_iterator(iterable $input): Iterator
{
if (\is_array($input)) {
return new \ArrayIterator($input);
} elseif ($input instanceof Iterator) {
return $input;
} else {
return new \IteratorIterator($input);
}
}',
],
'doublyNestedFunctionTemplates' => [
'code' => '<?php
/**
* @psalm-template Tk
* @psalm-template Tv
*
* @psalm-param iterable<Tk, Tv> $iterable
* @psalm-param (callable(Tk, Tv): bool)|null $predicate
*
* @psalm-return iterable<Tk, Tv>
*/
function filter_with_key(iterable $iterable, ?callable $predicate = null): iterable
{
return (static function () use ($iterable, $predicate): Generator {
$predicate = $predicate ??
/**
* @psalm-param Tk $_k
* @psalm-param Tv $v
*
* @return bool
*/
function($_k, $v) { return (bool) $v; };
foreach ($iterable as $k => $v) {
if ($predicate($k, $v)) {
yield $k => $v;
}
}
})();
}',
],
'allowClosureParamLowerBoundAndUpperBound' => [
'code' => '<?php
class Foo {}
/**
* @template TParam as Foo
* @psalm-param Closure(TParam): void $func
* @psalm-return Closure(TParam): TParam
*/
function takesClosure(callable $func): callable {
return
/**
* @psalm-param TParam $value
* @psalm-return TParam
*/
function ($value) use ($func) {
$func($value);
return $value;
};
}
$value = takesClosure(function(Foo $foo) : void {})(new Foo());',
],
'subtractTemplatedNull' => [
'code' => '<?php
/**
* @template T
* @param T|null $var
* @return T
*/
function notNull($var) {
if ($var === null) {
throw new \InvalidArgumentException("");
}
return $var;
}
function takesNullableString(?string $s) : string {
return notNull($s);
}',
],
'subtractTemplatedInt' => [
'code' => '<?php
/**
* @template T
* @param T|int $var
* @return T
*/
function notNull($var) {
if (\is_int($var)) {
throw new \InvalidArgumentException("");
}
return $var;
}
function takesNullableString(string|int $s) : string {
return notNull($s);
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0',
],
'templateChildClass' => [
'code' => '<?php
/** @template T */
class Collection {
/**
* @param T $t
*/
private function add($t) : void {}
/**
* @template TChild as T
* @param TChild $default
*
* @return TChild
*/
public function get($default)
{
$this->add($default);
return $default;
}
}',
],
'isArrayCheckOnTemplated' => [
'code' => '<?php
/**
* @template TIterable of iterable
*/
function toList(iterable $iterable): void {
if (is_array($iterable)) {}
}',
],
'transformNestedTemplateWherePossible' => [
'code' => '<?php
/**
* @template TValue
* @template TArray of non-empty-array<TValue>
* @param TArray $arr
* @return TValue
*/
function toList(array $arr): array {
return reset($arr);
}',
],
'callTemplatedFunctionWithTemplatedClassString' => [
'code' => '<?php
/**
* @template Ta of object
* @psalm-param Ta $obj
* @return Ta
*/
function a(string $str, object $obj) {
$class = get_class($obj);
return deserialize_object($str, $class);
}
/**
* @psalm-template Tb
* @psalm-param class-string<Tb> $type
* @psalm-return Tb
* @psalm-suppress InvalidReturnType
*/
function deserialize_object(string $data, string $type) {}',
],
'arrayKeyInTemplateOfArrayKey' => [
'code' => '<?php
/**
* @template TKey of array-key
* @template TValue
* @template TNewKey of array-key
* @template TNewValue
* @psalm-param iterable<TKey, TValue> $iterable
* @psalm-param callable(TKey): iterable<TNewKey, TNewValue> $mapper
* @psalm-return \Generator<TNewKey, TNewValue>
*/
function map(iterable $iterable, callable $mapper): Generator
{
foreach ($iterable as $key => $_) {
yield from $mapper($key);
}
}
/**
* @psalm-return iterable<array-key, \stdClass>
*/
function iter(): iterable
{
return [];
}
/**
* @template TKey of array-key
* @psalm-param TKey $key
* @psalm-return Generator<TKey, string>
*/
function mapper($key): Generator
{
yield $key => "a";
}
map(iter(), "mapper");',
],
'dontScreamForArithmeticsOnIntTemplates' => [
'code' => '<?php
/**
* @template T of int|string
* @param T $p
*/
function foo($p): void {
if (is_int($p)) {
$q = $p - 1;
}
}',
],
'dontScreamForArithmeticsOnFloatTemplates' => [
'code' => '<?php
/**
* @template T of ?float
* @param T $p
* @return (T is null ? null : float)
*/
function foo(?float $p): ?float
{
if ($p === null) {
return null;
}
return $p - 1;
}',
],
'literalIsAlwaysContainedInString' => [
'code' => '<?php
/**
* @template T
*/
interface Norm
{
/**
* @param T $input
* @return T
*/
public function normalize(mixed $input): mixed;
}
/**
* @implements Norm<string>
*/
class StringNorm implements Norm
{
public function normalize(mixed $input): mixed
{
return strtolower($input);
}
}
/**
* @template TNorm
*
* @param TNorm $value
* @param Norm<TNorm> $n
*/
function normalizeField(mixed $value, Norm $n): void
{
$n->normalize($value);
}
normalizeField("foo", new StringNorm());',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0',
],
'templateWithCommentAfterSimpleType' => [
'code' => '<?php
/**
* @template T of string
*
* lorem ipsumm
*
* @param T $t
*/
function foo(string $t): string
{
return $t;
}',
],
'typeWithNestedTemplates' => [
'code' => '<?php
/**
* @template T of object
*/
interface AType {}
/**
* @template T of object
* @template B of AType<T>
*/
final class BType {}
/**
* @param BType<object, AType<object>> $_value
*/
function test1(BType $_value): void {}
/**
* @param BType<stdClass, AType<stdClass>> $_value
*/
function test2(BType $_value): void {}',
],
];
}
public function providerInvalidCodeParse(): iterable
{
return [
'invalidTemplatedType' => [
'code' => '<?php
namespace FooFoo;
/**
* @template T
* @param T $x
* @return T
*/
function foo($x) {
return $x;
}
function bar(string $a): void { }
bar(foo(4));',
'error_message' => 'InvalidScalarArgument',
],
'invalidTemplatedStaticMethodType' => [
'code' => '<?php
namespace FooFoo;
class A {
/**
* @template T
* @param T $x
* @return T
*/
public static function foo($x) {
return $x;
}
}
function bar(string $a): void { }
bar(A::foo(4));',
'error_message' => 'InvalidScalarArgument',
],
'invalidTemplatedInstanceMethodType' => [
'code' => '<?php
namespace FooFoo;
class A {
/**
* @template T
* @param T $x
* @return T
*/
public function foo($x) {
return $x;
}
}
function bar(string $a): void { }
bar((new A())->foo(4));',
'error_message' => 'InvalidScalarArgument',
],
'replaceChildTypeNoHint' => [
'code' => '<?php
/**
* @template TKey as array-key
* @template TValue
* @param Traversable<TKey, TValue> $t
* @return array<TKey, TValue>
*/
function f(Traversable $t): array {
$ret = [];
foreach ($t as $k => $v) $ret[$k] = $v;
return $ret;
}
function g():Generator { yield new stdClass; }
takesArrayOfStdClass(f(g()));
/** @param array<stdClass> $p */
function takesArrayOfStdClass(array $p): void {}',
'error_message' => 'MixedArgumentTypeCoercion',
],
'classTemplateAsIncorrectClass' => [
'code' => '<?php
class Foo {}
class NotFoo {}
/**
* @template T as Foo
* @param T $x
* @return T
*/
function bar($x) {
return $x;
}
bar(new NotFoo());',
'error_message' => 'InvalidArgument',
],
'classTemplateAsIncorrectInterface' => [
'code' => '<?php
interface Foo {}
interface NotFoo {}
/**
* @template T as Foo
* @param T $x
* @return T
*/
function bar($x) {
return $x;
}
function takesNotFoo(NotFoo $f) : void {
bar($f);
}',
'error_message' => 'InvalidArgument',
],
'templateFunctionMethodCallWithoutMethod' => [
'code' => '<?php
namespace A\B;
class C {}
/**
* @template T as C
* @param T $some_t
*/
function foo($some_t) : void {
$some_t->bar();
}',
'error_message' => 'PossiblyUndefinedMethod',
],
'templateFunctionMethodCallWithoutAsType' => [
'code' => '<?php
namespace A\B;
/**
* @template T
* @param T $some_t
*/
function foo($some_t) : void {
$some_t->bar();
}',
'error_message' => 'MixedMethodCall',
],
'bindFirstTemplatedClosureParameterInvalidScalar' => [
'code' => '<?php
/**
* @template T
*
* @param Closure(T):void $t1
* @param T $t2
*/
function apply(Closure $t1, $t2) : void
{
$t1($t2);
}
apply(function(int $_i) : void {}, "hello");',
'error_message' => 'InvalidScalarArgument',
],
'bindFirstTemplatedClosureParameterTypeCoercion' => [
'code' => '<?php
/**
* @template T
*
* @param Closure(T):void $t1
* @param T $t2
*/
function apply(Closure $t1, $t2) : void
{
$t1($t2);
}
class A {}
class AChild extends A {}
apply(function(AChild $_i) : void {}, new A());',
'error_message' => 'ArgumentTypeCoercion',
],
'callableDoesNotReturnItself' => [
'code' => '<?php
$b =
/**
* @param callable():int $s
* @return string
*/
function(callable $s) {
return "#" . $s();
};
/**
* @template T1
* @param callable(callable():T1):T1 $s
* @return void
*/
function takesReturnTCallable(callable $s) {}
takesReturnTCallable($b);',
'error_message' => 'InvalidArgument',
],
'multipleArgConstraintWithMoreRestrictiveFirstArg' => [
'code' => '<?php
class A {}
class AChild extends A {}
/**
* @template T
* @param callable(T):void $c1
* @param callable(T):void $c2
* @param T $a
*/
function foo(callable $c1, callable $c2, $a): void {
$c1($a);
$c2($a);
}
foo(
function(AChild $_a) : void {},
function(A $_a) : void {},
new A()
);',
'error_message' => 'ArgumentTypeCoercion',
],
'multipleArgConstraintWithMoreRestrictiveSecondArg' => [
'code' => '<?php
class A {}
class AChild extends A {}
/**
* @template T
* @param callable(T):void $c1
* @param callable(T):void $c2
* @param T $a
*/
function foo(callable $c1, callable $c2, $a): void {
$c1($a);
$c2($a);
}
foo(
function(A $_a) : void {},
function(AChild $_a) : void {},
new A()
);',
'error_message' => 'ArgumentTypeCoercion',
],
'multipleArgConstraintWithLessRestrictiveThirdArg' => [
'code' => '<?php
class A {}
class AChild extends A {}
/**
* @template T
* @param callable(T):void $c1
* @param callable(T):void $c2
* @param T $a
*/
function foo(callable $c1, callable $c2, $a): void {
$c1($a);
$c2($a);
}
foo(
function(AChild $_a) : void {},
function(AChild $_a) : void {},
new A()
);',
'error_message' => 'ArgumentTypeCoercion',
],
'possiblyInvalidArgumentWithUnionFirstArg' => [
'code' => '<?php
/**
* @template T
* @param T $a
* @param T $b
* @return T
*/
function foo($a, $b) {
return rand(0, 1) ? $a : $b;
}
echo foo([], "hello");',
'error_message' => 'PossiblyInvalidArgument',
],
'possiblyInvalidArgumentWithUnionSecondArg' => [
'code' => '<?php
/**
* @template T
* @param T $a
* @param T $b
* @return T
*/
function foo($a, $b) {
return rand(0, 1) ? $a : $b;
}
echo foo("hello", []);',
'error_message' => 'PossiblyInvalidArgument',
],
'preventTemplateTypeAsBeingUsedInsideFunction' => [
'code' => '<?php
/**
* @template T of DateTime
* @param callable(T) $callable
*/
function foo(callable $callable) : void {
$callable(new \DateTime());
}',
'error_message' => 'InvalidArgument',
],
'preventWrongTemplateBeingPassed' => [
'code' => '<?php
/**
* @template T of DateTime
* @template T2 of DateTime
* @param callable(T): T $parameter
* @param T2 $value
* @return T
*/
function foo(callable $parameter, $value)
{
return $parameter($value);
}',
'error_message' => 'InvalidArgument',
],
'preventTemplateTypeReturnMoreGeneral' => [
'code' => '<?php
/**
* @template T of DateTimeInterface
* @param T $x
* @return T
*/
function foo($x)
{
return new \DateTime();
}',
'error_message' => 'InvalidReturnStatement',
],
'preventReturningString' => [
'code' => '<?php
/**
* @template T
* @psalm-param T $t
* @return T
*/
function mirror($t) {
return "string";
}',
'error_message' => 'InvalidReturnStatement',
],
'unTemplatedVarOnReturn' => [
'code' => '<?php
namespace Ns;
class A {}
class B {}
/**
* @template T
* @param T $t
* @return T
*/
function getAOrB($t) {
if ($t instanceof A) {
return new A();
}
return new B();
}',
'error_message' => 'InvalidReturnStatement',
],
'templateReturnTypeOfCallableWithIncompatibleType' => [
'code' => '<?php
class A {}
class B {
public static function returnsObjectOrNull() : ?A {
return random_int(0, 1) ? new A() : null;
}
}
/**
* @psalm-template T as object
* @psalm-param callable() : T $callback
* @psalm-return T
*/
function makeResultSet(callable $callback)
{
return $callback();
}
makeResultSet([B::class, "returnsObjectOrNull"]);',
'error_message' => 'InvalidArgument',
],
'templateInvokeArg' => [
'code' => '<?php
/**
* @template T
* @param callable(T):void $c
* @param T $param
*/
function apply(callable $c, $param):void{
call_user_func($c, $param);
}
class A {
public function __toString(){
return "a";
}
}
class B {}
class Printer{
public function __invoke(A $a) : void {
echo $a;
}
}
apply(new Printer(), new B());',
'error_message' => 'InvalidArgument',
],
'invalidTemplateDocblock' => [
'code' => '<?php
/** @template */
function f():void {}',
'error_message' => 'MissingDocblockType',
],
'returnNamedObjectWhereTemplateIsExpected' => [
'code' => '<?php
class Bar {}
/**
* @template T as object
* @param T $t
* @return T
*/
function shouldComplain(object $t) {
return new Bar();
}',
'error_message' => 'InvalidReturnStatement',
],
'returnIntersectionWhenTemplateIsExpectedForward' => [
'code' => '<?php
interface Baz {}
/**
* @template T as object
* @param T $t
* @return T&Baz
*/
function returnsTemplatedIntersection(object $t) {
return $t;
}',
'error_message' => 'LessSpecificReturnStatement',
],
'returnIntersectionWhenTemplateIsExpectedBackward' => [
'code' => '<?php
interface Baz {}
/**
* @template T as object
* @param T $t
* @return Baz&T
*/
function returnsTemplatedIntersection(object $t) {
return $t;
}',
'error_message' => 'LessSpecificReturnStatement',
],
'bottomTypeInClosureShouldClash' => [
'code' => '<?php
/**
* @template T
* @param class-string<T> $className
* @param Closure(T):void $outmaker
* @return T
*/
function createProxy(
string $className,
Closure $outmaker
) : object {
/** @psalm-suppress MixedMethodCall */
$t = new $className();
$outmaker($t);
return $t;
}
class A {
public function bar() : void {}
}
class B {}
createProxy(A::class, function(B $o):void {})->bar();',
'error_message' => 'InvalidArgument',
],
'bottomTypeInNamespacedCallableShouldClash' => [
'code' => '<?php
namespace Ns;
/**
* @template T
* @param class-string<T> $className
* @param callable(T):void $outmaker
* @return T
*/
function createProxy(
string $className,
callable $outmaker
) : object {
/** @psalm-suppress MixedMethodCall */
$t = new $className();
$outmaker($t);
return $t;
}
class A {
public function bar() : void {}
}
class B {}
function foo(B $o):void {}
createProxy(A::class, \'Ns\foo\')->bar();',
'error_message' => 'InvalidArgument',
],
'preventBadArraySubtyping' => [
'code' => '<?php
/**
* @template T as array{a: int}
* @return T
*/
function foo() : array {
$b = ["a" => 123];
return $b;
}',
'error_message' => 'InvalidReturnStatement',
],
'modifyTemplatedShape' => [
'code' => '<?php
/**
* @template T as array{a: int}
* @param T $s
* @return T
*/
function foo(array $s) : array {
$s["a"] = 123;
return $s;
}',
'error_message' => 'InvalidReturnStatement',
],
'preventArrayOverwriting' => [
'code' => '<?php
/**
* @template T
* @return T
*/
function foo(array $b) : array {
return $b;
}',
'error_message' => 'InvalidReturnStatement',
],
'catchIssueInTemplatedFunctionInsideClass' => [
'code' => '<?php
/**
* @template T
*/
interface Container {
/** @param T $value */
public function take($value): void;
}
class Foo {
/**
* @template T
* @param Container<T> $c
*/
function jsonFromEntityCollection(Container $c): void {
$c->take("foo");
}
}',
'error_message' => 'InvalidArgument',
],
'catchInvalidTemplateTypeWithNestedTemplates' => [
'code' => '<?php
/**
* @template T
*/
interface AType {}
/**
* @template T
* @template B of AType<T>
*/
final class BType {}
/**
* @param BType<string, AType<int>> $_value
*/
function test1(BType $_value): void {}
/**
* @param BType<int, AType<string>> $_value
*/
function test2(BType $_value): void {}',
'error_message' => 'InvalidTemplateParam',
],
];
}
}