From 97dc369fc262b92b93a58b48333e5509f1089a64 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 9 Oct 2022 05:08:48 +0200 Subject: [PATCH] PHP 8.1 | PassedParameters::hasParameters(): prevent false positives for first class callable declarations PHP 8.1 introduced a new callback declaration syntax, called "first class callables". This syntax can be easily confused with a function call. While it remains the responsibility of individual sniffs to verify if something is a function call before passing it to any of the `PassedParameters` methods, a little defensive coding to prevent incorrect results goes a long way. Includes tests. Refs: * https://wiki.php.net/rfc/first_class_callable_syntax --- PHPCSUtils/Utils/PassedParameters.php | 7 ++++++- .../PassedParameters/HasParametersTest.inc | 11 +++++++++++ .../PassedParameters/HasParametersTest.php | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/PHPCSUtils/Utils/PassedParameters.php b/PHPCSUtils/Utils/PassedParameters.php index 726793eb..361e9acd 100644 --- a/PHPCSUtils/Utils/PassedParameters.php +++ b/PHPCSUtils/Utils/PassedParameters.php @@ -66,6 +66,8 @@ class PassedParameters * * @since 1.0.0 * @since 1.0.0-alpha4 Added support for PHP 8.0 identifier name tokenization. + * @since 1.0.0-alpha4 Added defensive coding against PHP 8.1 first class callables + * being passed as if they were function calls. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. * @param int $stackPtr The position of function call name, @@ -138,8 +140,11 @@ public static function hasParameters(File $phpcsFile, $stackPtr, $isShortArray = return false; } + $ignore = Tokens::$emptyTokens; + $ignore[\T_ELLIPSIS] = \T_ELLIPSIS; // Prevent PHP 8.1 first class callables from being seen as function calls. + $closeParenthesis = $tokens[$next]['parenthesis_closer']; - $nextNextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($next + 1), ($closeParenthesis + 1), true); + $nextNextNonEmpty = $phpcsFile->findNext($ignore, ($next + 1), ($closeParenthesis + 1), true); if ($nextNextNonEmpty === $closeParenthesis) { // No parameters. diff --git a/Tests/Utils/PassedParameters/HasParametersTest.inc b/Tests/Utils/PassedParameters/HasParametersTest.inc index a2b3fcd4..cf69b775 100644 --- a/Tests/Utils/PassedParameters/HasParametersTest.inc +++ b/Tests/Utils/PassedParameters/HasParametersTest.inc @@ -163,6 +163,17 @@ $anon = new class() {}; /* testHasParamsAnonClass */ $anon = new class( $param1, $param2 ) {}; +/* testPHP81FirstClassCallableNotFunctionCallGlobalFunction */ +$fn = strlen(...); + +/* testPHP81FirstClassCallableNotFunctionCallOOMethod */ +$fn = $this->method( + ... +); + +/* testPHP81FirstClassCallableNotFunctionCallVariableStaticOOMethod */ +$fn = $name1::$name2( /*comment*/ ...); + // Intentional parse error. /* testNoCloseParenthesis */ $array = array(1, 2, 3 diff --git a/Tests/Utils/PassedParameters/HasParametersTest.php b/Tests/Utils/PassedParameters/HasParametersTest.php index cff91827..034f51f3 100644 --- a/Tests/Utils/PassedParameters/HasParametersTest.php +++ b/Tests/Utils/PassedParameters/HasParametersTest.php @@ -369,6 +369,24 @@ public function dataHasParameters() 'expected' => true, ], + // PHP 8.1 first class callables are callbacks, not function calls. + 'no-params-php81-first-class-callable-global-function' => [ + 'testMarker' => '/* testPHP81FirstClassCallableNotFunctionCallGlobalFunction */', + 'targetType' => \T_STRING, + 'expected' => false, + ], + 'no-params-php81-first-class-callable-oo-method' => [ + 'testMarker' => '/* testPHP81FirstClassCallableNotFunctionCallOOMethod */', + 'targetType' => \T_STRING, + 'expected' => false, + ], + 'no-params-php81-first-class-callable-variable-static-oo-method' => [ + 'testMarker' => '/* testPHP81FirstClassCallableNotFunctionCallVariableStaticOOMethod */', + 'targetType' => \T_VARIABLE, + 'expected' => false, + 'targetContent' => '$name2', + ], + // Defensive coding against parse errors and live coding. 'defense-in-depth-no-close-parens' => [ 'testMarker' => '/* testNoCloseParenthesis */',