,error_levels?:string[]}> */ public function providerValidCodeParse() { return [ '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 * * @extends 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);', ], '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', ], ], 'supportBareExtends' => [ ' */ 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', ], ], 'extendsTwiceSameNameCorrect' => [ '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', ], ], 'extendsTwiceDifferentNameUnbrokenChain' => [ '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', ], ], 'templateExtendsOnceAndBound' => [ ' */ class AnotherRepo extends Repo {} $a = new AnotherRepo(); $b = $a->findOne();', [ '$a' => 'AnotherRepo', '$b' => 'null|SpecificEntity', ], ], 'templateExtendsTwiceAndBound' => [ ' */ class CommonAppRepo extends Repo {} class SpecificEntity {} /** @template-extends CommonAppRepo */ class SpecificRepo extends CommonAppRepo {} $a = new SpecificRepo(); $b = $a->findOne();', [ '$a' => 'SpecificRepo', '$b' => 'null|SpecificEntity', ], ], 'multipleArgConstraints' => [ ' [ ' */ class SomeIterator implements IteratorAggregate { public function getIterator() { yield new Foo; } } $i = (new SomeIterator())->getIterator();', [ '$i' => 'Traversable', ], ], 'templateCountOnExtendedAndImplemented' => [ ' */ class Repository implements Selectable {} interface SomeEntity {} /** * @template-extends Repository */ class SomeRepository extends Repository {}', ], 'iterateOverExtendedArrayObjectWithParam' => [ ' */ class Collection extends ArrayObject { /** @var class-string */ public $class; /** @param class-string $class */ public function __construct(string $class) { $this->class = $class; } } /** @return Collection */ function getFooCollection() : Collection { return new Collection(Foo::class); } foreach (getFooCollection() as $i => $foo) { $foo->bar(); }', ], 'iterateOverExtendedArrayObjectWithoutParam' => [ ' */ class Collection extends ArrayObject { /** @var class-string */ public $class; /** @param class-string $class */ public function __construct(string $class) { $this->class = $class; } } function getFooCollection() : Collection { return new Collection(Foo::class); } foreach (getFooCollection() as $i => $foo) {}', ], 'iterateOverExtendedArrayObjectFromClassCall' => [ ' */ public static function getSelfCollection() : Collection { return new Collection(self::class); } } /** * @template T as O * @template-extends ArrayObject */ class Collection extends ArrayObject { /** @var class-string */ public $class; /** @param class-string $class */ public function __construct(string $class) { $this->class = $class; } } foreach (Foo::getSelfCollection() as $i => $foo) { $foo->bar(); }', ], 'iterateOverExtendedArrayObjectInsideClass' => [ ' $c */ public static function takesSelfCollection(Collection $c) : void { foreach ($c as $i => $foo) { $foo->bar(); } } } /** * @template T as O * @template-extends ArrayObject */ class Collection extends ArrayObject { /** @var class-string */ public $class; /** @param class-string $class */ public function __construct(string $class) { $this->class = $class; } }', ], 'iterateOverExtendedArrayObjectThisClassIteration' => [ ' */ class Collection extends ArrayObject { /** @var class-string */ public $class; /** @param class-string $class */ public function __construct(string $class) { $this->class = $class; } private function iterate() : void { foreach ($this as $o) {} } }', ], 'iterateOverExtendedArrayObjectThisClassIterationWithExplicitGetIterator' => [ ' */ public static function getSelfCollection() : Collection { return new Collection(self::class); } public function bar() : void {} } /** * @template T as O * @template-extends ArrayObject */ class Collection extends ArrayObject { /** @var class-string */ public $class; /** @param class-string $class */ public function __construct(string $class) { $this->class = $class; } /** * @return \ArrayIterator */ public function getIterator() { /** @var ArrayIterator */ return parent::getIterator(); } } /** @return Collection */ function getFooCollection() : Collection { return new Collection(Foo::class); } foreach (getFooCollection() as $i => $foo) { $foo->bar(); } foreach (Foo::getSelfCollection() as $i => $foo) { $foo->bar(); }', ], 'iterateOverSelfImplementedIterator' => [ ' */ class FooCollection implements Iterator { private function iterate() : void { foreach ($this as $foo) {} } public function current() { return new Foo(); } public function key(): int { return 0; } public function next(): void {} public function rewind(): void {} public function valid(): bool { return false; } }', ], 'traitUse' => [ ' */ abstract function elements() : array; /** * @return T|null */ public function first() { return $this->elements()[0] ?? null; } } class Service { /** * @use CollectionTrait */ use CollectionTrait; /** * @return array */ public function elements(): array { return [1, 2, 3, 4]; } }', ], 'extendedTraitUse' => [ ' */ abstract function elements() : array; /** * @return T|null */ public function first() { return $this->elements()[0] ?? null; } } /** * @template TValue */ trait BridgeTrait { /** * @use CollectionTrait */ use CollectionTrait; } class Service { /** * @use BridgeTrait */ use BridgeTrait; /** * @return array */ public function elements(): array { return [1, 2, 3, 4]; } }', ], 'extendedTraitUseAlreadyBound' => [ ' */ abstract function elements() : array; /** * @return T|null */ public function first() { return $this->elements()[0] ?? null; } } trait BridgeTrait { /** * @use CollectionTrait */ use CollectionTrait; } class Service { use BridgeTrait; /** * @return array */ public function elements(): array { return [1, 2, 3, 4]; } }', ], 'extendClassThatParameterizesTemplatedParent' => [ ' */ abstract function elements() : array; /** * @return T|null */ public function first() { return $this->elements()[0] ?? null; } } /** * @template-extends Collection */ abstract class Bridge extends Collection {} class Service extends Bridge { /** * @return array */ public function elements(): array { return [1, 2, 3]; } } $a = (new Service)->first();', [ '$a' => 'null|int', ], ], 'splObjectStorage' => [ ' */ public $handlers; /** * @param SplObjectStorage<\stdClass, mixed> $handlers */ public function __construct(SplObjectStorage $handlers) { $this->handlers = $handlers; } } /** @var SplObjectStorage<\stdClass, string> */ $storage = new SplObjectStorage(); new SomeService($storage); $c = new \stdClass(); $storage[$c] = "hello"; $b = $storage->offsetGet($c);', [ '$b' => 'string', ], ], 'extendsArrayIterator' => [ ' */ class Users extends ArrayIterator { public function __construct(User ...$users) { parent::__construct($users); } }', ], 'genericStaticAndSelf' => [ ' */ public function map(Closure $c); } /** * @template T */ class Box implements Functor { /** * @var T */ public $value; /** * @param T $x */ public function __construct($x) { $this->value = $x; } /** * @template U * * @param Closure(T):U $c * * @return self */ public function map(Closure $c) { return new Box($c($this->value)); } }', ], 'useGenericParentMethod' => [ ' */ class Foo extends ArrayObject { public function bar() : void { $c = $this->getArrayCopy(); foreach ($c as $d) { echo $d; } } }', ], 'templateExtendsDifferentNameWithStaticCall' => [ 't = $t; } /** * @param T $t * @return static */ public static function getContainer($t) { return new static($t); } /** * @return T */ public function getValue() { return $this->t; } } /** * @template T1 as object * @template-extends Container */ class ObjectContainer extends Container {} /** * @template T2 as A * @template-extends ObjectContainer */ class AContainer extends ObjectContainer {} class A { function foo() : void {} } $b = AContainer::getContainer(new A());', [ '$b' => 'AContainer', ], ], 'templateExtendsSameNameWithStaticCall' => [ 't = $t; } /** * @param T $t * @return static */ public static function getContainer($t) { return new static($t); } /** * @return T */ public function getValue() { return $this->t; } } /** * @template T as object * @template-extends Container */ class ObjectContainer extends Container {} /** * @template T as A * @template-extends ObjectContainer */ class AContainer extends ObjectContainer {} class A { function foo() : void {} } $b = AContainer::getContainer(new A());', [ '$b' => 'AContainer', ], ], 'returnParentExtendedTemplateProperty' => [ 't = $t; } } /** * @template-extends Container */ class IntContainer extends Container { public function __construct(int $i) { parent::__construct($i); } public function getValue() : int { return $this->t; } }', ], 'childSetInConstructor' => [ 't = $t; } } /** * @template T1 as object * @template-extends Container */ class ObjectContainer extends Container {}', ], 'grandChildSetInConstructor' => [ 't = $t; } } /** * @template T1 as object * @template-extends Container */ class ObjectContainer extends Container {} /** * @template T2 as A * @template-extends ObjectContainer */ class AContainer extends ObjectContainer {} class A {}', ], 'extendArrayObjectWithTemplateParams' => [ ' */ class C extends \ArrayObject { /** * @param array $kv */ public function __construct(array $kv) { parent::__construct($kv); } } $c = new C(["a" => 1]); $i = $c->getIterator();', [ '$c' => 'C', '$i' => 'ArrayIterator', ], ], 'extendsParamCountDifference' => [ ' */ abstract class Collection implements \Iterator {} /** * @param Collection $collection * @return \Iterator */ function foo(Collection $collection) { return $collection; }', ], 'dontInheritParamTemplatedTypeSameName' => [ ' */ private $t; /** * @param array $t */ public function __construct(array $t) { $this->t = $t; } /** * @inheritdoc */ public function add($t) : void { $this->t[] = $t; } } /** @param C $c */ function foo(C $c) : void { $c->add(new stdClass); }', ], 'dontInheritParamTemplatedTypeDifferentTemplateNames' => [ ' */ private $t; /** * @param array $t */ public function __construct(array $t) { $this->t = $t; } /** * @inheritdoc */ public function add($t) : void { $this->t[] = $t; } } /** @param C $c */ function foo(C $c) : void { $c->add(new stdClass); }', ], 'templateExtendsUnionType' => [ 't = $t; } } /** * @template TT * @template-extends A */ class B extends A {}', ], 'badTemplateImplementsUnionType' => [ ' */ class B implements I { /** @var int|string */ public $t; /** @param int|string $t */ public function __construct($t) { $this->t = $t; } }', ], 'badTemplateUseUnionType' => [ 't = $t; } } /** * @template TT */ class B { /** * @template-use T */ use T; }', ], 'extendWithTooFewArgs' => [ ' */ interface Collection extends IteratorAggregate { } /** * @psalm-suppress MissingTemplateParam * @template T * @template TKey of array-key * @template-implements Collection */ class ArrayCollection implements Collection { /** * @psalm-var T[] */ private $elements; /** * @psalm-param array $elements */ public function __construct(array $elements = []) { $this->elements = $elements; } public function getIterator() { return new ArrayIterator($this->elements); } /** * @psalm-suppress MissingTemplateParam * * @psalm-param array $elements * @psalm-return ArrayCollection */ protected function createFrom(array $elements) { return new static($elements); } }', ], 'abstractGetIterator' => [ ' */ interface Collection extends \IteratorAggregate { /** * @return \Iterator */ public function getIterator(): \Iterator; } abstract class Set implements Collection { public function forEach(callable $action): void { $i = $this->getIterator(); foreach ($this as $bar) { $action($bar); } } }', ], 'paramInsideTemplatedFunctionShouldKnowRestriction' => [ ' */ class IntHasher implements Hasher { function hash($value): int { return $value % 10; } } /** * @implements Hasher */ class StringHasher implements Hasher { function hash($value): int { return strlen($value); } }', ], 'implementsAndExtendsWithTemplate' => [ ' */ function toArray(); } /** * @template TDummy * @implements Collection */ class IntCollection implements Collection { /** @param TDummy $t */ public function __construct($t) { } public function toArray() { return ["foo"]; } }', ], 'templateNotExtendedButSignatureInherited' => [ 'example("str"));' ], 'allowWiderParentType' => [ ' */ class AStringer extends Stringer { public static function getString($t, object $o = null) : string { if ($o) { return parent::getString($o); } return "a"; } }' ], 'allowTraitExtendAndImplementWithExplicitParamType' => [ 'validate($value); $this->value = $value; } /** * @psalm-param T $value * * @param $value */ abstract protected function validate($value): void; } final class StringValidator { /** * @template-use ValueObjectTrait */ use ValueObjectTrait; /** * @param string $value */ protected function validate($value): void { if (strlen($value) > 30) { throw new \Exception("bad"); } } }', ], 'allowTraitExtendAndImplementWithoutExplicitParamType' => [ 'validate($value); $this->value = $value; } /** * @psalm-param T $value * * @param $value */ abstract protected function validate($value): void; } final class StringValidator { /** * @template-use ValueObjectTrait */ use ValueObjectTrait; protected function validate($value): void { if (strlen($value) > 30) { throw new \Exception("bad"); } } }', ], 'allowPassingToCovariantCollection' => [ ' */ class Collection extends \ArrayObject { /** * @param array $kv */ public function __construct(array $kv) { parent::__construct($kv); } } /** * @param Collection $list */ function getSounds(Traversable $list) : void { foreach ($list as $l) { $l->getSound(); } } /** * @param Collection $list */ function takesDogList(Collection $list) : void { getSounds($list); // this probably should not be an error }' ], ]; } /** * @return iterable */ public function providerInvalidCodeParse() { return [ 'extendsWithUnfulfilledNonTemplate' => [ ' */ class BarContainer extends Container { /** * @return Foo */ public function getItem() { return new Foo(); } }', 'error_message' => 'ImplementedReturnTypeMismatch - src' . DIRECTORY_SEPARATOR . 'somefile.php:29:36 - 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', ], 'mismatchingTypesAfterExtends' => [ ' */ class SomeIterator implements IteratorAggregate { /** * @return Traversable */ public function getIterator() { yield new Bar; } }', 'error_message' => 'ImplementedReturnTypeMismatch', ], 'mismatchingTypesAfterExtendsInherit' => [ ' */ class SomeIterator implements IteratorAggregate { public function getIterator() { yield new Bar; } }', 'error_message' => 'InvalidReturnType', ], 'badTemplateExtends' => [ 't = $t; } } /** * @template TT * @template-extends A */ class B extends A {}', 'error_message' => 'UndefinedClass', ], 'badTemplateExtendsInt' => [ 't = $t; } } /** * @template TT * @template-extends int */ class B extends A {}', 'error_message' => 'InvalidDocblock', ], 'badTemplateExtendsBadFormat' => [ 't = $t; } } /** * @template TT * @template-extends A< > */ class B extends A {}', 'error_message' => 'InvalidDocblock', ], 'badTemplateImplementsShouldBeExtends' => [ 't = $t; } } /** * @template TT * @template-implements A */ class B extends A {}', 'error_message' => 'InvalidDocblock', ], 'badTemplateImplements' => [ ' */ class B implements I {}', 'error_message' => 'UndefinedClass', ], 'badTemplateImplementsInt' => [ ' 'InvalidDocblock', ], 'badTemplateImplementsBadFormat' => [ ' */ class B implements I {}', 'error_message' => 'InvalidDocblock', ], 'badTemplateExtendsShouldBeImplements' => [ ' */ class B implements I {}', 'error_message' => 'InvalidDocblock', ], 'badTemplateUse' => [ 't = $t; } } /** * @template TT */ class B { /** * @template-use T */ use T; }', 'error_message' => 'UndefinedClass', ], 'badTemplateUseBadFormat' => [ 't = $t; } } /** * @template TT */ class B { /** * @template-use T< > */ use T; }', 'error_message' => 'InvalidDocblock', ], 'badTemplateUseInt' => [ 't = $t; } } /** * @template TT */ class B { /** * @template-use int */ use T; }', 'error_message' => 'InvalidDocblock', ], 'badTemplateExtendsShouldBeUse' => [ 't = $t; } } /** * @template TT */ class B { /** * @template-extends T */ use T; }', 'error_message' => 'InvalidDocblock', ], 'templateExtendsWithoutAllParams' => [ ' */ class CC extends A {}', 'error_message' => 'MissingTemplateParam', ], 'templateImplementsWithoutAllParams' => [ ' */ class CC implements I {}', 'error_message' => 'MissingTemplateParam', ], 'extendsTemplateButLikeBadly' => [ ' */ class SpecializedByInheritance extends Base {}', 'error_message' => 'InvalidTemplateParam', ], 'doInheritParamTemplatedTypeSameName' => [ ' */ class C implements I { /** @var array */ private $t; /** * @param array $t */ public function __construct(array $t) { $this->t = $t; } /** * @inheritdoc */ public function add($t) : void { $this->t[] = $t; } } /** @param C $c */ function foo(C $c) : void { $c->add(new stdClass); }', 'error_message' => 'InvalidArgument', ], 'doInheritParamTemplatedTypeDifferentTemplateNames' => [ ' */ class C implements I { /** @var array */ private $t; /** * @param array $t */ public function __construct(array $t) { $this->t = $t; } /** * @inheritdoc */ public function add($t) : void { $this->t[] = $t; } } /** @param C $c */ function foo(C $c) : void { $c->add(new stdClass); }', 'error_message' => 'InvalidArgument', ], 'invalidArgumentForInheritedImplementedInterfaceMethodParam' => [ ' */ interface I2 extends I1 {} /** * @template T as array-key * @template-implements I2 */ class C implements I2 { /** @var T */ private $t; /** @param T $t */ public function __construct($t) { $this->t = $t; } public function takeT($t) : void {} } /** @param C $c */ function bar(C $c) : void { $c->takeT(new stdClass); }', 'error_message' => 'InvalidArgument', ], 'implementsAndExtendsWithoutTemplate' => [ ' */ function toArray(); } /** * @implements Collection */ class IntCollection implements Collection { function toArray() { return ["foo"]; } }', 'error_message' => 'InvalidReturnType', ], 'implementsAndExtendsWithTemplate' => [ ' */ function toArray(); } /** * @template TDummy * @implements Collection */ class IntCollection implements Collection { /** @param TDummy $t */ public function __construct($t) { } public function toArray() { return ["foo"]; } }', 'error_message' => 'InvalidReturnType', ], 'implementsChildClassWithNonExtendedTemplate' => [ 't = $t; } /** * @param T $x * @return T */ function example($x) { return $x; } } class Child extends Base { function example($x) { return $x; } } /** @param Child $c */ function bar(Child $c) : void { ord($c->example("boris")); }', 'error_message' => 'MixedArgument - src/somefile.php:31:29 - Argument 1 of ord cannot be mixed, expecting string' ], ]; } }