From 6a8b39b6f6774af8d387f84b9ac4c9d3abbfca53 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 11 May 2024 05:23:16 +0200 Subject: [PATCH 1/2] PHP 8.4 | Collections::functionCallTokens(): add `T_EXIT` As of PHP 8.4, `exit` and `die` will become "special" function calls and when used as a constant, it will be transformed to a function call at compile time. See the RFC for full details. With this in mind, it is appropriate to add `T_EXIT` to the `functionCallTokens()` and by inheritance to the `parameterPassingTokens()`. Includes unit tests. Ref: https://wiki.php.net/rfc/exit-as-function --- PHPCSUtils/Tokens/Collections.php | 4 ++++ Tests/Tokens/Collections/FunctionCallTokensTest.php | 1 + Tests/Tokens/Collections/ParameterPassingTokensTest.php | 1 + 3 files changed, 6 insertions(+) diff --git a/PHPCSUtils/Tokens/Collections.php b/PHPCSUtils/Tokens/Collections.php index 6c8c2574..ef3f729b 100644 --- a/PHPCSUtils/Tokens/Collections.php +++ b/PHPCSUtils/Tokens/Collections.php @@ -692,6 +692,10 @@ public static function functionCallTokens() $tokens[\T_ANON_CLASS] = \T_ANON_CLASS; $tokens += self::$ooHierarchyKeywords; + // As of PHP 8.4, exit()/die() should be treated as function call tokens. + // Sniffs using this collection should safeguard against use as a constant. + $tokens[\T_EXIT] = \T_EXIT; + return $tokens; } diff --git a/Tests/Tokens/Collections/FunctionCallTokensTest.php b/Tests/Tokens/Collections/FunctionCallTokensTest.php index ce3a6ad4..4db0aa05 100644 --- a/Tests/Tokens/Collections/FunctionCallTokensTest.php +++ b/Tests/Tokens/Collections/FunctionCallTokensTest.php @@ -40,6 +40,7 @@ public function testFunctionCallTokens() \T_PARENT => \T_PARENT, \T_SELF => \T_SELF, \T_STATIC => \T_STATIC, + \T_EXIT => \T_EXIT, ]; $this->assertSame($expected, Collections::functionCallTokens()); diff --git a/Tests/Tokens/Collections/ParameterPassingTokensTest.php b/Tests/Tokens/Collections/ParameterPassingTokensTest.php index e26255cb..c29a8418 100644 --- a/Tests/Tokens/Collections/ParameterPassingTokensTest.php +++ b/Tests/Tokens/Collections/ParameterPassingTokensTest.php @@ -40,6 +40,7 @@ public function testParameterPassingTokens() \T_PARENT => \T_PARENT, \T_SELF => \T_SELF, \T_STATIC => \T_STATIC, + \T_EXIT => \T_EXIT, \T_ISSET => \T_ISSET, \T_UNSET => \T_UNSET, \T_ARRAY => \T_ARRAY, From 15deff1b132084af56e5f19cd825e3806982187e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 11 May 2024 05:38:41 +0200 Subject: [PATCH 2/2] PassedParameters: minor update for support of exit As the `Collections::parameterPassingTokens()` array now contains `T_EXIT`, `exit` and `die` will now also be analyzable via the methods in the `PassedParameters` class. This commit: * Updates the documentation and the exception message to reflect this. * Adds various tests with `exit` and `die`. --- PHPCSUtils/Utils/PassedParameters.php | 7 +++- .../GetParametersNamedTest.inc | 3 ++ .../GetParametersNamedTest.php | 14 +++++++ .../GetParametersSkipShortArrayCheckTest.php | 4 +- .../PassedParameters/GetParametersTest.inc | 3 ++ .../PassedParameters/GetParametersTest.php | 12 ++++++ .../PassedParameters/HasParametersTest.inc | 19 ++++++++++ .../PassedParameters/HasParametersTest.php | 38 +++++++++++++++++-- 8 files changed, 93 insertions(+), 7 deletions(-) diff --git a/PHPCSUtils/Utils/PassedParameters.php b/PHPCSUtils/Utils/PassedParameters.php index 5ed7c849..58d170ff 100644 --- a/PHPCSUtils/Utils/PassedParameters.php +++ b/PHPCSUtils/Utils/PassedParameters.php @@ -65,6 +65,9 @@ final class PassedParameters * a short array opener. * - If passed a `T_ISSET` or `T_UNSET` stack pointer, it will detect whether those * language constructs have "parameters". + * - If passed a `T_EXIT` stack pointer, it will treat it as a function call and detect whether + * it has been passed parameters. When the `T_EXIT` is used as a constant, the return value + * will be `false` (no parameters). * * @since 1.0.0 * @@ -94,7 +97,7 @@ public static function hasParameters(File $phpcsFile, $stackPtr, $isShortArray = throw OutOfBoundsStackPtr::create(2, '$stackPtr', $stackPtr); } - $acceptedTokens = 'function call, array, isset or unset'; + $acceptedTokens = 'function call, array, isset, unset or exit'; if (isset(Collections::parameterPassingTokens()[$tokens[$stackPtr]['code']]) === false) { throw UnexpectedTokenType::create(2, '$stackPtr', $acceptedTokens, $tokens[$stackPtr]['type']); } @@ -131,7 +134,7 @@ public static function hasParameters(File $phpcsFile, $stackPtr, $isShortArray = return true; } - // Deal with function calls, long arrays, isset and unset. + // Deal with function calls, long arrays, isset, unset and exit/die. // Next non-empty token should be the open parenthesis. if ($tokens[$next]['code'] !== \T_OPEN_PARENTHESIS) { return false; diff --git a/Tests/Utils/PassedParameters/GetParametersNamedTest.inc b/Tests/Utils/PassedParameters/GetParametersNamedTest.inc index 61cb163a..3be08af9 100644 --- a/Tests/Utils/PassedParameters/GetParametersNamedTest.inc +++ b/Tests/Utils/PassedParameters/GetParametersNamedTest.inc @@ -53,6 +53,9 @@ foo( label: $cond ? true : false, more: $cond ? CONSTANT_A : CONSTANT_B ); /* testTernaryWithFunctionCallsInThenElse */ echo $cond ? foo( label: $something ) : /* testTernaryWithFunctionCallsInElse */ bar( more: $something_else ); +/* testExitWithNamedParam */ +exit( status: 1 ); + /* testCompileErrorNamedBeforePositional */ // Not the concern of PHPCSUtils. Should still be handled. test(param: $bar, $foo); diff --git a/Tests/Utils/PassedParameters/GetParametersNamedTest.php b/Tests/Utils/PassedParameters/GetParametersNamedTest.php index 78916dd8..a91bbaec 100644 --- a/Tests/Utils/PassedParameters/GetParametersNamedTest.php +++ b/Tests/Utils/PassedParameters/GetParametersNamedTest.php @@ -426,6 +426,20 @@ public static function dataGetParameters() ], ], ], + 'named-args-for-exit' => [ + 'testMarker' => '/* testExitWithNamedParam */', + 'targetType' => \T_EXIT, + 'expected' => [ + 'status' => [ + 'name' => 'status', + 'name_token' => 3, + 'start' => 5, + 'end' => 7, + 'raw' => '1', + ], + ], + ], + 'named-args-compile-error-named-before-positional' => [ 'testMarker' => '/* testCompileErrorNamedBeforePositional */', 'targetType' => \T_STRING, diff --git a/Tests/Utils/PassedParameters/GetParametersSkipShortArrayCheckTest.php b/Tests/Utils/PassedParameters/GetParametersSkipShortArrayCheckTest.php index 41ae2e57..dc0ccbd7 100644 --- a/Tests/Utils/PassedParameters/GetParametersSkipShortArrayCheckTest.php +++ b/Tests/Utils/PassedParameters/GetParametersSkipShortArrayCheckTest.php @@ -45,7 +45,7 @@ public function testHasParametersDontSkipShortArrayCheck($testMarker, $targetTyp if ($expectException === true) { $this->expectException('PHPCSUtils\Exceptions\UnexpectedTokenType'); $this->expectExceptionMessage( - 'Argument #2 ($stackPtr) must be of type function call, array, isset or unset;' + 'Argument #2 ($stackPtr) must be of type function call, array, isset, unset or exit;' ); } @@ -96,7 +96,7 @@ public function testGetParametersSkipShortArrayCheck($testMarker, $targetType, $ if ($targetType === \T_OPEN_SQUARE_BRACKET) { $this->expectException('PHPCSUtils\Exceptions\UnexpectedTokenType'); $this->expectExceptionMessage( - 'Argument #2 ($stackPtr) must be of type function call, array, isset or unset;' + 'Argument #2 ($stackPtr) must be of type function call, array, isset, unset or exit;' ); } diff --git a/Tests/Utils/PassedParameters/GetParametersTest.inc b/Tests/Utils/PassedParameters/GetParametersTest.inc index 3295cf8b..56c1379d 100644 --- a/Tests/Utils/PassedParameters/GetParametersTest.inc +++ b/Tests/Utils/PassedParameters/GetParametersTest.inc @@ -120,6 +120,9 @@ if ( isset( /* testUnset */ unset( $variable, $object->property, static::$property, $array[$name], ); +/* testDie */ +die( $status ); + /* testAnonClass */ $anon = new class( $param1, $param2 ) { public function __construct($param1, $param2) {} diff --git a/Tests/Utils/PassedParameters/GetParametersTest.php b/Tests/Utils/PassedParameters/GetParametersTest.php index 8e435ead..61a5c6ea 100644 --- a/Tests/Utils/PassedParameters/GetParametersTest.php +++ b/Tests/Utils/PassedParameters/GetParametersTest.php @@ -523,6 +523,18 @@ public function test( $foo, $bar ) { ], ], ], + 'die' => [ + 'testMarker' => '/* testDie */', + 'targetType' => \T_EXIT, + 'expected' => [ + 1 => [ + 'start' => 2, + 'end' => 4, + 'raw' => '$status', + ], + ], + ], + 'anon-class' => [ 'testMarker' => '/* testAnonClass */', 'targetType' => \T_ANON_CLASS, diff --git a/Tests/Utils/PassedParameters/HasParametersTest.inc b/Tests/Utils/PassedParameters/HasParametersTest.inc index f02a6ab5..012b15d8 100644 --- a/Tests/Utils/PassedParameters/HasParametersTest.inc +++ b/Tests/Utils/PassedParameters/HasParametersTest.inc @@ -19,6 +19,13 @@ class Foo { /* testShortListNotShortArray */ [ $a, $b ] = $array; +/* testExitAsConstant */ +exit; + +/* testDieAsConstant */ +die; + + // Function calls: no parameters. /* testNoParamsFunctionCall1 */ @@ -163,6 +170,18 @@ unset( ); +/* testNoParamsExit */ +exit(); + +/* testHasParamsExit */ +exit( $code ); + +/* testNoParamsDie */ +die( /*comment*/ ); + +/* testHasParamsDie */ +die( $status ); + /* testNoParamsNoParensAnonClass */ $anon = new class extends FooBar {}; diff --git a/Tests/Utils/PassedParameters/HasParametersTest.php b/Tests/Utils/PassedParameters/HasParametersTest.php index 74d53dcc..333de8ec 100644 --- a/Tests/Utils/PassedParameters/HasParametersTest.php +++ b/Tests/Utils/PassedParameters/HasParametersTest.php @@ -62,7 +62,7 @@ public function testNotAnAcceptedTokenException() { $this->expectException('PHPCSUtils\Exceptions\UnexpectedTokenType'); $this->expectExceptionMessage( - 'Argument #2 ($stackPtr) must be of type function call, array, isset or unset;' + 'Argument #2 ($stackPtr) must be of type function call, array, isset, unset or exit;' ); $interface = $this->getTargetToken('/* testNotAnAcceptedToken */', \T_INTERFACE); @@ -83,7 +83,7 @@ public function testNotACallToConstructor($testMarker, $targetType) { $this->expectException('PHPCSUtils\Exceptions\UnexpectedTokenType'); $this->expectExceptionMessage( - 'Argument #2 ($stackPtr) must be of type function call, array, isset or unset;' + 'Argument #2 ($stackPtr) must be of type function call, array, isset, unset or exit;' ); $self = $this->getTargetToken($testMarker, $targetType); @@ -124,7 +124,7 @@ public function testNotAShortArray() { $this->expectException('PHPCSUtils\Exceptions\UnexpectedTokenType'); $this->expectExceptionMessage( - 'Argument #2 ($stackPtr) must be of type function call, array, isset or unset;' + 'Argument #2 ($stackPtr) must be of type function call, array, isset, unset or exit;' ); $self = $this->getTargetToken( @@ -387,6 +387,38 @@ public static function dataHasParameters() 'expected' => true, ], + // Exit/die. + 'exit as a constant' => [ + 'testMarker' => '/* testExitAsConstant */', + 'targetType' => \T_EXIT, + 'expected' => false, + ], + 'die as a constant' => [ + 'testMarker' => '/* testDieAsConstant */', + 'targetType' => \T_EXIT, + 'expected' => false, + ], + 'no-params-exit' => [ + 'testMarker' => '/* testNoParamsExit */', + 'targetType' => \T_EXIT, + 'expected' => false, + ], + 'has-params-exit' => [ + 'testMarker' => '/* testHasParamsExit */', + 'targetType' => \T_EXIT, + 'expected' => true, + ], + 'no-params-die' => [ + 'testMarker' => '/* testNoParamsDie */', + 'targetType' => \T_EXIT, + 'expected' => false, + ], + 'has-params-die' => [ + 'testMarker' => '/* testHasParamsDie */', + 'targetType' => \T_EXIT, + 'expected' => true, + ], + // Anonymous class instantiation. 'no-params-no-parens-anon-class' => [ 'testMarker' => '/* testNoParamsNoParensAnonClass */',