diff --git a/src/Psalm/Internal/Type/ParseTree/CallableParamTree.php b/src/Psalm/Internal/Type/ParseTree/CallableParamTree.php index 53bc98bbbc8..8cd9cf7354b 100644 --- a/src/Psalm/Internal/Type/ParseTree/CallableParamTree.php +++ b/src/Psalm/Internal/Type/ParseTree/CallableParamTree.php @@ -12,4 +12,11 @@ final class CallableParamTree extends ParseTree public bool $variadic = false; public bool $has_default = false; + + /** + * Param name, without the $ prefix + * + * @var null|non-empty-string + */ + public ?string $name = null; } diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index 1b652f09758..29e27602817 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -30,6 +30,7 @@ use function preg_match; use function strlen; use function strtolower; +use function substr; /** * @internal @@ -258,13 +259,17 @@ private function parseCallableParam(array $current_token, ParseTree $current_par $current_token = $this->t < $this->type_token_count ? $this->type_tokens[$this->t] : null; } - if (!$current_token || $current_token[0][0] !== '$') { + if (!$current_token || $current_token[0][0] !== '$' || strlen($current_token[0]) < 2) { throw new TypeParseTreeException('Unexpected token after space'); } $new_leaf = new CallableParamTree($current_parent); $new_leaf->has_default = $has_default; $new_leaf->variadic = $variadic; + $potential_name = substr($current_token[0], 1); + if ($potential_name !== false && $potential_name !== '') { + $new_leaf->name = $potential_name; + } if ($current_parent !== $this->current_leaf) { $new_leaf->children = [$this->current_leaf]; diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index fd807f584b1..f79cc3cfdc8 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -1277,6 +1277,7 @@ private static function getTypeFromCallableTree( foreach ($parse_tree->children as $child_tree) { $is_variadic = false; $is_optional = false; + $param_name = ''; if ($child_tree instanceof CallableParamTree) { if (isset($child_tree->children[0])) { @@ -1294,6 +1295,7 @@ private static function getTypeFromCallableTree( $is_variadic = $child_tree->variadic; $is_optional = $child_tree->has_default; + $param_name = $child_tree->name ?? ''; } else { if ($child_tree instanceof Value && strpos($child_tree->value, '$') > 0) { $child_tree->value = preg_replace('/(.+)\$.*/', '$1', $child_tree->value); @@ -1310,7 +1312,7 @@ private static function getTypeFromCallableTree( } $param = new FunctionLikeParameter( - '', + $param_name, false, $tree_type instanceof Union ? $tree_type : new Union([$tree_type]), null, diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index 39125c366bd..5078b15bbe8 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -15,6 +15,8 @@ final class FunctionLikeParameter implements HasAttributesInterface, TypeNode use UnserializeMemoryUsageSuppressionTrait; /** + * Parameter name, without `$` + * * @var string */ public $name; diff --git a/tests/CallableTest.php b/tests/CallableTest.php index fc8c36d212f..e10784b0b3f 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -1908,6 +1908,18 @@ function bar($cb_arg) {} foo("bar");', ], + 'callableWithNamedArguments' => [ + 'code' => <<<'PHP' + [], + 'ignored_issues' => [], + 'php_version' => '8.0', + ], ]; } @@ -2494,6 +2506,18 @@ function int_int_int_int_string(Closure $f): void {} 'ignored_issues' => [], 'php_version' => '8.0', ], + 'callableWithInvalidNamedArguments' => [ + 'code' => <<<'PHP' + 'InvalidNamedArgument', + 'ignored_issues' => [], + 'php_version' => '8.0', + ], ]; } }