,error_levels?:string[]}> */ public function providerValidCodeParse(): iterable { return [ 'validTemplatedType' => [ ' [ ' [ ' [ 'foo("string"));', ], 'genericArrayKeys' => [ ' $arr * @return list */ function my_array_keys($arr) { return array_keys($arr); } $a = my_array_keys(["hello" => 5, "goodbye" => new \Exception()]);', 'assertions' => [ '$a' => 'list', ], ], 'genericNonEmptyArrayKeys' => [ ' $arr * @return non-empty-list */ function my_array_keys($arr) { return array_keys($arr); } $a = my_array_keys(["hello" => 5, "goodbye" => new \Exception()]);', 'assertions' => [ '$a' => 'non-empty-list', ], ], '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' => 'int|null', '$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'], ], 'bindFirstTemplatedClosureParameterValid' => [ ' [ ' [ ' [ 'getIterator();', [ '$i' => 'Traversable', ], ], 'upcastArrayToIterable' => [ ' $collection * @return V * @psalm-suppress InvalidReturnType */ function first($collection) {} $one = first([1,2,3]);', [ '$one' => 'int', ], ], 'templateIntersectionLeft' => [ ' [ ' [ '|TReturn) $gen * @return array */ function call(callable $gen) : array { $return = $gen(); if ($return instanceof Generator) { return [$return->getReturn()]; } /** @var array */ $wrapped_gen = [$gen]; return $wrapped_gen; } $arr = call( function() { yield 1; return "hello"; } );', [ '$arr' => 'array', ], ], '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 {}, [rand(0, 1) ? 5 : false] );', ], 'functionTemplateUnionType' => [ ' [ '$s' => 'string', '$i' => 'int', ], ], 'reconcileTraversableTemplatedAndNormal' => [ 'getIterator(); $t = $a; } if (!$t instanceof Iterator) { return; } if (rand(0, 1) && rand(0, 1)) { $t->next(); } }', ], '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', ], ], 'dontGeneraliseBoundParamWithWiderCallable' => [ ' 'C', ], ], 'allowTemplateTypeBeingUsedInsideFunction' => [ ' [ ' [ ' [ ' [ 'id = $id; } public static function fromString(string $id): self { return new self($id); } } /** * @template T * @psalm-param callable(string): T $generator * @psalm-return callable(): T */ function idGenerator(callable $generator) { return static function () use ($generator) { return $generator("random id"); }; } function client(Id $id): void { } $staticIdGenerator = idGenerator([Id::class, "fromString"]); client($staticIdGenerator());' ], 'noCrashWhenTemplatedClassIsStatic' => [ 'newInstance(); }' ], 'unboundVariableIsEmpty' => [ ' [ ' $arr * @param-out list $arr */ function example_sort_by_ref(array &$arr): bool { $arr = array_values($arr); return true; } /** * @param array $array * @return list */ function example(array $array): array { example_sort_by_ref($array); return $array; }', ], 'narrowTemplateTypeWithIsObject' => [ ' [ ' [ ' [ ' $collection * @psalm-param callable(T, T): int $sorter * @psalm-return array */ function order(array $collection, callable $sorter): array { usort($collection, $sorter); return $collection; }' ], 'callableInference' => [ ' $a * @return list */ function baz(array $a) { return array_map("from_other", $a); }' ], 'templateFlipIntersection' => [ ' [ ' ...$iterators * @return Generator */ function joinBySplat(array ...$iterators): iterable { foreach ($iterators as $iter) { foreach ($iter as $value) { yield $value; } } } /** * @return Generator> */ function genIters(): Generator { yield [1,2,3]; yield [4,5,6]; } $values = joinBySplat(...genIters()); foreach ($values as $value) { echo $value; }' ], 'allowTemplatedCast' => [ ' [ ' $result * @param (callable(Tk, Tk): int) $comparator * * @preturn array */ function sort_by_key(array $result, callable $comparator): array { \uksort($result, $comparator); return $result; }' ], 'uksortNamespaced' => [ ' $result * @param (callable(Tk, Tk): int) $comparator * * @preturn array */ function sort_by_key(array $result, callable $comparator): array { \uksort($result, $comparator); return $result; }' ], 'mockObject' => [ ' $originalClassName * @psalm-return MockObject&T1 */ function foo(string $originalClassName): MockObject { return createMock($originalClassName); } /** * @psalm-suppress InvalidReturnType * @psalm-suppress InvalidReturnStatement * * @psalm-template T2 of object * @psalm-param class-string $originalClassName * @psalm-return MockObject&T2 */ function createMock(string $originalClassName): MockObject { return new MockObject; }' ], 'testStringCallableInference' => [ ' $iter * @return list */ function toArray(iterable $iter): array { $data = []; foreach ($iter as $val) { $data[] = $val; } return $data; } /** * @template T * @template U * @param callable(T): U $predicate * @return callable(iterable): iterable */ function map(callable $predicate): callable { return /** @param iterable $iter */ function(iterable $iter) use ($predicate): iterable { foreach ($iter as $key => $value) { yield $key => $predicate($value); } }; } /** @param list $strings */ function _test(array $strings): void {} $a = map([A::class, "dup"])(["a", "b", "c"]);', [ '$a' => 'iterable' ] ], 'testClosureCallableInference' => [ ' $iter * @return list */ function toArray(iterable $iter): array { $data = []; foreach ($iter as $val) { $data[] = $val; } return $data; } /** * @template T * @template U * @param callable(T): U $predicate * @return callable(iterable): iterable */ function map(callable $predicate): callable { return /** @param iterable $iter */ function(iterable $iter) use ($predicate): iterable { foreach ($iter as $key => $value) { yield $key => $predicate($value); } }; } /** @param list $strings */ function _test(array $strings): void {} $a = map( function (string $a) { return $a . $a; } )(["a", "b", "c"]);', [ '$a' => 'iterable' ] ], 'possiblyNullMatchesTemplateType' => [ ' 'A', ] ], 'possiblyNullMatchesAnotherTemplateType' => [ ' $className * @psalm-param Closure( * RealObjectType|null * ) : void $initializer */ function createProxy( string $className, Closure $initializer ) : void {} class Foo {} createProxy(Foo::class, function (?Foo $f) : void {});' ], 'assertIntersectionsOnTemplatedTypes' => [ ' [ ' $className * @param Closure(T):void $outmaker * @return T */ function createProxy( string $className, Closure $outmaker ) : object { /** @psalm-suppress MixedMethodCall */ $t = new $className(); $outmaker($t); return $t; } class A { public function bar() : void {} } createProxy(A::class, function(object $o):void {})->bar();' ], 'bottomTypeInNamespacedCallableShouldMatch' => [ ' $className * @param callable(T):void $outmaker * @return T */ function createProxy( string $className, callable $outmaker ) : object { /** @psalm-suppress MixedMethodCall */ $t = new $className(); $outmaker($t); return $t; } class A { public function bar() : void {} } function foo(A $o):void {} createProxy(A::class, \'Ns\foo\')->bar();', ], 'compareToEmptyArray' => [ ' [ ' [ ' [ ' $func($arg1, $arg2); } /** * @template TArg1 * @template TArg2 * @template TRes * * @template T as (Closure(): TRes | Closure(TArg1): TRes | Closure(TArg1, TArg2): TRes) * * @psalm-param T $fn * @psalm-param TArg1 $arg */ function foo(Closure $fn, $arg): void { $a = partial($fn, $arg); }', [], [], '7.4' ], 'mixedDoesntSwallowNull' => [ ' [ ' [ ' $input * @psalm-return Iterator */ function to_iterator(iterable $input): Iterator { if (\is_array($input)) { return new \ArrayIterator($input); } elseif ($input instanceof Iterator) { return $input; } else { return new \IteratorIterator($input); } }' ], 'doublyNestedFunctionTemplates' => [ ' $iterable * @psalm-param (callable(Tk, Tv): bool)|null $predicate * * @psalm-return iterable */ function filter_with_key(iterable $iterable, ?callable $predicate = null): iterable { return (static function () use ($iterable, $predicate): Generator { $predicate = $predicate ?? /** * @psalm-param Tk $_k * @psalm-param Tv $v * * @return bool */ function($_k, $v) { return (bool) $v; }; foreach ($iterable as $k => $v) { if ($predicate($k, $v)) { yield $k => $v; } } })(); }' ], 'allowClosureParamLowerBoundAndUpperBound' => [ ' [ ' [ ' [ 'add($default); return $default; } }' ], 'isArrayCheckOnTemplated' => [ ' [ ' * @param TArray $arr * @return TValue */ function toList(array $arr): array { return reset($arr); }' ], 'callTemplatedFunctionWithTemplatedClassString' => [ ' $type * @psalm-return Tb * @psalm-suppress InvalidReturnType */ function deserialize_object(string $data, string $type) {}' ], ]; } /** * @return iterable */ public function providerInvalidCodeParse(): iterable { 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', ], 'bindFirstTemplatedClosureParameterInvalidScalar' => [ ' 'InvalidScalarArgument', ], 'bindFirstTemplatedClosureParameterTypeCoercion' => [ ' 'ArgumentTypeCoercion', ], 'callableDoesNotReturnItself' => [ ' 'InvalidScalarArgument', ], 'multipleArgConstraintWithMoreRestrictiveFirstArg' => [ ' 'ArgumentTypeCoercion', ], 'multipleArgConstraintWithMoreRestrictiveSecondArg' => [ ' 'ArgumentTypeCoercion', ], 'multipleArgConstraintWithLessRestrictiveThirdArg' => [ ' 'ArgumentTypeCoercion', ], 'possiblyInvalidArgumentWithUnionFirstArg' => [ ' 'PossiblyInvalidArgument', ], 'possiblyInvalidArgumentWithUnionSecondArg' => [ ' 'PossiblyInvalidArgument', ], 'preventTemplateTypeAsBeingUsedInsideFunction' => [ ' 'InvalidArgument', ], 'preventWrongTemplateBeingPassed' => [ ' 'InvalidArgument', ], 'preventTemplateTypeReturnMoreGeneral' => [ ' 'InvalidReturnStatement', ], 'preventReturningString' => [ ' 'InvalidReturnStatement', ], 'unTemplatedVarOnReturn' => [ ' 'InvalidReturnStatement', ], 'templateReturnTypeOfCallableWithIncompatibleType' => [ ' 'InvalidArgument', ], 'templateInvokeArg' => [ ' 'InvalidArgument' ], 'invalidTemplateDocblock' => [ ' 'MissingDocblockType' ], 'returnNamedObjectWhereTemplateIsExpected' => [ ' 'InvalidReturnStatement', ], 'returnIntersectionWhenTemplateIsExpectedForward' => [ ' 'InvalidReturnStatement', ], 'returnIntersectionWhenTemplateIsExpectedBackward' => [ ' 'InvalidReturnStatement', ], 'bottomTypeInClosureShouldClash' => [ ' $className * @param Closure(T):void $outmaker * @return T */ function createProxy( string $className, Closure $outmaker ) : object { /** @psalm-suppress MixedMethodCall */ $t = new $className(); $outmaker($t); return $t; } class A { public function bar() : void {} } class B {} createProxy(A::class, function(B $o):void {})->bar();', 'error_message' => 'InvalidArgument' ], 'bottomTypeInNamespacedCallableShouldClash' => [ ' $className * @param callable(T):void $outmaker * @return T */ function createProxy( string $className, callable $outmaker ) : object { /** @psalm-suppress MixedMethodCall */ $t = new $className(); $outmaker($t); return $t; } class A { public function bar() : void {} } class B {} function foo(B $o):void {} createProxy(A::class, \'Ns\foo\')->bar();', 'error_message' => 'InvalidArgument' ], ]; } }