,error_levels?:string[]}> */ public function providerValidCodeParse(): iterable { return [ 'callStaticMethodOnTemplatedClassName' => [ ' $class */ function foo(string $class, array $args) : void { $class::bar($args); }', 'assertions' => [], 'error_levels' => ['MixedMethodCall'], ], 'returnTemplatedClassClassName' => [ ' $class * @return T|null */ public function loader(string $class) { return $class::load(); } } /** * @psalm-consistent-constructor */ class Foo { /** @return static */ public static function load() { return new static(); } } class FooChild extends Foo{} $a = (new I)->loader(FooChild::class);', 'assertions' => [ '$a' => 'FooChild|null', ], ], 'upcastIterableToTraversable' => [ ' $class */ function foo(string $class) : void { $a = new $class(); foreach ($a as $b) {} }', 'assertions' => [], 'error_levels' => ['MixedAssignment'], ], 'upcastGenericIterableToGenericTraversable' => [ ' * @param T::class $class */ function foo(string $class) : void { $a = new $class(); foreach ($a as $b) {} }', 'assertions' => [], 'error_levels' => [], ], 'understandTemplatedCalculationInOtherFunction' => [ ' [ ' $foo * * @return T * * @psalm-suppress MixedMethodCall */ function Foo(string $foo) : object { return new $foo; } echo Foo(DateTime::class)->format("c");', ], 'templatedClassStringParamAsClass' => [ ' $c_class * * @return C * @psalm-return T */ public static function get(string $c_class) : C { $c = new $c_class; $c->foo(); return $c; } } /** * @param class-string $c_class */ function bar(string $c_class) : void { $c = E::get($c_class); $c->foo(); } /** * @psalm-suppress ArgumentTypeCoercion */ function bat(string $c_class) : void { $c = E::get($c_class); $c->foo(); }', ], 'templatedClassStringParamAsObject' => [ ' $c_class * * @psalm-return T * @psalm-suppress MixedMethodCall */ public static function get(string $c_class) { return new $c_class; } } /** * @psalm-suppress ArgumentTypeCoercion */ function bat(string $c_class) : void { $c = E::get($c_class); $c->bar = "bax"; }', ], 'templatedClassStringParamMoreSpecific' => [ ' $c_class * * @return C * @psalm-return T */ public static function get(string $c_class) : C { $c = new $c_class; $c->foo(); return $c; } } /** * @param class-string $d_class */ function moreSpecific(string $d_class) : void { $d = E::get($d_class); $d->foo(); $d->faa(); }', ], 'templateFilterArrayWithIntersection' => [ ' $a * @param interface-string $type * @return array */ function filter(array $a, string $type): array { $result = []; foreach ($a as $item) { if (is_a($item, $type)) { $result[] = $item; } } return $result; } interface A {} interface B {} /** @var array */ $x = []; $y = filter($x, B::class);', [ '$y' => 'array', ], ], 'templateFilterWithIntersection' => [ ' $type * @return T&S */ function filter($item, string $type) { if (is_a($item, $type)) { return $item; }; throw new \UnexpectedValueException("bad"); } interface A {} interface B {} /** @var A */ $x = null; $y = filter($x, B::class);', [ '$y' => 'A&B', ], ], 'unionTOrClassStringTPassedClassString' => [ ' $someType * @psalm-return T * @psalm-suppress MixedMethodCall */ function getObject($someType) { if (is_object($someType)) { return $someType; } return new $someType(); } class C { function sayHello() : string { return "hi"; } } getObject(C::class)->sayHello();', ], 'unionTOrClassStringTPassedObject' => [ ' $someType * @psalm-return T * @psalm-suppress MixedMethodCall */ function getObject($someType) { if (is_object($someType)) { return $someType; } return new $someType(); } class C { function sayHello() : string { return "hi"; } } getObject(new C())->sayHello();', ], 'dontModifyByRefTemplatedArray' => [ ' $className * @param array $map * @param-out array $map * @param int $id * @return T * @psalm-suppress MixedMethodCall */ function get(string $className, array &$map, int $id) { if(!array_key_exists($id, $map)) { $map[$id] = new $className(); } return $map[$id]; } /** * @param array $mapA */ function getA(int $id, array $mapA): A { return get(A::class, $mapA, $id); } /** * @param array $mapB */ function getB(int $id, array $mapB): B { return get(B::class, $mapB, $id); }', ], 'unionClassStringTWithTReturnsObjectWhenCoerced' => [ ' $s * @return T * @psalm-suppress MixedMethodCall */ function bar($s) { if (is_object($s)) { return $s; } return new $s(); } function foo(string $s) : object { /** @psalm-suppress ArgumentTypeCoercion */ return bar($s); }', ], 'allowTemplatedIntersectionFirst' => [ ' $className * @psalm-return RequestedType&MockObject * @psalm-suppress MixedInferredReturnType * @psalm-suppress MixedReturnStatement */ function mock(string $className) { eval(\'"there be dragons"\'); return $instance; } class A { public function foo() : void {} } /** * @psalm-template UnknownType * @psalm-param class-string $className */ function useMockTemplated(string $className) : void { mock($className)->checkExpectations(); } mock(A::class)->foo();', ], 'allowTemplatedIntersectionFirstTemplatedMock' => [ ' $className * @psalm-return RequestedType&MockObject * @psalm-suppress MixedInferredReturnType * @psalm-suppress MixedReturnStatement */ function mock(string $className) { eval(\'"there be dragons"\'); return $instance; } class A { public function foo() : void {} } /** * @psalm-template UnknownType * @psalm-param class-string $className */ function useMockTemplated(string $className) : void { mock($className)->checkExpectations(); } mock(A::class)->foo();', ], 'allowTemplatedIntersectionSecond' => [ ' $className * @psalm-return MockObject&RequestedType * @psalm-suppress MixedInferredReturnType * @psalm-suppress MixedReturnStatement */ function mock(string $className) { eval(\'"there be dragons"\'); return $instance; } class A { public function foo() : void {} } /** * @psalm-param class-string $className */ function useMock(string $className) : void { mock($className)->checkExpectations(); } /** * @psalm-template UnknownType * @psalm-param class-string $className */ function useMockTemplated(string $className) : void { mock($className)->checkExpectations(); } mock(A::class)->foo();', ], 'returnClassString' => [ ' [ '|class-string $type * @return T1|T2 */ function f(string $type) { return new $type(); } f(A::class); f(B::class);', ], 'SKIPPED-compareToExactClassString' => [ ' */ private $typeName; /** * @param class-string $typeName */ public function __construct(string $typeName) { $this->typeName = $typeName; } /** * @param mixed $value * @return T */ public function cast($value) { if (is_object($value) && get_class($value) === $this->typeName) { return $value; } throw new RuntimeException(); } }', ], 'compareGetClassTypeString' => [ ' $typeName * @param mixed $value * @return T */ function cast($value, string $typeName) { if (is_object($value) && get_class($value) === $typeName) { return $value; } throw new RuntimeException(); }', ], 'instanceofTemplatedClassStringOnMixed' => [ ' $fooClass * @param mixed $foo * @return T */ function get($fooClass, $foo) { if ($foo instanceof $fooClass) { return $foo; } throw new \Exception(); }', ], 'instanceofTemplatedClassStringOnObjectType' => [ ' $fooClass * @return T */ function get($fooClass, Foo $foo) { if ($foo instanceof $fooClass) { return $foo; } throw new \Exception(); }', ], 'templateFromDifferentClassStrings' => [ ' $a1 * @param class-string $a2 * @return T */ function test(string $a1, string $a2) { if (rand(0, 1)) return new $a1(); return new $a2(); } $b_or_c = test(B::class, C::class);', [ '$b_or_c' => 'B|C', ] ], 'allowComparisonWithoutCrash' => [ ' [ ' $bars * @psalm-param class-string $class * @return array */ function getBarsThatAreInstancesOf(array $bars, string $class): array { return \array_filter( $bars, function (Bar $bar) use ($class): bool { return $bar instanceof $class; } ); } interface Bar {}' ], ]; } /** * @return iterable */ public function providerInvalidCodeParse(): iterable { return [ 'copyScopedClassInFunction' => [ ' $foo */ function Foo(string $foo) : string { return $foo; }', 'error_message' => 'ReservedWord', ], 'copyScopedClassInNamespacedFunction' => [ ' $foo */ function Foo(string $foo) : string { return $foo; }', 'error_message' => 'ReservedWord', ], 'constrainTemplateTypeWhenClassStringUsed' => [ ' $type * @psalm-return T */ public function getObject(string $type) { return 3; } }', 'error_message' => 'InvalidReturnStatement', ], 'forbidLossOfInformationWhenCoercing' => [ ' * @param T::class $class */ function foo(string $class) : void {} function bar(Traversable $t) : void { foo(get_class($t)); }', 'error_message' => 'MixedArgumentTypeCoercion', ], 'templateAsUnionClassStringPassingInvalidClass' => [ '|class-string $type * @return T1|T2 */ function f(string $type) { return new $type(); } f(C::class);', 'error_message' => 'InvalidArgument', ], 'bindToClassString' => [ ' $className * @param TClass $realInstance * * @return Closure(TClass) : void * @psalm-suppress InvalidReturnType */ function createInitializer(string $className, object $realInstance) : Closure {} function foo(object $realInstance) : void { $className = get_class($realInstance); /** @psalm-trace $i */ $i = createInitializer($className, $realInstance); }', 'error_message' => 'Closure(object):void' ], 'preventClassStringInPlaceOfTemplatedClassString' => [ ' $instance * @return T */ public function create(string $instance) : object; } /** @param FileManager $m */ function foo(FileManager $m) : void { $m->create(MusicFile::class); }', 'error_message' => 'InvalidArgument' ], ]; } }