1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-13 01:37:23 +01:00
psalm/tests/Template/FunctionClassStringTemplateTest.php
2021-12-04 21:55:53 +01:00

839 lines
28 KiB
PHP

<?php
namespace Psalm\Tests\Template;
use Psalm\Tests\TestCase;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
class FunctionClassStringTemplateTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
/**
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
*/
public function providerValidCodeParse(): iterable
{
return [
'callStaticMethodOnTemplatedClassName' => [
'<?php
/**
* @template T
* @param class-string<T> $class
*/
function foo(string $class, array $args) : void {
$class::bar($args);
}',
'assertions' => [],
'error_levels' => ['MixedMethodCall'],
],
'returnTemplatedClassClassName' => [
'<?php
class I {
/**
* @template T as Foo
* @param class-string<T> $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' => [
'<?php
/**
* @template T as iterable
* @param class-string<T> $class
*/
function foo(string $class) : void {
$a = new $class();
foreach ($a as $b) {}
}',
'assertions' => [],
'error_levels' => ['MixedAssignment'],
],
'upcastGenericIterableToGenericTraversable' => [
'<?php
/**
* @template T as iterable<int>
* @param T::class $class
*/
function foo(string $class) : void {
$a = new $class();
foreach ($a as $b) {}
}',
'assertions' => [],
'error_levels' => [],
],
'understandTemplatedCalculationInOtherFunction' => [
'<?php
/**
* @template T as Exception
* @param T::class $type
* @return T
* @psalm-suppress UnsafeInstantiation
*/
function a(string $type): Exception {
return new $type;
}
/**
* @template T as InvalidArgumentException
* @param T::class $type
* @return T
*/
function b(string $type): InvalidArgumentException {
return a($type);
}',
],
'objectReturn' => [
'<?php
/**
* @template T as object
*
* @param class-string<T> $foo
*
* @return T
*
* @psalm-suppress MixedMethodCall
*/
function Foo(string $foo) : object {
return new $foo;
}
echo Foo(DateTime::class)->format("c");',
],
'templatedClassStringParamAsClass' => [
'<?php
/**
* @psalm-consistent-constructor
*/
abstract class C {
public function foo() : void{}
}
class E {
/**
* @template T as C
* @param class-string<T> $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> $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' => [
'<?php
/**
* @psalm-consistent-constructor
*/
abstract class C {
public function foo() : void{}
}
class E {
/**
* @template T as object
* @param class-string<T> $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' => [
'<?php
/**
* @psalm-consistent-constructor
*/
abstract class C {
public function foo() : void{}
}
class D extends C {
public function faa() : void{}
}
class E {
/**
* @template T as C
* @param class-string<T> $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> $d_class
*/
function moreSpecific(string $d_class) : void {
$d = E::get($d_class);
$d->foo();
$d->faa();
}',
],
'templateFilterArrayWithIntersection' => [
'<?php
/**
* @template T as object
* @template S as object
* @param array<T> $a
* @param interface-string<S> $type
* @return array<T&S>
*/
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<A> */
$x = [];
$y = filter($x, B::class);',
[
'$y' => 'array<array-key, A&B>',
],
],
'templateFilterWithIntersection' => [
'<?php
/**
* @template T as object
* @template S as object
* @param T $item
* @param interface-string<S> $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' => [
'<?php
/**
* @psalm-template T of object
* @psalm-param T|class-string<T> $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' => [
'<?php
/**
* @psalm-template T of object
* @psalm-param T|class-string<T> $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' => [
'<?php
class A {}
class B {}
/**
* @template T of object
* @param class-string<T> $className
* @param array<T> $map
* @param-out array<T> $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<A> $mapA
*/
function getA(int $id, array $mapA): A {
return get(A::class, $mapA, $id);
}
/**
* @param array<B> $mapB
*/
function getB(int $id, array $mapB): B {
return get(B::class, $mapB, $id);
}',
],
'unionClassStringTWithTReturnsObjectWhenCoerced' => [
'<?php
/**
* @template T as object
* @param T|class-string<T> $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' => [
'<?php
class MockObject
{
public function checkExpectations() : void
{
}
}
/**
* @psalm-template RequestedType
* @psalm-param class-string<RequestedType> $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<UnknownType> $className
*/
function useMockTemplated(string $className) : void
{
mock($className)->checkExpectations();
}
mock(A::class)->foo();',
],
'allowTemplatedIntersectionFirstTemplatedMock' => [
'<?php
class MockObject
{
public function checkExpectations() : void
{
}
}
/**
* @psalm-template RequestedType
* @psalm-param class-string<RequestedType> $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<UnknownType> $className
*/
function useMockTemplated(string $className) : void
{
mock($className)->checkExpectations();
}
mock(A::class)->foo();',
],
'allowTemplatedIntersectionSecond' => [
'<?php
class MockObject
{
public function checkExpectations() : void
{
}
}
/**
* @psalm-template RequestedType
* @psalm-param class-string<RequestedType> $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<UnknownType> $className
*/
function useMockTemplated(string $className) : void
{
mock($className)->checkExpectations();
}
mock(A::class)->foo();',
],
'returnClassString' => [
'<?php
/**
* @template T
* @param T::class $s
* @return T::class
*/
function foo(string $s) : string {
return $s;
}
/**
* @param A::class $s
*/
function bar(string $s) : void {
}
class A {}
bar(foo(A::class));',
],
'templateAsUnionClassStringPassingValidClass' => [
'<?php
/**
* @psalm-consistent-constructor
*/
class A {}
/**
* @psalm-consistent-constructor
*/
class B {}
/**
* @template T1 as A
* @template T2 as B
* @param class-string<T1>|class-string<T2> $type
* @return T1|T2
*/
function f(string $type) {
return new $type();
}
f(A::class);
f(B::class);',
],
'compareToExactClassString' => [
'<?php
/**
* @template T as object
*/
class Type
{
/** @var class-string<T> */
private $typeName;
/**
* @param class-string<T> $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' => [
'<?php
/**
* @template T
* @param class-string<T> $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' => [
'<?php
interface Foo {}
/**
* @template T as Foo
* @param class-string<T> $fooClass
* @param mixed $foo
* @return T
*/
function get($fooClass, $foo) {
if ($foo instanceof $fooClass) {
return $foo;
}
throw new \Exception();
}',
],
'instanceofTemplatedClassStringOnObjectType' => [
'<?php
interface Foo {}
/**
* @template T as Foo
* @param class-string<T> $fooClass
* @return T
*/
function get($fooClass, Foo $foo) {
if ($foo instanceof $fooClass) {
return $foo;
}
throw new \Exception();
}',
],
'templateFromDifferentClassStrings' => [
'<?php
/**
* @psalm-consistent-constructor
*/
class A {}
class B extends A {}
class C extends A {}
/**
* @template T of A
* @param class-string<T> $a1
* @param class-string<T> $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' => [
'<?php
/**
* @template T as object
*
* @param T::class $e
* @param T::class $expected
*/
function bar(string $e, string $expected) : void {
if ($e !== $expected) {}
}',
],
'refineByArrayFilterIntersection' => [
'<?php
/**
* @template T
* @param array<Bar> $bars
* @psalm-param class-string<T> $class
* @return array<T&Bar>
*/
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<string,array{string,error_message:string,1?:string[],2?:bool,3?:string}>
*/
public function providerInvalidCodeParse(): iterable
{
return [
'copyScopedClassInFunction' => [
'<?php
/**
* @template Throwable as DOMNode
*
* @param class-string<Throwable> $foo
*/
function Foo(string $foo) : string {
return $foo;
}',
'error_message' => 'ReservedWord',
],
'copyScopedClassInNamespacedFunction' => [
'<?php
namespace Foo;
class Bar {}
/**
* @template Bar as DOMNode
*
* @param class-string<Bar> $foo
*/
function Foo(string $foo) : string {
return $foo;
}',
'error_message' => 'ReservedWord',
],
'constrainTemplateTypeWhenClassStringUsed' => [
'<?php
class GenericObjectFactory {
/**
* @psalm-template T
* @psalm-param class-string<T> $type
* @psalm-return T
*/
public function getObject(string $type)
{
return 3;
}
}',
'error_message' => 'InvalidReturnStatement',
],
'forbidLossOfInformationWhenCoercing' => [
'<?php
/**
* @template T as iterable<int>
* @param T::class $class
*/
function foo(string $class) : void {}
function bar(Traversable $t) : void {
foo(get_class($t));
}',
'error_message' => 'MixedArgumentTypeCoercion',
],
'templateAsUnionClassStringPassingInvalidClass' => [
'<?php
/**
* @psalm-consistent-constructor
*/
class A {}
/**
* @psalm-consistent-constructor
*/
class B {}
class C {}
/**
* @template T1 as A
* @template T2 as B
* @param class-string<T1>|class-string<T2> $type
* @return T1|T2
*/
function f(string $type) {
return new $type();
}
f(C::class);',
'error_message' => 'InvalidArgument',
],
'bindToClassString' => [
'<?php
/**
* @template TClass as object
*
* @param class-string<TClass> $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' => [
'<?php
class ImageFile {}
class MusicFile {}
/**
* @template T of object
*/
interface FileManager {
/**
* @param class-string<T> $instance
* @return T
*/
public function create(string $instance) : object;
}
/** @param FileManager<ImageFile> $m */
function foo(FileManager $m) : void {
$m->create(MusicFile::class);
}',
'error_message' => 'InvalidArgument'
],
];
}
}