diff --git a/tests/Template/TemplateExtendsTest.php b/tests/Template/ClassTemplateExtendsTest.php similarity index 99% rename from tests/Template/TemplateExtendsTest.php rename to tests/Template/ClassTemplateExtendsTest.php index e39699b2f..2814ca738 100644 --- a/tests/Template/TemplateExtendsTest.php +++ b/tests/Template/ClassTemplateExtendsTest.php @@ -4,7 +4,7 @@ namespace Psalm\Tests\Template; use Psalm\Tests\TestCase; use Psalm\Tests\Traits; -class TemplateExtendsTest extends TestCase +class ClassTemplateExtendsTest extends TestCase { use Traits\InvalidCodeAnalysisTestTrait; use Traits\ValidCodeAnalysisTestTrait; diff --git a/tests/Template/TemplateTest.php b/tests/Template/ClassTemplateTest.php similarity index 56% rename from tests/Template/TemplateTest.php rename to tests/Template/ClassTemplateTest.php index 1af096968..0c8798a9e 100644 --- a/tests/Template/TemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -4,7 +4,7 @@ namespace Psalm\Tests\Template; use Psalm\Tests\TestCase; use Psalm\Tests\Traits; -class TemplateTest extends TestCase +class ClassTemplateTest extends TestCase { use Traits\InvalidCodeAnalysisTestTrait; use Traits\ValidCodeAnalysisTestTrait; @@ -234,40 +234,6 @@ class TemplateTest extends TestCase ], 'error_levels' => ['MixedOperand'], ], - 'validTemplatedType' => [ - ' [ - ' [ 'bar(new A());', ], - 'validTemplatedStaticMethodType' => [ - ' [ - 'foo("string"));', - ], - 'genericArrayKeys' => [ - ' $arr - * @return array - */ - function my_array_keys($arr) { - return array_keys($arr); - } - - $a = my_array_keys(["hello" => 5, "goodbye" => new \Exception()]);', - 'assertions' => [ - '$a' => 'array', - ], - ], - 'genericArrayFlip' => [ - ' $arr - * @return array - */ - function my_array_flip($arr) { - return array_flip($arr); - } - - $b = my_array_flip(["hello" => 5, "goodbye" => 6]);', - 'assertions' => [ - '$b' => 'array', - ], - ], - 'byRefKeyValueArray' => [ - ' $arr - */ - function byRef(array &$arr) : void {} - - $b = ["a" => 5, "c" => 6]; - byRef($b);', - 'assertions' => [ - '$b' => 'array', - ], - ], - 'byRefMixedKeyArray' => [ - ' $arr - */ - function byRef(array &$arr) : void {} - - $b = ["a" => 5, "c" => 6]; - byRef($b);', - 'assertions' => [ - '$b' => 'array', - ], - ], - 'mixedArrayPop' => [ - ' $arr - * @return TValue|null - */ - function my_array_pop(array &$arr) { - return array_pop($arr); - } - - /** @var mixed */ - $b = ["a" => 5, "c" => 6]; - $a = my_array_pop($b);', - 'assertions' => [ - '$a' => 'mixed', - '$b' => 'array', - ], - 'error_levels' => ['MixedAssignment', 'MixedArgument'], - ], - 'genericArrayPop' => [ - ' $arr - * @return TValue|null - */ - function my_array_pop(array &$arr) { - return array_pop($arr); - } - - $b = ["a" => 5, "c" => 6]; - $a = my_array_pop($b);', - 'assertions' => [ - '$a' => 'null|int', - '$b' => 'array', - ], - ], 'intersectionTemplatedTypes' => [ ' [ - ' [ - ' [ 'map(function(Item $i): Item { return $i;})); takesCollectionOfItems($c->map(function(Item $i): Item { return $i;}));', ], - 'replaceChildTypeWithGenerator' => [ - ' $t - * @return array - */ - function f(Traversable $t): array { - $ret = []; - foreach ($t as $k => $v) $ret[$k] = $v; - return $ret; - } - - /** @return Generator */ - function g():Generator { yield new stdClass; } - - takesArrayOfStdClass(f(g())); - - /** @param array $p */ - function takesArrayOfStdClass(array $p): void {}', - ], 'noRepeatedTypeException' => [ ' [ - ' $arr - * @param array $arr2 - * @return array - */ - function splat_proof(array $arr, array $arr2) { - return $arr; - } - - $foo = [ - [1, 2, 3], - [1, 2], - ]; - - $a = splat_proof(...$foo);', - 'assertions' => [ - '$a' => 'array', - ], - ], - 'passArrayByRef' => [ - ' $_arr - * @return null|TValue - * @psalm-ignore-nullable-return - */ - function fRef(array &$_arr) { - return array_shift($_arr); - } - - /** - * @template TKey as array-key - * @template TValue - * - * @param array $_arr - * @return null|TValue - * @psalm-ignore-nullable-return - */ - function fNoRef(array $_arr) { - return array_shift($_arr); - }', - ], 'templatedInterfaceIteration' => [ 'add(new A);', ], - 'classTemplateAsCorrect' => [ - ' [ - ' [ - ' [ 'getFoo(); }', ], - 'templateFunctionVar' => [ - 'bar(); - - /** @var T&D */ - $b = $some_t; - $b->bar(); - - /** @var D&T */ - $b = $some_t; - $b->bar(); - - return $a; - }', - 'assertions' => [], - 'error_levels' => ['MixedAssignment', 'MissingParamType'], - ], - 'returnClassString' => [ - ' [ - ' [], - 'error_levels' => ['MixedMethodCall'], - ], - 'returnTemplatedClassClassName' => [ - ' $class - * @return T|null - */ - public function loader(string $class) { - return $class::load(); - } - } - - class Foo { - /** @return static */ - public static function load() { - return new static(); - } - } - - class FooChild extends Foo{} - - $a = (new I)->loader(FooChild::class);', - 'assertions' => [ - '$a' => 'null|FooChild', - ], - ], - 'upcastIterableToTraversable' => [ - ' [], - 'error_levels' => ['MixedAssignment'], - ], - 'upcastGenericIterableToGenericTraversable' => [ - ' - * @param T::class $class - */ - function foo(string $class) : void { - $a = new $class(); - - foreach ($a as $b) {} - }', - 'assertions' => [], - 'error_levels' => [], - ], - 'bindFirstTemplatedClosureParameter' => [ - ' [ ' ['MixedAssignment'], ], - 'doesntExtendTemplateAndDoesNotOverride' => [ ' 'array-key', ], ], - - 'callableReturnsItself' => [ - ' [ - ' [ - 'getIterator();', - [ - '$i' => 'Traversable', - ], - ], - 'upcastArrayToIterable' => [ - ' $collection - * @return V - * @psalm-suppress InvalidReturnType - */ - function first($collection) {} - - $one = first([1,2,3]);', - [ - '$one' => 'int', - ], - ], 'templateObjectLikeValues' => [ ' 'Collection', ], ], - 'understandTemplatedCalculationInOtherFunction' => [ - ' [ ' 'string', ], ], - 'objectReturn' => [ - ' $foo - * - * @return T - */ - function Foo(string $foo) : object { - return new $foo; - } - - echo Foo(DateTime::class)->format("c");', - ], - 'templateIntersectionLeft' => [ - ' [ - ' [ - '|TReturn) $gen - * @return array - */ - function call(callable $gen) : array { - $return = $gen(); - if ($return instanceof Generator) { - return [$gen->getReturn()]; - } - return [$gen]; - } - - $arr = call( - /** - * @return Generator - */ - function() { - yield 1; - return "hello"; - } - );', - [ - '$arr' => 'array', - ], - ], - '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 TypeCoercion - */ - function bat(string $c_class) : void { - $c = E::get($c_class); - $c->foo(); - }', - ], - 'templatedClassStringParamAsObject' => [ - ' $c_class - * - * @psalm-return T - */ - public static function get(string $c_class) { - return new $c_class; - } - } - - /** - * @psalm-suppress TypeCoercion - */ - 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(); - }', - ], - 'templateOfWithSpace' => [ - ' - */ - class Foo - { - } - - /** - * @param Foo> $a - */ - function bar(Foo $a) : void {}', - ], 'templateDefaultSimpleString' => [ ' [ - ' $y - */ - function example($x, $y): void {} - - example( - /** - * @param int|false $x - */ - function($x): void {}, - [strpos("str", "str")] - );', - ], 'reflectionClass' => [ ' [ - ' - */ - $b = [1, 2, 3]; - takesArray($b);', - ], 'ignoreTooManyGenericObjectArgs' => [ ' $c */ function bar(C $c) : void {}', ], - 'functionTemplateUnionType' => [ - ' [ - '$s' => 'string', - '$i' => 'int', - ], - ], 'unionAsTypeReturnType' => [ ' [ - 'getIterator(); - $t = $a; - } - - if (!$t instanceof Iterator) { - return; - } - - if (rand(0, 1) && rand(0, 1)) { - $t->next(); - } - }', - ], - 'templateArrayIntersection' => [ - ' $a - * @param class-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', - ] - ], 'templateEmptyParamCoercion' => [ ' [ - ' - * - * @param T $o - * @param K $name - * - * @return T[K] - */ - function getOffset(array $o, $name) { - return $o[$name]; - } - - $a = ["foo" => "hello", "bar" => 2]; - - $b = getOffset($a, "foo"); - $c = getOffset($a, "bar");', - [ - '$b' => 'string', - '$c' => 'int', - ] - ], 'keyOfClassTemplateAcceptingIndexedAccess' => [ ' [ - ' $someType - * @psalm-return T - */ - 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 - */ - function getObject($someType) { - if (is_object($someType)) { - return $someType; - } - - return new $someType(); - } - - class C { - function sayHello() : string { - return "hi"; - } - } - - getObject(new C())->sayHello();' - ], 'SKIPPED-templatedInterfaceIntersectionFirst' => [ ' 'C&I' ] ], - 'dontModifyByRefTemplatedArray' => [ - ' $className - * @param array $map - * @param-out array $map - * @param int $id - * @return T - */ - 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); - }' - ], - 'dontGeneraliseBoundParamWithWiderCallable' => [ - ' 'C', - ] - ], - 'unionClassStringTWithTReturnsObjectWhenCoerced' => [ - ' $s - * @return T - */ - function bar($s) { - if (is_object($s)) { - return $s; - } - - return new $s(); - } - - function foo(string $s) : object { - /** @psalm-suppress ArgumentTypeCoercion */ - return bar($s); - }' - ], 'keyOfArrayGet' => [ ' [ - ' $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();' - ], - 'allowTemplateTypeBeingUsedInsideFunction' => [ - ' [ - ' [ - ' 'InvalidScalarArgument', - ], - 'invalidTemplatedStaticMethodType' => [ - ' 'InvalidScalarArgument', - ], - 'invalidTemplatedInstanceMethodType' => [ - 'foo(4));', - 'error_message' => 'InvalidScalarArgument', - ], - 'replaceChildTypeNoHint' => [ - ' $t - * @return array - */ - function f(Traversable $t): array { - $ret = []; - foreach ($t as $k => $v) $ret[$k] = $v; - return $ret; - } - - function g():Generator { yield new stdClass; } - - takesArrayOfStdClass(f(g())); - - /** @param array $p */ - function takesArrayOfStdClass(array $p): void {}', - 'error_message' => 'MixedArgumentTypeCoercion', - ], 'restrictTemplateInputWithClassString' => [ ' 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:20:34 - Argument 1 of type expects string, callable(State):(T as mixed)&Foo provided', ], - 'classTemplateAsIncorrectClass' => [ - ' 'InvalidArgument', - ], - 'classTemplateAsIncorrectInterface' => [ - ' 'InvalidArgument', - ], - 'templateFunctionMethodCallWithoutMethod' => [ - 'bar(); - }', - 'error_message' => 'PossiblyUndefinedMethod', - ], - 'templateFunctionMethodCallWithoutAsType' => [ - 'bar(); - }', - 'error_message' => 'MixedMethodCall', - ], - 'forbidLossOfInformationWhenCoercing' => [ - ' - * @param T::class $class - */ - function foo(string $class) : void {} - - function bar(Traversable $t) : void { - foo(get_class($t)); - }', - 'error_message' => 'MixedArgumentTypeCoercion', - ], - 'bindFirstTemplatedClosureParameter' => [ - ' 'InvalidScalarArgument', - ], - 'bindFirstTemplatedClosureParameterTypeCoercion' => [ - ' 'ArgumentTypeCoercion', - ], - - 'callableDoesNotReturnItself' => [ - ' 'InvalidScalarArgument', - ], - 'multipleArgConstraintWithMoreRestrictiveFirstArg' => [ - ' 'ArgumentTypeCoercion', - ], - 'multipleArgConstraintWithMoreRestrictiveSecondArg' => [ - ' 'ArgumentTypeCoercion', - ], - 'multipleArgConstraintWithLessRestrictiveThirdArg' => [ - ' 'ArgumentTypeCoercion', - ], - 'possiblyInvalidArgumentWithUnionFirstArg' => [ - ' 'PossiblyInvalidArgument', - ], - 'possiblyInvalidArgumentWithUnionSecondArg' => [ - ' 'PossiblyInvalidArgument', - ], 'templateWithNoReturn' => [ 'add(5, []);', 'error_message' => 'InvalidArgument', ], - 'copyScopedClassInFunction' => [ - ' $foo - */ - function Foo(string $foo) : string { - return $foo; - }', - 'error_message' => 'ReservedWord', - ], - 'copyScopedClassInNamespacedFunction' => [ - ' $foo - */ - function Foo(string $foo) : string { - return $foo; - }', - 'error_message' => 'ReservedWord', - ], 'copyScopedClassInNamespacedClass' => [ 'ame = "Luigi";', 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:47:29 - Argument 1 of CharacterRow::__set expects string(id)|string(name)|string(height), string(ame) provided', ], - 'constrainTemplateTypeWhenClassStringUsed' => [ - ' $type - * @psalm-return T - */ - public function getObject(string $type) - { - return 3; - } - }', - 'error_message' => 'InvalidReturnStatement' - ], - 'preventTemplateTypeAsBeingUsedInsideFunction' => [ - ' 'InvalidArgument' - ], - 'preventWrongTemplateBeingPassed' => [ - ' 'InvalidArgument' - ], - 'preventTemplateTypeReturnMoreGeneral' => [ - ' 'InvalidReturnStatement' - ], ]; } } diff --git a/tests/Template/FunctionTemplateTest.php b/tests/Template/FunctionTemplateTest.php new file mode 100644 index 000000000..c91497814 --- /dev/null +++ b/tests/Template/FunctionTemplateTest.php @@ -0,0 +1,1578 @@ +,error_levels?:string[]}> + */ + public function providerValidCodeParse() + { + return [ + 'validTemplatedType' => [ + ' [ + ' [ + ' [ + 'foo("string"));', + ], + 'genericArrayKeys' => [ + ' $arr + * @return array + */ + function my_array_keys($arr) { + return array_keys($arr); + } + + $a = my_array_keys(["hello" => 5, "goodbye" => new \Exception()]);', + 'assertions' => [ + '$a' => 'array', + ], + ], + 'genericArrayFlip' => [ + ' $arr + * @return array + */ + function my_array_flip($arr) { + return array_flip($arr); + } + + $b = my_array_flip(["hello" => 5, "goodbye" => 6]);', + 'assertions' => [ + '$b' => 'array', + ], + ], + 'byRefKeyValueArray' => [ + ' $arr + */ + function byRef(array &$arr) : void {} + + $b = ["a" => 5, "c" => 6]; + byRef($b);', + 'assertions' => [ + '$b' => 'array', + ], + ], + 'byRefMixedKeyArray' => [ + ' $arr + */ + function byRef(array &$arr) : void {} + + $b = ["a" => 5, "c" => 6]; + byRef($b);', + 'assertions' => [ + '$b' => 'array', + ], + ], + 'mixedArrayPop' => [ + ' $arr + * @return TValue|null + */ + function my_array_pop(array &$arr) { + return array_pop($arr); + } + + /** @var mixed */ + $b = ["a" => 5, "c" => 6]; + $a = my_array_pop($b);', + 'assertions' => [ + '$a' => 'mixed', + '$b' => 'array', + ], + 'error_levels' => ['MixedAssignment', 'MixedArgument'], + ], + 'genericArrayPop' => [ + ' $arr + * @return TValue|null + */ + function my_array_pop(array &$arr) { + return array_pop($arr); + } + + $b = ["a" => 5, "c" => 6]; + $a = my_array_pop($b);', + 'assertions' => [ + '$a' => 'null|int', + '$b' => 'array', + ], + ], + 'templateCallableReturnType' => [ + ' [ + ' [ + ' $t + * @return array + */ + function f(Traversable $t): array { + $ret = []; + foreach ($t as $k => $v) $ret[$k] = $v; + return $ret; + } + + /** @return Generator */ + function g():Generator { yield new stdClass; } + + takesArrayOfStdClass(f(g())); + + /** @param array $p */ + function takesArrayOfStdClass(array $p): void {}', + ], + + 'splatTemplateParam' => [ + ' $arr + * @param array $arr2 + * @return array + */ + function splat_proof(array $arr, array $arr2) { + return $arr; + } + + $foo = [ + [1, 2, 3], + [1, 2], + ]; + + $a = splat_proof(...$foo);', + 'assertions' => [ + '$a' => 'array', + ], + ], + 'passArrayByRef' => [ + ' $_arr + * @return null|TValue + * @psalm-ignore-nullable-return + */ + function fRef(array &$_arr) { + return array_shift($_arr); + } + + /** + * @template TKey as array-key + * @template TValue + * + * @param array $_arr + * @return null|TValue + * @psalm-ignore-nullable-return + */ + function fNoRef(array $_arr) { + return array_shift($_arr); + }', + ], + + 'classTemplateAsCorrect' => [ + ' [ + ' [ + ' [ + 'bar(); + + /** @var T&D */ + $b = $some_t; + $b->bar(); + + /** @var D&T */ + $b = $some_t; + $b->bar(); + + return $a; + }', + 'assertions' => [], + 'error_levels' => ['MixedAssignment', 'MissingParamType'], + ], + 'returnClassString' => [ + ' [ + ' [], + 'error_levels' => ['MixedMethodCall'], + ], + 'returnTemplatedClassClassName' => [ + ' $class + * @return T|null + */ + public function loader(string $class) { + return $class::load(); + } + } + + class Foo { + /** @return static */ + public static function load() { + return new static(); + } + } + + class FooChild extends Foo{} + + $a = (new I)->loader(FooChild::class);', + 'assertions' => [ + '$a' => 'null|FooChild', + ], + ], + 'upcastIterableToTraversable' => [ + ' [], + 'error_levels' => ['MixedAssignment'], + ], + 'upcastGenericIterableToGenericTraversable' => [ + ' + * @param T::class $class + */ + function foo(string $class) : void { + $a = new $class(); + + foreach ($a as $b) {} + }', + 'assertions' => [], + 'error_levels' => [], + ], + 'bindFirstTemplatedClosureParameter' => [ + ' [ + ' [ + ' [ + 'getIterator();', + [ + '$i' => 'Traversable', + ], + ], + 'upcastArrayToIterable' => [ + ' $collection + * @return V + * @psalm-suppress InvalidReturnType + */ + function first($collection) {} + + $one = first([1,2,3]);', + [ + '$one' => 'int', + ], + ], + 'understandTemplatedCalculationInOtherFunction' => [ + ' [ + ' $foo + * + * @return T + */ + function Foo(string $foo) : object { + return new $foo; + } + + echo Foo(DateTime::class)->format("c");', + ], + 'templateIntersectionLeft' => [ + ' [ + ' [ + '|TReturn) $gen + * @return array + */ + function call(callable $gen) : array { + $return = $gen(); + if ($return instanceof Generator) { + return [$gen->getReturn()]; + } + return [$gen]; + } + + $arr = call( + /** + * @return Generator + */ + function() { + yield 1; + return "hello"; + } + );', + [ + '$arr' => 'array', + ], + ], + '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 TypeCoercion + */ + function bat(string $c_class) : void { + $c = E::get($c_class); + $c->foo(); + }', + ], + 'templatedClassStringParamAsObject' => [ + ' $c_class + * + * @psalm-return T + */ + public static function get(string $c_class) { + return new $c_class; + } + } + + /** + * @psalm-suppress TypeCoercion + */ + 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(); + }', + ], + 'templateOfWithSpace' => [ + ' + */ + class Foo + { + } + + /** + * @param Foo> $a + */ + function bar(Foo $a) : void {}', + ], + 'allowUnionTypeParam' => [ + ' $y + */ + function example($x, $y): void {} + + example( + /** + * @param int|false $x + */ + function($x): void {}, + [strpos("str", "str")] + );', + ], + 'ignoreTooManyArrayArgs' => [ + ' + */ + $b = [1, 2, 3]; + takesArray($b);', + ], + 'functionTemplateUnionType' => [ + ' [ + '$s' => 'string', + '$i' => 'int', + ], + ], + 'reconcileTraversableTemplatedAndNormal' => [ + 'getIterator(); + $t = $a; + } + + if (!$t instanceof Iterator) { + return; + } + + if (rand(0, 1) && rand(0, 1)) { + $t->next(); + } + }', + ], + 'templateArrayIntersection' => [ + ' $a + * @param class-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', + ] + ], + 'keyOfTemplate' => [ + ' + * + * @param T $o + * @param K $name + * + * @return T[K] + */ + function getOffset(array $o, $name) { + return $o[$name]; + } + + $a = ["foo" => "hello", "bar" => 2]; + + $b = getOffset($a, "foo"); + $c = getOffset($a, "bar");', + [ + '$b' => 'string', + '$c' => 'int', + ] + ], + 'unionTOrClassStringTPassedClassString' => [ + ' $someType + * @psalm-return T + */ + 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 + */ + 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 + */ + 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); + }' + ], + 'dontGeneraliseBoundParamWithWiderCallable' => [ + ' 'C', + ] + ], + 'unionClassStringTWithTReturnsObjectWhenCoerced' => [ + ' $s + * @return T + */ + 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();' + ], + 'allowTemplateTypeBeingUsedInsideFunction' => [ + ' [ + ' + */ + public function providerInvalidCodeParse() + { + return [ + 'invalidTemplatedType' => [ + ' 'InvalidScalarArgument', + ], + 'invalidTemplatedStaticMethodType' => [ + ' 'InvalidScalarArgument', + ], + 'invalidTemplatedInstanceMethodType' => [ + 'foo(4));', + 'error_message' => 'InvalidScalarArgument', + ], + 'replaceChildTypeNoHint' => [ + ' $t + * @return array + */ + function f(Traversable $t): array { + $ret = []; + foreach ($t as $k => $v) $ret[$k] = $v; + return $ret; + } + + function g():Generator { yield new stdClass; } + + takesArrayOfStdClass(f(g())); + + /** @param array $p */ + function takesArrayOfStdClass(array $p): void {}', + 'error_message' => 'MixedArgumentTypeCoercion', + ], + 'classTemplateAsIncorrectClass' => [ + ' 'InvalidArgument', + ], + 'classTemplateAsIncorrectInterface' => [ + ' 'InvalidArgument', + ], + 'templateFunctionMethodCallWithoutMethod' => [ + 'bar(); + }', + 'error_message' => 'PossiblyUndefinedMethod', + ], + 'templateFunctionMethodCallWithoutAsType' => [ + 'bar(); + }', + 'error_message' => 'MixedMethodCall', + ], + 'forbidLossOfInformationWhenCoercing' => [ + ' + * @param T::class $class + */ + function foo(string $class) : void {} + + function bar(Traversable $t) : void { + foo(get_class($t)); + }', + 'error_message' => 'MixedArgumentTypeCoercion', + ], + 'bindFirstTemplatedClosureParameter' => [ + ' 'InvalidScalarArgument', + ], + 'bindFirstTemplatedClosureParameterTypeCoercion' => [ + ' 'ArgumentTypeCoercion', + ], + + 'callableDoesNotReturnItself' => [ + ' 'InvalidScalarArgument', + ], + 'multipleArgConstraintWithMoreRestrictiveFirstArg' => [ + ' 'ArgumentTypeCoercion', + ], + 'multipleArgConstraintWithMoreRestrictiveSecondArg' => [ + ' 'ArgumentTypeCoercion', + ], + 'multipleArgConstraintWithLessRestrictiveThirdArg' => [ + ' 'ArgumentTypeCoercion', + ], + 'possiblyInvalidArgumentWithUnionFirstArg' => [ + ' 'PossiblyInvalidArgument', + ], + 'possiblyInvalidArgumentWithUnionSecondArg' => [ + ' 'PossiblyInvalidArgument', + ], + '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' + ], + 'preventTemplateTypeAsBeingUsedInsideFunction' => [ + ' 'InvalidArgument' + ], + 'preventWrongTemplateBeingPassed' => [ + ' 'InvalidArgument' + ], + 'preventTemplateTypeReturnMoreGeneral' => [ + ' 'InvalidReturnStatement' + ], + ]; + } +}