From 7c706d1483542e0714fd3c49eb2533d9700f401c Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:18:27 +0100 Subject: [PATCH] accepted_args and named arguments improvements * Fix https://github.com/psalm/psalm-plugin-wordpress/issues/53 * add support for named args "accepted_args" in add_action/filter * improve support for named arguments by fixing the FunctionLikeParameter names to match the actual param names * fix HookInvalidArgs to not report error when psalm already reports an InvalidArgument error for accepted args --- Plugin.php | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/Plugin.php b/Plugin.php index 37ed86b..e05cdbf 100644 --- a/Plugin.php +++ b/Plugin.php @@ -878,7 +878,7 @@ public static function getFunctionParams( FunctionParamsProviderEvent $event ) : // like this, it will give error that the do_action does not expect any arguments, thus prompting the dev to add phpdoc or remove args // if this should be ignored return []; instead // return [ - // new FunctionLikeParameter( 'Hook', false, Type::getNonEmptyString(), null, null, null, false ), + // new FunctionLikeParameter( 'hook_name', false, Type::getNonEmptyString(), null, null, null, false ), // ]; return []; } @@ -931,11 +931,25 @@ public static function getFunctionParams( FunctionParamsProviderEvent $event ) : } // Check how many args the filter is registered with. + $accepted_args_provided = false; if ( $is_invoke ) { // need to deduct 1, since the first argument (string) is the hardcoded hook name, which is added manually later on, since it's not in the PHPDoc $num_args = count( $call_args ) - 1; } else { - $num_args = isset( $call_args[ 3 ]->value->value ) ? max( 0, (int) $call_args[ 3 ]->value->value ) : 1; + $num_args = 1; + if ( isset( $call_args[ 3 ]->value->value ) && !isset( $call_args[ 3 ]->name ) ) { + $num_args = max( 0, (int) $call_args[ 3 ]->value->value ); + $accepted_args_provided = true; + } + + // named arguments + foreach ( $call_args as $call_arg ) { + if ( isset( $call_arg->name ) && $call_arg->name instanceof PhpParser\Node\Identifier && $call_arg->name->name === 'accepted_args' ) { + $num_args = max( 0, (int) $call_arg->value->value ); + $accepted_args_provided = true; + break; + } + } } // if the PHPDoc is missing from the do_action/apply_filters, the types can be empty - we assign "mixed" when loading stubs in that case though to avoid this @@ -950,7 +964,9 @@ public static function getFunctionParams( FunctionParamsProviderEvent $event ) : } else { // add_action default has 1 arg, but this hook does not accept any args if ( $is_action ) { - if ( $code_location ) { + // if accepted args are not provided, it will use the default value, so we need to give an error + // if it's provided psalm will report an InvalidArgument error by default already + if ( $code_location && $accepted_args_provided === false ) { IssueBuffer::accepts( new HookInvalidArgs( 'Hook "' . $hook_name . '" does not accept any args, but the default number of args of add_action is 1. Please pass 0 as 4th argument', @@ -1032,8 +1048,9 @@ public static function getFunctionParams( FunctionParamsProviderEvent $event ) : $args_type = $max_params === 0 || $min_args >= $max_params ? Type::getInt( false, $max_params ) : new Union([ new TIntRange( $min_args, $max_params )] ); $return = [ - new FunctionLikeParameter( 'Hook', false, Type::getNonEmptyString(), null, null, null, false ), - new FunctionLikeParameter( 'Callback', false, new Union( [ + // the first argument of each FunctionLikeParameter must match the param name of the function to allow the use of named arguments + new FunctionLikeParameter( 'hook_name', false, Type::getNonEmptyString(), null, null, null, false ), + new FunctionLikeParameter( 'callback', false, new Union( [ new TCallable( 'callable', $hook_params, @@ -1041,8 +1058,8 @@ public static function getFunctionParams( FunctionParamsProviderEvent $event ) : ), ] ), null, null, null, false ), // $is_optional arg in FunctionLikeParameter is true by default, so we can just set type of int directly without null (since it's not nullable anyway) - new FunctionLikeParameter( 'Priority', false, Type::getInt() ), - new FunctionLikeParameter( 'Args', false, $args_type ), + new FunctionLikeParameter( 'priority', false, Type::getInt() ), + new FunctionLikeParameter( 'accepted_args', false, $args_type ), ]; return $return;