From f5175b847222a6ed855f4ab271a25fca96c737e3 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 00:22:23 +0200 Subject: [PATCH 1/2] AbstractFunctionParameterSniff: don't ignore first class callables ... but pass them to a dedicated `process_first_class_callable()` method instead to allow sniffs to decide whether to flag these or not. Typical use-case for why first class callables should not be ignored by default: A sniff which ALWAYS flags the use of a certain function, but has different error messages depending on whether parameters are passed or not. In that situation, I believe first class callables should be treated the same as other uses of the target function. First class callables are basically syntax sugar for closures (example: https://3v4l.org/cra8s) and if the sniff would flag the use of the target function within a closure, it is only reasonable to also flag the use of the target function as a first class callable. This commit implements this. The commit does not include tests as there are no sniffs in WPCS for which the above would apply. However, I have manually tested this change via a sniff in an external standard for which this change is relevant. --- WordPress/AbstractFunctionParameterSniff.php | 39 +++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/WordPress/AbstractFunctionParameterSniff.php b/WordPress/AbstractFunctionParameterSniff.php index 4f5f64e47d..d18a1f7589 100644 --- a/WordPress/AbstractFunctionParameterSniff.php +++ b/WordPress/AbstractFunctionParameterSniff.php @@ -72,7 +72,19 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content $parameters = PassedParameters::getParameters( $this->phpcsFile, $stackPtr ); if ( empty( $parameters ) ) { - return $this->process_no_parameters( $stackPtr, $group_name, $matched_content ); + /* + * Check if this is a first class callable. + * + * No need for extensive defensive coding as the `is_targetted_token()` method has already + * validated the open and close parentheses exist. + */ + $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + $firstNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next + 1 ), null, true ); + if ( \T_ELLIPSIS === $this->tokens[ $firstNonEmpty ]['code'] ) { + return $this->process_first_class_callable( $stackPtr, $group_name, $matched_content ); + } else { + return $this->process_no_parameters( $stackPtr, $group_name, $matched_content ); + } } else { return $this->process_parameters( $stackPtr, $group_name, $matched_content, $parameters ); } @@ -104,15 +116,6 @@ public function is_targetted_token( $stackPtr ) { return false; } - // First class callable. - $firstNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next + 1 ), null, true ); - if ( \T_ELLIPSIS === $this->tokens[ $firstNonEmpty ]['code'] ) { - $secondNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $firstNonEmpty + 1 ), null, true ); - if ( \T_CLOSE_PARENTHESIS === $this->tokens[ $secondNonEmpty ]['code'] ) { - return false; - } - } - return true; } @@ -147,4 +150,20 @@ abstract public function process_parameters( $stackPtr, $group_name, $matched_co * normal file processing. */ public function process_no_parameters( $stackPtr, $group_name, $matched_content ) {} + + /** + * Process the function if it is used as a first class callable. + * + * Defaults to doing nothing. Can be overloaded in child classes to implement specific checks + * on first class callable use of the function. + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * + * @return int|void Integer stack pointer to skip forward or void to continue + * normal file processing. + */ + public function process_first_class_callable( $stackPtr, $group_name, $matched_content ) {} } From d0c204c39ddb19a077c29f58fa556538e05cca35 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 14:14:13 +0200 Subject: [PATCH 2/2] [SQUASH-ME] AbstractFunctionParameterSniff: rename a variable for clarity --- WordPress/AbstractFunctionParameterSniff.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/AbstractFunctionParameterSniff.php b/WordPress/AbstractFunctionParameterSniff.php index d18a1f7589..46487dd53f 100644 --- a/WordPress/AbstractFunctionParameterSniff.php +++ b/WordPress/AbstractFunctionParameterSniff.php @@ -78,8 +78,8 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content * No need for extensive defensive coding as the `is_targetted_token()` method has already * validated the open and close parentheses exist. */ - $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); - $firstNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next + 1 ), null, true ); + $openParens = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + $firstNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $openParens + 1 ), null, true ); if ( \T_ELLIPSIS === $this->tokens[ $firstNonEmpty ]['code'] ) { return $this->process_first_class_callable( $stackPtr, $group_name, $matched_content ); } else {