[ 'code' => ' [ 'code' => ' [ 'code' => ' $a . "blah", $bar );', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '7.4', ], 'inferArgFromClassContext' => [ 'code' => ' $a + $b);', 'assertions' => [ '$a' => 'int', ], 'ignored_issues' => [], 'php_version' => '7.4', ], 'inferArgFromClassContextWithNamedArguments' => [ 'code' => ' $_a + $_b, bar: fn($_a, $_b) => $_a + $_b, baz: fn($_a, $_b) => $_a + $_b, );', 'assertions' => [ '$a' => 'int', ], 'ignored_issues' => [], 'php_version' => '7.4', ], 'inferArgFromClassContextInGenericContext' => [ 'code' => ' */ public function map(Closure $ab): ArrayList { throw new RuntimeException("???"); } } /** * @template T * @param ArrayList $list * @return ArrayList */ function asTupled(ArrayList $list): ArrayList { return $list->map(function ($_a) { return [$_a]; }); } /** @var ArrayList $a */ $a = new ArrayList(); $b = asTupled($a);', 'assertions' => [ '$b' => 'ArrayList', ], ], 'inferArgByPreviousMethodArg' => [ 'code' => ' $list * @param callable(A): B $first * @param callable(B): C $second * @return list */ public function map(array $list, callable $first, callable $second): array { throw new RuntimeException("never"); } } $result = (new ArrayList())->map([1, 2, 3], fn($i) => ["num" => $i], fn($i) => ["object" => $i]);', 'assertions' => [ '$result' => 'list', ], ], 'inferArgByPreviousFunctionArg' => [ 'code' => ' $_collection * @param callable(A): B $_ab * @return list */ function map(iterable $_collection, callable $_ab) { return []; } /** @template T */ final class Foo { /** @return Foo */ public function toInt() { throw new RuntimeException("???"); } } /** @var list> */ $items = []; $inferred = map($items, function ($i) { return $i->toInt(); });', 'assertions' => [ '$inferred' => 'list>', ], ], 'inferTemplateForExplicitlyTypedArgByPreviousFunctionArg' => [ 'code' => ' $_collection * @param callable(A): B $_ab * @return list */ function map(iterable $_collection, callable $_ab) { return []; } /** @template T */ final class Foo { /** @return Foo */ public function toInt() { throw new RuntimeException("???"); } } /** @var list> */ $items = []; $inferred = map($items, function (Foo $i) { return $i->toInt(); });', 'assertions' => [ '$inferred' => 'list>', ], ], 'doNotInferTemplateForExplicitlyTypedWithPhpdocArgByPreviousFunctionArg' => [ 'code' => ' $_collection * @param callable(A): B $_ab * @return list */ function map(iterable $_collection, callable $_ab) { return []; } /** @template T */ final class Foo { } /** @var list> */ $items = []; $inferred = map($items, /** @param Foo $i */ function ($i) { return $i; } );', 'assertions' => [ '$inferred' => 'list', ], ], 'inferTemplateOfHighOrderFunctionArgByPreviousArg' => [ 'code' => ' */ function getList() { throw new RuntimeException("???"); } /** * @template T * @return Closure(T): T */ function id() { throw new RuntimeException("???"); } /** * @template A * @template B * * @param list $_items * @param callable(A): B $_ab * @return list */ function map(array $_items, callable $_ab) { throw new RuntimeException("???"); } $result = map(getList(), id()); ', 'assertions' => [ '$result' => 'list', ], ], 'inferTemplateOfHighOrderFunctionArgByPreviousArgInClassContext' => [ 'code' => ' */ public function map(callable $ab) { throw new RuntimeException("???"); } } /** * @return ArrayList */ function getList() { throw new RuntimeException("???"); } /** * @template T * @return Closure(T): T */ function id() { throw new RuntimeException("???"); } $result = getList()->map(id()); ', 'assertions' => [ '$result' => 'ArrayList', ], ], 'inferTemplateOfHighOrderFunctionFromMethodArgByPreviousArg' => [ 'code' => '): T */ public function flatten() { throw new RuntimeException("???"); } } /** * @return list> */ function getList() { throw new RuntimeException("???"); } /** * @template T * @return Closure(list): T */ function flatten() { throw new RuntimeException("???"); } /** * @template A * @template B * * @param list $_a * @param callable(A): B $_ab * @return list */ function map(array $_a, callable $_ab) { throw new RuntimeException("???"); } $ops = new Ops; $result = map(getList(), $ops->flatten()); ', 'assertions' => [ '$result' => 'list', ], ], 'inferTemplateOfHighOrderFunctionFromStaticMethodArgByPreviousArg' => [ 'code' => '): T */ public static function flatten() { throw new RuntimeException("???"); } } /** * @return list> */ function getList() { throw new RuntimeException("???"); } /** * @template T * @return Closure(list): T */ function flatten() { throw new RuntimeException("???"); } /** * @template A * @template B * * @param list $_a * @param callable(A): B $_ab * @return list */ function map(array $_a, callable $_ab) { throw new RuntimeException("???"); } $result = map(getList(), StaticOps::flatten()); ', 'assertions' => [ '$result' => 'list', ], ], 'inferInvokableClassCallable' => [ 'code' => 'ab = Closure::fromCallable($ab); } /** * @template K * @param array $a * @return array */ public function __invoke(array $a): array { $b = []; foreach ($a as $k => $v) { $b[$k] = ($this->ab)($v); } return $b; } } /** * @template A * @template B * @param A $a * @param callable(A): B $ab * @return B */ function pipe(mixed $a, callable $ab): mixed { return $ab($a); } /** * @return array */ function getDict(): array { return ["fst" => 1, "snd" => 2, "thr" => 3]; } $result = pipe(getDict(), new MapOperator(fn($i) => ["num" => $i])); ', 'assertions' => [ '$result' => 'array', ], 'ignored_issues' => [], 'php_version' => '8.0', ], 'inferConstCallableLikeFirstClassCallable' => [ 'code' => '): list */ function map(callable $callback): Closure { return fn(array $list) => array_map($callback, $list); } /** * @template A * @template B * @param A $a * @param callable(A): B $ab * @return B */ function pipe1(mixed $a, callable $ab): mixed { return $ab($a); } /** * @template A * @template B * @template C * @param A $a * @param callable(A): B $ab * @param callable(B): C $bc * @return C */ function pipe2(mixed $a, callable $ab, callable $bc): mixed { return $bc($ab($a)); } } namespace App { use Functions\Module; use function Functions\map; use function Functions\pipe1; use function Functions\pipe2; use const Functions\classId; use const Functions\id; $class_const_id = pipe1([42], Module::id); $class_const_composition = pipe1([42], map(Module::id)); $class_const_sequential = pipe2([42], map(fn($i) => ["num" => $i]), Module::id); $class_const_alias_id = pipe1([42], classId); $class_const_alias_composition = pipe1([42], map(classId)); $class_const_alias_sequential = pipe2([42], map(fn($i) => ["num" => $i]), classId); $const_id = pipe1([42], id); $const_composition = pipe1([42], map(id)); $const_sequential = pipe2([42], map(fn($i) => ["num" => $i]), id); $string_id = pipe1([42], "Functions\id"); $string_composition = pipe1([42], map("Functions\id")); $string_sequential = pipe2([42], map(fn($i) => ["num" => $i]), "Functions\id"); $class_string_id = pipe1([42], "Functions\Module::id"); $class_string_composition = pipe1([42], map("Functions\Module::id")); $class_string_sequential = pipe2([42], map(fn($i) => ["num" => $i]), "Functions\Module::id"); } ', 'assertions' => [ '$class_const_id===' => 'list{42}', '$class_const_composition===' => 'list<42>', '$class_const_sequential===' => 'list', '$class_const_alias_id===' => 'list{42}', '$class_const_alias_composition===' => 'list<42>', '$class_const_alias_sequential===' => 'list', '$const_id===' => 'list{42}', '$const_composition===' => 'list<42>', '$const_sequential===' => 'list', '$string_id===' => 'list{42}', '$string_composition===' => 'list<42>', '$string_sequential===' => 'list', '$class_string_id===' => 'list{42}', '$class_string_composition===' => 'list<42>', '$class_string_sequential===' => 'list', ], 'ignored_issues' => [], 'php_version' => '8.0', ], 'inferPipelineWithPartiallyAppliedFunctions' => [ 'code' => '): list */ function filter(callable $_predicate): Closure { throw new RuntimeException("???"); } /** * @template A * @template B * * @param callable(A): B $_ab * @return Closure(list): list */ function map(callable $_ab): Closure { throw new RuntimeException("???"); } /** * @template T * @return (Closure(list): (non-empty-list | null)) */ function asNonEmptyList(): Closure { throw new RuntimeException("???"); } /** * @template T * @return Closure(T): T */ function id(): Closure { throw new RuntimeException("???"); } /** * @template A * @template B * @template C * @template D * @template E * @template F * * @param A $arg * @param callable(A): B $ab * @param callable(B): C $bc * @param callable(C): D $cd * @param callable(D): E $de * @param callable(E): F $ef * @return F */ function pipe4(mixed $arg, callable $ab, callable $bc, callable $cd, callable $de, callable $ef): mixed { return $ef($de($cd($bc($ab($arg))))); } /** * @template TFoo of string * @template TBar of bool */ final class Item { /** * @param TFoo $foo * @param TBar $bar */ public function __construct( public string $foo, public bool $bar, ) { } } /** * @return list */ function getList(): array { return []; } $result = pipe4( getList(), filter(fn($i) => $i->bar), filter(fn(Item $i) => $i->foo !== "bar"), map(fn($i) => new Item("test: " . $i->foo, $i->bar)), asNonEmptyList(), id(), );', 'assertions' => [ '$result' => 'non-empty-list>|null', ], 'ignored_issues' => [], 'php_version' => '8.0', ], 'inferPipelineWithPartiallyAppliedFunctionsAndFirstClassCallable' => [ 'code' => '): list */ function map(callable $callback): Closure { return fn($array) => array_map($callback, $array); } /** * @return list */ function getNums(): array { return []; } /** * @template T of float|int */ final class ObjectNum { /** * @psalm-param T $value */ public function __construct( public readonly float|int $value, ) {} } /** * @return list> */ function getObjectNums(): array { return []; } $id = pipe(getNums(), id(...)); $wrapped_id = pipe(getNums(), map(id(...))); $id_nested = pipe(getObjectNums(), map(id(...))); $id_nested_simple = pipe(getObjectNums(), id(...)); ', 'assertions' => [ '$id' => 'list', '$wrapped_id' => 'list', '$id_nested' => 'list>', '$id_nested_simple' => 'list>', ], 'ignored_issues' => [], 'php_version' => '8.1', ], 'inferFirstClassCallableWithGenericObject' => [ 'code' => ' $container * @return A */ function unwrap(Container $container) { return $container->value; } $result = pipe( new Container(42), unwrap(...), ); ', 'assertions' => [ '$result===' => '42', ], 'ignored_issues' => [], 'php_version' => '8.1', ], 'inferFirstClassCallableOnMethodCall' => [ 'code' => 'a), $processB($this->b)]; } } /** * @template A * @param A $value * @return A */ function id(mixed $value): mixed { return $value; } function intToString(int $value): string { return (string) $value; } /** * @template A * @param A $value * @return list{A} */ function singleToList(mixed $value): array { return [$value]; } $processor = new Processor(a: 1, b: 2); $test_id = $processor->process(id(...), id(...)); $test_complex = $processor->process(intToString(...), singleToList(...)); ', 'assertions' => [ '$test_id' => 'list{int, int}', '$test_complex' => 'list{string, list{int}}', ], 'ignored_issues' => [], 'php_version' => '8.1', ], 'inferFirstClassCallableOnMethodCallWithMultipleParams' => [ 'code' => 'a, $this->b, $this->c); } } /** * @template A * @template B * @template C * @param A $value1 * @param B $value2 * @param C $value3 * @return list{A, B, C} */ function tripleId(mixed $value1, mixed $value2, mixed $value3): array { return [$value1, $value2, $value3]; } $processor = new Processor(a: 1, b: 2, c: 3); $test = $processor->process(tripleId(...)); ', 'assertions' => [ '$test' => 'list{int, int, int}', ], 'ignored_issues' => [], 'php_version' => '8.1', ], 'inferFirstClassCallableOnMethodCallWithTemplatedAndNonTemplatedParams' => [ 'code' => 'param1, $this->param2); } } /** * @template T of int|float * @param T $param2 * @return array{param1: int, param2: T} */ function appHandler1(int $param1, int|float $param2): array { return ["param1" => $param1, "param2" => $param2]; } /** * @template T of int|float * @param T $param1 * @return array{param1: T, param2: int} */ function appHandler2(int|float $param1, int $param2): array { return ["param1" => $param1, "param2" => $param2]; } /** * @return array{param1: int, param2: int} */ function appHandler3(int $param1, int $param2): array { return ["param1" => $param1, "param2" => $param2]; } $app = new App(param1: 42, param2: 42); $result1 = $app->run(appHandler1(...)); $result2 = $app->run(appHandler2(...)); $result3 = $app->run(appHandler3(...)); ', 'assertions' => [ '$result1===' => 'array{param1: int, param2: 42}', '$result2===' => 'array{param1: 42, param2: int}', '$result3===' => 'array{param1: int, param2: int}', ], 'ignored_issues' => [], 'php_version' => '8.1', ], 'inferTypeWhenClosureParamIsOmitted' => [ 'code' => '): list */ function iterate(callable $callback): Closure { return function(array $list) use ($callback) { foreach ($list as $item) { $callback($item); } return $list; }; } $result1 = pipe( [1, 2, 3], iterate(fn($i) => print_r($i)), ); $result2 = pipe( [1, 2, 3], iterate(fn() => print_r("noop")), );', 'assertions' => [ '$result1===' => 'list<1|2|3>', '$result2===' => 'list<1|2|3>', ], 'ignored_issues' => [], 'php_version' => '8.1', ], 'varReturnType' => [ 'code' => ' [ '$a' => 'int', ], ], 'varReturnTypeArray' => [ 'code' => ' $a + 1; $a = $add_one(1);', 'assertions' => [ '$a' => 'int', ], 'ignored_issues' => [], 'php_version' => '7.4', ], 'varCallableParamReturnType' => [ 'code' => ' [ 'code' => ' [ 'code' => ' $a . "blah"; }', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '7.4', ], 'callable' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ '$a' => 'list{string, string}', '$b' => 'list{string, string}', '$c' => 'list{string, string}', '$d' => 'list{string, string}', '$e' => 'list{string, string}', '$f' => 'list{string, string}', ], ], 'arrayCallableMethod' => [ 'code' => ' [ 'code' => ' [ 'code' => ' $b ? 1 : 0; } $arr = [5, 4, 3, 1, 2]; usort($arr, "fooBar"); } }', ], 'closureSelf' => [ 'code' => 'subitems = array_map( function(self $i): self { return $i; }, $in ); } } new A([new A, new A]);', ], 'possiblyUndefinedFunction' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => 'callMeMaybe("foo");', ], 'isCallableString' => [ 'code' => ' [ 'code' => ' [ 'code' => 'callable = $callable; } public function callTheCallableDirectly(): bool { return ($this->callable)(); } public function callTheCallableIndirectly(): bool { $r = $this->callable; return $r(); } }', ], 'invokableProperties' => [ 'code' => 'invokable = $invokable; } public function callTheInvokableDirectly(): bool { return ($this->invokable)(); } public function callTheInvokableIndirectly(): bool { $r = $this->invokable; return $r(); } }', ], 'nullableReturnTypeShorthand' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' $b; } } f("strcmp"); f([new C, "m"]); f([C::class, "m"]);', ], 'callableWithSpaces' => [ 'code' => ' [ 'code' => ' [ 'code' => 'func2(function(B $x): void {}); $c->func2(function(B $x): void {}); class A {} class B extends A { /** * @param callable(self) $f */ function func2(callable $f): void { $f($this); } }', ], 'callableParentArg' => [ 'code' => 'func3(function(A $x): void {}); $c->func3(function(A $x): void {}); class A {} class B extends A { /** * @param callable(parent) $f */ function func3(callable $f): void { $f($this); } }', ], 'callableStaticArg' => [ 'code' => 'func1(function(B $x): void {}); $c->func1(function(C $x): void {}); class A {} class B extends A { /** * @param callable(static) $f */ function func1(callable $f): void { $f($this); } }', ], 'callableStaticReturn' => [ 'code' => 'func1(function(): C { return new C(); });', ], 'callableSelfReturn' => [ 'code' => 'func2(function() { return new B(); }); $c->func2(function() { return new C(); });', ], 'callableParentReturn' => [ 'code' => 'func3(function() { return new A(); });', ], 'selfArrayMapCallableWrongClass' => [ 'code' => ' */ public function bar() { return array_map([Foo::class, "foo"], [1,2,3]); } /** @return array */ public function bat() { return array_map([Foo::class, "baz"], [1]); } }', ], 'dynamicCallableArray' => [ 'code' => 'value = $value; } }', ], 'callableIsArrayAssertion' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => 'id = $id; } /** * @return static */ final public static function fromString(string $id): self { return new static($id); } } final class CriterionId extends Id { } final class CriterionIds { /** * @psalm-var non-empty-list */ private $ids; /** * @psalm-param non-empty-list $ids */ private function __construct(array $ids) { $this->ids = $ids; } /** * @psalm-param non-empty-list $ids */ public static function fromStrings(array $ids): self { return new self(array_map([CriterionId::class, "fromString"], $ids)); } }', ], 'offsetOnCallable' => [ 'code' => ' [ 'code' => ' [ '$classOrObject' => 'class-string|object', '$method' => 'string', ], ], 'callableInterface' => [ 'code' => ' [ 'code' => ' "ASC", "start_time" => "ASC"]);', ], 'callOnInvokableOrCallable' => [ 'code' => ' [ 'code' => 'takesACall(function() {return $this;}); } }', ], 'returnClosureReturningStatic' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [], 'ignored_issues' => [], 'php_version' => '8.0', ], 'callableArrayTypes' => [ 'code' => ' [ '$a' => 'class-string|object', '$b' => 'string', '$c' => 'list{class-string|object, string}', ], ], ]; } public function providerInvalidCodeParse(): iterable { return [ 'undefinedCallableClass' => [ 'code' => 'getFoo()($argOne, $argTwo); } }', 'error_message' => 'InvalidFunctionCall', 'ignored_issues' => ['UndefinedClass', 'MixedInferredReturnType'], ], 'undefinedCallableMethodFullString' => [ 'code' => ' 'UndefinedMethod', ], 'undefinedCallableMethodClassConcat' => [ 'code' => ' 'UndefinedMethod', ], 'undefinedCallableMethodArray' => [ 'code' => ' 'InvalidArgument', ], 'undefinedCallableMethodArrayWithoutClass' => [ 'code' => ' 'InvalidArgument', ], 'undefinedCallableMethodClass' => [ 'code' => ' 'UndefinedClass', ], 'undefinedCallableFunction' => [ 'code' => ' 'UndefinedFunction', ], 'stringFunctionCall' => [ 'code' => ' 'MixedAssignment', ], 'wrongCallableReturnType' => [ 'code' => ' 'InvalidReturnStatement', ], 'checkCallableTypeString' => [ 'code' => ' 'InvalidScalarArgument', ], 'checkCallableTypeArrayInstanceFirstArg' => [ 'code' => ' $b; } } f([new C, "m"]);', 'error_message' => 'InvalidScalarArgument', ], 'checkCallableTypeArrayClassStringFirstArg' => [ 'code' => ' $b; } } f([C::class, "m"]);', 'error_message' => 'InvalidScalarArgument', ], 'callableWithSpaceAfterColonBadVarArg' => [ 'code' => 'p = function (string $s, string $t): stdClass { return new stdClass; }; } }', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'callableWithSpaceBeforeColonBadVarArg' => [ 'code' => 'p = function (string $s, string $t): stdClass { return new stdClass; }; } }', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'callableWithSpacesEitherSideOfColonBadVarArg' => [ 'code' => 'p = function (string $s, string $t): stdClass { return new stdClass; }; } }', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'badArrayMapArrayCallable' => [ 'code' => ' 'InvalidArgument', ], 'noFatalErrorOnMissingClassWithSlash' => [ 'code' => ' 'InvalidArgument', ], 'noFatalErrorOnMissingClassWithoutSlash' => [ 'code' => ' 'InvalidArgument', ], 'preventStringDocblockType' => [ 'code' => ' 'MismatchingDocblockParamType', ], 'moreSpecificCallable' => [ 'code' => ' 'MixedArgumentTypeCoercion', ], 'undefinedVarInBareCallable' => [ 'code' => ' 'UndefinedVariable', ], 'dontQualifyStringCallables' => [ 'code' => ' 'UndefinedFunction', ], 'badCustomFunction' => [ 'code' => ' 'InvalidScalarArgument', ], 'emptyCallable' => [ 'code' => ' 'InvalidFunctionCall', ], 'ImpureFunctionCall' => [ 'code' => ' $values * @psalm-param (callable(T): numeric) $num_func * * @psalm-return null|T * * @psalm-pure */ function max_by(array $values, callable $num_func) { $max = null; $max_num = null; foreach ($values as $value) { $value_num = $num_func($value); if (null === $max_num || $value_num >= $max_num) { $max = $value; $max_num = $value_num; } } return $max; } $c = max_by([1, 2, 3], static function(int $a): int { return $a + mt_rand(0, $a); }); echo $c; ', 'error_message' => 'ImpureFunctionCall', 'ignored_issues' => [], ], 'constructCallableFromClassStringArray' => [ 'code' => ' $c */ function foo(string $c) : void { takesCallableReturningString([$c, "bar"]); }', 'error_message' => 'InvalidArgument', ], 'inexistantCallableinCallableString' => [ 'code' => ' 'InvalidArgument', ], 'mismatchParamTypeFromDocblock' => [ 'code' => ' */ public function map(Closure $effect): ArrayList { throw new RuntimeException("???"); } } /** * @template T * @template B * * @param ArrayList $list * @return ArrayList */ function genericContext(ArrayList $list): ArrayList { return $list->map( /** @param B $_a */ function ($_a) { return [$_a]; } ); }', 'error_message' => 'InvalidArgument', ], 'invalidFirstClassCallableCannotBeInferred' => [ 'code' => 'param1); } } /** * @template P1 of int|float * @param P1 $param1 * @return array{param1: P1} */ function appHandler(mixed $param1): array { return ["param1" => $param1]; } $result = (new App(param1: [42]))->run(appHandler(...)); ', 'error_message' => 'InvalidArgument', 'ignored_issues' => [], 'php_version' => '8.1', ], 'variadicClosureAssignability' => [ 'code' => ' 'InvalidScalarArgument', 'ignored_issues' => [], 'php_version' => '8.0', ], ]; } }