[
'T = $T;
}
/**
* @return T
*/
public function bar() {
$t = $this->T;
return new $t();
}
}
$at = "A";
/** @var Foo */
$afoo = new Foo($at);
$afoo_bar = $afoo->bar();
$bfoo = new Foo(B::class);
$bfoo_bar = $bfoo->bar();
// this shouldn’t cause a problem as it’s a docbblock type
if (!($bfoo_bar instanceof B)) {}
$c = C::class;
$cfoo = new Foo($c);
$cfoo_bar = $cfoo->bar();',
'assertions' => [
'$afoo' => 'Foo',
'$afoo_bar' => 'A',
'$bfoo' => 'Foo',
'$bfoo_bar' => 'B',
'$cfoo' => 'Foo',
'$cfoo_bar' => 'C',
],
'error_levels' => [
'MixedReturnStatement',
'LessSpecificReturnStatement',
'RedundantConditionGivenDocblockType',
'TypeCoercion'
],
],
'classTemplateSelfs' => [
'T = $T;
}
/**
* @return T
*/
public function bar() {
$t = $this->T;
return new $t();
}
}
class E {
/**
* @return Foo
*/
public static function getFoo() {
return new Foo(__CLASS__);
}
/**
* @return Foo
*/
public static function getFoo2() {
return new Foo(self::class);
}
/**
* @return Foo
*/
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',
'$efoo2' => 'Foo',
'$efoo3' => 'Foo',
'$gfoo' => 'Foo',
'$gfoo2' => 'Foo',
'$gfoo3' => 'Foo',
],
'error_levels' => [
'LessSpecificReturnStatement',
'RedundantConditionGivenDocblockType',
],
],
'classTemplateExternalClasses' => [
'T = $T;
}
/**
* @return T
*/
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',
'$efoo_bar' => 'Exception',
'$ffoo' => 'Foo',
'$ffoo_bar' => 'LogicException',
],
'error_levels' => ['LessSpecificReturnStatement'],
],
'classTemplateContainer' => [
'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;
}
}
$afoo = new Foo(new A());
$afoo_bar = $afoo->bar();',
'assertions' => [
'$afoo' => 'Foo',
'$afoo_bar' => 'A',
],
'error_levels' => ['MixedOperand'],
],
'validTemplatedType' => [
' [
' [
' [
'foo("string"));',
],
'genericArrayKeys' => [
' $arr
* @return array
*/
function my_array_keys($arr) {
return array_keys($arr);
}
$a = my_array_keys(["hello" => 5, "goodbye" => new \Exception()]);',
'assertions' => [
'$a' => 'array',
],
],
'genericArrayFlip' => [
' $arr
* @return array
*/
function my_array_flip($arr) {
return array_flip($arr);
}
$b = my_array_flip(["hello" => 5, "goodbye" => 6]);',
'assertions' => [
'$b' => 'array',
],
],
'byRefKeyValueArray' => [
' $arr
*/
function byRef(array &$arr) : void {}
$b = ["a" => 5, "c" => 6];
byRef($b);',
'assertions' => [
'$b' => 'array',
],
],
'byRefMixedKeyArray' => [
' $arr
*/
function byRef(array &$arr) : void {}
$b = ["a" => 5, "c" => 6];
byRef($b);',
'assertions' => [
'$b' => 'array',
],
],
'mixedArrayPop' => [
' $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',
],
'error_levels' => ['MixedAssignment', 'MixedArgument'],
],
'genericArrayPop' => [
' $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' => 'null|int',
'$b' => 'array',
],
],
'intersectionTemplatedTypes' => [
' */
private $data;
/** @psalm-param iterable $data */
public function __construct(iterable $data) {
$this->data = $data;
}
}
class Item {}
/** @psalm-param Collection- $c */
function takesCollectionOfItems(Collection $c): void {}
/** @psalm-var iterable
- $data2 */
$data2 = [];
takesCollectionOfItems(new Collection($data2));
/** @psalm-var iterable
- &Countable $data */
$data = [];
takesCollectionOfItems(new Collection($data));',
],
'templateCallableReturnType' => [
' [
' [
' */
private $data;
/** @param array $data */
public function __construct(array $data) {
$this->data = $data;
}
/**
* @template T
* @param Closure(TValue):T $func
* @return ArrayCollection
*/
public function map(Closure $func) {
return new static(array_map($func, $this->data));
}
}
class Item {}
/**
* @param ArrayCollection $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;}));'
],
'replaceChildTypeWithGenerator' => [
' $t
* @return array
*/
function f(Traversable $t): array {
$ret = [];
foreach ($t as $k => $v) $ret[$k] = $v;
return $ret;
}
/** @return Generator */
function g():Generator { yield new stdClass; }
takesArrayOfStdClass(f(g()));
/** @param array $p */
function takesArrayOfStdClass(array $p): void {}',
],
'noRepeatedTypeException' => [
' */
private $items;
/**
* @param class-string $type
* @template-typeof 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
* @param T $item
*/
public function add($item): void
{
$foo = $this->ensureFoo([$item]);
$foo->add($item);
}
/**
* @template T
* @param array $items
* @return Foo
*/
private function ensureFoo(array $items): Foo
{
$type = $items[0] instanceof A ? A::class : B::class;
return new Foo($type);
}
}
class A {}
class B {}'
],
'collectionOfClosure' => [
'
* @psalm-suppress MixedTypeCoercion
*/
public function filter(Closure $p) {
return $this;
}
}
class I {}
/** @var Collection> $c */
$c = new Collection;
$c->filter(
/** @param Collection $elt */
function(Collection $elt): bool { return (bool) rand(0,1); }
);
$c->filter(
/** @param Collection $elt */
function(Collection $elt): bool { return true; }
);',
],
'splatTemplateParam' => [
' $arr
* @param array $arr2
* @return array
*/
function splat_proof(array $arr, array $arr2) {
return $arr;
}
$foo = [
[1, 2, 3],
[1, 2],
];
$a = splat_proof(... $foo);',
'assertions' => [
'$a' => 'array',
],
],
'passArrayByRef' => [
' $_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 $_arr
* @return null|TValue
* @psalm-ignore-nullable-return
*/
function fNoRef(array $_arr) {
return array_shift($_arr);
}',
],
'templatedInterfaceIteration' => [
' */
public function getIterator();
}
class Collection implements ICollection {
/** @var array */
private $data;
public function __construct(array $data) {
$this->data = $data;
}
/** @psalm-suppress LessSpecificImplementedReturnType */
public function getIterator(): \Traversable {
return new \ArrayIterator($this->data);
}
}
/** @var ICollection */
$c = new Collection(["a" => 1]);
foreach ($c as $k => $v) { atan($v); strlen($k); }'
],
'templatedInterfaceGetIteratorIteration' => [
' */
public function getIterator();
}
class Collection implements ICollection {
/** @var array */
private $data;
public function __construct(array $data) {
$this->data = $data;
}
/** @psalm-suppress LessSpecificImplementedReturnType */
public function getIterator(): \Traversable {
return new \ArrayIterator($this->data);
}
}
/** @var ICollection */
$c = new Collection(["a" => 1]);
foreach ($c->getIterator() as $k => $v) { atan($v); strlen($k); }'
],
'implictIteratorTemplating' => [
'
*/
class SomeIterator implements IteratorAggregate
{
function getIterator()
{
yield 1;
}
}
/** @param \IteratorAggregate $i */
function takesIteratorOfInts(\IteratorAggregate $i) : void {
foreach ($i as $j) {
echo $j;
}
}
takesIteratorOfInts(new SomeIterator());'
],
'allowTemplatedIntersectionToExtend' => [
'bar = $bar;
}
/**
* @return T&Foo
*/
public function makeFoo()
{
return $this->bar;
}
}'
],
'restrictTemplateInputWithTClassGoodInput' => [
' */
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);',
],
'classTemplateAsCorrect' => [
' [
' [
' [
't = $t;
}
/**
* @return T
*/
public function getFoo()
{
return $this->t;
}
}
function passFoo(Foo $f) : Foo {
return (new FooGetter($f))->getFoo();
}',
],
'templateFunctionVar' => [
'bar();
/** @var T&D */
$b = $some_t;
$b->bar();
/** @var D&T */
$b = $some_t;
$b->bar();
return $a;
}',
'assertions' => [],
'error_levels' => ['MixedAssignment', 'MissingParamType'],
],
'returnClassString' => [
' [
' [],
'error_levels' => ['MixedMethodCall'],
],
'returnTemplatedClassClassName' => [
'loader(FooChild::class);',
'assertions' => [
'$a' => 'null|FooChild',
],
],
'upcastIterableToTraversable' => [
' [],
'error_levels' => ['MixedAssignment'],
],
'upcastGenericIterableToGenericTraversable' => [
'
* @param T::class $class
*/
function foo(string $class) : void {
$a = new $class();
foreach ($a as $b) {}
}',
'assertions' => [],
'error_levels' => [],
],
'bindFirstTemplatedClosureParameter' => [
' [
'
*/
private $type;
/**
* @param class-string $type
*/
public function __construct(string $type) {
$this->type = $type;
}
/**
* @return class-string
*/
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 $c */
function handleCollectionOfFoo(Collection $c) : void {
if ($c->getType() === FooChild::class) {}
}',
],
'getEquateClass' => [
'obj = $obj;
}
/**
* @param T $object
*/
public function bar(Foo $object) : void
{
if ($this->obj === $object) {}
}
}',
],
'allowComparisonGetTypeResult' => [
'
*/
private $type;
/**
* @param class-string $type
*/
public function __construct(string $type) {
$this->type = $type;
}
/**
* @return class-string|null
*/
public function getType()
{
return $this->type;
}
}
function foo(Collection $c) : void {
$val = $c->getType();
if (!$val) {}
if ($val) {}
}',
],
'mixedTemplatedParamOutWithNoExtendedTemplate' => [
'v = $v;
}
/**
* @return TValue
*/
public function getValue()
{
return $this->v;
}
}
/**
* @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();',
[
'$a' => 'KeyValueContainer',
'$b' => 'mixed'
],
'error_levels' => ['MixedAssignment'],
],
'mixedTemplatedParamOutDifferentParamName' => [
'v = $v;
}
/**
* @return TValue
*/
public function getValue()
{
return $this->v;
}
}
/**
* @template TKey
* @template Tv
*/
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();',
[
'$a' => 'KeyValueContainer',
'$b' => 'mixed'
],
'error_levels' => ['MixedAssignment'],
],
'doesntExtendTemplateAndDoesNotOverride' => [
'id = $id;
}
/**
* @return T
*/
public function getID()
{
return $this->id;
}
}
class AppUser extends User {}
$au = new AppUser(-1);
$id = $au->getId();',
[
'$au' => 'AppUser',
'$id' => 'array-key',
]
],
'callableReturnsItself' => [
' [
' [
'getIterator();',
[
'$i' => 'Traversable',
]
],
'upcastArrayToIterable' => [
' $collection
* @return V
* @psalm-suppress InvalidReturnType
*/
function first($collection) {}
$one = first([1,2,3]);',
[
'$one' => 'int',
]
],
'templateObjectLikeValues' => [
',1:Collection}
* @psalm-suppress InvalidReturnType
*/
public function partition() {}
}
/** @var Collection $c */
$c = new Collection;
[$partA, $partB] = $c->partition();',
[
'$partA' => 'Collection',
'$partB' => 'Collection',
]
],
'understandTemplatedCalculationInOtherFunction' => [
' [
'add(5, "hello");
$list->add("hello", 5);
/** @var SplDoublyLinkedList */
$templated_list = new SplDoublyLinkedList();
$templated_list->add(5, "hello");
$a = $templated_list->bottom();',
[
'$a' => 'string',
]
],
'objectReturn' => [
' $foo
*
* @return T
*/
function Foo(string $foo) : object {
return new $foo;
}
echo Foo(DateTime::class)->format("c");',
],
'genericInterface' => [
' $t
* @return T
*/
function generic(string $t) {
return f($t)->get();
}
/** @template T as object */
interface I {
/** @return T */
public function get() {}
}
/**
* @template T as object
* @template-implements I
*/
class C implements I {
/**
* @var T
*/
public $t;
/**
* @param T $t
*/
public function __construct(object $t) {
$this->t = $t;
}
/**
* @return T
*/
public function get() {
return $this->t;
}
}
/**
* @template T as object
* @param class-string $t
* @return I
*/
function f(string $t) {
return new C(new $t);
}',
],
];
}
/**
* @return array
*/
public function providerInvalidCodeParse()
{
return [
'invalidTemplatedType' => [
' 'InvalidScalarArgument',
],
'invalidTemplatedStaticMethodType' => [
' 'InvalidScalarArgument',
],
'invalidTemplatedInstanceMethodType' => [
'foo(4));',
'error_message' => 'InvalidScalarArgument',
],
'replaceChildTypeNoHint' => [
' $t
* @return array
*/
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 $p */
function takesArrayOfStdClass(array $p): void {}',
'error_message' => 'MixedTypeCoercion',
],
'restrictTemplateInputWithClassString' => [
' */
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' => [
' */
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' => [
' 'InvalidArgument - src/somefile.php:20 - Argument 1 of type expects string, callable(State):(T as mixed)&Foo provided',
],
'classTemplateAsIncorrectClass' => [
' 'InvalidArgument',
],
'classTemplateAsIncorrectInterface' => [
' 'InvalidArgument',
],
'templateFunctionMethodCallWithoutMethod' => [
'bar();
}',
'error_message' => 'PossiblyUndefinedMethod',
],
'templateFunctionMethodCallWithoutAsType' => [
'bar();
}',
'error_message' => 'MixedMethodCall',
],
'forbidLossOfInformationWhenCoercing' => [
'
* @param T::class $class
*/
function foo(string $class) : void {}
function bar(Traversable $t) : void {
foo(get_class($t));
}',
'error_message' => 'MixedTypeCoercion',
],
'bindFirstTemplatedClosureParameter' => [
' 'InvalidScalarArgument',
],
'bindFirstTemplatedClosureParameterTypeCoercion' => [
' 'TypeCoercion',
],
'callableDoesNotReturnItself' => [
' 'InvalidScalarArgument',
],
'multipleArgConstraintWithMoreRestrictiveFirstArg' => [
' 'TypeCoercion',
],
'multipleArgConstraintWithMoreRestrictiveSecondArg' => [
' 'TypeCoercion',
],
'multipleArgConstraintWithLessRestrictiveThirdArg' => [
' 'TypeCoercion',
],
'possiblyInvalidArgumentWithUnionFirstArg' => [
' 'PossiblyInvalidArgument',
],
'possiblyInvalidArgumentWithUnionSecondArg' => [
' 'PossiblyInvalidArgument',
],
'templateWithNoReturn' => [
' 'InvalidReturnType',
],
'templateInvalidDocblockArgument' => [
'
* @psalm-suppress InvalidReturnType
*/
function violate($p) {}',
'error_message' => 'InvalidTemplateParam',
],
'doublyLinkedListBadParam' => [
' */
$templated_list = new SplDoublyLinkedList();
$templated_list->add(5, []);',
'error_message' => 'InvalidArgument',
],
'classTemplateUnionType' => [
' 'InvalidDocblock'
],
'functionTemplateUnionType' => [
' 'InvalidDocblock'
],
];
}
}