diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index 9fdd3fcc482..0ea26080433 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -235,6 +235,46 @@ private function createMethodParam(array $current_token, ParseTree $current_pare $this->current_leaf = $new_parent_leaf; } + /** + * @param array{0: string, 1: int, 2?: string} $current_token + */ + private function parseCallableParam(array $current_token, ParseTree $current_parent): void + { + $variadic = false; + $has_default = false; + + if ($current_token[0] === '&') { + ++$this->t; + $current_token = $this->t < $this->type_token_count ? $this->type_tokens[$this->t] : null; + } elseif ($current_token[0] === '...') { + $variadic = true; + + ++$this->t; + $current_token = $this->t < $this->type_token_count ? $this->type_tokens[$this->t] : null; + } elseif ($current_token[0] === '=') { + $has_default = true; + + ++$this->t; + $current_token = $this->t < $this->type_token_count ? $this->type_tokens[$this->t] : null; + } + + if (!$current_token || $current_token[0][0] !== '$') { + throw new TypeParseTreeException('Unexpected token after space'); + } + + $new_leaf = new CallableParamTree($current_parent); + $new_leaf->has_default = $has_default; + $new_leaf->variadic = $variadic; + + if ($current_parent !== $this->current_leaf) { + $new_leaf->children = [$this->current_leaf]; + array_pop($current_parent->children); + } + $current_parent->children[] = $new_leaf; + + $this->current_leaf = $new_leaf; + } + private function handleLessThan(): void { if (!$this->current_leaf instanceof FieldEllipsis) { @@ -553,24 +593,27 @@ private function handleSpace(): void $current_parent = $this->current_leaf->parent; - if ($current_parent instanceof CallableTree) { - return; - } - - while ($current_parent && !$current_parent instanceof MethodTree) { + //while ($current_parent && !$method_or_callable_parent) { + while ($current_parent && !$current_parent instanceof MethodTree && !$current_parent instanceof CallableTree) { $this->current_leaf = $current_parent; $current_parent = $current_parent->parent; } $next_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null; - if (!$current_parent instanceof MethodTree || !$next_token) { + if (!($current_parent instanceof MethodTree || $current_parent instanceof CallableTree) || !$next_token) { throw new TypeParseTreeException('Unexpected space'); } - ++$this->t; - $this->createMethodParam($next_token, $current_parent); + if ($current_parent instanceof MethodTree) { + ++$this->t; + $this->createMethodParam($next_token, $current_parent); + } + if ($current_parent instanceof CallableTree) { + ++$this->t; + $this->parseCallableParam($next_token, $current_parent); + } } private function handleQuestionMark(): void diff --git a/tests/TypeAnnotationTest.php b/tests/TypeAnnotationTest.php index 8cd1fb2189b..29ee8f6581f 100644 --- a/tests/TypeAnnotationTest.php +++ b/tests/TypeAnnotationTest.php @@ -679,6 +679,68 @@ class Foo { '$output===' => 'callable():int', ], ], + 'callableFormats' => [ + 'code' => '): array + * @psalm-type I callable(array $e): array + * @psalm-type J callable(array ...): string + * @psalm-type K callable(array ...$e): string + * @psalm-type L \Closure(int, int): string + * + * @method ma(): A + * @method mb(): B + * @method mc(): C + * @method md(): D + * @method me(): E + * @method mf(): F + * @method mg(): G + * @method mh(): H + * @method mi(): I + * @method mj(): J + * @method mk(): K + * @method ml(): L + */ + class Foo { + public function __call(string $method, array $params) { return 1; } + } + + $foo = new \Foo(); + $output_ma = $foo->ma(); + $output_mb = $foo->mb(); + $output_mc = $foo->mc(); + $output_md = $foo->md(); + $output_me = $foo->me(); + $output_mf = $foo->mf(); + $output_mg = $foo->mg(); + $output_mh = $foo->mh(); + $output_mi = $foo->mi(); + $output_mj = $foo->mj(); + $output_mk = $foo->mk(); + $output_ml = $foo->ml(); + ', + 'assertions' => [ + '$output_ma===' => 'callable(int, int):string', + '$output_mb===' => 'callable(int, int=):string', + '$output_mc===' => 'callable(int, string):void', + '$output_md===' => 'callable(string):mixed', + '$output_me===' => 'callable(string):mixed', + '$output_mf===' => 'callable(float...):(int|null)', + '$output_mg===' => 'callable(float...):(int|null)', + '$output_mh===' => 'callable(array):array', + '$output_mi===' => 'callable(array):array', + '$output_mj===' => 'callable(array...):string', + '$output_mk===' => 'callable(array...):string', + '$output_ml===' => 'Closure(int, int):string', + ], + ], 'unionOfStringsContainingBraceChar' => [ 'code' => '