,error_levels?:string[]}> */ public function providerValidCodeParse(): iterable { return [ 'cachingIterator' => [ 'hasNext(); $key = $decoratorIterator->key(); $value = $decoratorIterator->current(); ', 'assertions' => [ '$key' => 'int', '$value' => 'string', '$next' => 'bool', ], ], 'infiniteIterator' => [ 'key(); $value = $decoratorIterator->current(); ', 'assertions' => [ '$key' => 'int', '$value' => 'string', ], ], 'limitIterator' => [ 'key(); $value = $decoratorIterator->current(); ', 'assertions' => [ '$key' => 'int', '$value' => 'string', ], ], 'callbackFilterIterator' => [ 'key(); $value = $decoratorIterator->current(); ', 'assertions' => [ '$key' => 'int', '$value' => 'string', ], ], 'noRewindIterator' => [ 'key(); $value = $decoratorIterator->current(); ', 'assertions' => [ '$key' => 'int', '$value' => 'string', ], ], 'classTemplate' => [ ' $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 * @psalm-suppress ArgumentTypeCoercion */ $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', 'DocblockTypeContradiction', ], ], 'classTemplateSelfs' => [ ' */ public $T; /** * @param class-string $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 */ 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 */ 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', '$efoo_bar' => 'Exception', '$ffoo' => 'Foo', '$ffoo_bar' => 'LogicException', ], 'error_levels' => ['LessSpecificReturnStatement'], ], 'classTemplateContainerSimpleCall' => [ 'obj = $obj; } /** * @return T */ public function bar() { return $this->obj; } } $afoo = new Foo(new A()); $afoo_bar = $afoo->bar();', 'assertions' => [ '$afoo' => 'Foo', '$afoo_bar' => 'A', ], ], 'classTemplateContainerThisCall' => [ '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; } }', [], 'error_levels' => ['MixedOperand'], ], 'validPsalmTemplatedClassType' => [ 'bar(new A());', ], '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));', ], 'repeatedCall' => [ ' */ 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;}));', ], 'noRepeatedTypeException' => [ ' */ private $type; /** @var array */ private $items; /** * @param class-string $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 $items * @return Foo */ private function ensureFoo(array $items): Foo { /** @var class-string */ $type = $items[0] instanceof A ? A::class : B::class; return new Foo($type); } } class A {} class B {}', ], 'collectionOfClosure' => [ ' */ 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; } );', ], 'templatedInterfaceIteration' => [ ' */ public function getIterator(); } class Collection implements ICollection { /** @var array */ private $data; public function __construct(array $data) { $this->data = $data; } 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); }', ], '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);', ], 'classTemplateFunctionImplementsInterface' => [ 't = $t; } /** * @return T */ public function getFoo() { return $this->t; } } function passFoo(Foo $f) : Foo { return (new FooGetter($f))->getFoo(); }', ], 'getPropertyOnClass' => [ ' */ 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) {} }', ], 'getMagicPropertyOnClass' => [ 'x; $b_y = $b->y; ', 'assertions' => [ '$b_x' => 'A|null', '$b_y' => 'A|null', ], ], 'getMagicPropertyOnThis' => [ 'x instanceof X) {} if ($this->y instanceof X) {} } } ', ], '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', ], ], 'templateTKeyedArrayValues' => [ ',1:Collection} * @psalm-suppress InvalidReturnType */ public function partition() {} } /** @var Collection $c */ $c = new Collection; [$partA, $partB] = $c->partition();', [ '$partA' => 'Collection', '$partB' => 'Collection', ], ], 'doublyLinkedListConstructor' => [ 'add(5, "hello"); $list->add("hello", 5); /** @var SplDoublyLinkedList */ $templated_list = new SplDoublyLinkedList(); $templated_list->add(5, "hello"); $a = $templated_list->bottom();', [ '$a' => 'string', ], ], 'templateDefaultSimpleString' => [ 't = $t; } } $c = new C();', 'assertions' => [ '$c===' => 'C', ], ], 'SKIPPED-templateDefaultConstant' => [ 't = $t; } } $e = new E();', 'assertions' => [ '$e===' => 'E', ], ], 'SKIPPED-templateDefaultClassMemberConstant' => [ 't = $t; } } $e = new E();', 'assertions' => [ '$e===' => 'E', ], ], 'templateDefaultClassConstant' => [ ' */ public $t; /** * @param class-string $t */ function __construct(string $t = D::class) { $this->t = $t; } } $e = new E();', 'assertions' => [ '$e===' => 'E', ], ], 'allowNullablePropertyAssignment' => [ ' $foo */ public function __construct(I $foo) { $this->bar = $foo->get(); } }', ], 'reflectionClass' => [ ' $name */ class CustomReflectionClass { /** * @var class-string */ public $name; /** * @param T|class-string $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 $className * @return CustomReflectionClass */ function getTypeOf(string $className) { return new CustomReflectionClass($className); }', ], 'psalmReflectionClass' => [ ' $name */ class CustomReflectionClass { /** * @var class-string */ public $name; /** * @param T|class-string $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 $className * @return CustomReflectionClass */ function getTypeOf(string $className) { return new CustomReflectionClass($className); }', ], 'ignoreTooManyGenericObjectArgs' => [ 't = $t; } } /** @param C $c */ function takesC(C $c) : void {} /** * @psalm-suppress TooManyTemplateParams * @var C */ $c = new C(5); takesC($c);', ], 'classTemplateUnionType' => [ ' $c */ function foo(C $c) : void {} /** @param C $c */ function bar(C $c) : void {}', ], 'unionAsTypeReturnType' => [ ' */ public function filter(Closure $p); }', ], 'converterObject' => [ ' $records */ public $records; /** * @param array $records */ public function __construct(array $records) { $this->records = $records; } /** * @template Q2 as object * * @param Q2 $obj2 * * @return array */ 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 */ public function appendProperty(object $obj): array { return $this->appender($obj); } }', ], 'converterClassString' => [ ' $records */ public $records; /** * @param array $records */ public function __construct(array $records) { $this->records = $records; } /** * @template Q as object * * @param class-string $obj * * @return array */ public function appendProperty(string $obj): array { return $this->appender($obj); } /** * @template Q as object * * @param class-string $obj2 * * @return array * * @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' => [ ' [ ' */ 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 isset($this->data[$property]) ? $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 { /** @var T&I */ 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"));' ], 'weakReferenceIsTyped' => [ 'get(); ', [ '$ex' => 'Exception|null' ], ], 'weakReferenceIsCovariant' => [ ' $_ref */ function acceptsThrowableRef(WeakReference $_ref): void {} acceptsThrowableRef(WeakReference::create(new Exception)); ' ], 'mapTypeParams' => [ ' */ public $arr; /** @param array $arr */ function __construct(array $arr) { $this->arr = $arr; } } /** * @template TInputKey as array-key * @template TInputValue * @param Map $map * @return Map */ function copyMapUsingProperty(Map $map): Map { return new Map($map->arr); }', ], 'mapStaticClassTemplatedFromClassString' => [ ' $t * @return T */ function f(string $t) { return $t::factory(); } /** @template T of Base */ class C { /** @var class-string */ private string $t; /** @param class-string $t */ public function __construct($t) { $this->t = $t; } /** @return T */ public function f(): Base { $t = $this->t; return $t::factory(); } }' ], 'nullableTemplateAs' => [ 'arrayObject = $arrayObject; } /** * @psalm-assert-if-true Foo $this * @psalm-assert-if-true ArrayObject $this->arrayObject */ public function hasArray(): bool { return $this->arrayObject instanceof \ArrayObject; } /** @return T */ public function toMaybeArray() { if ($this->hasArray()) { return $this->arrayObject->getArrayCopy(); } return null; } }' ], 'uasortCallableInMethod' => [ ' $collection * @psalm-param callable(T, T): int $sorter * @psalm-return array */ function order(array $collection, callable $sorter): array { usort($collection, $sorter); return $collection; } }' ], 'intersectOnTOfObject' => [ 'setMethodPrefixInterceptor( function(AccessInterceptorInterface $i) : string { return "hello"; } ); }' ], 'assertionOnTemplatedClassString' => [ ' $type * @psalm-return EQB */ 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 */ protected $type; /** * @psalm-param class-string $type */ public function __construct(string $type) { $this->type = $type; } }' ], 'createEmptyArrayCollection' => [ ' * @var array */ private $elements = []; /** * Initializes a new ArrayCollection. * * @param array $elements * * @psalm-param array $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; } }', [ '$a' => 'ArrayCollection' ] ], 'newGenericBecomesPropertyTypeValidArg' => [ ' */ 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 * @var array */ private $elements = []; /** * Initializes a new ArrayCollection. * * @param array $elements * * @psalm-param array $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' => [ ' */ 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 * @var array */ private $elements; /** * @param array $elements * * @psalm-param array $elements */ public function __construct(array $elements = []) { $this->elements = $elements; } /** * @psalm-param Closure(T=):bool $p * @psalm-return self */ public function filter(Closure $p) { return $this; } }' ], 'unionClassStringInferenceAndDefaultEmptyArray' => [ ' $items */ protected $items = []; /** * @param array $items */ public function __construct(array $items = []) { $this->items = $items; } /** * @template C as object * @param class-string $classString * @param array $elements * @return Collection */ public static function fromClassString(string $classString, array $elements = []) : Collection { return new Collection($elements); } }', [ '$packages' => 'Collection' ] ], 'assertSameOnTemplatedProperty' => [ 'contents = $contents; } /** @param E $thing */ public function contains(object $thing) : bool { if ($this->contents !== $thing) { return false; } return true; } }' ], 'assertNotNullOnTemplatedProperty' => [ ' */ $a = new A(); if (null !== $a->filter) {}' ], 'setTemplatedPropertyOutsideClass' => [ 'value = $value; } } /** @psalm-var Watcher $watcher */ $watcher = new Watcher(0); $watcher->value = 0;' ], 'callableAsClassStringArray' => [ '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 */ private array $ids; /** * @psalm-param list $ids */ private function __construct(array $ids) { $this->ids = $ids; } /** * @template T1 of Id * @psalm-param T1 $class * @psalm-param list $ids * @psalm-return self */ 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 $class * @psalm-param list $ids * @psalm-return self */ public static function fromStrings(string $class, array $ids): self { return new self(array_map([$class, "fromString"], $ids)); } }' ], 'noCrashTemplateInsideGenerator' => [ ' */ private \Iterator $values; /** * @param \Iterator $values */ public function __construct(\Iterator $values) { $this->values = $values; } /** * @param T $element * * @return self */ 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), ); } }', [], ['MissingClosureParamType'] ], 'templatedPropertyAllowsNull' => [ 'key = $key; } }' ], 'templatePropertyWithoutParams' => [ ' */ 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' => [ 'x = 1; }' ], 'multipleMatchingObjectsInUnion' => [ '> $containers * @return T */ function unwrap(array $containers) { return array_map( fn($container) => $container->get(), $containers )[0]; } /** * @param array|Container> $typed_containers */ function takesDifferentTypes(array $typed_containers) : void { $ret = unwrap($typed_containers); if (is_string($ret)) {} if (is_int($ret)) {} }', [], [], '7.4' ], 'templateWithLateResolvedType' => [ ' $foo */ $foo = new Foo();' ], 'SKIPPED-extendedPropertyTypeParameterised' => [ 'createMap(); $date = $map->get("test"); echo $date->format("Y"); } /** * @return Map */ abstract protected function createMap(): Map; }' ], 'looseEquality' => [ 'level = $level; } /** * @psalm-return self */ 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 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' => [ 'entity = $qux; } } }' ], 'flippedParamsMethodInside' => [ ' */ public abstract function getTraversable() : Traversable; /** * @param Foo $flipped * @return Traversable */ public function getFlippedTraversable(Foo $flipped): Traversable { return $flipped->getTraversable(); } }' ], 'flippedParamsMethodOutside' => [ ' $flipped * @return Traversable */ function getFlippedTraversable(Foo $flipped): Traversable { return $flipped->getTraversable(); } /** * @template A * @template B */ abstract class Foo { /** @return Traversable */ public abstract function getTraversable() : Traversable; }' ], 'flippedParamsPropertyInside' => [ ' */ public $traversable; /** * @param Foo $flipped * @return Traversable */ public function getFlippedTraversable(Foo $flipped): Traversable { return $flipped->traversable; } }' ], 'flippedParamsPropertyOutside' => [ ' $flipped * @return Traversable */ function getFlippedTraversable(Foo $flipped): Traversable { return $flipped->traversable; } /** * @template A * @template B */ abstract class Foo { /** @var Traversable */ public $traversable; }' ], 'simpleTemplate' => [ ' $f */ function takesFMixed(F $f) : void {} function sendsF(F $f) : void { takesFMixed($f); }' ], ]; } /** * @return iterable */ public function providerInvalidCodeParse(): iterable { 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:AlmostFooMap 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' ], 'noClassTemplatesInStaticMethods' => [ ' 'UndefinedDocblockClass' ], 'newGenericBecomesPropertyTypeInvalidArg' => [ ' */ 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 * @var array */ private $elements = []; /** * Initializes a new ArrayCollection. * * @param array $elements * * @psalm-param array $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' => [ ' $foos */ function consume(iterable $foos): void {} /** @param IteratorAggregate $t */ function foo(IteratorAggregate $t) : void { consume($t); }', 'error_message' => 'InvalidArgument', ], ]; } }