[ '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'], ], 'phanTuple' => [ '_0 = $_0; } /** * @return int * The arity of this tuple */ public function arity(): int { return (int)static::ARITY; } /** * @return array * An array of all elements in this tuple. */ public function toArray(): array { return [ $this->_0, ]; } } /** * A tuple of 2 elements. * * @template T0 * The type of element zero * * @template T1 * The type of element one * * @inherits Tuple1 */ class Tuple2 extends Tuple1 { /** @var int */ const ARITY = 2; /** @var T1 */ public $_1; /** * @param T0 $_0 * The 0th element * * @param T1 $_1 * The 1st element */ public function __construct($_0, $_1) { parent::__construct($_0); $this->_1 = $_1; } /** * @return array * An array of all elements in this tuple. */ public function toArray(): array { return [ $this->_0, $this->_1, ]; } } $a = new Tuple2("cool", 5); /** @return void */ function takes_int(int $i) {} /** @return void */ function takes_string(string $s) {} takes_string($a->_0); takes_int($a->_1);', ], '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): EntitySeries { $type = $items[0] instanceof A ? A::class : B::class; return new Foo($type); } } class A {} class B {}' ], 'collectionOfClosure' => [ ' */ public function filter(Closure $p); } 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'], ], '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; } }', ], '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'], ], 'templateExtendsSameName' => [ 'v = $v; } /** * @return TValue */ public function getValue() { return $this->v; } } /** * @template TKey * @template TValue * @template-extends ValueContainer */ 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' => 'int' ], ], 'templateExtendsDifferentName' => [ 'v = $v; } /** * @return TValue */ public function getValue() { return $this->v; } } /** * @template TKey * @template Tv * @template-extends ValueContainer */ 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' => 'int' ], ], 'extendsWithNonTemplate' => [ ' */ class FooContainer extends Container { /** * @return Foo */ public function getItem() { return new Foo(); } } /** * @template TItem * @param Container $c * @return TItem */ function getItemFromContainer(Container $c) { return $c->getItem(); } $fc = new FooContainer(); $f1 = $fc->getItem(); $f2 = getItemFromContainer($fc);', [ '$fc' => 'FooContainer', '$f1' => 'Foo', '$f2' => 'Foo', ] ], 'allowExtendingParameterisedTypeParam' => [ ' */ class FooContainer extends Container { /** @param Foo $obj */ public function uri($obj) : string { return "hello"; } }' ], 'extendsWithNonTemplateWithoutImplementing' => [ 'id = $id; } /** * @return T */ public function getID() { return $this->id; } } /** * @template-extends User */ class AppUser extends User {} $au = new AppUser(-1); $id = $au->getId();', [ '$au' => 'AppUser', '$id' => 'int', ] ], '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', ] ], 'extendsTwiceSameName' => [ 'v = $v; } /** * @return T */ public function getValue() { return $this->v; } } /** * @template T * @template-extends Container */ class ChildContainer extends Container {} /** * @template T * @template-extends ChildContainer */ class GrandChildContainer extends ChildContainer {} $fc = new GrandChildContainer(5); $a = $fc->getValue();', [ '$a' => 'int', ] ], 'extendsTwiceDifferentName' => [ 'v = $v; } /** * @return T1 */ public function getValue() { return $this->v; } } /** * @template T2 * @template-extends Container */ class ChildContainer extends Container {} /** * @template T3 * @template-extends ChildContainer */ class GrandChildContainer extends ChildContainer {} $fc = new GrandChildContainer(5); $a = $fc->getValue();', [ '$a' => 'int', ] ], ]; } /** * @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', ], 'restrictTemplateInput' => [ ' */ 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', ], 'extendsWithUnfulfilledNonTemplate' => [ ' */ class BarContainer extends Container { /** * @return Foo */ public function getItem() { return new Foo(); } }', 'error_message' => 'ImplementedReturnTypeMismatch - src/somefile.php:29 - The return type \'A\Bar\' for', ], 'extendTemplateAndDoesNotOverrideWithWrongArg' => [ 'id = $id; } /** * @return T */ public function getID() { return $this->id; } } /** * @template-extends User */ class AppUser extends User {} $au = new AppUser("string");', 'error_message' => 'InvalidScalarArgument', ], 'extendsTwiceDifferentNameBrokenChain' => [ 'v = $v; } /** * @return T1 */ public function getValue() { return $this->v; } } /** * @template T2 */ class ChildContainer extends Container {} /** * @template T3 * @template-extends ChildContainer */ class GrandChildContainer extends ChildContainer {} $fc = new GrandChildContainer(5); $a = $fc->getValue();', 'error_message' => 'MixedAssignment', ], 'extendsTwiceSameNameBrokenChain' => [ 'v = $v; } /** * @return T */ public function getValue() { return $this->v; } } /** * @template T */ class ChildContainer extends Container {} /** * @template T * @template-extends ChildContainer */ class GrandChildContainer extends ChildContainer {} $fc = new GrandChildContainer(5); $a = $fc->getValue();', 'error_message' => 'MixedAssignment', ], 'extendsTwiceSameNameLastDoesNotExtend' => [ 'v = $v; } /** * @return T */ public function getValue() { return $this->v; } } /** * @template T * @template-extends Container */ class ChildContainer extends Container {} /** * @template T */ class GrandChildContainer extends ChildContainer {} $fc = new GrandChildContainer(5); $a = $fc->getValue();', 'error_message' => 'MixedAssignment', ], ]; } }