$obj * * @return array */ public function appendProperty(string $obj): array { return $this->appender($obj); } /** * @template Q as object * * @param class-string$obj2 * * @return array */ 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' => [ ' [ ' */ private $elements; /** * @param array$elements */ public function __construct(array $elements = []) { $this->elements = $elements; } } /** @psalm-suppress MixedArgument */ $c = new ArrayCollection($_GET["a"]);', [ '$c' => 'ArrayCollection ', ], ], 'doNotCombineTypes' => [ 't = $t; } /** * @return T */ public function get() { return $this->t; } } /** * @param C $a * @param C $b * @return C|C */ 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();', [ '$random_collection' => 'C|C', '$a_or_b' => 'A|B', ], ], 'inferClosureParamTypeFromContext' => [ ' */ function map(callable $action): self; } /** * @template T */ interface Optional { /** * @return T */ function get(); } /** * @param Collection > $collection * @return Collection */ function expandOptions(Collection $collection) : Collection { return $collection->map( function ($optional) { return $optional->get(); } ); }', ], 'templateEmptyParamCoercion' => [ ' */ 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 {} takesCollectionOfItems(new Collection()); takesCollectionOfItems(new Collection([]));', ], 'templatedGet' => [ ' */ protected $data = []; /** @param array
$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;', [ '$a' => 'string', ], ], 'templateAsArray' => [ ' */ 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 $this->data[$property] ?? null; } /** * @param scalar|array|object|null $value */ public function __set(string $property, $value) { $this->data[$property] = $value; } }', ], 'keyOfClassTemplateAcceptingIndexedAccess' => [ 'data = $data; } /** * @template K as key-of
* * @param K $property * @param TData[K] $value */ public function __set(string $property, $value) { $this->data[$property] = $value; } }', ], 'keyOfClassTemplateReturningIndexedAccess' => [ 'data = $data; } /** * @template K as key-of * * @param K $property * * @return TData[K] */ public function __get(string $property) { return $this->data[$property]; } }', ], 'SKIPPED-templatedInterfaceIntersectionFirst' => [ '&IChild */ function makeConcrete() : IChild { return new class() implements IChild { public function foo() { return new C(); } }; } $a = makeConcrete()->foo();', [ '$a' => 'C', ], ], 'templatedInterfaceIntersectionSecond' => [ ' */ function makeConcrete() : IChild { return new class() implements IChild { public function foo() { return new C(); } }; } $a = makeConcrete()->foo();', [ '$a' => 'C', ], ], 'returnTemplateIntersectionGenericObjectAndTemplate' => [ ' $className * * @psalm-return T&I */ function makeConcrete(string $className) : object { return new class() extends C implements I { public function getMe() { return $this; } }; } $a = makeConcrete(C::class);', [ '$a' => 'C&I ', ], ], 'keyOfArrayGet' => [ ' */ abstract class Foo { /** * @var DATA */ protected $data; /** * @param DATA $data */ public function __construct(array $data) { $this->data = $data; } /** * @template K as key-of * * @param K $property * * @return DATA[K] */ public function __get(string $property) { return $this->data[$property]; } }', ], 'keyOfArrayRandomKey' => [ ' */ abstract class Foo { /** * @var DATA */ protected $data; /** * @param DATA $data */ public function __construct(array $data) { $this->data = $data; } /** * @return key-of */ abstract public function getRandomKey() : string; }', ], 'allowBoolTemplateCoercion' => [ ' */ function test(): TestPromise { return new TestPromise(true); }', ], 'classTemplatedPropertyEmptyAssignment' => [ ' */ private $FooArray; public function __construct() { $this->FooArray = new Foo(function(): array { return []; }); } }', ], 'classTemplatedPropertyAssignmentWithMoreSpecificArray' => [ ' */ private $FooArray; public function __construct() { $this->FooArray = new Foo(function(): array { return []; }); } }', ], 'insideClosureVarTemplate' => [ ' [ ' $a * @psalm-return ReflectionClass */ public function reflection(string $a) { return new ReflectionClass($a); } }', ], 'anonymousClassMustNotBreakParentTemplate' => [ 'value = $val; new class extends Foo {}; } /** @psalm-return ?T */ public function get() { return $this->value; } }' ], 'templatedInvoke' => [ 'value = $val; } /** @return T */ public function get() { return $this->value; } /** * @param T $val * @return Foo */ public function __invoke($val) { return new static($val); } /** * @param T $val * @return Foo */ public function create($val) { return new static($val); } } function bar(string $s) : string { $foo = new Foo($s); $bar = $foo($s); return $bar->get(); }' ], 'templatedLiteralStringReplacement' => [ 'value = $value; } /** * @psalm-return T */ public function value() { return $this->value; } } /** * @template T * @psalm-param T $value * @psalm-return Value */ function value($value): Value { return new Value($value); } /** * @psalm-param Value $value */ function client($value): void {} client(value("awdawd"));' ], 'yieldFromGenericObjectNotExtendingIterator' => [ ' */ public Foo $vector; /** * @param Foo $v */ public function __construct(Foo $v) { $this->vector = $v; } public function getIterator(): Iterator { yield from $this->vector; } }', [], ['TooManyTemplateParams'] ], 'coerceEmptyArrayToGeneral' => [ ' */ 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 $_ */ function takesFooArray($_): void {}', ], 'allowListAcceptance' => [ ' */ public $values; /** @param list $values */ function __construct(array $values) { $this->values = $values; } } /** @return Collection */ function makeStringCollection() { return new Collection(getStringList()); // gets typed as Collection for some reason } /** @return list */ function getStringList(): array { return ["foo", "baz"]; }' ], 'allowListAcceptanceIntoArray' => [ ' */ public $values; /** @param array $values */ function __construct(array $values) { $this->values = $values; } } /** @return Collection */ function makeStringCollection() { return new Collection(getStringList()); // gets typed as Collection for some reason } /** @return list */ function getStringList(): array { return ["foo", "baz"]; }' ], 'allowInternalNullCheck' => [ 'parent = $parent; } public function hasNoParent() : bool { return $this->parent === null; // So TP does contain null } }' ], 'useMethodWithExistingGenericParam' => [ ' */ public function filter(Closure $p); } /** * @param Collection $c * @psalm-return Collection */ function filter(Collection $c, string $name) { return $c->filter( function (Bar $f) use ($name) { return $f->getFoo() === "foo"; } ); }' ], 'unboundVariableIsEmptyInInstanceMethod' => [ 'collectInstance("a");' ], 'unboundVariableIsEmptyInStaticMethod' => [ ' [ ' $x * * @return array */ function iterableToArray (iterable $x): array { if (is_array($x)) { return $x; } else { return iterator_to_array($x); } } /** * @param Traversable $t * @return array */ function withParams(Traversable $t) : array { return iterableToArray($t); }', ], 'templateStaticWithParam' => [ ' */ private $elements; /** * @param list $elements */ public function __construct(array $elements) { $this->elements = $elements; } /** * @template U * @param callable(T=):U $callback * @return static */ public function map(callable $callback) { return new static(array_values(array_map($callback, $this->elements))); } } /** @param ArrayCollection $ints */ function takesInts(ArrayCollection $ints) :void {} takesInts((new ArrayCollection([ "a", "bc" ]))->map("strlen"));' ], ]; } /** * @return iterable */ public function providerInvalidCodeParse() { return [ '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' . DIRECTORY_SEPARATOR . 'somefile.php:20:34 - Argument 1 of type expects string, callable(State):(T as mixed)&Foo provided', ], 'templateWithNoReturn' => [ ' 'InvalidReturnType', ], 'templateInvalidDocblockArgument' => [ ' * @psalm-suppress InvalidReturnType */ function violate($p) {}', 'error_message' => 'InvalidTemplateParam', ], 'doublyLinkedListBadParam' => [ ' */ $templated_list = new SplDoublyLinkedList(); $templated_list->add(5, []);', 'error_message' => 'InvalidArgument', ], 'copyScopedClassInNamespacedClass' => [ ' 'ReservedWord', ], 'duplicateTemplateFunction' => [ ' */ static function of($value): self { return new self($value); } /** * @param T $value */ private function __construct($value) { $this->value = $value; } }', 'error_message' => 'InvalidDocblock', ], 'preventDogCatSnafu' => [ ' $list */ function addAnimal(Collection $list) : void { $list->add(new Cat()); } /** * @param Collection $list */ function takesDogList(Collection $list) : void { addAnimal($list); // this should be an error }', 'error_message' => 'InvalidArgument', ], 'templateEmptyParamCoercionChangeVariable' => [ ' */ private $data; /** @psalm-param iterable $data */ public function __construct(iterable $data = []) { $this->data = $data; } } /** @psalm-param Collection $c */ function takesStringCollection(Collection $c): void {} /** @psalm-param Collection $c */ function takesIntCollection(Collection $c): void {} $collection = new Collection(); takesStringCollection($collection); takesIntCollection($collection);', 'error_message' => 'InvalidScalarArgument', ], 'argumentExpectsFleshOutTIndexedAccess' => [ 'data = $data; } /** * @template K as key-of * * @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 * * @param K $property * @param TData[K] $value */ public function __set(string $property, $value) { // data updating would go here $this->data[$property] = $value; } } /** @extends Row */ 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 string(height)|string(id)|string(name), string(ame) provided', ], 'specialiseTypeBeforeReturning' => [ ' */ function returnFooBase() { $f = new Foo(new Derived()); takesFooDerived($f); return $f; } /** * @param Foo $foo */ function takesFooDerived($foo): void {}', 'error_message' => 'InvalidReturnStatement' ], 'possiblySpecialiseTypeBeforeReturning' => [ ' */ function returnFooBase() { $f = new Foo(new Derived()); if (rand(0, 1)) { takesFooDerived($f); } return $f; } /** * @param Foo $foo */ function takesFooDerived($foo): void {}', 'error_message' => 'InvalidReturnStatement' ], 'preventUseWithMoreSpecificParamInt' => [ ' $col */ function usesCollection(Collection $col): void { $col->add(456); }', 'error_message' => 'InvalidArgument' ], 'preventUseWithMoreSpecificParamEmptyArray' => [ ' $col */ function usesCollection(Collection $col): void { $col->add([]); }', 'error_message' => 'InvalidArgument' ], 'preventTemplatedCorrectionBeingWrittenTo' => [ ' */ private $data; /** @param array $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 $i */ function takesCollectionOfItems(ArrayCollection $i): void { $i->addItem(10, new OtherSubItem); } $subitem_collection = new ArrayCollection([ new SubItem ]); takesCollectionOfItems($subitem_collection);', 'error_message' => 'InvalidArgument' ], ]; } }