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

5029 lines
174 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Psalm\Tests\Template;
use Psalm\Tests\TestCase;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
use const DIRECTORY_SEPARATOR;
class ClassTemplateTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
public function providerValidCodeParse(): iterable
{
return [
'templateIntersection' => [
'code' => '<?php
interface EntityInterface
{
public function getId(): string;
}
/**
* @phpstan-template T of EntityInterface
*/
interface RepositoryInterface
{
/**
* @return T|null
*/
public function byId(string $id);
}
final class Foo implements EntityInterface
{
public function getId(): string
{
return "42";
}
}
/**
* @phpstan-implements RepositoryInterface<Foo>
*/
final class FooRepository implements RepositoryInterface
{
/**
* @var Foo[]
*/
public array $elements = [];
public function byId(string $id): ?Foo
{
return $this->elements[$id] ?? null;
}
}
',
],
'cachingIterator' => [
'code' => '<?php
$input = range("a", "z");
$arrayIterator = new ArrayIterator($input);
$decoratorIterator = new CachingIterator($arrayIterator);
$next = $decoratorIterator->hasNext();
$key = $decoratorIterator->key();
$value = $decoratorIterator->current();
',
'assertions' => [
'$key' => 'int<0, max>|null',
'$value' => 'null|string',
'$next' => 'bool',
],
],
'infiniteIterator' => [
'code' => '<?php
$input = range("a", "z");
$arrayIterator = new ArrayIterator($input);
$decoratorIterator = new InfiniteIterator($arrayIterator);
$key = $decoratorIterator->key();
$value = $decoratorIterator->current();
',
'assertions' => [
'$key' => 'int<0, max>|null',
'$value' => 'null|string',
],
],
'limitIterator' => [
'code' => '<?php
$input = range("a", "z");
$arrayIterator = new ArrayIterator($input);
$decoratorIterator = new LimitIterator($arrayIterator, 1, 1);
$key = $decoratorIterator->key();
$value = $decoratorIterator->current();
',
'assertions' => [
'$key' => 'int<0, max>|null',
'$value' => 'null|string',
],
],
'callbackFilterIterator' => [
'code' => '<?php
$input = range("a", "z");
$arrayIterator = new ArrayIterator($input);
$decoratorIterator = new CallbackFilterIterator(
$arrayIterator,
static function (string $value): bool {return "a" === $value;}
);
$key = $decoratorIterator->key();
$value = $decoratorIterator->current();
',
'assertions' => [
'$key' => 'int<0, max>|null',
'$value' => 'null|string',
],
],
'noRewindIterator' => [
'code' => '<?php
$input = range("a", "z");
$arrayIterator = new ArrayIterator($input);
$decoratorIterator = new NoRewindIterator($arrayIterator);
$key = $decoratorIterator->key();
$value = $decoratorIterator->current();
',
'assertions' => [
'$key' => 'int<0, max>|null',
'$value' => 'null|string',
],
],
'classTemplate' => [
'code' => '<?php
class A {}
class B {}
class C {}
class D {}
/**
* @template T as object
*/
class Foo {
/** @var T::class */
public $T;
/**
* @param class-string<T> $T
*/
public function __construct(string $T) {
$this->T = $T;
}
/**
* @return T
* @psalm-suppress MixedMethodCall
*/
public function bar() {
$t = $this->T;
return new $t();
}
}
$at = "A";
/**
* @var Foo<A>
* @psalm-suppress ArgumentTypeCoercion
*/
$afoo = new Foo($at);
$afoo_bar = $afoo->bar();
$bfoo = new Foo(B::class);
$bfoo_bar = $bfoo->bar();
// this shouldnt cause a problem as its a docbblock type
if (!($bfoo_bar instanceof B)) {}
$c = C::class;
$cfoo = new Foo($c);
$cfoo_bar = $cfoo->bar();',
'assertions' => [
'$afoo' => 'Foo<A>',
'$afoo_bar' => 'A',
'$bfoo' => 'Foo<B>',
'$bfoo_bar' => 'B',
'$cfoo' => 'Foo<C>',
'$cfoo_bar' => 'C',
],
'ignored_issues' => [
'MixedReturnStatement',
'LessSpecificReturnStatement',
'DocblockTypeContradiction',
],
],
'classTemplateSelfs' => [
'code' => '<?php
/**
* @template T as object
*/
class Foo {
/** @var class-string<T> */
public $T;
/**
* @param class-string<T> $T
*/
public function __construct(string $T) {
$this->T = $T;
}
/**
* @return T
* @psalm-suppress MixedMethodCall
*/
public function bar() {
$t = $this->T;
return new $t();
}
}
class E {
/**
* @return Foo<self>
*/
public static function getFoo() {
return new Foo(__CLASS__);
}
/**
* @return Foo<self>
*/
public static function getFoo2() {
return new Foo(self::class);
}
/**
* @return Foo<static>
*/
public static function getFoo3() {
return new Foo(static::class);
}
}
class G extends E {}
$efoo = E::getFoo();
$efoo2 = E::getFoo2();
$efoo3 = E::getFoo3();
$gfoo = G::getFoo();
$gfoo2 = G::getFoo2();
$gfoo3 = G::getFoo3();',
'assertions' => [
'$efoo' => 'Foo<E>',
'$efoo2' => 'Foo<E>',
'$efoo3' => 'Foo<E>',
'$gfoo' => 'Foo<E>',
'$gfoo2' => 'Foo<E>',
'$gfoo3' => 'Foo<G>',
],
'ignored_issues' => [
'LessSpecificReturnStatement',
'RedundantConditionGivenDocblockType',
],
],
'classTemplateExternalClasses' => [
'code' => '<?php
/**
* @template T as object
*/
class Foo {
/** @var T::class */
public $T;
/**
* @param class-string<T> $T
*/
public function __construct(string $T) {
$this->T = $T;
}
/**
* @return T
* @psalm-suppress MixedMethodCall
*/
public function bar() {
$t = $this->T;
return new $t();
}
}
$efoo = new Foo(\Exception::class);
$efoo_bar = $efoo->bar();
$ffoo = new Foo(\LogicException::class);
$ffoo_bar = $ffoo->bar();',
'assertions' => [
'$efoo' => 'Foo<Exception>',
'$efoo_bar' => 'Exception',
'$ffoo' => 'Foo<LogicException>',
'$ffoo_bar' => 'LogicException',
],
'ignored_issues' => ['LessSpecificReturnStatement'],
],
'classTemplateContainerSimpleCall' => [
'code' => '<?php
class A {}
/**
* @template T
*/
class Foo {
/** @var T */
public $obj;
/**
* @param T $obj
*/
public function __construct($obj) {
$this->obj = $obj;
}
/**
* @return T
*/
public function bar() {
return $this->obj;
}
}
$afoo = new Foo(new A());
$afoo_bar = $afoo->bar();',
'assertions' => [
'$afoo' => 'Foo<A>',
'$afoo_bar' => 'A',
],
],
'classTemplateContainerThisCall' => [
'code' => '<?php
/**
* @template T
*/
class Foo {
/** @var T */
public $obj;
/**
* @param T $obj
*/
public function __construct($obj) {
$this->obj = $obj;
}
/**
* @return T
*/
public function bar() {
return $this->obj;
}
/**
* @return T
*/
public function bat() {
return $this->bar();
}
public function __toString(): string {
return "hello " . $this->obj;
}
}',
'assertions' => [],
'ignored_issues' => ['MixedOperand'],
],
'validPsalmTemplatedClassType' => [
'code' => '<?php
class A {}
/**
* @psalm-template T
*/
class Foo {
/**
* @param T $x
*/
public function bar($x): void { }
}
$afoo = new Foo();
$afoo->bar(new A());',
],
'intersectionTemplatedTypes' => [
'code' => '<?php
namespace NS;
use Countable;
/** @template T */
class Collection
{
/** @psalm-var iterable<T> */
private $data;
/** @psalm-param iterable<T> $data */
public function __construct(iterable $data) {
$this->data = $data;
}
}
class Item {}
/** @psalm-param Collection<Item> $c */
function takesCollectionOfItems(Collection $c): void {}
/** @psalm-var iterable<Item> $data2 */
$data2 = [];
takesCollectionOfItems(new Collection($data2));
/** @psalm-var iterable<Item>&Countable $data */
$data = [];
takesCollectionOfItems(new Collection($data));',
],
'repeatedCall' => [
'code' => '<?php
namespace NS;
use Closure;
/**
* @template TKey as array-key
* @template TValue
* @psalm-consistent-constructor
* @psalm-consistent-templates
*/
class ArrayCollection {
/** @var array<TKey,TValue> */
private $data;
/** @param array<TKey,TValue> $data */
public function __construct(array $data) {
$this->data = $data;
}
/**
* @template T
* @param Closure(TValue):T $func
* @return ArrayCollection<TKey,T>
*/
public function map(Closure $func) {
return new static(array_map($func, $this->data));
}
}
class Item {}
/**
* @param ArrayCollection<array-key,Item> $i
*/
function takesCollectionOfItems(ArrayCollection $i): void {}
$c = new ArrayCollection([ new Item ]);
takesCollectionOfItems($c);
takesCollectionOfItems($c->map(function(Item $i): Item { return $i;}));
takesCollectionOfItems($c->map(function(Item $i): Item { return $i;}));',
],
'noRepeatedTypeException' => [
'code' => '<?php
/** @template T as object */
class Foo
{
/**
* @psalm-var class-string<T>
*/
private $type;
/** @var array<T> */
private $items;
/**
* @param class-string<T> $type
*/
public function __construct(string $type)
{
if (!in_array($type, [A::class, B::class], true)) {
throw new \InvalidArgumentException;
}
$this->type = $type;
$this->items = [];
}
/** @param T $item */
public function add($item): void
{
$this->items[] = $item;
}
}
class FooFacade
{
/**
* @template T as object
* @param T $item
*/
public function add(object $item): void
{
$foo = $this->ensureFoo([$item]);
$foo->add($item);
}
/**
* @template T as object
* @param array<mixed,T> $items
* @return Foo<T>
*/
private function ensureFoo(array $items): Foo
{
/** @var class-string<T> */
$type = $items[0] instanceof A ? A::class : B::class;
return new Foo($type);
}
}
class A {}
class B {}',
],
'collectionOfClosure' => [
'code' => '<?php
/**
* @template TKey
* @template TValue
*/
class Collection {
/**
* @param Closure(TValue):bool $p
* @return Collection<TKey,TValue>
*/
public function filter(Closure $p) {
return $this;
}
}
class I {}
/** @var Collection<mixed,Collection<mixed,I>> $c */
$c = new Collection;
$c->filter(
/** @param Collection<mixed,I> $elt */
function(Collection $elt): bool { return (bool) rand(0,1); }
);
$c->filter(
/** @param Collection<mixed,I> $elt */
function(Collection $elt): bool { return true; }
);',
],
'templatedInterfaceIteration' => [
'code' => '<?php
namespace NS;
/**
* @template TKey
* @template TValue
*
* @extends \IteratorAggregate<TKey, TValue>
*/
interface ICollection extends \IteratorAggregate {
/** @return \Traversable<TKey,TValue> */
public function getIterator();
}
/**
* @template TKey as array-key
* @template TValue
*
* @implements ICollection<TKey, TValue>
*/
class Collection implements ICollection {
/** @var array<TKey, TValue> */
private $data;
/**
* @param array<TKey, TValue> $data
*/
public function __construct(array $data) {
$this->data = $data;
}
/**
* @return \Traversable<TKey, TValue>
*/
public function getIterator(): \Traversable {
return new \ArrayIterator($this->data);
}
}
$c = new Collection(["a" => 1]);
foreach ($c as $k => $v) { atan($v); strlen($k); }',
],
'allowTemplatedIntersectionToExtend' => [
'code' => '<?php
interface Foo {}
interface AlmostFoo {
/**
* @return Foo
*/
public function makeFoo();
}
/**
* @template T
*/
final class AlmostFooMap implements AlmostFoo {
/** @var T&Foo */
private $bar;
/**
* @param T&Foo $bar
*/
public function __construct(Foo $bar)
{
$this->bar = $bar;
}
/**
* @return T&Foo
*/
public function makeFoo()
{
return $this->bar;
}
}',
],
'restrictTemplateInputWithTClassGoodInput' => [
'code' => '<?php
namespace Bar;
/** @template T as object */
class Foo
{
/**
* @psalm-var T::class
*/
private $type;
/** @var array<T> */
private $items;
/**
* @param T::class $type
*/
public function __construct(string $type)
{
if (!in_array($type, [A::class, B::class], true)) {
throw new \InvalidArgumentException;
}
$this->type = $type;
$this->items = [];
}
/** @param T $item */
public function add($item): void
{
$this->items[] = $item;
}
}
class A {}
class B {}
$foo = new Foo(A::class);
$foo->add(new A);',
],
'classTemplateFunctionImplementsInterface' => [
'code' => '<?php
namespace A\B;
interface Foo {}
interface IFooGetter {
/**
* @return Foo
*/
public function getFoo();
}
/**
* @template T as Foo
*/
class FooGetter implements IFooGetter {
/** @var T */
private $t;
/**
* @param T $t
*/
public function __construct(Foo $t)
{
$this->t = $t;
}
/**
* @return T
*/
public function getFoo()
{
return $this->t;
}
}
function passFoo(Foo $f) : Foo {
return (new FooGetter($f))->getFoo();
}',
],
'getPropertyOnClass' => [
'code' => '<?php
class Foo {
/** @var int */
public $id = 0;
}
/**
* @template T as Foo
*/
class Collection {
/**
* @var class-string<T>
*/
private $type;
/**
* @param class-string<T> $type
*/
public function __construct(string $type) {
$this->type = $type;
}
/**
* @return class-string<T>
*/
public function getType()
{
return $this->type;
}
/**
* @param T $object
*/
public function bar(Foo $object) : void
{
if ($this->getType() !== get_class($object)) {
return;
}
echo $object->id;
}
}
class FooChild extends Foo {}
/** @param Collection<Foo> $c */
function handleCollectionOfFoo(Collection $c) : void {
if ($c->getType() === FooChild::class) {}
}',
],
'getMagicPropertyOnClass' => [
'code' => '<?php
class A {}
/**
* @template T as A
* @property ?T $x
*/
class B {
/** @var ?T */
public $y;
public function __get() {}
}
$b = new B();
$b_x = $b->x;
$b_y = $b->y;
',
'assertions' => [
'$b_x' => 'A|null',
'$b_y' => 'A|null',
],
],
'getMagicPropertyOnThis' => [
'code' => '<?php
abstract class A {}
class X extends A {}
/**
* @template T as A
* @property ?T $x
*/
class B {
/** @var ?T */
public $y;
public function __get() {}
public function test(): void {
if ($this->x instanceof X) {}
if ($this->y instanceof X) {}
}
}
',
],
'getEquateClass' => [
'code' => '<?php
class Foo {
/** @var int */
public $id = 0;
}
/**
* @template T as Foo
*/
class Container {
/**
* @var T
*/
private $obj;
/**
* @param T $obj
*/
public function __construct(Foo $obj) {
$this->obj = $obj;
}
/**
* @param T $object
*/
public function bar(Foo $object) : void
{
if ($this->obj === $object) {}
}
}',
],
'allowComparisonGetTypeResult' => [
'code' => '<?php
class Foo {}
/**
* @template T as Foo
*/
class Collection {
/**
* @var class-string<T>
*/
private $type;
/**
* @param class-string<T> $type
*/
public function __construct(string $type) {
$this->type = $type;
}
/**
* @return class-string<T>|null
*/
public function getType()
{
return $this->type;
}
}
function foo(Collection $c) : void {
$val = $c->getType();
if (!$val) {}
if ($val) {}
}',
],
'mixedTemplatedParamOutWithNoExtendedTemplate' => [
'code' => '<?php
/**
* @template TValue
*/
class ValueContainer
{
/**
* @var TValue
*/
private $v;
/**
* @param TValue $v
*/
public function __construct($v)
{
$this->v = $v;
}
/**
* @return TValue
*/
public function getValue()
{
return $this->v;
}
}
/**
* @psalm-suppress MissingTemplateParam
* @template TKey
* @template TValue
*/
class KeyValueContainer extends ValueContainer
{
/**
* @var TKey
*/
private $k;
/**
* @param TKey $k
* @param TValue $v
*/
public function __construct($k, $v)
{
$this->k = $k;
parent::__construct($v);
}
/**
* @return TKey
*/
public function getKey()
{
return $this->k;
}
}
$a = new KeyValueContainer("hello", 15);
$b = $a->getValue();',
'assertions' => [
'$a' => 'KeyValueContainer<string, int>',
'$b' => 'mixed',
],
'ignored_issues' => ['MixedAssignment'],
],
'mixedTemplatedParamOutDifferentParamName' => [
'code' => '<?php
/**
* @template TValue
*/
class ValueContainer
{
/**
* @var TValue
*/
private $v;
/**
* @param TValue $v
*/
public function __construct($v)
{
$this->v = $v;
}
/**
* @return TValue
*/
public function getValue()
{
return $this->v;
}
}
/**
* @template TKey
* @template Tv
*
* @psalm-suppress MissingTemplateParam
*/
class KeyValueContainer extends ValueContainer
{
/**
* @var TKey
*/
private $k;
/**
* @param TKey $k
* @param Tv $v
*/
public function __construct($k, $v)
{
$this->k = $k;
parent::__construct($v);
}
/**
* @return TKey
*/
public function getKey()
{
return $this->k;
}
}
$a = new KeyValueContainer("hello", 15);
$b = $a->getValue();',
'assertions' => [
'$a' => 'KeyValueContainer<string, int>',
'$b' => 'mixed',
],
'ignored_issues' => ['MixedAssignment'],
],
'doesntExtendTemplateAndDoesNotOverride' => [
'code' => '<?php
/**
* @template T as array-key
*/
abstract class User
{
/**
* @var T
*/
private $id;
/**
* @param T $id
*/
public function __construct($id)
{
$this->id = $id;
}
/**
* @return T
*/
public function getID()
{
return $this->id;
}
}
/**
* @psalm-suppress MissingTemplateParam
*/
class AppUser extends User {}
$au = new AppUser(-1);
$id = $au->getId();',
'assertions' => [
'$au' => 'AppUser',
'$id' => 'array-key',
],
],
'templateTKeyedArrayValues' => [
'code' => '<?php
/**
* @template TKey
* @template TValue
*/
class Collection {
/**
* @return array{0:Collection<TKey,TValue>,1:Collection<TKey,TValue>}
* @psalm-suppress InvalidReturnType
*/
public function partition() {}
}
/** @var Collection<int,string> $c */
$c = new Collection;
[$partA, $partB] = $c->partition();',
'assertions' => [
'$partA' => 'Collection<int, string>',
'$partB' => 'Collection<int, string>',
],
],
'doublyLinkedListConstructor' => [
'code' => '<?php
$list = new SplDoublyLinkedList();
$list->add(5, "hello");
$list->add(5, 1);
/** @var SplDoublyLinkedList<string> */
$templated_list = new SplDoublyLinkedList();
$templated_list->add(5, "hello");
$a = $templated_list->bottom();',
'assertions' => [
'$a' => 'string',
],
],
'templateDefaultSimpleString' => [
'code' => '<?php
/**
* @template T as string
*/
class C {
/** @var T */
public $t;
/**
* @param T $t
*/
function __construct(string $t = "hello") {
$this->t = $t;
}
}
$c = new C();',
'assertions' => [
'$c===' => "C<'hello'>",
],
],
'SKIPPED-templateDefaultConstant' => [
'code' => '<?php
const FOO = "bar";
/**
* @template T as string
*/
class E {
/** @var T */
public $t;
/**
* @param T $t
*/
function __construct(string $t = FOO) {
$this->t = $t;
}
}
$e = new E();',
'assertions' => [
'$e===' => 'E<string(bar)>',
],
],
'SKIPPED-templateDefaultClassMemberConstant' => [
'code' => '<?php
class D {
const FOO = "bar";
}
/**
* @template T as string
*/
class E {
/** @var T */
public $t;
/**
* @param T $t
*/
function __construct(string $t = D::FOO) {
$this->t = $t;
}
}
$e = new E();',
'assertions' => [
'$e===' => 'E<string(bar)>',
],
],
'templateDefaultClassConstant' => [
'code' => '<?php
class D {}
/**
* @template T as object
*/
class E {
/** @var class-string<T> */
public $t;
/**
* @param class-string<T> $t
*/
function __construct(string $t = D::class) {
$this->t = $t;
}
}
$e = new E();',
'assertions' => [
'$e===' => 'E<D>',
],
],
'allowNullablePropertyAssignment' => [
'code' => '<?php
/**
* @template T1
*/
interface I {
/**
* @return T1
*/
public function get();
}
/**
* @template T2
*/
class C {
/**
* @var T2|null
*/
private $bar;
/**
* @param I<T2> $foo
*/
public function __construct(I $foo) {
$this->bar = $foo->get();
}
}',
],
'reflectionClass' => [
'code' => '<?php
/**
* @template T as object
*
* @property-read class-string<T> $name
*/
class CustomReflectionClass {
/**
* @var class-string<T>
*/
public $name;
/**
* @param T|class-string<T> $argument
*/
public function __construct($argument) {
if (is_object($argument)) {
$this->name = get_class($argument);
} else {
$this->name = $argument;
}
}
}
/**
* @template T as object
* @param class-string<T> $className
* @return CustomReflectionClass<T>
*/
function getTypeOf(string $className) {
return new CustomReflectionClass($className);
}',
],
'psalmReflectionClass' => [
'code' => '<?php
/**
* @template T as object
*
* @psalm-property-read class-string<T> $name
*/
class CustomReflectionClass {
/**
* @var class-string<T>
*/
public $name;
/**
* @param T|class-string<T> $argument
*/
public function __construct($argument) {
if (is_object($argument)) {
$this->name = get_class($argument);
} else {
$this->name = $argument;
}
}
}
/**
* @template T as object
* @param class-string<T> $className
* @return CustomReflectionClass<T>
*/
function getTypeOf(string $className) {
return new CustomReflectionClass($className);
}',
],
'ignoreTooManyGenericObjectArgs' => [
'code' => '<?php
/**
* @template T
*/
class C {
/** @var T */
public $t;
/** @param T $t */
public function __construct($t) {
$this->t = $t;
}
}
/** @param C<int> $c */
function takesC(C $c) : void {}
/**
* @psalm-suppress TooManyTemplateParams
* @var C<int, int>
*/
$c = new C(5);
takesC($c);',
],
'classTemplateUnionType' => [
'code' => '<?php
/**
* @template T0 as int|string
*/
class C {
/**
* @param T0 $t
*/
public function foo($t) : void {}
}
/** @param C<int> $c */
function foo(C $c) : void {}
/** @param C<string> $c */
function bar(C $c) : void {}',
],
'unionAsTypeReturnType' => [
'code' => '<?php
/**
* @template TKey of ?array-key
* @template T
*/
interface Collection
{
/**
* @param Closure(T=):bool $p
* @return Collection<TKey, T>
*/
public function filter(Closure $p);
}',
],
'converterObject' => [
'code' => '<?php
/**
* @template I as array-key
* @template V
*/
class Converter
{
/**
* @var array<I, V> $records
*/
public $records;
/**
* @param array<I, V> $records
*/
public function __construct(array $records) {
$this->records = $records;
}
/**
* @template Q2 as object
*
* @param Q2 $obj2
*
* @return array<I, V|Q2>
*/
private function appender(object $obj2): array
{
$arr = [];
foreach ($this->records as $key => $obj) {
if (rand(0, 1)) {
$obj = $obj2;
}
$arr[$key] = $obj;
}
return $arr;
}
/**
* @template Q1 as object
*
* @param Q1 $obj
*
* @return array<I, V|Q1>
*/
public function appendProperty(object $obj): array
{
return $this->appender($obj);
}
}',
],
'converterClassString' => [
'code' => '<?php
/**
* @template I as array-key
* @template V
*/
class Converter
{
/**
* @var array<I, V> $records
*/
public $records;
/**
* @param array<I, V> $records
*/
public function __construct(array $records) {
$this->records = $records;
}
/**
* @template Q as object
*
* @param class-string<Q> $obj
*
* @return array<I, V|Q>
*/
public function appendProperty(string $obj): array
{
return $this->appender($obj);
}
/**
* @template Q as object
*
* @param class-string<Q> $obj2
*
* @return array<I, V|Q>
*
* @psalm-suppress MixedMethodCall
*/
private function appender(string $obj2): array
{
$arr = [];
foreach ($this->records as $key => $obj) {
if (rand(0, 1)) {
$obj = new $obj2;
}
$arr[$key] = $obj;
}
return $arr;
}
}',
],
'allowTemplateReconciliation' => [
'code' => '<?php
/**
* @template T
*/
abstract class C {
/** @param T $t */
public function foo($t): void {
if (!$t) {}
if ($t) {}
}
}',
],
'allowTemplateParamsToCoerceToMinimumTypes' => [
'code' => '<?php
/**
* @psalm-template TKey of array-key
* @psalm-template T
*/
class ArrayCollection
{
/**
* @var array<TKey,T>
*/
private $elements;
/**
* @param array<TKey,T> $elements
*/
public function __construct(array $elements = [])
{
$this->elements = $elements;
}
}
/** @psalm-suppress MixedArgument */
$c = new ArrayCollection($GLOBALS["a"]);',
'assertions' => [
'$c' => 'ArrayCollection<array-key, mixed>',
],
],
'doNotCombineTypes' => [
'code' => '<?php
class A {}
class B {}
/**
* @template T
*/
class C {
/**
* @var T
*/
private $t;
/**
* @param T $t
*/
public function __construct($t) {
$this->t = $t;
}
/**
* @return T
*/
public function get() {
return $this->t;
}
}
/**
* @param C<A> $a
* @param C<B> $b
* @return C<A>|C<B>
*/
function randomCollection(C $a, C $b) : C {
if (rand(0, 1)) {
return $a;
}
return $b;
}
$random_collection = randomCollection(new C(new A), new C(new B));
$a_or_b = $random_collection->get();',
'assertions' => [
'$random_collection' => 'C<A>|C<B>',
'$a_or_b' => 'A|B',
],
],
'doNotCombineTypesWhenMemoized' => [
'code' => '<?php
class A {}
class B {}
/**
* @template T
*/
class C {
/**
* @var T
*/
private $t;
/**
* @param T $t
*/
public function __construct($t) {
$this->t = $t;
}
/**
* @return T
* @psalm-mutation-free
*/
public function get() {
return $this->t;
}
}
/** @var C<A>|C<B> $random_collection **/
$a_or_b = $random_collection->get();',
'assertions' => [
'$random_collection' => 'C<A>|C<B>',
'$a_or_b' => 'A|B',
],
],
'inferClosureParamTypeFromContext' => [
'code' => '<?php
/**
* @template E
*/
interface Collection {
/**
* @template R
* @param callable(E):R $action
* @return Collection<R>
*/
function map(callable $action): self;
}
/**
* @template T
*/
interface Optional {
/**
* @return T
*/
function get();
}
/**
* @param Collection<Optional<string>> $collection
* @return Collection<string>
*/
function expandOptions(Collection $collection) : Collection {
return $collection->map(
function ($optional) {
return $optional->get();
}
);
}',
],
'templateEmptyParamCoercion' => [
'code' => '<?php
namespace NS;
use Countable;
/** @template T */
class Collection
{
/** @psalm-var iterable<T> */
private $data;
/** @psalm-param iterable<T> $data */
public function __construct(iterable $data = []) {
$this->data = $data;
}
}
class Item {}
/** @psalm-param Collection<Item> $c */
function takesCollectionOfItems(Collection $c): void {}
takesCollectionOfItems(new Collection());
takesCollectionOfItems(new Collection([]));',
],
'templatedGet' => [
'code' => '<?php
/**
* @template P as string
* @template V as mixed
*/
class PropertyBag {
/** @var array<P,V> */
protected $data = [];
/** @param array<P,V> $data */
public function __construct(array $data) {
$this->data = $data;
}
/** @param P $name */
public function __isset(string $name): bool {
return isset($this->data[$name]);
}
/**
* @param P $name
* @return V
*/
public function __get(string $name) {
return $this->data[$name];
}
}
$p = new PropertyBag(["a" => "data for a", "b" => "data for b"]);
$a = $p->a;',
'assertions' => [
'$a' => 'string',
],
],
'templateAsArray' => [
'code' => '<?php
/**
* @template DATA as array<string, scalar|array|object|null>
*/
abstract class Foo {
/**
* @var DATA
*/
protected $data;
/**
* @param DATA $data
*/
public function __construct(array $data) {
$this->data = $data;
}
/**
* @return scalar|array|object|null
*/
public function __get(string $property) {
return isset($this->data[$property]) ? $this->data[$property] : null;
}
}',
],
'keyOfClassTemplateAcceptingIndexedAccess' => [
'code' => '<?php
/**
* @template TData as array
*/
abstract class DataBag {
/**
* @var TData
*/
protected $data;
/**
* @param TData $data
*/
public function __construct(array $data) {
$this->data = $data;
}
/**
* @template K as key-of<TData>
*
* @param K $property
* @param TData[K] $value
*/
public function __set(string $property, $value) {
$this->data[$property] = $value;
}
}',
],
'keyOfClassTemplateReturningIndexedAccess' => [
'code' => '<?php
/**
* @template TData as array
*/
abstract class DataBag {
/**
* @var TData
*/
protected $data;
/**
* @param TData $data
*/
public function __construct(array $data) {
$this->data = $data;
}
/**
* @template K as key-of<TData>
*
* @param K $property
*
* @return TData[K]
*/
public function __get(string $property) {
return $this->data[$property];
}
}',
],
'SKIPPED-templatedInterfaceIntersectionFirst' => [
'code' => '<?php
/** @psalm-template T */
interface IParent {
/** @psalm-return T */
function foo();
}
interface IChild extends IParent {}
class C {}
/** @psalm-return IParent<C>&IChild */
function makeConcrete() : IChild {
return new class() implements IChild {
public function foo() {
return new C();
}
};
}
$a = makeConcrete()->foo();',
'assertions' => [
'$a' => 'C',
],
],
'templatedInterfaceIntersectionSecond' => [
'code' => '<?php
/** @psalm-template T */
interface IParent {
/** @psalm-return T */
function foo();
}
/** @psalm-suppress MissingTemplateParam */
interface IChild extends IParent {}
class C {}
/** @psalm-return IChild&IParent<C> */
function makeConcrete() : IChild {
return new class() implements IChild {
public function foo() {
return new C();
}
};
}
$a = makeConcrete()->foo();',
'assertions' => [
'$a' => 'C',
],
],
'returnTemplateIntersectionGenericObjectAndTemplate' => [
'code' => '<?php
/** @psalm-template Tp */
interface I {
/** @psalm-return Tp */
function getMe();
}
class C {}
/**
* @psalm-template T as object
*
* @psalm-param class-string<T> $className
*
* @psalm-return T&I<T>
*
* @psalm-suppress MissingTemplateParam
*/
function makeConcrete(string $className) : object
{
/** @var T&I<T> */
return new class() extends C implements I {
public function getMe() {
return $this;
}
};
}
$a = makeConcrete(C::class);',
'assertions' => [
'$a' => 'C&I<C>',
],
],
'keyOfArrayGet' => [
'code' => '<?php
/**
* @template DATA as array<string, int|bool>
*/
abstract class Foo {
/**
* @var DATA
*/
protected $data;
/**
* @param DATA $data
*/
public function __construct(array $data) {
$this->data = $data;
}
/**
* @template K as key-of<DATA>
*
* @param K $property
*
* @return DATA[K]
*/
public function __get(string $property) {
return $this->data[$property];
}
}',
],
'keyOfArrayRandomKey' => [
'code' => '<?php
/**
* @template DATA as array<string, int|bool>
*/
abstract class Foo {
/**
* @var DATA
*/
protected $data;
/**
* @param DATA $data
*/
public function __construct(array $data) {
$this->data = $data;
}
/**
* @return key-of<DATA>
*/
abstract public function getRandomKey() : string;
}',
],
'allowBoolTemplateCoercion' => [
'code' => '<?php
/** @template T */
class TestPromise {
/** @psalm-param T $value */
public function __construct($value) {}
}
/** @return TestPromise<bool> */
function test(): TestPromise {
return new TestPromise(true);
}',
],
'classTemplatedPropertyEmptyAssignment' => [
'code' => '<?php
/** @template T */
class Foo {
/** @param \Closure():T $closure */
public function __construct($closure) {}
}
class Bar {
/** @var Foo<array> */
private $FooArray;
public function __construct() {
$this->FooArray = new Foo(function(): array { return []; });
}
}',
],
'classTemplatedPropertyAssignmentWithMoreSpecificArray' => [
'code' => '<?php
/** @template T */
class Foo {
/** @param \Closure():T $closure */
public function __construct($closure) {}
}
class Bar {
/** @var Foo<array> */
private $FooArray;
public function __construct() {
$this->FooArray = new Foo(function(): array { return []; });
}
}',
],
'insideClosureVarTemplate' => [
'code' => '<?php
/**
* @template T of object
*/
class Foo {
/**
* @psalm-return callable(T): ?T
*/
public function bar() {
return
/**
* @param T $data
* @return ?T
*/
function($data) {
$data = rand(0, 1) ? $data : null;
return $data;
};
}
}',
],
'reflectTemplatedClass' => [
'code' => '<?php
/** @template T1 of object */
class Foo {
/**
* @param class-string<T1> $a
* @psalm-return ReflectionClass<T1>
*/
public function reflection(string $a) {
return new ReflectionClass($a);
}
}',
],
'anonymousClassMustNotBreakParentTemplate' => [
'code' => '<?php
/** @template T */
class Foo {
/** @psalm-var ?T */
private $value;
/** @psalm-param T $val */
public function set($val) : void {
$this->value = $val;
/** @psalm-suppress MissingTemplateParam */
new class extends Foo {};
}
/** @psalm-return ?T */
public function get() {
return $this->value;
}
}',
],
'templatedInvoke' => [
'code' => '<?php
/**
* @template T
* @psalm-consistent-constructor
* @psalm-consistent-templates
*/
class Foo {
/** @var T */
private $value;
/** @param T $val */
public function __construct($val) {
$this->value = $val;
}
/** @return T */
public function get() {
return $this->value;
}
/**
* @param T $val
* @return Foo<T>
*/
public function __invoke($val) {
return new static($val);
}
/**
* @param T $val
* @return Foo<T>
*/
public function create($val) {
return new static($val);
}
}
function bar(string $s) : string {
$foo = new Foo($s);
$bar = $foo($s);
return $bar->get();
}',
],
'templatedLiteralStringReplacement' => [
'code' => '<?php
/**
* @template T
*/
final class Value {
/**
* @psalm-var T
*/
private $value;
/**
* @psalm-param T $value
*/
public function __construct($value) {
$this->value = $value;
}
/**
* @psalm-return T
*/
public function value() {
return $this->value;
}
}
/**
* @template T
* @psalm-param T $value
* @psalm-return Value<T>
*/
function value($value): Value {
return new Value($value);
}
/**
* @psalm-param Value<string> $value
*/
function client($value): void {}
client(value("awdawd"));',
],
'yieldFromGenericObjectNotExtendingIterator' => [
'code' => '<?php
/** @extends \ArrayObject<int, int> */
class Foo extends \ArrayObject {}
class A {
/**
* @var Foo<string>
*/
public Foo $vector;
/**
* @param Foo<string> $v
*/
public function __construct(Foo $v) {
$this->vector = $v;
}
public function getIterator(): Iterator
{
yield from $this->vector;
}
}',
'assertions' => [],
'ignored_issues' => ['TooManyTemplateParams'],
],
'coerceEmptyArrayToGeneral' => [
'code' => '<?php
/** @template-covariant T */
class Foo
{
/** @param \Closure(string):T $closure */
public function __construct($closure) {}
}
class Bar
{
/** @var Foo<array> */
private $FooArray;
public function __construct() {
$this->FooArray = new Foo(function(string $s): array {
/** @psalm-suppress MixedAssignment */
$json = \json_decode($s, true);
if (! \is_array($json)) {
return [];
}
return $json;
});
takesFooArray($this->FooArray);
}
}
/** @param Foo<array> $_ */
function takesFooArray($_): void {}',
],
'allowListAcceptance' => [
'code' => '<?php
/** @template T */
class Collection
{
/** @var list<T> */
public $values;
/** @param list<T> $values */
function __construct(array $values)
{
$this->values = $values;
}
}
/** @return Collection<string> */
function makeStringCollection()
{
return new Collection(getStringList()); // gets typed as Collection<mixed> for some reason
}
/** @return list<string> */
function getStringList(): array
{
return ["foo", "baz"];
}',
],
'allowListAcceptanceIntoArray' => [
'code' => '<?php
/** @template T */
class Collection
{
/** @var array<T> */
public $values;
/** @param array<T> $values */
function __construct(array $values)
{
$this->values = $values;
}
}
/** @return Collection<string> */
function makeStringCollection()
{
return new Collection(getStringList()); // gets typed as Collection<mixed> for some reason
}
/** @return list<string> */
function getStringList(): array
{
return ["foo", "baz"];
}',
],
'allowInternalNullCheck' => [
'code' => '<?php
/**
* @template TP as ?scalar
*/
class Entity
{
/**
* @var TP
*/
private $parent;
/** @param TP $parent */
public function __construct($parent) {
$this->parent = $parent;
}
public function hasNoParent() : bool
{
return $this->parent === null; // So TP does contain null
}
}',
],
'useMethodWithExistingGenericParam' => [
'code' => '<?php
class Bar {
public function getFoo(): string {
return "foo";
}
}
/**
* @template TKey
* @template T
*/
interface Collection {
/**
* @param Closure(T=):bool $p
* @return Collection<TKey, T>
*/
public function filter(Closure $p);
}
/**
* @param Collection<int, Bar> $c
* @psalm-return Collection<int, Bar>
*/
function filter(Collection $c, string $name) {
return $c->filter(
function (Bar $f) use ($name) {
return $f->getFoo() === "foo";
}
);
}',
],
'unboundVariableIsEmptyInInstanceMethod' => [
'code' => '<?php
class A {
/**
* @template TE
* @template TR
*
* @param TE $elt
* @param TR ...$elts
*
* @return TE|TR
*/
public function collectInstance($elt, ...$elts) {
$ret = $elt;
foreach ($elts as $item) {
if (rand(0, 1)) {
$ret = $item;
}
}
return $ret;
}
}
echo (new A)->collectInstance("a");',
],
'unboundVariableIsEmptyInStaticMethod' => [
'code' => '<?php
class A {
/**
* @template TE
* @template TR
*
* @param TE $elt
* @param TR ...$elts
*
* @return TE|TR
*/
public static function collectStatic($elt, ...$elts) {
$ret = $elt;
foreach ($elts as $item) {
if (rand(0, 1)) {
$ret = $item;
}
}
return $ret;
}
}
echo A::collectStatic("a");',
],
'traversableToIterable' => [
'code' => '<?php
/**
* @template T1 as array-key
* @template T2
*
* @param iterable<T1,T2> $x
*
* @return array<T1,T2>
*/
function iterableToArray (iterable $x): array {
if (is_array($x)) {
return $x;
}
else {
return iterator_to_array($x);
}
}
/**
* @param Traversable<int, int> $t
* @return array<int, int>
*/
function withParams(Traversable $t) : array {
return iterableToArray($t);
}',
],
'templateStaticWithParam' => [
'code' => '<?php
/**
* @template T
* @psalm-consistent-constructor
* @psalm-consistent-templates
*/
class ArrayCollection {
/** @var list<T> */
private $elements;
/**
* @param list<T> $elements
*/
public function __construct(array $elements) {
$this->elements = $elements;
}
/**
* @template U
* @param callable(T=):U $callback
* @return static<U>
*/
public function map(callable $callback) {
/** @psalm-suppress RedundantFunctionCall */
return new static(array_values(array_map($callback, $this->elements)));
}
}
/** @param ArrayCollection<int<0, max>> $ints */
function takesInts(ArrayCollection $ints) :void {}
/** @param ArrayCollection<int|string> $ints */
function takesIntsOrStrings(ArrayCollection $ints) :void {}
/** @return list<string> */
function getList() :array {return [];}
takesInts((new ArrayCollection(getList()))->map("strlen"));
/** @return ($s is "string" ? string : int) */
function foo(string $s) {
if ($s === "string") {
return "hello";
}
return 5;
}
takesIntsOrStrings((new ArrayCollection(getList()))->map("foo"));
/**
* @template T
* @extends ArrayCollection<T>
*/
class LazyArrayCollection extends ArrayCollection {}',
],
'weakReferenceIsTyped' => [
'code' => '<?php
$e = new Exception;
$r = WeakReference::create($e);
$ex = $r->get();
',
'assertions' => [ '$ex' => 'Exception|null' ],
],
'weakReferenceIsCovariant' => [
'code' => '<?php
/** @param WeakReference<Throwable> $_ref */
function acceptsThrowableRef(WeakReference $_ref): void {}
acceptsThrowableRef(WeakReference::create(new Exception));
',
],
'mapTypeParams' => [
'code' => '<?php
/**
* @template TKey as array-key
* @template TValue
*/
class Map {
/** @var array<TKey, TValue> */
public $arr;
/** @param array<TKey, TValue> $arr */
function __construct(array $arr) {
$this->arr = $arr;
}
}
/**
* @template TInputKey as array-key
* @template TInputValue
* @param Map<TInputKey, TInputValue> $map
* @return Map<TInputKey, TInputValue>
*/
function copyMapUsingProperty(Map $map): Map {
return new Map($map->arr);
}',
],
'mapStaticClassTemplatedFromClassString' => [
'code' => '<?php
/**
* @psalm-consistent-constructor
*/
class Base {
/** @return static */
public static function factory(): self {
return new static();
}
}
/**
* @template T of Base
* @param class-string<T> $t
* @return T
*/
function f(string $t) {
return $t::factory();
}
/** @template T of Base */
class C {
/** @var class-string<T> */
private string $t;
/** @param class-string<T> $t */
public function __construct($t) {
$this->t = $t;
}
/** @return T */
public function f(): Base {
$t = $this->t;
return $t::factory();
}
}',
],
'uasortCallableInMethod' => [
'code' => '<?php
class C {
/**
* @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;
}
}',
],
'intersectOnTOfObject' => [
'code' => '<?php
/**
* @psalm-template TO of object
*/
interface A {
/**
* @psalm-param Closure(TO&A):mixed $c
*/
public function setClosure(Closure $c): void;
}
function foo(A $i) : void {
$i->setClosure(
function(A $i) : string {
return "hello";
}
);
}',
],
'assertionOnTemplatedClassString' => [
'code' => '<?php
class TEM {
/**
* @template Entity as object
* @psalm-param class-string<Entity> $type
* @psalm-return EQB<Entity>
*/
public function createEQB(string $type) {
if (!class_exists($type)) {
throw new InvalidArgumentException();
}
return new EQB($type);
}
}
/**
* @template Entity as object
*/
class EQB {
/**
* @psalm-var class-string<Entity>
*/
protected $type;
/**
* @psalm-param class-string<Entity> $type
*/
public function __construct(string $type) {
$this->type = $type;
}
}',
],
'createEmptyArrayCollection' => [
'code' => '<?php
$a = new ArrayCollection([]);
/**
* @psalm-template TKey of array-key
* @psalm-template T
*/
class ArrayCollection
{
/**
* An array containing the entries of this collection.
*
* @psalm-var array<TKey,T>
* @var array
*/
private $elements = [];
/**
* Initializes a new ArrayCollection.
*
* @param array $elements
*
* @psalm-param array<TKey,T> $elements
*/
public function __construct(array $elements = [])
{
$this->elements = $elements;
}
/**
* @param TKey $key
* @param T $t
*/
public function add($key, $t) : void {
$this->elements[$key] = $t;
}
}',
'assertions' => [
'$a' => 'ArrayCollection<never, never>',
],
],
'newGenericBecomesPropertyTypeValidArg' => [
'code' => '<?php
class B {}
class A {
/** @var ArrayCollection<int, B> */
public ArrayCollection $b_collection;
public function __construct() {
$this->b_collection = new ArrayCollection([]);
$this->b_collection->add(5, new B());
}
}
/**
* @psalm-template TKey of array-key
* @psalm-template T
*/
class ArrayCollection
{
/**
* An array containing the entries of this collection.
*
* @psalm-var array<TKey,T>
* @var array
*/
private $elements = [];
/**
* Initializes a new ArrayCollection.
*
* @param array $elements
*
* @psalm-param array<TKey,T> $elements
*/
public function __construct(array $elements = [])
{
$this->elements = $elements;
}
/**
* @param TKey $key
* @param T $t
*/
public function add($key, $t) : void {
$this->elements[$key] = $t;
}
}',
],
'allowPropertyCoercion' => [
'code' => '<?php
class Test
{
/**
* @var ArrayCollection<int, DateTime>
*/
private $c;
public function __construct()
{
$this->c = new ArrayCollection();
$this->c->filter(function (DateTime $dt): bool {
return $dt === $dt;
});
}
}
/**
* @psalm-template TKey of array-key
* @psalm-template T
*/
class ArrayCollection
{
/**
* @psalm-var array<TKey,T>
* @var array
*/
private $elements;
/**
* @param array $elements
*
* @psalm-param array<TKey,T> $elements
*/
public function __construct(array $elements = [])
{
$this->elements = $elements;
}
/**
* @psalm-param Closure(T=):bool $p
* @psalm-return self<TKey, T>
*/
public function filter(Closure $p)
{
return $this;
}
}',
],
'unionClassStringInferenceAndDefaultEmptyArray' => [
'code' => '<?php
class A{}
$packages = Collection::fromClassString(A::class);
/**
* @template T
*/
class Collection{
/** @var array<T> $items */
protected $items = [];
/**
* @param array<string, T> $items
*/
public function __construct(array $items = [])
{
$this->items = $items;
}
/**
* @template C as object
* @param class-string<C> $classString
* @param array<string, C> $elements
* @return Collection<C>
*/
public static function fromClassString(string $classString, array $elements = []) : Collection
{
return new Collection($elements);
}
}',
'assertions' => [
'$packages' => 'Collection<A>',
],
],
'assertSameOnTemplatedProperty' => [
'code' => '<?php
/** @template E as object */
final class Box
{
/** @var E */
private $contents;
/** @param E $contents */
public function __construct(object $contents)
{
$this->contents = $contents;
}
/** @param E $thing */
public function contains(object $thing) : bool
{
if ($this->contents !== $thing) {
return false;
}
return true;
}
}',
],
'assertNotNullOnTemplatedProperty' => [
'code' => '<?php
/**
* @template T of object
*/
final class A {
/**
* @psalm-var ?callable(T): bool
*/
public $filter;
}
/** @psalm-var A<A> */
$a = new A();
if (null !== $a->filter) {}',
],
'setTemplatedPropertyOutsideClass' => [
'code' => '<?php
/**
* @template TValue as scalar
*/
class Watcher {
/**
* @psalm-var TValue
*/
public $value;
/**
* @psalm-param TValue $value
*/
public function __construct($value) {
$this->value = $value;
}
}
/** @psalm-var Watcher<int> $watcher */
$watcher = new Watcher(0);
$watcher->value = 0;',
],
'callableAsClassStringArray' => [
'code' => '<?php
abstract class Id
{
protected string $id;
final protected function __construct(string $id)
{
$this->id = $id;
}
/**
* @return static
*/
final public static function fromString(string $id): self
{
return new static($id);
}
}
/**
* @template T of Id
*/
final class Ids
{
/**
* @psalm-var list<T>
*/
private array $ids;
/**
* @psalm-param list<T> $ids
*/
private function __construct(array $ids)
{
$this->ids = $ids;
}
/**
* @template T1 of Id
* @psalm-param T1 $class
* @psalm-param list<string> $ids
* @psalm-return self<T1>
*/
public static function fromObjects(Id $class, array $ids): self
{
return new self(array_map([$class, "fromString"], $ids));
}
/**
* @template T1 of Id
* @psalm-param class-string<T1> $class
* @psalm-param list<string> $ids
* @psalm-return self<T1>
*/
public static function fromStrings(string $class, array $ids): self
{
return new self(array_map([$class, "fromString"], $ids));
}
}',
],
'doNotForgetAssertion' => [
'code' => '<?php
class a {
public ?int $expr = null;
}
/**
* @template T as int
*/
class b
{
public function test(
a $_
): void {
}
}
class c
{
public static function analyze(
a $container,
b $test,
): int {
$test->test($container);
if ($container->expr) {
if (random_int(0, 1)) {
self::test(
$container,
);
}
return $container->expr;
}
return 0;
}
private static function test(
a $_,
): void {
}
}',
],
'noCrashTemplateInsideGenerator' => [
'code' => '<?php
namespace Foo;
/**
* @template T
*/
final class Set
{
/** @var \Iterator<T> */
private \Iterator $values;
/**
* @param \Iterator<T> $values
*/
public function __construct(\Iterator $values)
{
$this->values = $values;
}
/**
* @param T $element
*
* @return self<T>
*/
public function __invoke($element): self
{
return new self(
(
function($values, $element): \Generator {
/** @var T $value */
foreach ($values as $value) {
yield $value;
}
yield $element;
}
)($this->values, $element),
);
}
}',
'assertions' => [],
'ignored_issues' => ['MissingClosureParamType'],
],
'templatedPropertyAllowsNull' => [
'code' => '<?php
/**
* @template TKey as string|null
*/
class A {
/** @var TKey */
public $key;
/**
* @param TKey $key
*/
public function __construct(?string $key)
{
$this->key = $key;
}
}',
],
'templatePropertyWithoutParams' => [
'code' => '<?php
/**
* @template T of object
*/
class Batch {
/**
* @var iterable<T>
*/
public iterable $objects = [];
/**
* @var callable(T): void
*/
public $onEach;
public function __construct() {
$this->onEach = function (): void {};
}
}
function handle(Batch $message, object $o): void {
$fn = $message->onEach;
$fn($o);
}',
],
'changePropertyTypeOfTemplate' => [
'code' => '<?php
class A {
public int $x = 0;
}
/**
* @template T as A
* @param T $obj
* @param-out T $obj
*/
function foo(A &$obj): void {
$obj->x = 1;
}',
],
'multipleMatchingObjectsInUnion' => [
'code' => '<?php
/** @template-covariant T */
interface Container {
/** @return T */
public function get();
}
/**
* @template T
* @param array<Container<T>> $containers
* @return T
*/
function unwrap(array $containers) {
return array_map(
fn($container) => $container->get(),
$containers
)[0];
}
/**
* @param array<Container<int>|Container<string>> $typed_containers
*/
function takesDifferentTypes(array $typed_containers) : void {
$ret = unwrap($typed_containers);
if (is_string($ret)) {}
if (is_int($ret)) {}
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '7.4',
],
'templateWithLateResolvedType' => [
'code' => '<?php
/**
* @template A of Enum::TYPE_*
*/
class Foo {}
class Enum
{
const TYPE_ONE = 1;
const TYPE_TWO = 2;
}
/** @var Foo<Enum::TYPE_ONE> $foo */
$foo = new Foo();',
],
'SKIPPED-extendedPropertyTypeParameterised' => [
'code' => '<?php
namespace App;
use DateTimeImmutable;
use Ds\Map;
abstract class Z
{
public function test(): void
{
$map = $this->createMap();
$date = $map->get("test");
echo $date->format("Y");
}
/**
* @return Map<string, DateTimeImmutable>
*/
abstract protected function createMap(): Map;
}',
],
'looseEquality' => [
'code' => '<?php
/**
* @psalm-immutable
* @template T of self::READ_UNCOMMITTED|self::READ_COMMITTED|self::REPEATABLE_READ|self::SERIALIZABLE
*/
final class TransactionIsolationLevel
{
private const READ_UNCOMMITTED = "read uncommitted";
private const READ_COMMITTED = "read committed";
private const REPEATABLE_READ = "repeatable read";
private const SERIALIZABLE = "serializable";
/**
* @psalm-var T $level
*/
private string $level;
/**
* @psalm-param T $level
*/
private function __construct(string $level)
{
$this->level = $level;
}
/**
* @psalm-return self<self::READ_UNCOMMITTED>
*/
public static function readUncommitted(): self
{
return new self(self::READ_UNCOMMITTED);
}
/**
* @psalm-return T
*/
public function toString(): string
{
return $this->level;
}
/**
* @psalm-template TResult
* @psalm-param pure-callable(self::READ_UNCOMMITTED): TResult $readUncommitted
* @psalm-return TResult
*/
public function resolve(callable $readUncommitted) {
if ($this->level == self::READ_UNCOMMITTED) {
return $readUncommitted($this->level);
}
throw new \LogicException("bad");
}
}',
],
'narrowTemplateTypeWithInstanceof' => [
'code' => '<?php
class Foo {}
class Bar {}
/** @template FooOrBarOrNull of Foo|Bar|null */
class Resolved
{
/**
* @var FooOrBarOrNull
*/
private $entity = null;
/**
* @psalm-param FooOrBarOrNull $qux
*/
public function __contruct(?object $qux)
{
if ($qux instanceof Foo) {
$this->entity = $qux;
}
}
}',
],
'flippedParamsMethodInside' => [
'code' => '<?php
/**
* @template A
* @template B
*/
abstract class Foo
{
/** @return Traversable<A, B> */
public abstract function getTraversable() : Traversable;
/**
* @param Foo<B, A> $flipped
* @return Traversable<B, A>
*/
public function getFlippedTraversable(Foo $flipped): Traversable
{
return $flipped->getTraversable();
}
}',
],
'flippedParamsMethodOutside' => [
'code' => '<?php
/**
* @template B
* @template A
* @param Foo<B, A> $flipped
* @return Traversable<B, A>
*/
function getFlippedTraversable(Foo $flipped): Traversable {
return $flipped->getTraversable();
}
/**
* @template A
* @template B
*/
abstract class Foo
{
/** @return Traversable<A, B> */
public abstract function getTraversable() : Traversable;
}',
],
'flippedParamsPropertyInside' => [
'code' => '<?php
/**
* @template A
* @template B
*/
abstract class Foo
{
/** @var Traversable<A, B> */
public $traversable;
/**
* @param Foo<B, A> $flipped
* @return Traversable<B, A>
*/
public function getFlippedTraversable(Foo $flipped): Traversable
{
return $flipped->traversable;
}
}',
],
'flippedParamsPropertyOutside' => [
'code' => '<?php
/**
* @template B
* @template A
* @param Foo<B, A> $flipped
* @return Traversable<B, A>
*/
function getFlippedTraversable(Foo $flipped): Traversable {
return $flipped->traversable;
}
/**
* @template A
* @template B
*/
abstract class Foo
{
/** @var Traversable<A, B> */
public $traversable;
}',
],
'simpleTemplate' => [
'code' => '<?php
/** @template T */
interface F {}
/** @param F<mixed> $f */
function takesFMixed(F $f) : void {}
function sendsF(F $f) : void {
takesFMixed($f);
}',
],
'arrayCollectionMapInternal' => [
'code' => '<?php
/**
* @psalm-template TKey of array-key
* @psalm-template T
* @psalm-consistent-constructor
* @psalm-consistent-templates
*/
class ArrayCollection
{
/** @psalm-var array<TKey,T> */
private $elements;
/** @psalm-param array<TKey,T> $elements */
public function __construct(array $elements = [])
{
$this->elements = $elements;
}
/**
* @template TNewKey of array-key
* @template TNew
* @psalm-param array<TNewKey, TNew> $elements
* @psalm-return static<TNewKey, TNew>
*/
protected static function createFrom(array $elements)
{
return new static($elements);
}
/**
* @psalm-template U
* @psalm-param Closure(T=):U $func
* @psalm-return static<TKey, U>
*/
public function map(Closure $func)
{
$new_elements = array_map($func, $this->elements);
return self::createFrom($new_elements);
}
}',
],
'arrayCollectionMapExternal' => [
'code' => '<?php
/**
* @psalm-template TKey of array-key
* @psalm-template T
* @psalm-consistent-constructor
*/
class ArrayCollection
{
/** @psalm-var array<TKey,T> */
private $elements;
/** @psalm-param array<TKey,T> $elements */
public function __construct(array $elements = [])
{
$this->elements = $elements;
}
/**
* @psalm-template U
* @psalm-param Closure(T=):U $func
* @psalm-return ArrayCollection<TKey, U>
*/
public function map(Closure $func)
{
$new_elements = array_map($func, $this->elements);
return Creator::createFrom($new_elements);
}
}
class Creator {
/**
* @template TNewKey of array-key
* @template TNew
* @psalm-param array<TNewKey, TNew> $elements
* @psalm-return ArrayCollection<TNewKey, TNew>
*/
public static function createFrom(array $elements) {
return new ArrayCollection($elements);
}
}',
],
'templateWithClassConstants' => [
'code' => '<?php
/**
* @psalm-immutable
* @template T of self::A|self::B|self::C
*/
final class Foo
{
public const A = "aa";
public const B = "bb";
public const C = "cc";
/**
* @psalm-var T $level
*/
private string $level;
/**
* @psalm-param T $level
*/
public function __construct(string $level)
{
$this->level = $level;
}
}
/**
* @psalm-return Foo<Foo::A>
*/
function getFooA(): Foo {
return new Foo(Foo::A);
}',
],
'callTemplatedMethodOnSameClass' => [
'code' => '<?php
/**
* @template T as object
*/
class Mapper {
/**
* @param T $e
* @return T
*/
public function foo($e) {
return $e;
}
/**
* @param T $e
* @return T
*/
public function passthru($e) {
return $this->foo($e);
}
}',
],
'templatedStaticUnion' => [
'code' => '<?php
/**
* @template T
* @psalm-consistent-templates
*/
abstract class A {
/**
* @var T
*/
private $v;
/**
* @param T $v
*/
final public function __construct($v) {
$this->v = $v;
}
/**
* @return static<T>
*/
public function foo(): A {
if (rand(0, 1)) {
return new static($this->v);
} else {
return new static($this->v);
}
}
}',
],
'templatedTypeWithLimitGoesIntoTemplatedType' => [
'code' => '<?php
/**
* @template T as object
*/
abstract class A {}
function takesA(A $a) : void {}
function foo(A $a) : void {
takesA($a);
}',
],
'templateIsAComplexMultilineType' => [
'code' => '<?php
/**
* @template T of array{
* a: string,
* b: int
* }
*/
class MyContainer {
/** @var T */
private $value;
/** @param T $value */
public function __construct($value) {
$this->value = $value;
}
/** @return T */
public function getValue() {
return $this->value;
}
}',
],
'newWithoutInferredTemplate' => [
'code' => '<?php
/**
* @psalm-template T2 of object
*/
final class Foo {}
$f = new Foo();',
'assertions' => [
'$f' => 'Foo<object>',
],
],
'PHP80-weakmapIsGeneric' => [
'code' => '<?php
/** @param WeakMap<Throwable,int> $wm */
function isCountable(WeakMap $wm): int {
return count($wm);
}
/**
* @param WeakMap<Throwable,int> $wm
* @return array{Throwable,int}
*/
function isTraverable(WeakMap $wm): array {
foreach ($wm as $k => $v) {
return [$k, $v];
}
throw new RuntimeException;
}
/**
* @param WeakMap<Throwable,int> $wm
* @return Traversable<Throwable,int>
*/
function hasAggregateIterator(WeakMap $wm): Traversable {
return $wm->getIterator();
}
/**
* @param WeakMap<Throwable,int> $wm
*/
function readsLikeAnArray(WeakMap $wm): int {
$ex = new Exception;
if (isset($wm[$ex])) {
return $wm[$ex];
}
throw new RuntimeException;
}
/**
* @param WeakMap<Throwable,int> $wm
*/
function writesLikeAnArray(WeakMap $wm): void {
$ex = new Exception;
$wm[$ex] = 42;
}
',
],
'combineTwoTemplatedArrays' => [
'code' => '<?php
/** @template T */
class Option
{
/** @param T $v */
public function __construct(private $v) {}
/**
* @template E
* @param E $else
* @return T|E
*/
public function getOrElse($else)
{
return rand(0, 1) === 1 ? $this->v : $else;
}
}
$opt = new Option([1, 3]);
$b = $opt->getOrElse([2, 4])[0];',
'assertions' => [
'$b===' => '1|2',
],
],
'generaliseTemplatedString' => [
'code' => '<?php
/** @template TData */
class Container {
/** @var TData */
public $data;
/** @param TData $data */
public function __construct($data) {
$this->data = $data;
}
}
/** @param Container<string> $r */
function takesContainer(Container $r): void {
$r->data = "David";
}
$me = new Container("Matthew");
takesContainer($me);
if ($me->data === "David") {}',
],
'generaliseTemplatedArray' => [
'code' => '<?php
/** @template TData */
class Container {
/** @var TData */
public $data;
/** @param TData $data */
public function __construct($data) {
$this->data = $data;
}
}
/** @param Container<array{name: string}> $r */
function takesContainer(Container $r): void {
$r->data = ["name" => "David"];
}
$me = new Container(["name" => "Matthew"]);
takesContainer($me);
if ($me->data["name"] === "David") {}',
],
'allowCovariantBoundsMismatchSameContainers' => [
'code' => '<?php
/**
* @param Collection<Dog> $c
* @param Collection<Cat> $d
*/
function bar(Collection $c, Collection $d): Dog|Cat {
return foo($c, $d);
}
/** @template-covariant T of object */
interface Collection {
/** @return T */
public function get(): object;
}
class Cat {}
class Dog {}
/**
* @template T of object
* @param Collection<T> $c
* @param Collection<T> $d
* @return T
*/
function foo(Collection $c, Collection $d): object {
return rand(0, 1) ? $c->get() : $d->get();
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0',
],
'allowCovariantBoundsMismatchDifferentContainers' => [
'code' => '<?php
/**
* @param Collection1<Dog> $c
* @param Collection2<Cat> $d
*/
function bar(Collection1 $c, Collection2 $d): Dog|Cat {
return foo($c, $d);
}
/** @template-covariant T of object */
interface Collection1 {
/** @return T */
public function get(): object;
}
/** @template-covariant T of object */
interface Collection2 {
/** @return T */
public function get(): object;
}
class Cat {}
class Dog {}
/**
* @template T of object
* @param Collection1<T> $c
* @param Collection2<T> $d
* @return T
*/
function foo(Collection1 $c, Collection2 $d): object {
return rand(0, 1) ? $c->get() : $d->get();
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0',
],
'allowCovariantBoundsMismatchContainerAndObject' => [
'code' => '<?php
/**
* @param Collection<Cat> $d
*/
function bar(Dog $c, Collection $d): Dog|Cat {
$animal = foo($c, $d);
if ($animal instanceof Dog) {}
if ($animal instanceof Cat) {}
return $animal;
}
/** @template-covariant T of object */
interface Collection {
/** @return T */
public function get(): object;
}
class Cat {}
class Dog {}
/**
* @template T of object
* @param T $c
* @param Collection<T> $d
* @return T
*/
function foo(object $c, Collection $d): object {
return rand(0, 1) ? $c : $d->get();
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0',
],
'allowCompatibleGenerics' => [
'code' => '<?php
/** @template T of object */
interface A {}
/** @template T of object */
interface B {}
/**
* @template T of object
* @param A<T> $a
* @param B<T> $b
*/
function foo(A $a, B $b): void {}
/**
* @param A<stdClass> $a
* @param B<stdClass> $b
*/
function bar(A $a, B $b): void {
foo($a, $b);
}',
],
'templateOnDocblockMethod' => [
'code' => '<?php
/**
* @template T
* @method T get()
* @method void set(T $value)
*/
class Container
{
public function __call(string $name, array $args) {}
}
class A {}
function foo(A $a): void {}
/** @var Container<A> $container */
$container = new Container();
$container->set(new A());
foo($container->get());
',
],
'templateOnDocblockMethodOnInterface' => [
'code' => '<?php
/**
* @template T
* @method T get()
* @method void set(T $value)
*/
interface Container
{
}
class A {}
function foo(A $a): void {}
/** @var Container<A> $container */
$container->set(new A());
foo($container->get());
',
],
'refineTemplateTypeOfUnion' => [
'code' => '<?php
/** @psalm-template T as One|Two|Three */
class A {
/** @param T $t */
public function __construct(
private object $t
) {}
public function foo(): void {
if ($this->t instanceof One || $this->t instanceof Two) {}
}
}
final class One {}
final class Two {}
final class Three {}',
],
'refineTemplateTypeOfUnionMoreComplex' => [
'code' => '<?php
/** @psalm-template T as One|Two|Three */
class A {
/** @param T $t */
public function __construct(
private object $t
) {}
public function foo(): void {
if ($this->t instanceof One && rand(0, 1)) {}
if ($this->t instanceof Two) {}
}
}
final class One {}
final class Two {}
final class Three {}',
],
'issue7825' => [
'code' => '<?php
interface Stub {}
interface ProxyQueryInterface {}
class MockObject {}
/** @phpstan-template T of ProxyQueryInterface */
interface PagerInterface {}
/** @phpstan-template T of ProxyQueryInterface */
class Datagrid
{
/** @var T */
private $query;
/** @var PagerInterface<T> */
private $pager;
/**
* @phpstan-param T $query
* @phpstan-param PagerInterface<T> $pager
*/
public function __construct(
ProxyQueryInterface $query,
PagerInterface $pager
) {
$this->pager = $pager;
$this->query = $query;
}
}
interface FormBuilderInterface {}
/** @template T of FieldDescriptionInterface */
class FieldDescriptionCollection {}
interface FieldDescriptionInterface {}
abstract class Test
{
/** @var Datagrid<ProxyQueryInterface&Stub> */
private Datagrid $datagrid;
/** @var PagerInterface<ProxyQueryInterface&Stub>&MockObject */
private $pager;
/** @var ProxyQueryInterface&Stub */
private $query;
/** @var FieldDescriptionCollection<FieldDescriptionInterface> */
private FieldDescriptionCollection $columns;
private FormBuilderInterface $formBuilder;
/**
* @psalm-template RealInstanceType of object
* @psalm-param class-string<RealInstanceType> $originalClassName
* @psalm-return MockObject&RealInstanceType
*/
abstract protected function createMock(string $originalClassName): MockObject;
/**
* @psalm-template RealInstanceType of object
* @psalm-param class-string<RealInstanceType> $originalClassName
* @psalm-return Stub&RealInstanceType
*/
abstract protected function createStub(string $originalClassName): Stub;
protected function setUp(): void
{
$this->query = $this->createStub(ProxyQueryInterface::class);
$this->columns = new FieldDescriptionCollection();
/** @var PagerInterface<ProxyQueryInterface&Stub>&MockObject $pager */
$pager = $this->createMock(PagerInterface::class);
$this->pager = $pager;
$this->datagrid = new Datagrid($this->query, $pager);
}
}',
],
'complexTypes' => [
'code' => '<?php
/**
* @template T
*/
class Future {
/**
* @param T $v
*/
public function __construct(private $v) {}
/** @return T */
public function get() { return $this->v; }
}
/**
* @template TTObject
*
* @extends Future<ArrayObject<int, TTObject>>
*/
class FutureB extends Future {
/** @param TTObject $data */
public function __construct($data) { parent::__construct(new ArrayObject([$data])); }
}
$a = new FutureB(123);
$r = $a->get();',
'assertions' => [
'$a===' => 'FutureB<123>',
'$r===' => 'ArrayObject<int, 123>',
],
],
'return TemplatedClass<static>' => [
'code' => '<?php
/**
* @template-covariant A
* @psalm-immutable
*/
final class Maybe
{
/**
* @param null|A $value
*/
public function __construct(private $value = null) {}
/**
* @template B
* @param B $value
* @return Maybe<B>
*
* @psalm-pure
*/
public static function just($value): self
{
return new self($value);
}
}
abstract class Test
{
final private function __construct() {}
/** @return Maybe<static> */
final public static function create(): Maybe
{
return Maybe::just(new static());
}
}',
],
'return list<static> created in a static method of another class' => [
'code' => '<?php
final class Lister
{
/**
* @template B
* @param B $value
* @return list<B>
*
* @psalm-pure
*/
public static function mklist($value): array
{
return [ $value ];
}
}
abstract class Test
{
final private function __construct() {}
/** @return list<static> */
final public static function create(): array
{
return Lister::mklist(new static());
}
}',
],
'use TemplatedClass<static> as an intermediate variable inside a method' => [
'code' => '<?php
/**
* @template-covariant A
* @psalm-immutable
*/
final class Maybe
{
/**
* @param A $value
*/
public function __construct(public $value) {}
/**
* @template B
* @param B $value
* @return Maybe<B>
*
* @psalm-pure
*/
public static function just($value): self
{
return new self($value);
}
}
abstract class Test
{
final private function __construct() {}
final public static function create(): static
{
$maybe = Maybe::just(new static());
return $maybe->value;
}
}',
],
'static is the return type of an analyzed static method' => [
'code' => '<?php
abstract class A
{
}
final class B extends A
{
public static function create(): static
{
return new self();
}
}
final class Service
{
public function do(): void
{
$this->acceptA(B::create());
}
private function acceptA(A $_a): void
{
}
}',
],
'undefined class in function dockblock' => [
'code' => '<?php
/**
* @psalm-suppress UndefinedDocblockClass
*
* @param DoesNotExist<int> $baz
*/
function foobar(DoesNotExist $baz): void {}
/**
* @psalm-suppress UndefinedDocblockClass, UndefinedClass
* @var DoesNotExist
*/
$baz = new DoesNotExist();
foobar($baz);',
],
'promoted property with template' => [
'code' => '<?php
/**
* @template T
*/
class A {
public function __construct(
/** @var T */
public mixed $t
) {}
}
$a = new A(5);
$t = $a->t;
',
'assertions' => [
'$a' => 'A<int>',
'$t' => 'int',
],
'ignored_issues' => [],
'php_version' => '8.0',
],
'template of simple type with additional comment without dot' => [
'code' => '<?php
/**
* @psalm-template T of string
*
* lorem ipsum
*/
class Foo {
/** @psalm-var T */
public string $t;
/** @psalm-param T $t */
public function __construct(string $t) {
$this->t = $t;
}
/**
* @psalm-return T
*/
public function t(): string {
return $this->t;
}
}
$t = (new Foo(\'\'))->t();
',
'assertions' => [
'$t===' => '\'\'',
],
],
'template of simple type with additional comment with dot' => [
'code' => '<?php
/**
* @psalm-template T of string
*
* lorem ipsum.
*/
class Foo {
/** @psalm-var T */
public string $t;
/** @psalm-param T $t */
public function __construct(string $t) {
$this->t = $t;
}
/**
* @psalm-return T
*/
public function t(): string {
return $this->t;
}
}
$t = (new Foo(\'\'))->t();
',
'assertions' => [
'$t===' => '\'\'',
],
],
'mixedAssignment' => [
'code' => '<?php
/** @template T */
abstract class Foo {
/** @psalm-var T */
protected $value;
/** @psalm-param T $value */
public function __construct($value)
{
/** @var T */
$value = $this->normalize($value);
$this->value = $value;
}
/**
* @psalm-param T $value
* @psalm-return T
*/
protected function normalize($value)
{
return $value;
}
}
',
],
'typesOrderInsideImplementsNotMatter' => [
'code' => '<?php
/** @template T */
interface I {}
/**
* @template T
* @extends I<T>
*/
interface ExtendedI extends I {}
/**
* @template T
* @implements ExtendedI<T|null>
*/
final class TWithNull implements ExtendedI
{
/** @param T $_value */
public function __construct($_value) {}
}
/**
* @template T
* @implements ExtendedI<null|T>
*/
final class NullWithT implements ExtendedI
{
/** @param T $_value */
public function __construct($_value) {}
}
/** @param I<null|int> $_type */
function nullWithInt(I $_type): void {}
/** @param I<int|null> $_type */
function intWithNull(I $_type): void {}
nullWithInt(new TWithNull(1));
nullWithInt(new NullWithT(1));
intWithNull(new TWithNull(1));
intWithNull(new NullWithT(1));',
],
'intersectParentTemplateReturnWithConcreteChildReturn' => [
'code' => '<?php
/** @template T */
interface Aggregator
{
/**
* @psalm-param T ...$values
* @psalm-return T
*/
public function aggregate(...$values): mixed;
}
/** @implements Aggregator<int|float|null> */
final class AverageAggregator implements Aggregator
{
public function aggregate(...$values): null|int|float
{
if (!$values) {
return null;
}
return array_sum($values) / count($values);
}
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0',
],
];
}
public function providerInvalidCodeParse(): iterable
{
return [
'restrictTemplateInputWithClassString' => [
'code' => '<?php
/** @template T as object */
class Foo
{
/**
* @psalm-var class-string
*/
private $type;
/** @var array<T> */
private $items;
/**
* @param T::class $type
*/
public function __construct(string $type)
{
if (!in_array($type, [A::class, B::class], true)) {
throw new \InvalidArgumentException;
}
$this->type = $type;
$this->items = [];
}
/** @param T $item */
public function add($item): void
{
$this->items[] = $item;
}
}
class A {}
class B {}
$foo = new Foo(A::class);
$foo->add(new B);',
'error_message' => 'InvalidArgument',
],
'restrictTemplateInputWithTClassBadInput' => [
'code' => '<?php
/** @template T */
class Foo
{
/**
* @psalm-var class-string
*/
private $type;
/** @var array<T> */
private $items;
/**
* @param T::class $type
*/
public function __construct(string $type)
{
if (!in_array($type, [A::class, B::class], true)) {
throw new \InvalidArgumentException;
}
$this->type = $type;
$this->items = [];
}
/** @param T $item */
public function add($item): void
{
$this->items[] = $item;
}
}
class A {}
class B {}
$foo = new Foo(A::class);
$foo->add(new B);',
'error_message' => 'InvalidArgument',
],
'templatedClosureProperty' => [
'code' => '<?php
final class State
{}
interface Foo
{}
function type(string ...$_p): void {}
/**
* @template T
*/
final class AlmostFooMap
{
/**
* @param callable(State):(T&Foo) $closure
*/
public function __construct(callable $closure)
{
type($closure);
}
}',
'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:20:34 - Argument 1 of type expects string, but callable(State):(T:AlmostFooMap as mixed)&Foo provided',
],
'templateWithNoReturn' => [
'code' => '<?php
/**
* @template T
*/
class A {
/** @return T */
public function foo() {}
}',
'error_message' => 'InvalidReturnType',
],
'templateInvalidDocblockArgument' => [
'code' => '<?php
/** @template T as object */
class Generic {}
/**
* @template T
* @param T $p
* @return Generic<T>
* @psalm-suppress InvalidReturnType
*/
function violate($p) {}',
'error_message' => 'InvalidTemplateParam',
],
'doublyLinkedListBadParam' => [
'code' => '<?php
/** @var SplDoublyLinkedList<string> */
$templated_list = new SplDoublyLinkedList();
$templated_list->add(5, []);',
'error_message' => 'InvalidArgument',
],
'copyScopedClassInNamespacedClass' => [
'code' => '<?php
namespace Foo;
/**
* @template Bar as DOMNode
*/
class Bar {}',
'error_message' => 'ReservedWord',
],
'duplicateTemplateFunction' => [
'code' => '<?php
/**
* @template T
*/
class Foo
{
/** @var T */
private $value;
/**
* @template T
* @param T $value
* @return self<T>
*/
static function of($value): self
{
return new self($value);
}
/**
* @param T $value
*/
private function __construct($value)
{
$this->value = $value;
}
}',
'error_message' => 'InvalidDocblock',
],
'preventDogCatSnafu' => [
'code' => '<?php
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
/**
* @template T
*/
class Collection {
/**
* @param T $t
*/
public function add($t) : void {}
}
/**
* @param Collection<Animal> $list
*/
function addAnimal(Collection $list) : void {
$list->add(new Cat());
}
/**
* @param Collection<Dog> $list
*/
function takesDogList(Collection $list) : void {
addAnimal($list); // this should be an error
}',
'error_message' => 'InvalidArgument',
],
'templateEmptyParamCoercionChangeVariable' => [
'code' => '<?php
namespace NS;
use Countable;
/** @template T */
class Collection
{
/** @psalm-var iterable<T> */
private $data;
/** @psalm-param iterable<T> $data */
public function __construct(iterable $data = []) {
$this->data = $data;
}
}
/** @psalm-param Collection<string> $c */
function takesStringCollection(Collection $c): void {}
/** @psalm-param Collection<int> $c */
function takesIntCollection(Collection $c): void {}
$collection = new Collection();
takesStringCollection($collection);
takesIntCollection($collection);',
'error_message' => 'InvalidArgument',
],
'argumentExpectsFleshOutTIndexedAccess' => [
'code' => '<?php
/**
* @template TData as array
*/
abstract class Row {
/**
* @var TData
*/
protected $data;
/**
* @param TData $data
*/
public function __construct(array $data) {
$this->data = $data;
}
/**
* @template K as key-of<TData>
*
* @param K $property
*
* @return TData[K]
*/
public function __get(string $property) {
// validation logic would go here
return $this->data[$property];
}
/**
* @template K as key-of<TData>
*
* @param K $property
* @param TData[K] $value
*/
public function __set(string $property, $value) {
// data updating would go here
$this->data[$property] = $value;
}
}
/** @extends Row<array{id: int, name: string, height: float}> */
class CharacterRow extends Row {}
$mario = new CharacterRow(["id" => 5, "name" => "Mario", "height" => 3.5]);
$mario->ame = "Luigi";',
'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . "somefile.php:47:29 - Argument 1 of CharacterRow::__set expects 'height'|'id'|'name', but 'ame' provided",
],
'specialiseTypeBeforeReturning' => [
'code' => '<?php
class Base {}
class Derived extends Base {}
/**
* @template T of Base
*/
class Foo {
/**
* @param T $t
*/
public function __construct ($t) {}
}
/**
* @return Foo<Base>
*/
function returnFooBase() {
$f = new Foo(new Derived());
takesFooDerived($f);
return $f;
}
/**
* @param Foo<Derived> $foo
*/
function takesFooDerived($foo): void {}',
'error_message' => 'InvalidReturnStatement',
],
'possiblySpecialiseTypeBeforeReturning' => [
'code' => '<?php
class Base {}
class Derived extends Base {}
/**
* @template T of Base
*/
class Foo {
/**
* @param T $t
*/
public function __construct ($t) {}
}
/**
* @return Foo<Base>
*/
function returnFooBase() {
$f = new Foo(new Derived());
if (rand(0, 1)) {
takesFooDerived($f);
}
return $f;
}
/**
* @param Foo<Derived> $foo
*/
function takesFooDerived($foo): void {}',
'error_message' => 'InvalidReturnStatement',
],
'preventUseWithMoreSpecificParamInt' => [
'code' => '<?php
/** @template T */
abstract class Collection {
/** @param T $elem */
public function add($elem): void {}
}
/**
* @template T
* @param Collection<T> $col
*/
function usesCollection(Collection $col): void {
$col->add(456);
}',
'error_message' => 'InvalidArgument',
],
'preventUseWithMoreSpecificParamEmptyArray' => [
'code' => '<?php
/** @template T */
abstract class Collection {
/** @param T $elem */
public function add($elem): void {}
}
/**
* @template T
* @param Collection<T> $col
*/
function usesCollection(Collection $col): void {
$col->add([]);
}',
'error_message' => 'InvalidArgument',
],
'preventTemplatedCorrectionBeingWrittenTo' => [
'code' => '<?php
namespace NS;
/**
* @template TKey
* @template TValue
*/
class ArrayCollection {
/** @var array<TKey,TValue> */
private $data;
/** @param array<TKey,TValue> $data */
public function __construct(array $data) {
$this->data = $data;
}
/**
* @param TKey $key
* @param TValue $value
*/
public function addItem($key, $value) : void {
$this->data[$key] = $value;
}
}
class Item {}
class SubItem extends Item {}
class OtherSubItem extends Item {}
/**
* @param ArrayCollection<int,Item> $i
*/
function takesCollectionOfItems(ArrayCollection $i): void {
$i->addItem(10, new OtherSubItem);
}
$subitem_collection = new ArrayCollection([ new SubItem ]);
takesCollectionOfItems($subitem_collection);',
'error_message' => 'InvalidArgument',
],
'noClassTemplatesInStaticMethods' => [
'code' => '<?php
/**
* @template T
*/
class C {
/**
* @param T $t
*/
public static function foo($t) : void {}
}',
'error_message' => 'UndefinedDocblockClass',
],
'newGenericBecomesPropertyTypeInvalidArg' => [
'code' => '<?php
class B {}
class C {}
class A {
/** @var ArrayCollection<int, B> */
public ArrayCollection $b_collection;
public function __construct() {
$this->b_collection = new ArrayCollection([]);
$this->b_collection->add(5, new C());
}
}
/**
* @psalm-template TKey
* @psalm-template T
*/
class ArrayCollection
{
/**
* An array containing the entries of this collection.
*
* @psalm-var array<TKey,T>
* @var array
*/
private $elements = [];
/**
* Initializes a new ArrayCollection.
*
* @param array $elements
*
* @psalm-param array<TKey,T> $elements
*/
public function __construct(array $elements = [])
{
$this->elements = $elements;
}
/**
* @param TKey $key
* @param T $t
*/
public function add($key, $t) : void {
$this->elements[$key] = $t;
}
}',
'error_message' => 'InvalidArgument',
],
'preventIteratorAggregateToIterableWithDifferentTypes' => [
'code' => '<?php
class Foo {}
class Bar {}
/** @param iterable<int, Foo> $foos */
function consume(iterable $foos): void {}
/** @param IteratorAggregate<int, Bar> $t */
function foo(IteratorAggregate $t) : void {
consume($t);
}',
'error_message' => 'InvalidArgument',
],
'preventPassingToBoundParam' => [
'code' => '<?php
/**
* @template T
*/
class Container
{
/** @var T */
private $t;
/** @param T $t */
public function __construct($t)
{
$this->t = $t;
}
/**
* @param T $t
* @return T
*/
protected function makeNew($t)
{
return $t;
}
/**
* @template U
* @param U $u
*/
public function map($u) : void
{
$this->makeNew($u);
}
}',
'error_message' => 'InvalidArgument',
],
'bindRedirectedTemplate' => [
'code' => '<?php
/**
* @template TIn
* @template TOut
*/
final class Map
{
/** @param Closure(TIn): TOut $c */
public function __construct(private Closure $c) {}
/**
* @template TIn2 as list<TIn>
* @param TIn2 $in
* @return list<TOut>
*/
public function __invoke(array $in) : array {
return array_map(
$this->c,
$in
);
}
}
$m = new Map(fn(int $num) => (string) $num);
$m(["a"]);',
'error_message' => 'InvalidScalarArgument',
'ignored_issues' => [],
'php_version' => '8.0',
],
'bindClosureParamAccurately' => [
'code' => '<?php
/**
* @template TKey
* @template TValue
*/
interface Collection {
/**
* @template T
* @param Closure(TValue):T $func
* @return Collection<TKey,T>
*/
public function map(Closure $func);
}
/**
* @param Collection<int, string> $c
*/
function f(Collection $c): void {
$fn = function(int $_p): bool { return true; };
$c->map($fn);
}',
'error_message' => 'InvalidScalarArgument',
],
'limitTemplateTypeWithSameName' => [
'code' => '<?php
/**
* @template T as object
*/
abstract class A {}
function takesA(A $a) : void {}
/** @param A<stdClass> $a */
function foo(A $a) : void {
takesA($a);
}',
'error_message' => 'InvalidArgument',
],
'limitTemplateTypeExtended' => [
'code' => '<?php
/**
* @template T as object
*/
abstract class A {}
/**
* @extends A<stdClass>
*/
class AChild extends A {}
function takesA(A $a) : void {}
$child = new AChild();
takesA($child);',
'error_message' => 'InvalidArgument',
],
'noCrashTemplatedClosure' => [
'code' => '<?php
/**
* @template TCallback as Closure():string
*/
class A {
/** @var TCallback */
private $callback;
/** @param TCallback $callback */
public function __construct(Closure $callback) {
$this->callback = $callback;
}
/** @param TCallback $callback */
public function setCallback(Closure $callback): void {
$this->callback = $callback;
}
}
$a = new A(function() { return "a";});
$a->setCallback(function() { return "b";});',
'error_message' => 'InvalidArgument',
],
'preventBoundsMismatchDifferentContainers' => [
'code' => '<?php
/**
* @param Collection1<Dog> $c
* @param Collection2<Cat> $d
*/
function bar(Collection1 $c, Collection2 $d): void {
foo($c, $d);
}
/** @template T of object */
interface Collection1 {
/** @param T $item */
public function add(object $item): void;
}
/** @template T of object */
interface Collection2 {
/** @param T $item */
public function add(object $item): void;
/** @return T */
public function get(): object;
}
class Cat {}
class Dog {}
/**
* @template T of object
* @param Collection1<T> $c
* @param Collection2<T> $d
*/
function foo(Collection1 $c, Collection2 $d): void {
$c->add($d->get());
}',
'error_message' => 'InvalidArgument',
],
'preventBoundsMismatchSameContainers' => [
'code' => '<?php
/**
* @param Collection<Dog> $c
* @param Collection<Cat> $d
*/
function bar(Collection $c, Collection $d): void {
foo($c, $d);
}
/** @template T of object */
interface Collection {
/** @param T $item */
public function add(object $item): void;
/** @return T */
public function get(): object;
}
class Cat {}
class Dog {}
/**
* @template T of object
* @param Collection<T> $c
* @param Collection<T> $d
*/
function foo(Collection $c, Collection $d): void {
$c->add($d->get());
}',
'error_message' => 'InvalidArgument',
],
'preventBoundsMismatchDifferentBoundLevels' => [
'code' => '<?php
/**
* @param Collection<Dog> $c
*/
function bar(Collection $c): void {
foo($c, new Cat());
}
/** @template T of object */
interface Collection {
/** @param T $item */
public function add(object $item): void;
}
class Cat {}
class Dog {}
/**
* @template T of object
* @param Collection<T> $c
* @param T $d
*/
function foo(Collection $c, object $d): void {
$c->add($d);
}',
'error_message' => 'InvalidArgument',
],
'invalidTemplateArgumentOnDocblockMethod' => [
'code' => '<?php
/**
* @template T
* @method void set(T $value)
*/
class Container
{
public function __call(string $name, array $args) {}
}
class A {}
class B {}
/** @var Container<A> $container */
$container = new Container();
$container->set(new B());',
'error_message' => 'InvalidArgument',
],
'refineTemplateTypeOfUnionAccurately' => [
'code' => '<?php
/** @psalm-template T as One|Two|Three */
class A {
/** @param T $t */
public function __construct(
private object $t
) {}
/** @return int */
public function foo() {
if ($this->t instanceof One || $this->t instanceof Two) {
return $this->t;
}
throw new \Exception();
}
}
final class One {}
final class Two {}
final class Three {}',
'error_message' => 'InvalidReturnStatement - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:40 - The inferred type \'T:A as One|Two\' ',
],
'preventMixedNestedCoercion' => [
'code' => '<?php
/** @template T */
class MyCollection {
/** @param array<T> $members */
public function __construct(public array $members) {}
}
/**
* @param MyCollection<string> $c
* @return MyCollection<mixed>
*/
function getMixedCollection(MyCollection $c): MyCollection {
return $c;
}',
'error_message' => 'InvalidReturnStatement',
],
];
}
}