From f2ea5c76dfe9c31260e81a901712e91673428f02 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 10 May 2025 19:13:03 +0200 Subject: [PATCH 01/14] GH Actions: update for new upstream 4.x branch --- .github/workflows/test.yml | 14 +++++++------- Tests/BackCompat/Helper/GetVersionTest.php | 2 +- composer.json | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 65616d21..21ff2396 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -108,13 +108,13 @@ jobs: experimental: true - php: '7.4' - phpcs_version: '4.0.x-dev' + phpcs_version: '4.x-dev' risky: false experimental: true # Run risky tests separately. - php: '7.4' - phpcs_version: '4.0.x-dev' + phpcs_version: '4.x-dev' risky: true experimental: true @@ -151,7 +151,7 @@ jobs: - name: Setup ini config id: set_ini run: | - if [[ "${{ matrix.phpcs_version }}" != "dev-master" && "${{ matrix.phpcs_version }}" != "4.0.x-dev" ]]; then + if [[ "${{ matrix.phpcs_version }}" != "dev-master" && "${{ matrix.phpcs_version }}" != "4.x-dev" ]]; then echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On' >> "$GITHUB_OUTPUT" else echo 'PHP_INI=error_reporting=-1, display_errors=On' >> "$GITHUB_OUTPUT" @@ -214,7 +214,7 @@ jobs: if: ${{ matrix.risky == false }} run: vendor/bin/phpunit -c ${{ steps.phpunit_config.outputs.FILE }} --no-coverage env: - PHPCS_VERSION: ${{ matrix.phpcs_version == '4.0.x-dev' && '4.0.0' || matrix.phpcs_version }} + PHPCS_VERSION: ${{ matrix.phpcs_version == '4.x-dev' && '4.0.0' || matrix.phpcs_version }} PHPCSUTILS_USE_CACHE: false - name: Run the unit tests with caching (non-risky) @@ -223,7 +223,7 @@ jobs: vendor/bin/phpunit -c ${{ steps.phpunit_config.outputs.FILE }} --testsuite PHPCSUtils --no-coverage ${{ steps.phpunit_config.outputs.EXTRA_ARGS }} env: - PHPCS_VERSION: ${{ matrix.phpcs_version == '4.0.x-dev' && '4.0.0' || matrix.phpcs_version }} + PHPCS_VERSION: ${{ matrix.phpcs_version == '4.x-dev' && '4.0.0' || matrix.phpcs_version }} PHPCSUTILS_USE_CACHE: true # Only run the "compare with PHPCS" group against dev-master as it ensures that PHPCSUtils @@ -233,7 +233,7 @@ jobs: # "nothing" is excluded to force PHPUnit to ignore the settings in phpunit.xml.dist. run: vendor/bin/phpunit -c ${{ steps.phpunit_config.outputs.FILE }} --no-coverage --group compareWithPHPCS --exclude-group nothing env: - PHPCS_VERSION: ${{ matrix.phpcs_version == '4.0.x-dev' && '4.0.0' || matrix.phpcs_version }} + PHPCS_VERSION: ${{ matrix.phpcs_version == '4.x-dev' && '4.0.0' || matrix.phpcs_version }} # Run the "xtra" group against high and low PHPCS as these are tests safeguarding PHPCS itself. - name: Run the unit tests (risky, xtra) @@ -241,7 +241,7 @@ jobs: # "nothing" is excluded to force PHPUnit to ignore the settings in phpunit.xml.dist. run: vendor/bin/phpunit -c ${{ steps.phpunit_config.outputs.FILE }} --no-coverage --group xtra --exclude-group nothing env: - PHPCS_VERSION: ${{ matrix.phpcs_version == '4.0.x-dev' && '4.0.0' || matrix.phpcs_version }} + PHPCS_VERSION: ${{ matrix.phpcs_version == '4.x-dev' && '4.0.0' || matrix.phpcs_version }} #### CODE COVERAGE STAGE #### diff --git a/Tests/BackCompat/Helper/GetVersionTest.php b/Tests/BackCompat/Helper/GetVersionTest.php index 1098969d..77a36da6 100644 --- a/Tests/BackCompat/Helper/GetVersionTest.php +++ b/Tests/BackCompat/Helper/GetVersionTest.php @@ -54,7 +54,7 @@ public function testGetVersion() if ($expected === 'dev-master') { $this->assertTrue(\version_compare(self::DEVMASTER, $result, '<=')); - } elseif ($expected === '4.0.x-dev@dev') { + } elseif ($expected === '4.x-dev') { $this->assertTrue(\version_compare('4.0.0', $result, '==')); } else { $this->assertSame($expected, $result); diff --git a/composer.json b/composer.json index 756001ce..03d4f652 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ }, "require" : { "php" : ">=5.4", - "squizlabs/php_codesniffer" : "^3.13.0 || 4.0.x-dev@dev", + "squizlabs/php_codesniffer" : "^3.13.0 || 4.x-dev@dev", "dealerdirect/phpcodesniffer-composer-installer" : "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0" }, "require-dev" : { From d434dccd2e30aa3de401b26ccb347fa049e9352c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 30 May 2025 18:44:00 +0200 Subject: [PATCH 02/14] BCTokens: sync with PHPCS 4.0 [1] / removed JS tokens PHPCSStandards/PHP_CodeSniffer 983 (JS/CSS support drop) removes the `T_ZSR_EQUAL`, `T__PROPERTY` and `T_OBJECT` tokens, including removing them from the `Tokens::$assignmentTokens`, `Tokens::$blockOpeners` and `Tokens::$scopeOpeners` token arrays. This commit aligns these token arrays in the `BCFile` class with their PHPCS 4.0 contents. Includes updated tests. --- PHPCSUtils/BackCompat/BCTokens.php | 83 +++++++++++++++- Tests/BackCompat/BCFile/GetConditionTest.php | 2 - .../BCTokens/AssignmentTokensTest.php | 86 +++++++++++++++++ .../BackCompat/BCTokens/BlockOpenersTest.php | 74 +++++++++++++++ .../BackCompat/BCTokens/ScopeOpenersTest.php | 95 +++++++++++++++++++ .../BCTokens/UnchangedTokenArraysTest.php | 70 -------------- 6 files changed, 335 insertions(+), 75 deletions(-) create mode 100644 Tests/BackCompat/BCTokens/AssignmentTokensTest.php create mode 100644 Tests/BackCompat/BCTokens/BlockOpenersTest.php create mode 100644 Tests/BackCompat/BCTokens/ScopeOpenersTest.php diff --git a/PHPCSUtils/BackCompat/BCTokens.php b/PHPCSUtils/BackCompat/BCTokens.php index 79f7b8a5..69a0713f 100644 --- a/PHPCSUtils/BackCompat/BCTokens.php +++ b/PHPCSUtils/BackCompat/BCTokens.php @@ -44,8 +44,6 @@ * @since 1.0.0 * * @method static array arithmeticTokens() Tokens that represent arithmetic operators. - * @method static array assignmentTokens() Tokens that represent assignments. - * @method static array blockOpeners() Tokens that open code blocks. * @method static array booleanOperators() Tokens that perform boolean operations. * @method static array bracketTokens() Tokens that represent brackets and parenthesis. * @method static array castTokens() Tokens that represent type casting. @@ -65,7 +63,6 @@ * @method static array phpcsCommentTokens() Tokens that are comments containing PHPCS * instructions. * @method static array scopeModifiers() Tokens that represent scope modifiers. - * @method static array scopeOpeners() Tokens that are allowed to open scopes. * @method static array stringTokens() Tokens that represent strings. * Note that `T_STRING`s are NOT represented in this * list as this list is about _text_ strings. @@ -98,6 +95,56 @@ public static function __callStatic($name, $args) throw InvalidTokenArray::create($name); } + /** + * Tokens that represent assignments. + * + * Retrieve the PHPCS assignments tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - PHPCS 4.0.0: The JS specific `T_ZSR_EQUAL` token is no longer available and has been removed from the array. + * + * @see \PHP_CodeSniffer\Util\Tokens::$assignmentTokens Original array. + * + * @since 1.0.0 + * + * @return array Token array. + */ + public static function assignmentTokens() + { + $tokens = Tokens::$assignmentTokens; + + if (\defined('T_ZSR_EQUAL') && isset($tokens[\T_ZSR_EQUAL])) { + unset($tokens[\T_ZSR_EQUAL]); + } + + return $tokens; + } + + /** + * Tokens that open code blocks. + * + * Retrieve the PHPCS block opener tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - PHPCS 4.0.0: The JS specific `T_OBJECT` token is no longer available and has been removed from the array. + * + * @see \PHP_CodeSniffer\Util\Tokens::$blockOpeners Original array. + * + * @since 1.0.0 + * + * @return array Token array. + */ + public static function blockOpeners() + { + $tokens = Tokens::$blockOpeners; + + if (\defined('T_OBJECT') && isset($tokens[\T_OBJECT])) { + unset($tokens[\T_OBJECT]); + } + + return $tokens; + } + /** * Tokens that represent the names of called functions. * @@ -120,4 +167,34 @@ public static function functionNameTokens() return $tokens; } + + /** + * Tokens that are allowed to open scopes. + * + * Retrieve the PHPCS scope opener tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - PHPCS 4.0.0: The JS specific `T_PROPERTY` and `T_OBJECT` tokens are no longer available + * and have been removed from the array. + * + * @see \PHP_CodeSniffer\Util\Tokens::$scopeOpeners Original array. + * + * @since 1.0.0 + * + * @return array Token array. + */ + public static function scopeOpeners() + { + $tokens = Tokens::$scopeOpeners; + + if (\defined('T_PROPERTY') && isset($tokens[\T_PROPERTY])) { + unset($tokens[\T_PROPERTY]); + } + + if (\defined('T_OBJECT') && isset($tokens[\T_OBJECT])) { + unset($tokens[\T_OBJECT]); + } + + return $tokens; + } } diff --git a/Tests/BackCompat/BCFile/GetConditionTest.php b/Tests/BackCompat/BCFile/GetConditionTest.php index c66b24e9..7ffb430a 100644 --- a/Tests/BackCompat/BCFile/GetConditionTest.php +++ b/Tests/BackCompat/BCFile/GetConditionTest.php @@ -116,8 +116,6 @@ class GetConditionTest extends UtilityMethodTestCase 'T_TRY' => false, 'T_CATCH' => false, 'T_FINALLY' => false, - 'T_PROPERTY' => false, - 'T_OBJECT' => false, 'T_USE' => false, ]; diff --git a/Tests/BackCompat/BCTokens/AssignmentTokensTest.php b/Tests/BackCompat/BCTokens/AssignmentTokensTest.php new file mode 100644 index 00000000..5fc44779 --- /dev/null +++ b/Tests/BackCompat/BCTokens/AssignmentTokensTest.php @@ -0,0 +1,86 @@ + \T_EQUAL, + \T_AND_EQUAL => \T_AND_EQUAL, + \T_OR_EQUAL => \T_OR_EQUAL, + \T_CONCAT_EQUAL => \T_CONCAT_EQUAL, + \T_DIV_EQUAL => \T_DIV_EQUAL, + \T_MINUS_EQUAL => \T_MINUS_EQUAL, + \T_POW_EQUAL => \T_POW_EQUAL, + \T_MOD_EQUAL => \T_MOD_EQUAL, + \T_MUL_EQUAL => \T_MUL_EQUAL, + \T_PLUS_EQUAL => \T_PLUS_EQUAL, + \T_XOR_EQUAL => \T_XOR_EQUAL, + \T_DOUBLE_ARROW => \T_DOUBLE_ARROW, + \T_SL_EQUAL => \T_SL_EQUAL, + \T_SR_EQUAL => \T_SR_EQUAL, + \T_COALESCE_EQUAL => \T_COALESCE_EQUAL, + ]; + + $this->assertSame($expected, BCTokens::assignmentTokens()); + } + + /** + * Test whether the method in BCTokens is still in sync with the latest version of PHPCS. + * + * This group is not run by default and has to be specifically requested to be run. + * + * @group compareWithPHPCS + * + * @return void + */ + public function testPHPCSAssignmentTokens() + { + $version = Helper::getVersion(); + + if (\version_compare($version, '3.99.99', '>') === true) { + $this->assertSame(Tokens::$assignmentTokens, BCTokens::assignmentTokens()); + } else { + /* + * Don't fail this test on the difference between PHPCS 4.x and 3.x. + * This test is only run against `dev-master` and `dev-master` is still PHPCS 3.x. + */ + $expected = Tokens::$assignmentTokens; + unset($expected[\T_ZSR_EQUAL]); + + $result = BCTokens::assignmentTokens(); + + $this->assertSame($expected, $result); + } + } +} diff --git a/Tests/BackCompat/BCTokens/BlockOpenersTest.php b/Tests/BackCompat/BCTokens/BlockOpenersTest.php new file mode 100644 index 00000000..0e438658 --- /dev/null +++ b/Tests/BackCompat/BCTokens/BlockOpenersTest.php @@ -0,0 +1,74 @@ + \T_OPEN_CURLY_BRACKET, + \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, + \T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS, + ]; + + $this->assertSame($expected, BCTokens::blockOpeners()); + } + + /** + * Test whether the method in BCTokens is still in sync with the latest version of PHPCS. + * + * This group is not run by default and has to be specifically requested to be run. + * + * @group compareWithPHPCS + * + * @return void + */ + public function testPHPCSBlockOpeners() + { + $version = Helper::getVersion(); + + if (\version_compare($version, '3.99.99', '>') === true) { + $this->assertSame(Tokens::$blockOpeners, BCTokens::blockOpeners()); + } else { + /* + * Don't fail this test on the difference between PHPCS 4.x and 3.x. + * This test is only run against `dev-master` and `dev-master` is still PHPCS 3.x. + */ + $expected = Tokens::$blockOpeners; + unset($expected[\T_OBJECT]); + + $result = BCTokens::blockOpeners(); + + $this->assertSame($expected, $result); + } + } +} diff --git a/Tests/BackCompat/BCTokens/ScopeOpenersTest.php b/Tests/BackCompat/BCTokens/ScopeOpenersTest.php new file mode 100644 index 00000000..071bc0e1 --- /dev/null +++ b/Tests/BackCompat/BCTokens/ScopeOpenersTest.php @@ -0,0 +1,95 @@ + \T_CLASS, + \T_ANON_CLASS => \T_ANON_CLASS, + \T_INTERFACE => \T_INTERFACE, + \T_TRAIT => \T_TRAIT, + \T_ENUM => \T_ENUM, + \T_NAMESPACE => \T_NAMESPACE, + \T_FUNCTION => \T_FUNCTION, + \T_CLOSURE => \T_CLOSURE, + \T_IF => \T_IF, + \T_SWITCH => \T_SWITCH, + \T_CASE => \T_CASE, + \T_DECLARE => \T_DECLARE, + \T_DEFAULT => \T_DEFAULT, + \T_WHILE => \T_WHILE, + \T_ELSE => \T_ELSE, + \T_ELSEIF => \T_ELSEIF, + \T_FOR => \T_FOR, + \T_FOREACH => \T_FOREACH, + \T_DO => \T_DO, + \T_TRY => \T_TRY, + \T_CATCH => \T_CATCH, + \T_FINALLY => \T_FINALLY, + \T_USE => \T_USE, + \T_MATCH => \T_MATCH, + ]; + + $this->assertSame($expected, BCTokens::scopeOpeners()); + } + + /** + * Test whether the method in BCTokens is still in sync with the latest version of PHPCS. + * + * This group is not run by default and has to be specifically requested to be run. + * + * @group compareWithPHPCS + * + * @return void + */ + public function testPHPCSScopeOpeners() + { + $version = Helper::getVersion(); + + if (\version_compare($version, '3.99.99', '>') === true) { + $this->assertSame(Tokens::$scopeOpeners, BCTokens::scopeOpeners()); + } else { + /* + * Don't fail this test on the difference between PHPCS 4.x and 3.x. + * This test is only run against `dev-master` and `dev-master` is still PHPCS 3.x. + */ + $expected = Tokens::$scopeOpeners; + unset($expected[\T_PROPERTY], $expected[\T_OBJECT]); + + $result = BCTokens::scopeOpeners(); + + $this->assertSame($expected, $result); + } + } +} diff --git a/Tests/BackCompat/BCTokens/UnchangedTokenArraysTest.php b/Tests/BackCompat/BCTokens/UnchangedTokenArraysTest.php index 52fde065..7f82798a 100644 --- a/Tests/BackCompat/BCTokens/UnchangedTokenArraysTest.php +++ b/Tests/BackCompat/BCTokens/UnchangedTokenArraysTest.php @@ -26,30 +26,6 @@ final class UnchangedTokenArraysTest extends TestCase { - /** - * Tokens that represent assignments. - * - * @var array - */ - private $assignmentTokens = [ - \T_EQUAL => \T_EQUAL, - \T_AND_EQUAL => \T_AND_EQUAL, - \T_OR_EQUAL => \T_OR_EQUAL, - \T_CONCAT_EQUAL => \T_CONCAT_EQUAL, - \T_DIV_EQUAL => \T_DIV_EQUAL, - \T_MINUS_EQUAL => \T_MINUS_EQUAL, - \T_POW_EQUAL => \T_POW_EQUAL, - \T_MOD_EQUAL => \T_MOD_EQUAL, - \T_MUL_EQUAL => \T_MUL_EQUAL, - \T_PLUS_EQUAL => \T_PLUS_EQUAL, - \T_XOR_EQUAL => \T_XOR_EQUAL, - \T_DOUBLE_ARROW => \T_DOUBLE_ARROW, - \T_SL_EQUAL => \T_SL_EQUAL, - \T_SR_EQUAL => \T_SR_EQUAL, - \T_COALESCE_EQUAL => \T_COALESCE_EQUAL, - \T_ZSR_EQUAL => \T_ZSR_EQUAL, - ]; - /** * Tokens that represent equality comparisons. * @@ -146,40 +122,6 @@ final class UnchangedTokenArraysTest extends TestCase \T_BINARY_CAST => \T_BINARY_CAST, ]; - /** - * Tokens that are allowed to open scopes. - * - * @var array - */ - private $scopeOpeners = [ - \T_CLASS => \T_CLASS, - \T_ANON_CLASS => \T_ANON_CLASS, - \T_INTERFACE => \T_INTERFACE, - \T_TRAIT => \T_TRAIT, - \T_ENUM => \T_ENUM, - \T_NAMESPACE => \T_NAMESPACE, - \T_FUNCTION => \T_FUNCTION, - \T_CLOSURE => \T_CLOSURE, - \T_IF => \T_IF, - \T_SWITCH => \T_SWITCH, - \T_CASE => \T_CASE, - \T_DECLARE => \T_DECLARE, - \T_DEFAULT => \T_DEFAULT, - \T_WHILE => \T_WHILE, - \T_ELSE => \T_ELSE, - \T_ELSEIF => \T_ELSEIF, - \T_FOR => \T_FOR, - \T_FOREACH => \T_FOREACH, - \T_DO => \T_DO, - \T_TRY => \T_TRY, - \T_CATCH => \T_CATCH, - \T_FINALLY => \T_FINALLY, - \T_PROPERTY => \T_PROPERTY, - \T_OBJECT => \T_OBJECT, - \T_USE => \T_USE, - \T_MATCH => \T_MATCH, - ]; - /** * Tokens that represent scope modifiers. * @@ -208,18 +150,6 @@ final class UnchangedTokenArraysTest extends TestCase \T_FINAL => \T_FINAL, ]; - /** - * Tokens that open code blocks. - * - * @var array - */ - private $blockOpeners = [ - \T_OPEN_CURLY_BRACKET => \T_OPEN_CURLY_BRACKET, - \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, - \T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS, - \T_OBJECT => \T_OBJECT, - ]; - /** * Tokens that don't represent code. * From 75ec55601fe8027483343330cb822637f03b3b81 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 30 May 2025 18:45:17 +0200 Subject: [PATCH 03/14] BCTokens: sync with PHPCS 4.0 [2] / anon class is function name token PHPCSStandards/PHP_CodeSniffer 47 adds the `T_ANON_CLASS` token to the `Tokens::$functionNameTokens` array. Includes updated tests. --- PHPCSUtils/BackCompat/BCTokens.php | 7 ++++--- Tests/BackCompat/BCTokens/FunctionNameTokensTest.php | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/PHPCSUtils/BackCompat/BCTokens.php b/PHPCSUtils/BackCompat/BCTokens.php index 69a0713f..8ec3a4c5 100644 --- a/PHPCSUtils/BackCompat/BCTokens.php +++ b/PHPCSUtils/BackCompat/BCTokens.php @@ -152,7 +152,7 @@ public static function blockOpeners() * * Changelog for the PHPCS native array: * - Introduced in PHPCS 2.3.3. - * - PHPCS 4.0.0: `T_NAME_QUALIFIED`, `T_NAME_FULLY_QUALIFIED` and `T_NAME_RELATIVE` added to the array. + * - PHPCS 4.0.0: `T_NAME_QUALIFIED`, `T_NAME_FULLY_QUALIFIED`, `T_NAME_RELATIVE` and `T_ANON_CLASS` added to the array. * * @see \PHP_CodeSniffer\Util\Tokens::$functionNameTokens Original array. * @@ -162,8 +162,9 @@ public static function blockOpeners() */ public static function functionNameTokens() { - $tokens = Tokens::$functionNameTokens; - $tokens += Collections::nameTokens(); + $tokens = Tokens::$functionNameTokens; + $tokens += Collections::nameTokens(); + $tokens[\T_ANON_CLASS] = \T_ANON_CLASS; return $tokens; } diff --git a/Tests/BackCompat/BCTokens/FunctionNameTokensTest.php b/Tests/BackCompat/BCTokens/FunctionNameTokensTest.php index 733aa00e..73d35ede 100644 --- a/Tests/BackCompat/BCTokens/FunctionNameTokensTest.php +++ b/Tests/BackCompat/BCTokens/FunctionNameTokensTest.php @@ -51,6 +51,7 @@ public function testFunctionNameTokens() \T_NAME_QUALIFIED => \T_NAME_QUALIFIED, \T_NAME_FULLY_QUALIFIED => \T_NAME_FULLY_QUALIFIED, \T_NAME_RELATIVE => \T_NAME_RELATIVE, + \T_ANON_CLASS => \T_ANON_CLASS, ]; \asort($expected); @@ -85,6 +86,7 @@ public function testPHPCSFunctionNameTokens() $expected[\T_NAME_QUALIFIED] = \T_NAME_QUALIFIED; $expected[\T_NAME_FULLY_QUALIFIED] = \T_NAME_FULLY_QUALIFIED; $expected[\T_NAME_RELATIVE] = \T_NAME_RELATIVE; + $expected[\T_ANON_CLASS] = \T_ANON_CLASS; \asort($expected); From bb46cd4bd0a24222a11ddbb5e33612b7af19792f Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 2 Jun 2025 02:32:31 +0200 Subject: [PATCH 04/14] BCTokens: sync with PHPCS 4.0 [3] / closure use is parenthesis owner PHPCSStandards/PHP_CodeSniffer 1013 made `T_USE` for closure use a parenthesis owner. While the token will now be included in the `BCTokens::parenthesisOpeners()` return value, while on PHPCS 3.x, it will still not be a parentheses owner. Use the methods in the `Parentheses` class to work out whether `T_USE` is actually a parenthesis owner in a PHPCS cross-version compatible manner (commit upcoming). Includes updated tests. --- PHPCSUtils/BackCompat/BCTokens.php | 33 ++++++++++++++++++- .../BCTokens/ParenthesisOpenersTest.php | 28 ++++++++++++---- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/PHPCSUtils/BackCompat/BCTokens.php b/PHPCSUtils/BackCompat/BCTokens.php index 8ec3a4c5..7f549115 100644 --- a/PHPCSUtils/BackCompat/BCTokens.php +++ b/PHPCSUtils/BackCompat/BCTokens.php @@ -59,7 +59,6 @@ * @method static array methodPrefixes() Tokens that can prefix a method name. * @method static array ooScopeTokens() Tokens that open class and object scopes. * @method static array operators() Tokens that perform operations. - * @method static array parenthesisOpeners() Token types that open parenthesis. * @method static array phpcsCommentTokens() Tokens that are comments containing PHPCS * instructions. * @method static array scopeModifiers() Tokens that represent scope modifiers. @@ -169,6 +168,38 @@ public static function functionNameTokens() return $tokens; } + /** + * Token types that open parentheses. + * + * Retrieve the PHPCS parenthesis openers tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - Introduced in PHPCS 0.0.5. + * - PHPCS 4.0.0: `T_USE` added to the array. + * + * Note: While `T_USE` will be included in the return value for this method, the + * associated parentheses will not have the `'parenthesis_owner'` index set + * until PHPCS 4.0.0. Use the {@see \PHPCSUtils\Utils\Parentheses::getOwner()} + * or {@see \PHPCSUtils\Utils\Parentheses::hasOwner()} methods if you need to check + * for a `T_USE` parentheses owner. + * + * @see \PHP_CodeSniffer\Util\Tokens::$parenthesisOpeners Original array. + * @see \PHPCSUtils\Utils\Parentheses Class holding utility methods for + * working with the `'parenthesis_...'` + * index keys in a token array. + * + * @since 1.0.0 + * + * @return array Token array. + */ + public static function parenthesisOpeners() + { + $tokens = Tokens::$parenthesisOpeners; + $tokens[\T_USE] = \T_USE; + + return $tokens; + } + /** * Tokens that are allowed to open scopes. * diff --git a/Tests/BackCompat/BCTokens/ParenthesisOpenersTest.php b/Tests/BackCompat/BCTokens/ParenthesisOpenersTest.php index dd409f28..917b33b9 100644 --- a/Tests/BackCompat/BCTokens/ParenthesisOpenersTest.php +++ b/Tests/BackCompat/BCTokens/ParenthesisOpenersTest.php @@ -18,7 +18,7 @@ /** * Test class. * - * @covers \PHPCSUtils\BackCompat\BCTokens::__callStatic + * @covers \PHPCSUtils\BackCompat\BCTokens::parenthesisOpeners * * @group tokens * @@ -34,12 +34,12 @@ final class ParenthesisOpenersTest extends TestCase */ public function testParenthesisOpeners() { - $version = Helper::getVersion(); $expected = [ \T_ARRAY => \T_ARRAY, \T_LIST => \T_LIST, \T_FUNCTION => \T_FUNCTION, \T_CLOSURE => \T_CLOSURE, + \T_USE => \T_USE, \T_ANON_CLASS => \T_ANON_CLASS, \T_WHILE => \T_WHILE, \T_FOR => \T_FOR, @@ -52,10 +52,6 @@ public function testParenthesisOpeners() \T_MATCH => \T_MATCH, ]; - if (\version_compare($version, '3.99.99', '>=') === true) { - $expected[\T_USE] = \T_USE; - } - \asort($expected); $result = BCTokens::parenthesisOpeners(); @@ -75,6 +71,24 @@ public function testParenthesisOpeners() */ public function testPHPCSParenthesisOpeners() { - $this->assertSame(Tokens::$parenthesisOpeners, BCTokens::parenthesisOpeners()); + $version = Helper::getVersion(); + + if (\version_compare($version, '3.99.99', '>') === true) { + $this->assertSame(Tokens::$parenthesisOpeners, BCTokens::parenthesisOpeners()); + } else { + /* + * Don't fail this test on the difference between PHPCS 4.x and 3.x. + * This test is only run against `dev-master` and `dev-master` is still PHPCS 3.x. + */ + $expected = Tokens::$parenthesisOpeners; + $expected[\T_USE] = \T_USE; + + \asort($expected); + + $result = BCTokens::parenthesisOpeners(); + \asort($result); + + $this->assertSame($expected, $result); + } } } From 0ba978d6bd62095851250398b463dd877c66aecf Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 30 May 2025 18:46:31 +0200 Subject: [PATCH 05/14] BCTokens: sync with PHPCS 4.0 [4] / more parenthesis owners PHPCSStandards/PHP_CodeSniffer 1014 made `T_ISSET`, `T_UNSET`, `T_EMPTY`, `T_EVAL` and `T_EXIT` parenthesis owners. While the token will now be included in the `BCTokens::parenthesisOpeners()` return value, while on PHPCS 3.x, they will still not be parentheses owners. Use the methods in the `Parentheses` class to work out whether any of these tokens is actually a parenthesis owner in a PHPCS cross-version compatible manner. Note: The `Parentheses` methods have supported these tokens as parenthesis owners since PHPCSUtils 1.0.0. Includes updated tests. --- PHPCSUtils/BackCompat/BCTokens.php | 21 ++++++++++++------- .../BCTokens/ParenthesisOpenersTest.php | 14 +++++++++++-- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/PHPCSUtils/BackCompat/BCTokens.php b/PHPCSUtils/BackCompat/BCTokens.php index 7f549115..40ccc158 100644 --- a/PHPCSUtils/BackCompat/BCTokens.php +++ b/PHPCSUtils/BackCompat/BCTokens.php @@ -175,13 +175,13 @@ public static function functionNameTokens() * * Changelog for the PHPCS native array: * - Introduced in PHPCS 0.0.5. - * - PHPCS 4.0.0: `T_USE` added to the array. + * - PHPCS 4.0.0: `T_USE` (for closures), `T_ISSET`, `T_UNSET`, `T_EMPTY`, `T_EVAL` and `T_EXIT` added to the array. * - * Note: While `T_USE` will be included in the return value for this method, the - * associated parentheses will not have the `'parenthesis_owner'` index set - * until PHPCS 4.0.0. Use the {@see \PHPCSUtils\Utils\Parentheses::getOwner()} - * or {@see \PHPCSUtils\Utils\Parentheses::hasOwner()} methods if you need to check - * for a `T_USE` parentheses owner. + * **Important**: While `T_USE`, `T_ISSET`, `T_UNSET`, `T_EMPTY`, `T_EVAL` and `T_EXIT` will be included + * in the return value for this method, the associated parentheses will not have the `'parenthesis_owner'` index + * set until PHPCS 4.0.0. + * Use the {@see \PHPCSUtils\Utils\Parentheses::getOwner()} or {@see \PHPCSUtils\Utils\Parentheses::hasOwner()} methods + * if you need to check for whether any of these tokens are a parentheses owner. * * @see \PHP_CodeSniffer\Util\Tokens::$parenthesisOpeners Original array. * @see \PHPCSUtils\Utils\Parentheses Class holding utility methods for @@ -194,8 +194,13 @@ public static function functionNameTokens() */ public static function parenthesisOpeners() { - $tokens = Tokens::$parenthesisOpeners; - $tokens[\T_USE] = \T_USE; + $tokens = Tokens::$parenthesisOpeners; + $tokens[\T_USE] = \T_USE; + $tokens[\T_ISSET] = \T_ISSET; + $tokens[\T_UNSET] = \T_UNSET; + $tokens[\T_EMPTY] = \T_EMPTY; + $tokens[\T_EVAL] = \T_EVAL; + $tokens[\T_EXIT] = \T_EXIT; return $tokens; } diff --git a/Tests/BackCompat/BCTokens/ParenthesisOpenersTest.php b/Tests/BackCompat/BCTokens/ParenthesisOpenersTest.php index 917b33b9..f2cffeec 100644 --- a/Tests/BackCompat/BCTokens/ParenthesisOpenersTest.php +++ b/Tests/BackCompat/BCTokens/ParenthesisOpenersTest.php @@ -50,6 +50,11 @@ public function testParenthesisOpeners() \T_CATCH => \T_CATCH, \T_DECLARE => \T_DECLARE, \T_MATCH => \T_MATCH, + \T_ISSET => \T_ISSET, + \T_EMPTY => \T_EMPTY, + \T_UNSET => \T_UNSET, + \T_EVAL => \T_EVAL, + \T_EXIT => \T_EXIT, ]; \asort($expected); @@ -80,8 +85,13 @@ public function testPHPCSParenthesisOpeners() * Don't fail this test on the difference between PHPCS 4.x and 3.x. * This test is only run against `dev-master` and `dev-master` is still PHPCS 3.x. */ - $expected = Tokens::$parenthesisOpeners; - $expected[\T_USE] = \T_USE; + $expected = Tokens::$parenthesisOpeners; + $expected[\T_USE] = \T_USE; + $expected[\T_ISSET] = \T_ISSET; + $expected[\T_EMPTY] = \T_EMPTY; + $expected[\T_UNSET] = \T_UNSET; + $expected[\T_EVAL] = \T_EVAL; + $expected[\T_EXIT] = \T_EXIT; \asort($expected); From a51914c4047f246c5f6582dade66f760b6fff900 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 30 May 2025 18:48:19 +0200 Subject: [PATCH 06/14] BCTokens: sync with PHPCS 4.0 [5] / namespaced name tokens PHPCSStandards/PHP_CodeSniffer 1020 (namespaced names) adds a new `Tokens::NAME_TOKENS` token array and also adds these tokens to the `Tokens::$functionNameTokens` token array. This last bit was already handled previously. This commit now adds the mirror function for the `Tokens::NAME_TOKENS` token array. Includes updated tests. --- PHPCSUtils/BackCompat/BCTokens.php | 19 ++++++ Tests/BackCompat/BCTokens/NameTokensTest.php | 64 ++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 Tests/BackCompat/BCTokens/NameTokensTest.php diff --git a/PHPCSUtils/BackCompat/BCTokens.php b/PHPCSUtils/BackCompat/BCTokens.php index 40ccc158..49b8c155 100644 --- a/PHPCSUtils/BackCompat/BCTokens.php +++ b/PHPCSUtils/BackCompat/BCTokens.php @@ -168,6 +168,25 @@ public static function functionNameTokens() return $tokens; } + /** + * Tokens used for "names", be it namespace, OO, function or constant names. + * + * Retrieve the PHPCS name tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - Introduced in PHPCS 4.0.0. + * + * @see \PHP_CodeSniffer\Util\Tokens::NAME_TOKENS Original array. + * + * @since 1.1.0 + * + * @return array Token array. + */ + public static function nameTokens() + { + return Collections::nameTokens(); + } + /** * Token types that open parentheses. * diff --git a/Tests/BackCompat/BCTokens/NameTokensTest.php b/Tests/BackCompat/BCTokens/NameTokensTest.php new file mode 100644 index 00000000..1cb13d8f --- /dev/null +++ b/Tests/BackCompat/BCTokens/NameTokensTest.php @@ -0,0 +1,64 @@ + \T_STRING, + \T_NAME_QUALIFIED => \T_NAME_QUALIFIED, + \T_NAME_FULLY_QUALIFIED => \T_NAME_FULLY_QUALIFIED, + \T_NAME_RELATIVE => \T_NAME_RELATIVE, + ]; + + $this->assertSame($expected, BCTokens::nameTokens()); + } + + /** + * Test whether the method in BCTokens is still in sync with the latest version of PHPCS. + * + * This group is not run by default and has to be specifically requested to be run. + * + * @group compareWithPHPCS + * + * @return void + */ + public function testPHPCSNameTokens() + { + if (\version_compare(Helper::getVersion(), '3.99.99', '>') === false) { + $this->markTestSkipped('Test only applicable to PHPCS >= 4.x'); + } + + $this->assertSame(Tokens::NAME_TOKENS, BCTokens::nameTokens()); // @phpstan-ignore classConstant.notFound + } +} From b7ba1b804f7e08cb56fb6991521cad92fcbf28b0 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 12 May 2025 06:42:41 +0200 Subject: [PATCH 07/14] UtilityMethodTestCase: make compatible with PHPCS 4.0 This commit introduces a test double for the PHPCS native `Ruleset` class. PHPCS >= 4.0 is stricter about registering sniffs, so when "mocking" a registered sniff, it will throw a "No sniffs were registered." `RuntimeException`. Most utility method tests don't need for any sniffs to be registered, they just need a `Ruleset` object to be available to allow for creating a `File` object. This test double accommodates that by catching the exception. --- PHPCSUtils/TestUtils/RulesetDouble.php | 57 ++++++++++++ .../TestUtils/UtilityMethodTestCase.php | 7 +- .../RulesetDouble/InvalidSniffRef.xml | 6 ++ .../RulesetDouble/RulesetDoubleTest.php | 86 +++++++++++++++++++ Tests/Utils/FilePath/GetNameTest.php | 4 +- Tests/Utils/FilePath/IsStdinTest.php | 4 +- 6 files changed, 157 insertions(+), 7 deletions(-) create mode 100644 PHPCSUtils/TestUtils/RulesetDouble.php create mode 100644 Tests/TestUtils/RulesetDouble/InvalidSniffRef.xml create mode 100644 Tests/TestUtils/RulesetDouble/RulesetDoubleTest.php diff --git a/PHPCSUtils/TestUtils/RulesetDouble.php b/PHPCSUtils/TestUtils/RulesetDouble.php new file mode 100644 index 00000000..10a3b6cb --- /dev/null +++ b/PHPCSUtils/TestUtils/RulesetDouble.php @@ -0,0 +1,57 @@ +getMessage()) !== 'ERROR: No sniffs were registered.') { + // Rethrow the exception to fail the test, as this is not the exception we expected. + throw $e; + } + } + } +} diff --git a/PHPCSUtils/TestUtils/UtilityMethodTestCase.php b/PHPCSUtils/TestUtils/UtilityMethodTestCase.php index 4853fbf0..4a02ca01 100644 --- a/PHPCSUtils/TestUtils/UtilityMethodTestCase.php +++ b/PHPCSUtils/TestUtils/UtilityMethodTestCase.php @@ -20,6 +20,7 @@ use PHPCSUtils\Exceptions\TestMarkerNotFound; use PHPCSUtils\Exceptions\TestTargetNotFound; use PHPCSUtils\TestUtils\ConfigDouble; +use PHPCSUtils\TestUtils\RulesetDouble; use PHPUnit\Framework\Attributes\AfterClass; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\BeforeClass; @@ -30,8 +31,7 @@ /** * Base class for use when testing utility methods for PHP_CodeSniffer. * - * This class is compatible with PHP_CodeSniffer 3.x and contains preliminary compatibility with 4.x - * based on its currently known state/roadmap. + * This class is compatible with PHP_CodeSniffer 3.x and 4.x. * * This class is compatible with {@link https://phpunit.de/ PHPUnit} 4.5 - 11.x providing the PHPCSUtils * autoload file is included in the test bootstrap. For more information about that, please consult @@ -110,6 +110,7 @@ * @since 1.0.0 * @since 1.0.7 Compatible with PHPUnit 10. * @since 1.1.0 Compatible with PHPUnit 11. + * @since 1.1.0 Compatible with PHP_CodeSniffer 4.0.0. */ abstract class UtilityMethodTestCase extends TestCase { @@ -226,7 +227,7 @@ public static function setUpTestFile() // Also set a tab-width to enable testing tab-replaced vs `orig_content`. $config->tabWidth = static::$tabWidth; - $ruleset = new Ruleset($config); + $ruleset = new RulesetDouble($config); self::$phpcsFile = self::parseFile($caseFile, $ruleset, $config); } diff --git a/Tests/TestUtils/RulesetDouble/InvalidSniffRef.xml b/Tests/TestUtils/RulesetDouble/InvalidSniffRef.xml new file mode 100644 index 00000000..cd0bbcb5 --- /dev/null +++ b/Tests/TestUtils/RulesetDouble/InvalidSniffRef.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/Tests/TestUtils/RulesetDouble/RulesetDoubleTest.php b/Tests/TestUtils/RulesetDouble/RulesetDoubleTest.php new file mode 100644 index 00000000..7b90a355 --- /dev/null +++ b/Tests/TestUtils/RulesetDouble/RulesetDoubleTest.php @@ -0,0 +1,86 @@ +sniffs = ['Generic.Files.ByteOrderMark']; + + $ruleset = new RulesetDouble($config); + + $this->assertInstanceOf('\PHP_CodeSniffer\Ruleset', $ruleset); + $this->assertCount(1, $ruleset->sniffs, 'Ruleset did not register exactly 1 sniff'); + } + + /** + * Verify that creating a ruleset object while limiting the ruleset to a sniff which doesn't exist, + * creates a Ruleset object without any sniffs registered and does not throw an exception. + * + * @return void + */ + public function testRegisteringSniffWhichDoesntExist() + { + $config = new ConfigDouble(); + + // Limit the ruleset to just one sniff. + $config->sniffs = ['Dummy.Dummy.Dummy']; + + $ruleset = new RulesetDouble($config); + + $this->assertInstanceOf('\PHP_CodeSniffer\Ruleset', $ruleset); + $this->assertCount(0, $ruleset->sniffs, 'Ruleset has registered a sniff, even though it doesn\'t exist'); + } + + /** + * Verify that if creating a ruleset object would lead to an exception other than the "no sniffs registered" + * exception, the exception will still be thrown. + * + * @return void + */ + public function testOtherExceptionsAreRethrown() + { + $message = 'ERROR: Referenced sniff "./MissingFile.xml" does not exist.' . \PHP_EOL; + $message .= 'ERROR: No sniffs were registered.' . \PHP_EOL . \PHP_EOL; + + $exception = 'PHP_CodeSniffer\Exceptions\RuntimeException'; + + $this->expectException($exception); + $this->expectExceptionMessage($message); + + $standard = __DIR__ . '/InvalidSniffRef.xml'; + $config = new ConfigDouble(["--standard=$standard"]); + new RulesetDouble($config); + } +} diff --git a/Tests/Utils/FilePath/GetNameTest.php b/Tests/Utils/FilePath/GetNameTest.php index 4d47391c..c6d29a7f 100644 --- a/Tests/Utils/FilePath/GetNameTest.php +++ b/Tests/Utils/FilePath/GetNameTest.php @@ -11,8 +11,8 @@ namespace PHPCSUtils\Tests\Utils\FilePath; use PHP_CodeSniffer\Files\DummyFile; -use PHP_CodeSniffer\Ruleset; use PHPCSUtils\TestUtils\ConfigDouble; +use PHPCSUtils\TestUtils\RulesetDouble; use PHPCSUtils\Utils\FilePath; use PHPUnit\Framework\TestCase; @@ -55,7 +55,7 @@ public static function setUpConfigRuleset() self::$config->sniffs = ['Dummy.Dummy.Dummy']; // Limiting it to just one (dummy) sniff. self::$config->cache = false; - self::$ruleset = new Ruleset(self::$config); + self::$ruleset = new RulesetDouble(self::$config); } /** diff --git a/Tests/Utils/FilePath/IsStdinTest.php b/Tests/Utils/FilePath/IsStdinTest.php index 50533d31..556bd918 100644 --- a/Tests/Utils/FilePath/IsStdinTest.php +++ b/Tests/Utils/FilePath/IsStdinTest.php @@ -11,8 +11,8 @@ namespace PHPCSUtils\Tests\Utils\FilePath; use PHP_CodeSniffer\Files\DummyFile; -use PHP_CodeSniffer\Ruleset; use PHPCSUtils\TestUtils\ConfigDouble; +use PHPCSUtils\TestUtils\RulesetDouble; use PHPCSUtils\Utils\FilePath; use PHPUnit\Framework\TestCase; @@ -55,7 +55,7 @@ public static function setUpConfigRuleset() self::$config->sniffs = ['Dummy.Dummy.Dummy']; // Limiting it to just one (dummy) sniff. self::$config->cache = false; - self::$ruleset = new Ruleset(self::$config); + self::$ruleset = new RulesetDouble(self::$config); } /** From cad314b9c82690e9e20190df033069b1f08aa503 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 11 May 2025 19:56:04 +0200 Subject: [PATCH 08/14] Tests: deal with difference in tokenization of PHP open tag in PHPCS 4.0 The whitespace which may be included in a long PHP open tag is tokenized as a separate `T_WHITESPACE` token as of PHPCS 4.0. This commit updates token position expectations to take this into account. Related to upstream: * PHPCSStandards/PHP_CodeSniffer 593 * PHPCSStandards/PHP_CodeSniffer 1015 --- Tests/BackCompat/BCFile/FindStartOfStatementTest.php | 4 +++- .../Fixers/SpacesFixer/SpacesFixerExceptionsTest.php | 8 +++++--- .../UtilityMethodTestCase/GetTargetTokenTest.php | 12 +++++++++++- .../UtilityMethodTestCase/SetUpTestFileTest.php | 9 ++++++++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Tests/BackCompat/BCFile/FindStartOfStatementTest.php b/Tests/BackCompat/BCFile/FindStartOfStatementTest.php index 09baa6b8..9eb39160 100644 --- a/Tests/BackCompat/BCFile/FindStartOfStatementTest.php +++ b/Tests/BackCompat/BCFile/FindStartOfStatementTest.php @@ -473,8 +473,10 @@ public function testNestedMatch() */ public function testOpenTag() { + $php8Names = parent::usesPhp8NameTokens(); + $start = $this->getTargetToken('/* testOpenTag */', T_OPEN_TAG); - $start += 2; + $start += ($php8Names === true) ? 3 : 2; $found = BCFile::findStartOfStatement(self::$phpcsFile, $start); $this->assertSame(($start - 1), $found); diff --git a/Tests/Fixers/SpacesFixer/SpacesFixerExceptionsTest.php b/Tests/Fixers/SpacesFixer/SpacesFixerExceptionsTest.php index 7327aa65..264a96b8 100644 --- a/Tests/Fixers/SpacesFixer/SpacesFixerExceptionsTest.php +++ b/Tests/Fixers/SpacesFixer/SpacesFixerExceptionsTest.php @@ -89,8 +89,9 @@ public function testFirstTokenWhitespace() $this->expectException('PHPCSUtils\Exceptions\UnexpectedTokenType'); $this->expectExceptionMessage('Argument #2 ($stackPtr) must be of type any, except whitespace;'); - $stackPtr = $this->getTargetToken('/* testPassingWhitespace1 */', \T_WHITESPACE); - SpacesFixer::checkAndFix(self::$phpcsFile, $stackPtr, 10, 0, 'Dummy'); + $stackPtr = $this->getTargetToken('/* testPassingWhitespace1 */', \T_WHITESPACE); + $secondPtr = $this->getTargetToken('/* testPassingWhitespace1 */', \T_ECHO); + SpacesFixer::checkAndFix(self::$phpcsFile, $stackPtr, $secondPtr, 0, 'Dummy'); } /** @@ -103,8 +104,9 @@ public function testSecondTokenWhitespace() $this->expectException('PHPCSUtils\Exceptions\UnexpectedTokenType'); $this->expectExceptionMessage('Argument #3 ($secondPtr) must be of type any, except whitespace;'); + $stackPtr = $this->getTargetToken('/* testPassingWhitespace1 */', \T_CONSTANT_ENCAPSED_STRING); $secondPtr = $this->getTargetToken('/* testPassingWhitespace2 */', \T_WHITESPACE); - SpacesFixer::checkAndFix(self::$phpcsFile, 10, $secondPtr, 0, 'Dummy'); + SpacesFixer::checkAndFix(self::$phpcsFile, $stackPtr, $secondPtr, 0, 'Dummy'); } /** diff --git a/Tests/TestUtils/UtilityMethodTestCase/GetTargetTokenTest.php b/Tests/TestUtils/UtilityMethodTestCase/GetTargetTokenTest.php index 4f2814c7..7fe2839e 100644 --- a/Tests/TestUtils/UtilityMethodTestCase/GetTargetTokenTest.php +++ b/Tests/TestUtils/UtilityMethodTestCase/GetTargetTokenTest.php @@ -67,7 +67,8 @@ public function testGetTargetToken($expected, $commentString, $tokenType, $token */ public static function dataGetTargetToken() { - return [ + // Token offsets based on PHPCS 3.x tokenization. + $data = [ 'single-token-type' => [ 'expected' => 6, 'commentString' => '/* testFindingTarget */', @@ -120,6 +121,15 @@ public static function dataGetTargetToken() 'tokenContent' => "'bar'", ], ]; + + // The PHP open tag + whitespace is two tokens in PHPCS 4.x, while it is one in PHPCS 3.x. + if (parent::usesPhp8NameTokens() === true) { + foreach ($data as $key => $dataset) { + ++$data[$key]['expected']; + } + } + + return $data; } /** diff --git a/Tests/TestUtils/UtilityMethodTestCase/SetUpTestFileTest.php b/Tests/TestUtils/UtilityMethodTestCase/SetUpTestFileTest.php index faa92555..0454d131 100644 --- a/Tests/TestUtils/UtilityMethodTestCase/SetUpTestFileTest.php +++ b/Tests/TestUtils/UtilityMethodTestCase/SetUpTestFileTest.php @@ -75,7 +75,14 @@ public function testSetUp() $this->assertNotSame('0', self::$phpcsVersion, 'phpcsVersion was not set'); $this->assertInstanceOf('PHP_CodeSniffer\Files\File', self::$phpcsFile); - $this->assertSame(57, self::$phpcsFile->numTokens); + + if (parent::usesPhp8NameTokens() === true) { + // The PHP open tag + whitespace is two tokens in PHPCS 4.x. + $this->assertSame(58, self::$phpcsFile->numTokens); + } else { + // PHPCS 3.x. + $this->assertSame(57, self::$phpcsFile->numTokens); + } $tokens = self::$phpcsFile->getTokens(); $this->assertIsArray($tokens); From 57e89804776f4ed3a81fdeb9b8d578628880129e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 2 Jun 2025 02:33:35 +0200 Subject: [PATCH 09/14] Parentheses::getOwner(): T_USE is now a parenthesis owner `T_USE` tokens used for closure use statements are parentheses owners as of PHPCS 4.0.0. This commit updated the `Parentheses::getOwner()` method to treat closure `T_USE` tokens as parentheses owners, PHPCS cross-version. Includes additional unit tests for this case. Refs: * PHPCSStandards/PHP_CodeSniffer 1013 * squizlabs/PHP_CodeSniffer 2593 * squizlabs/PHP_CodeSniffer 3104 --- PHPCSUtils/Utils/Parentheses.php | 8 ++-- Tests/Utils/Parentheses/ParenthesesTest.inc | 5 +++ Tests/Utils/Parentheses/ParenthesesTest.php | 43 ++++++++++++++++++++- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/PHPCSUtils/Utils/Parentheses.php b/PHPCSUtils/Utils/Parentheses.php index 998b64f9..651ba410 100644 --- a/PHPCSUtils/Utils/Parentheses.php +++ b/PHPCSUtils/Utils/Parentheses.php @@ -17,8 +17,8 @@ * Utility functions for use when examining parenthesis tokens and arbitrary tokens wrapped * in parentheses. * - * In contrast to PHPCS natively, `isset()`, `unset()`, `empty()`, `exit()`, `die()` and `eval()` - * will be considered parentheses owners by the functions in this class. + * In contrast to PHPCS natively (< 4.0), `isset()`, `unset()`, `empty()`, `exit()`, `die()`, `eval()` + * and closure `use()` will be considered parentheses owners by the functions in this class. * * @since 1.0.0 */ @@ -29,8 +29,9 @@ final class Parentheses * Extra tokens which should be considered parentheses owners. * * - `T_ISSET`, `T_UNSET`, `T_EMPTY`, `T_EXIT` and `T_EVAL` are not PHPCS native parentheses - * owners, but are considered such for the purposes of this class. + * owners until PHPCS 4.0.0, but are considered such for the purposes of this class. * Also see {@link https://github.com/squizlabs/PHP_CodeSniffer/issues/3118 PHPCS#3118}. + * - `T_USE` when used for a closure use statement also became a parentheses owner in PHPCS 4.0.0. * * @since 1.0.0 * @@ -42,6 +43,7 @@ final class Parentheses \T_EMPTY => \T_EMPTY, \T_EXIT => \T_EXIT, \T_EVAL => \T_EVAL, + \T_USE => \T_USE, ]; /** diff --git a/Tests/Utils/Parentheses/ParenthesesTest.inc b/Tests/Utils/Parentheses/ParenthesesTest.inc index d3549c4d..d516b1b9 100644 --- a/Tests/Utils/Parentheses/ParenthesesTest.inc +++ b/Tests/Utils/Parentheses/ParenthesesTest.inc @@ -72,6 +72,11 @@ $m = match(count($a)) { default => false, }; +/* testClosureUseInArray */ +$array = array( + 'cl' => function () use /*comment*/ ($var) {}, +); + // Intentional parse error. This has to be the last test in the file. /* testParseError */ declare(ticks=1 diff --git a/Tests/Utils/Parentheses/ParenthesesTest.php b/Tests/Utils/Parentheses/ParenthesesTest.php index c97bdf96..51cbf105 100644 --- a/Tests/Utils/Parentheses/ParenthesesTest.php +++ b/Tests/Utils/Parentheses/ParenthesesTest.php @@ -192,6 +192,11 @@ final class ParenthesesTest extends UtilityMethodTestCase 'code' => \T_VARIABLE, 'content' => '$a', ], + 'testClosureUseInArray' => [ + 'marker' => '/* testClosureUseInArray */', + 'code' => \T_VARIABLE, + 'content' => '$var', + ], 'testParseError-1' => [ 'marker' => '/* testParseError */', 'code' => \T_LNUMBER, @@ -225,6 +230,7 @@ final class ParenthesesTest extends UtilityMethodTestCase 'T_LIST' => false, 'T_FUNCTION' => false, 'T_CLOSURE' => false, + 'T_USE' => false, 'T_ANON_CLASS' => false, 'T_WHILE' => false, 'T_FOR' => false, @@ -955,6 +961,24 @@ public static function dataWalkParentheses() 'lastIfElseOwner' => false, ], ], + 'testClosureUseInArray' => [ + 'testName' => 'testClosureUseInArray', + 'expectedResults' => [ + 'firstOpener' => -17, + 'firstCloser' => 7, + 'firstOwner' => -18, + // T_USE is not really a scope opener here, but it is what it is. + 'firstScopeOwnerOpener' => -1, + 'firstScopeOwnerCloser' => 1, + 'firstScopeOwnerOwner' => -5, + 'lastOpener' => -1, + 'lastCloser' => 1, + 'lastOwner' => -5, + 'lastArrayOpener' => -17, + 'lastFunctionCloser' => false, + 'lastIfElseOwner' => false, + ], + ], 'testParseError-1' => [ 'testName' => 'testParseError-1', 'expectedResults' => [ @@ -1198,6 +1222,13 @@ public static function dataHasOwner() 'T_MATCH' => true, ], ], + 'testClosureUseInArray' => [ + 'testName' => 'testClosureUseInArray', + 'expectedResults' => [ + 'T_ARRAY' => true, + 'T_USE' => true, + ], + ], 'testParseError-1' => [ 'testName' => 'testParseError-1', 'expectedResults' => [], @@ -1340,6 +1371,11 @@ public static function dataFirstOwnerIn() 'validOwners' => [\T_CATCH], 'expected' => false, ], + 'testClosureUseInArray' => [ + 'testName' => 'testClosureUseInArray', + 'validOwners' => [\T_USE], + 'expected' => false, + ], ]; } @@ -1371,7 +1407,7 @@ public function testLastOwnerIn($testName, $validOwners, $expected) * * @see testLastOwnerIn() For the array format. * - * @return array|false>> + * @return array|false>> */ public static function dataLastOwnerIn() { @@ -1462,6 +1498,11 @@ public static function dataLastOwnerIn() 'validOwners' => [\T_CATCH, \T_MATCH], 'expected' => false, ], + 'testClosureUseInArray' => [ + 'testName' => 'testClosureUseInArray', + 'validOwners' => [\T_USE], + 'expected' => -5, + ], ]; } } From 183b4589b5b2f939283f0904fcf3bb22f3e2905f Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 30 May 2025 02:43:47 +0200 Subject: [PATCH 10/14] BCFile::getMethodParameters(): sync with PHPCS 4.0 / T_USE is parenthesis owner This change was already previously handled in PHPCSUtils. This commit just syncs in the new tests as introduced upstream. Ref: PHPCSStandards/PHP_CodeSniffer 1013 --- .../GetMethodParametersParseError3Test.inc | 5 ++ .../GetMethodParametersParseError3Test.php | 40 ++++++++++++ .../GetMethodParametersParseError4Test.inc | 5 ++ .../GetMethodParametersParseError4Test.php | 40 ++++++++++++ .../GetParametersParseError3Test.php | 62 +++++++++++++++++++ .../GetParametersParseError4Test.php | 62 +++++++++++++++++++ 6 files changed, 214 insertions(+) create mode 100644 Tests/BackCompat/BCFile/GetMethodParametersParseError3Test.inc create mode 100644 Tests/BackCompat/BCFile/GetMethodParametersParseError3Test.php create mode 100644 Tests/BackCompat/BCFile/GetMethodParametersParseError4Test.inc create mode 100644 Tests/BackCompat/BCFile/GetMethodParametersParseError4Test.php create mode 100644 Tests/Utils/FunctionDeclarations/GetParametersParseError3Test.php create mode 100644 Tests/Utils/FunctionDeclarations/GetParametersParseError4Test.php diff --git a/Tests/BackCompat/BCFile/GetMethodParametersParseError3Test.inc b/Tests/BackCompat/BCFile/GetMethodParametersParseError3Test.inc new file mode 100644 index 00000000..6609a8ef --- /dev/null +++ b/Tests/BackCompat/BCFile/GetMethodParametersParseError3Test.inc @@ -0,0 +1,5 @@ +expectPhpcsException('$stackPtr was not a valid T_USE'); + + $target = $this->getTargetToken('/* testParseError */', [\T_USE]); + BCFile::getMethodParameters(self::$phpcsFile, $target); + } +} diff --git a/Tests/BackCompat/BCFile/GetMethodParametersParseError4Test.inc b/Tests/BackCompat/BCFile/GetMethodParametersParseError4Test.inc new file mode 100644 index 00000000..10e0d009 --- /dev/null +++ b/Tests/BackCompat/BCFile/GetMethodParametersParseError4Test.inc @@ -0,0 +1,5 @@ +getTargetToken('/* testParseError */', [\T_USE]); + $result = BCFile::getMethodParameters(self::$phpcsFile, $target); + + $this->assertSame([], $result); + } +} diff --git a/Tests/Utils/FunctionDeclarations/GetParametersParseError3Test.php b/Tests/Utils/FunctionDeclarations/GetParametersParseError3Test.php new file mode 100644 index 00000000..b6644406 --- /dev/null +++ b/Tests/Utils/FunctionDeclarations/GetParametersParseError3Test.php @@ -0,0 +1,62 @@ +expectPhpcsException('The value of argument #2 ($stackPtr) must be the pointer to a closure use statement'); + + $target = $this->getTargetToken('/* testParseError */', [\T_USE]); + FunctionDeclarations::getParameters(self::$phpcsFile, $target); + } +} diff --git a/Tests/Utils/FunctionDeclarations/GetParametersParseError4Test.php b/Tests/Utils/FunctionDeclarations/GetParametersParseError4Test.php new file mode 100644 index 00000000..fb07fa76 --- /dev/null +++ b/Tests/Utils/FunctionDeclarations/GetParametersParseError4Test.php @@ -0,0 +1,62 @@ +getTargetToken('/* testParseError */', [\T_USE]); + $result = FunctionDeclarations::getParameters(self::$phpcsFile, $target); + + $this->assertSame([], $result); + } +} From 8471a1c92996d2190963c9b29419ac21e2aa5c3e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 18 May 2025 16:37:16 +0200 Subject: [PATCH 11/14] BCFile::getMemberProperties(): sync with PHPCS 4.0 / remove parse error warning + PHP 8.4 interface properties As of PHPCS 4.0, the `File::getMemberProperties()` method: * ... will handle properties in interfaces to support PHP 8.4, in which this is now allowed. * ... will no longer throw a parse error warning. This commit makes the necessary updates in PHPCSUtils for the same: * Adds `T_INTERFACE` to the `Collections::ooPropertyScopes()` for PHP 8.4 properties in interfaces support. * Updates the test expectations for the `Scopes::isOOProperty()` method to allow for PHP 8.4 properties in interfaces. * Updates the `BCFile::getMemberProperties()` polyfill to mirror the PHPCS 4.0 version of the method. * Updates various tests for both the `BCFile::getMemberProperties()` and the `Variables::getMemberProperties()` methods to be in line with the changes. Ref: PHPCSStandards/PHP_CodeSniffer 991 --- PHPCSUtils/BackCompat/BCFile.php | 135 +++++++++++++++++- PHPCSUtils/Tokens/Collections.php | 3 +- PHPCSUtils/Utils/Variables.php | 2 +- .../BCFile/GetMemberPropertiesTest.php | 19 ++- Tests/Utils/Scopes/IsOOPropertyTest.inc | 2 +- Tests/Utils/Scopes/IsOOPropertyTest.php | 2 +- .../Variables/GetMemberPropertiesDiffTest.inc | 12 +- .../Variables/GetMemberPropertiesDiffTest.php | 33 ----- .../Variables/GetMemberPropertiesTest.php | 20 --- 9 files changed, 152 insertions(+), 76 deletions(-) diff --git a/PHPCSUtils/BackCompat/BCFile.php b/PHPCSUtils/BackCompat/BCFile.php index bd311cca..05844f66 100644 --- a/PHPCSUtils/BackCompat/BCFile.php +++ b/PHPCSUtils/BackCompat/BCFile.php @@ -512,7 +512,8 @@ public static function getMethodProperties(File $phpcsFile, $stackPtr) * * Changelog for the PHPCS native function: * - Introduced in PHPCS 0.0.5. - * - The upstream method has received no significant updates since PHPCS 3.13.0. + * - PHPCS 4.0: properties in interfaces (PHP 8.4+) are accepted. + * - PHPCS 4.0: will no longer throw a parse error warning. * * @see \PHP_CodeSniffer\Files\File::getMemberProperties() Original source. * @see \PHPCSUtils\Utils\Variables::getMemberProperties() PHPCSUtils native improved version. @@ -531,7 +532,137 @@ public static function getMethodProperties(File $phpcsFile, $stackPtr) */ public static function getMemberProperties(File $phpcsFile, $stackPtr) { - return $phpcsFile->getMemberProperties($stackPtr); + $tokens = $phpcsFile->getTokens(); + + if ($tokens[$stackPtr]['code'] !== T_VARIABLE) { + throw new RuntimeException('$stackPtr must be of type T_VARIABLE'); + } + + $conditions = $tokens[$stackPtr]['conditions']; + $conditions = array_keys($conditions); + $ptr = array_pop($conditions); + if (isset($tokens[$ptr]) === false + || isset(Tokens::$ooScopeTokens[$tokens[$ptr]['code']]) === false + || $tokens[$ptr]['code'] === T_ENUM + ) { + throw new RuntimeException('$stackPtr is not a class member var'); + } + + // Make sure it's not a method parameter. + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + $parenthesis = array_keys($tokens[$stackPtr]['nested_parenthesis']); + $deepestOpen = array_pop($parenthesis); + if ($deepestOpen > $ptr + && isset($tokens[$deepestOpen]['parenthesis_owner']) === true + && $tokens[$tokens[$deepestOpen]['parenthesis_owner']]['code'] === T_FUNCTION + ) { + throw new RuntimeException('$stackPtr is not a class member var'); + } + } + + $valid = [ + T_PUBLIC => T_PUBLIC, + T_PRIVATE => T_PRIVATE, + T_PROTECTED => T_PROTECTED, + T_STATIC => T_STATIC, + T_VAR => T_VAR, + T_READONLY => T_READONLY, + T_FINAL => T_FINAL, + ]; + + $valid += Tokens::$emptyTokens; + + $scope = 'public'; + $scopeSpecified = false; + $isStatic = false; + $isReadonly = false; + $isFinal = false; + + $startOfStatement = $phpcsFile->findPrevious( + [ + T_SEMICOLON, + T_OPEN_CURLY_BRACKET, + T_CLOSE_CURLY_BRACKET, + T_ATTRIBUTE_END, + ], + ($stackPtr - 1) + ); + + for ($i = ($startOfStatement + 1); $i < $stackPtr; $i++) { + if (isset($valid[$tokens[$i]['code']]) === false) { + break; + } + + switch ($tokens[$i]['code']) { + case T_PUBLIC: + $scope = 'public'; + $scopeSpecified = true; + break; + case T_PRIVATE: + $scope = 'private'; + $scopeSpecified = true; + break; + case T_PROTECTED: + $scope = 'protected'; + $scopeSpecified = true; + break; + case T_STATIC: + $isStatic = true; + break; + case T_READONLY: + $isReadonly = true; + break; + case T_FINAL: + $isFinal = true; + break; + } + } + + $type = ''; + $typeToken = false; + $typeEndToken = false; + $nullableType = false; + + if ($i < $stackPtr) { + // We've found a type. + $valid = Collections::propertyTypeTokens(); + + for ($i; $i < $stackPtr; $i++) { + if ($tokens[$i]['code'] === T_VARIABLE) { + // Hit another variable in a group definition. + break; + } + + if ($tokens[$i]['code'] === T_NULLABLE) { + $nullableType = true; + } + + if (isset($valid[$tokens[$i]['code']]) === true) { + $typeEndToken = $i; + if ($typeToken === false) { + $typeToken = $i; + } + + $type .= $tokens[$i]['content']; + } + } + + if ($type !== '' && $nullableType === true) { + $type = '?' . $type; + } + } + + return [ + 'scope' => $scope, + 'scope_specified' => $scopeSpecified, + 'is_static' => $isStatic, + 'is_readonly' => $isReadonly, + 'is_final' => $isFinal, + 'type' => $type, + 'type_token' => $typeToken, + 'type_end_token' => $typeEndToken, + 'nullable_type' => $nullableType, + ]; } /** diff --git a/PHPCSUtils/Tokens/Collections.php b/PHPCSUtils/Tokens/Collections.php index 885679d2..5cfada12 100644 --- a/PHPCSUtils/Tokens/Collections.php +++ b/PHPCSUtils/Tokens/Collections.php @@ -415,7 +415,7 @@ final class Collections /** * OO scopes in which properties can be declared. * - * Note: interfaces can not declare properties. + * - PHP 8.4 added support for properties in interfaces. * * @since 1.0.0 Use the {@see Collections::ooPropertyScopes()} method for access. * @@ -424,6 +424,7 @@ final class Collections private static $ooPropertyScopes = [ \T_CLASS => \T_CLASS, \T_ANON_CLASS => \T_ANON_CLASS, + \T_INTERFACE => \T_INTERFACE, \T_TRAIT => \T_TRAIT, ]; diff --git a/PHPCSUtils/Utils/Variables.php b/PHPCSUtils/Utils/Variables.php index c45e5aed..bbb64410 100644 --- a/PHPCSUtils/Utils/Variables.php +++ b/PHPCSUtils/Utils/Variables.php @@ -80,7 +80,7 @@ final class Variables * Retrieve the visibility and implementation properties of a class member variable. * * Main differences with the PHPCS version: - * - Removed the parse error warning for properties in interfaces. + * - Removed the parse error warning for properties in enums (PHPCS 4.0 makes the same change). * This will now throw the same _"$stackPtr is not a class member var"_ runtime exception as * other non-property variables passed to the method. * - Defensive coding against incorrect calls to this method. diff --git a/Tests/BackCompat/BCFile/GetMemberPropertiesTest.php b/Tests/BackCompat/BCFile/GetMemberPropertiesTest.php index 187d8502..e3680a4d 100644 --- a/Tests/BackCompat/BCFile/GetMemberPropertiesTest.php +++ b/Tests/BackCompat/BCFile/GetMemberPropertiesTest.php @@ -636,9 +636,19 @@ public static function dataGetMemberProperties() 'nullable_type' => false, ], ], - 'invalid-property-in-interface' => [ + 'property-in-interface' => [ 'identifier' => '/* testInterfaceProperty */', - 'expected' => [], + 'expected' => [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'is_final' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], ], 'property-in-nested-class-1' => [ 'identifier' => '/* testNestedProperty 1 */', @@ -1033,10 +1043,6 @@ public static function dataGetMemberProperties() 'nullable_type' => false, ], ], - 'invalid-property-in-enum' => [ - 'identifier' => '/* testEnumProperty */', - 'expected' => [], - ], 'php8.1-single-intersection-type' => [ 'identifier' => '/* testPHP81IntersectionTypes */', 'expected' => [ @@ -1400,6 +1406,7 @@ public static function dataNotClassProperty() 'method parameter in anon class nested in ternary' => ['/* testNestedMethodParam 1 */'], 'method parameter in anon class nested in function call' => ['/* testNestedMethodParam 2 */'], 'method parameter in enum' => ['/* testEnumMethodParamNotProperty */'], + 'property in enum (parse error)' => ['/* testEnumProperty */'], ]; } diff --git a/Tests/Utils/Scopes/IsOOPropertyTest.inc b/Tests/Utils/Scopes/IsOOPropertyTest.inc index d765c0c5..88090f69 100644 --- a/Tests/Utils/Scopes/IsOOPropertyTest.inc +++ b/Tests/Utils/Scopes/IsOOPropertyTest.inc @@ -41,7 +41,7 @@ $a = new class { }; interface MyInterface { - // Intentional parse error. Properties are not allowed in interfaces. + // Properties are allowed in interfaces since PHP 8.4 (with a property hook). /* testInterfaceProp */ public $interfaceProp = false; diff --git a/Tests/Utils/Scopes/IsOOPropertyTest.php b/Tests/Utils/Scopes/IsOOPropertyTest.php index 0bdbe6d3..dff3c3da 100644 --- a/Tests/Utils/Scopes/IsOOPropertyTest.php +++ b/Tests/Utils/Scopes/IsOOPropertyTest.php @@ -125,7 +125,7 @@ public static function dataIsOOProperty() ], 'interface-property' => [ 'testMarker' => '/* testInterfaceProp */', - 'expected' => false, + 'expected' => true, ], 'interface-method-param' => [ 'testMarker' => '/* testInterfaceMethodParameter */', diff --git a/Tests/Utils/Variables/GetMemberPropertiesDiffTest.inc b/Tests/Utils/Variables/GetMemberPropertiesDiffTest.inc index 055ad9c6..d04cc775 100644 --- a/Tests/Utils/Variables/GetMemberPropertiesDiffTest.inc +++ b/Tests/Utils/Variables/GetMemberPropertiesDiffTest.inc @@ -1,13 +1,3 @@ expectException('PHPCSUtils\Exceptions\ValueError'); - $this->expectExceptionMessage('The value of argument #2 ($stackPtr) must be the pointer to a class member var'); - - $variable = $this->getTargetToken($testMarker, \T_VARIABLE); - Variables::getMemberProperties(self::$phpcsFile, $variable); - } - - /** - * Data provider. - * - * @see testNotClassPropertyException() - * - * @return array> - */ - public static function dataNotClassPropertyException() - { - return [ - 'interface property' => ['/* testInterfaceProperty */'], - 'enum property' => ['/* testEnumProperty */'], - ]; - } } diff --git a/Tests/Utils/Variables/GetMemberPropertiesTest.php b/Tests/Utils/Variables/GetMemberPropertiesTest.php index 83fc8ca4..738de7aa 100644 --- a/Tests/Utils/Variables/GetMemberPropertiesTest.php +++ b/Tests/Utils/Variables/GetMemberPropertiesTest.php @@ -90,26 +90,6 @@ public function testNotAVariableException() Variables::getMemberProperties(self::$phpcsFile, $next); } - /** - * Data provider. - * - * @see testGetMemberProperties() - * - * @return array>> - */ - public static function dataGetMemberProperties() - { - $data = parent::dataGetMemberProperties(); - - /* - * Remove the data sets related to the invalid interface/enum properties. - * These will now throw an exception instead. - */ - unset($data['invalid-property-in-interface'], $data['invalid-property-in-enum']); - - return $data; - } - /** * Verify that the build-in caching is used when caching is enabled. * From a542524c8a97a5e460c73e861cf3249b3be8cfed Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 18 May 2025 18:45:18 +0200 Subject: [PATCH 12/14] BCFile::getDeclarationName(): sync with PHPCS 4.0 / stop accepting tokens for non-named structures The `[BC]File::getDeclarationName()` method - for historic reasons - accepted the `T_CLOSURE` and `T_ANON_CLASS` tokens, even though these structures will never have a name, and returned `null` for those tokens. This commit changes the `BCFile::getDeclarationName()` method to no longer accept those tokens and throw an exception if they are passed to the method instead. As a secondary change, when the name of a valid structure cannot be determined, the method will now no longer return `null`, but will return an empty string. This normalizes the return type of the method to always return a string (or throw an exception). Includes updated unit tests to match. This change mirrors the upstream change made to the `File::getDeclarationName()` method in PHPCS 4.0. Note: this change is NOT mirrored in the `ObjectDeclarations::getName()` method, as changing it there would constitute a breaking change for PHPCSUtils, so that change needs to wait until PHPCSUtils 2.0. Ref: PHPCSStandards/PHP_CodeSniffer 1007 --- PHPCSUtils/BackCompat/BCFile.php | 51 ++++++++++++++++--- PHPCSUtils/Utils/ObjectDeclarations.php | 4 ++ .../BCFile/GetDeclarationNameJSTest.php | 34 ++++++------- .../GetDeclarationNameParseError1Test.php | 12 ++--- .../GetDeclarationNameParseError2Test.php | 12 ++--- .../BCFile/GetDeclarationNameTest.php | 34 ++++++------- .../ObjectDeclarations/GetNameJSTest.php | 24 +++++++-- .../GetNameParseError1Test.php | 7 ++- .../GetNameParseError2Test.php | 7 ++- .../Utils/ObjectDeclarations/GetNameTest.php | 24 +++++++-- 10 files changed, 142 insertions(+), 67 deletions(-) diff --git a/PHPCSUtils/BackCompat/BCFile.php b/PHPCSUtils/BackCompat/BCFile.php index 05844f66..393b9074 100644 --- a/PHPCSUtils/BackCompat/BCFile.php +++ b/PHPCSUtils/BackCompat/BCFile.php @@ -76,7 +76,8 @@ final class BCFile * * Changelog for the PHPCS native function: * - Introduced in PHPCS 0.0.5. - * - The upstream method has received no significant updates since PHPCS 3.13.0. + * - PHPCS 4.0: The method no longer accepts `T_CLOSURE` and `T_ANON_CLASS` tokens. + * - PHPCS 4.0: The method will now always return a string. * * @see \PHP_CodeSniffer\Files\File::getDeclarationName() Original source. * @see \PHPCSUtils\Utils\ObjectDeclarations::getName() PHPCSUtils native improved version. @@ -88,17 +89,53 @@ final class BCFile * which declared the class, interface, * trait, enum or function. * - * @return string|null The name of the class, interface, trait, enum, or function; - * or `NULL` if the function or class is anonymous or - * in case of a parse error/live coding. + * @return string The name of the class, interface, trait, or function or an empty string + * if the name could not be determined (live coding). * * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type - * `T_FUNCTION`, `T_CLASS`, `T_ANON_CLASS`, - * `T_CLOSURE`, `T_TRAIT`, `T_ENUM` or `T_INTERFACE`. + * `T_FUNCTION`, `T_CLASS`, `T_TRAIT`, `T_ENUM`, or `T_INTERFACE`. */ public static function getDeclarationName(File $phpcsFile, $stackPtr) { - return $phpcsFile->getDeclarationName($stackPtr); + $tokens = $phpcsFile->getTokens(); + + $tokenCode = $tokens[$stackPtr]['code']; + + if ($tokenCode !== T_FUNCTION + && $tokenCode !== T_CLASS + && $tokenCode !== T_INTERFACE + && $tokenCode !== T_TRAIT + && $tokenCode !== T_ENUM + ) { + throw new RuntimeException('Token type "' . $tokens[$stackPtr]['type'] . '" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'); + } + + if ($tokenCode === T_FUNCTION + && strtolower($tokens[$stackPtr]['content']) !== 'function' + ) { + // This is a function declared without the "function" keyword. + // So this token is the function name. + return $tokens[$stackPtr]['content']; + } + + $stopPoint = $phpcsFile->numTokens; + if (isset($tokens[$stackPtr]['parenthesis_opener']) === true) { + // For functions, stop searching at the parenthesis opener. + $stopPoint = $tokens[$stackPtr]['parenthesis_opener']; + } elseif (isset($tokens[$stackPtr]['scope_opener']) === true) { + // For OO tokens, stop searching at the open curly. + $stopPoint = $tokens[$stackPtr]['scope_opener']; + } + + $content = ''; + for ($i = $stackPtr; $i < $stopPoint; $i++) { + if ($tokens[$i]['code'] === T_STRING) { + $content = $tokens[$i]['content']; + break; + } + } + + return $content; } /** diff --git a/PHPCSUtils/Utils/ObjectDeclarations.php b/PHPCSUtils/Utils/ObjectDeclarations.php index 478c5c77..8b1cae71 100644 --- a/PHPCSUtils/Utils/ObjectDeclarations.php +++ b/PHPCSUtils/Utils/ObjectDeclarations.php @@ -46,6 +46,10 @@ final class ObjectDeclarations * being extended/interface being implemented. * Using this version of the utility method, either the complete name (invalid or not) will * be returned or `null` in case of no name (parse error). + * - The PHPCS 4.0 change to no longer accept tokens for anonymous structures (T_CLOSURE/T_ANON_CLASS) + * has not been applied to this method (yet). This will change in PHPCSUtils 2.0. + * - The PHPCS 4.0 change to normalize the return type to `string` and no longer return `null` + * has not been applied to this method (yet). This will change in PHPCSUtils 2.0. * * @see \PHP_CodeSniffer\Files\File::getDeclarationName() Original source. * @see \PHPCSUtils\BackCompat\BCFile::getDeclarationName() Cross-version compatible version of the original. diff --git a/Tests/BackCompat/BCFile/GetDeclarationNameJSTest.php b/Tests/BackCompat/BCFile/GetDeclarationNameJSTest.php index 57303527..a749ec21 100644 --- a/Tests/BackCompat/BCFile/GetDeclarationNameJSTest.php +++ b/Tests/BackCompat/BCFile/GetDeclarationNameJSTest.php @@ -10,6 +10,7 @@ namespace PHPCSUtils\Tests\BackCompat\BCFile; +use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\BackCompat\BCFile; use PHPCSUtils\Tests\PolyfilledTestCase; @@ -35,43 +36,38 @@ class GetDeclarationNameJSTest extends PolyfilledTestCase /** * Test receiving an expected exception when a non-supported token is passed. * - * @return void - */ - public function testInvalidTokenPassed() - { - $this->expectPhpcsException('Token type "T_STRING" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'); - - $target = $this->getTargetToken('/* testInvalidTokenPassed */', \T_STRING); - BCFile::getDeclarationName(self::$phpcsFile, $target); - } - - /** - * Test receiving "null" when passed an anonymous construct or in case of a parse error. - * - * @dataProvider dataGetDeclarationNameNull + * @dataProvider dataInvalidTokenPassed * * @param string $testMarker The comment which prefaces the target token in the test file. * @param int|string $targetType Token type of the token to get as stackPtr. * * @return void */ - public function testGetDeclarationNameNull($testMarker, $targetType) + public function testInvalidTokenPassed($testMarker, $targetType) { + $tokenName = Tokens::tokenName($targetType); + $this->expectPhpcsException( + 'Token type "' . $tokenName . '" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM' + ); + $target = $this->getTargetToken($testMarker, $targetType); - $result = BCFile::getDeclarationName(self::$phpcsFile, $target); - $this->assertNull($result); + BCFile::getDeclarationName(self::$phpcsFile, $target); } /** * Data provider. * - * @see GetDeclarationNameTest::testGetDeclarationNameNull() + * @see testInvalidTokenPassed() For the array format. * * @return array> */ - public static function dataGetDeclarationNameNull() + public static function dataInvalidTokenPassed() { return [ + 'unsupported token T_STRING' => [ + 'testMarker' => '/* testInvalidTokenPassed */', + 'targetType' => \T_STRING, + ], 'closure' => [ 'testMarker' => '/* testClosure */', 'targetType' => \T_CLOSURE, diff --git a/Tests/BackCompat/BCFile/GetDeclarationNameParseError1Test.php b/Tests/BackCompat/BCFile/GetDeclarationNameParseError1Test.php index e3ce3392..7a73887f 100644 --- a/Tests/BackCompat/BCFile/GetDeclarationNameParseError1Test.php +++ b/Tests/BackCompat/BCFile/GetDeclarationNameParseError1Test.php @@ -26,30 +26,30 @@ class GetDeclarationNameParseError1Test extends PolyfilledTestCase { /** - * Test receiving "null" in case of a parse error. + * Test receiving an empty string in case of a parse error. * - * @dataProvider dataGetDeclarationNameNull + * @dataProvider dataGetDeclarationName * * @param string $testMarker The comment which prefaces the target token in the test file. * @param int|string $targetType Token type of the token to get as stackPtr. * * @return void */ - public function testGetDeclarationNameNull($testMarker, $targetType) + public function testGetDeclarationName($testMarker, $targetType) { $target = $this->getTargetToken($testMarker, $targetType); $result = BCFile::getDeclarationName(self::$phpcsFile, $target); - $this->assertNull($result); + $this->assertSame('', $result); } /** * Data provider. * - * @see testGetDeclarationNameNull() For the array format. + * @see testGetDeclarationName() For the array format. * * @return array> */ - public static function dataGetDeclarationNameNull() + public static function dataGetDeclarationName() { return [ 'unfinished function/live coding' => [ diff --git a/Tests/BackCompat/BCFile/GetDeclarationNameParseError2Test.php b/Tests/BackCompat/BCFile/GetDeclarationNameParseError2Test.php index b95eff30..851a3acf 100644 --- a/Tests/BackCompat/BCFile/GetDeclarationNameParseError2Test.php +++ b/Tests/BackCompat/BCFile/GetDeclarationNameParseError2Test.php @@ -26,30 +26,30 @@ class GetDeclarationNameParseError2Test extends PolyfilledTestCase { /** - * Test receiving "null" in case of a parse error. + * Test receiving an empty string in case of a parse error. * - * @dataProvider dataGetDeclarationNameNull + * @dataProvider dataGetDeclarationName * * @param string $testMarker The comment which prefaces the target token in the test file. * @param int|string $targetType Token type of the token to get as stackPtr. * * @return void */ - public function testGetDeclarationNameNull($testMarker, $targetType) + public function testGetDeclarationName($testMarker, $targetType) { $target = $this->getTargetToken($testMarker, $targetType); $result = BCFile::getDeclarationName(self::$phpcsFile, $target); - $this->assertNull($result); + $this->assertSame('', $result); } /** * Data provider. * - * @see testGetDeclarationNameNull() For the array format. + * @see testGetDeclarationName() For the array format. * * @return array> */ - public static function dataGetDeclarationNameNull() + public static function dataGetDeclarationName() { return [ 'unfinished closure/live coding' => [ diff --git a/Tests/BackCompat/BCFile/GetDeclarationNameTest.php b/Tests/BackCompat/BCFile/GetDeclarationNameTest.php index 3dcc0140..1f32876c 100644 --- a/Tests/BackCompat/BCFile/GetDeclarationNameTest.php +++ b/Tests/BackCompat/BCFile/GetDeclarationNameTest.php @@ -10,6 +10,7 @@ namespace PHPCSUtils\Tests\BackCompat\BCFile; +use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\BackCompat\BCFile; use PHPCSUtils\Tests\PolyfilledTestCase; @@ -28,43 +29,38 @@ class GetDeclarationNameTest extends PolyfilledTestCase /** * Test receiving an expected exception when a non-supported token is passed. * - * @return void - */ - public function testInvalidTokenPassed() - { - $this->expectPhpcsException('Token type "T_STRING" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'); - - $target = $this->getTargetToken('/* testInvalidTokenPassed */', \T_STRING); - BCFile::getDeclarationName(self::$phpcsFile, $target); - } - - /** - * Test receiving "null" when passed an anonymous construct or in case of a parse error. - * - * @dataProvider dataGetDeclarationNameNull + * @dataProvider dataInvalidTokenPassed * * @param string $testMarker The comment which prefaces the target token in the test file. * @param int|string $targetType Token type of the token to get as stackPtr. * * @return void */ - public function testGetDeclarationNameNull($testMarker, $targetType) + public function testInvalidTokenPassed($testMarker, $targetType) { + $tokenName = Tokens::tokenName($targetType); + $this->expectPhpcsException( + 'Token type "' . $tokenName . '" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM' + ); + $target = $this->getTargetToken($testMarker, $targetType); - $result = BCFile::getDeclarationName(self::$phpcsFile, $target); - $this->assertNull($result); + BCFile::getDeclarationName(self::$phpcsFile, $target); } /** * Data provider. * - * @see testGetDeclarationNameNull() For the array format. + * @see testInvalidTokenPassed() For the array format. * * @return array> */ - public static function dataGetDeclarationNameNull() + public static function dataInvalidTokenPassed() { return [ + 'unsupported token T_STRING' => [ + 'testMarker' => '/* testInvalidTokenPassed */', + 'targetType' => \T_STRING, + ], 'closure' => [ 'testMarker' => '/* testClosure */', 'targetType' => \T_CLOSURE, diff --git a/Tests/Utils/ObjectDeclarations/GetNameJSTest.php b/Tests/Utils/ObjectDeclarations/GetNameJSTest.php index 01280ec2..b1ab77b7 100644 --- a/Tests/Utils/ObjectDeclarations/GetNameJSTest.php +++ b/Tests/Utils/ObjectDeclarations/GetNameJSTest.php @@ -52,7 +52,7 @@ public static function setUpTestFile() * * @return void */ - public function testInvalidTokenPassed() + public function testTrulyInvalidTokenPassed() { $this->expectException('PHPCSUtils\Exceptions\UnexpectedTokenType'); $this->expectExceptionMessage( @@ -66,22 +66,40 @@ public function testInvalidTokenPassed() /** * Test receiving "null" when passed an anonymous construct or in case of a parse error. * + * Note: the upstream and the BCFile method no longer returns `null`, but throws an exception. + * For PHPCSUtils, this change needs to wait for the next major. + * * {@internal Method name not adjusted as otherwise it wouldn't overload the parent method.} * - * @dataProvider dataGetDeclarationNameNull + * @dataProvider dataInvalidTokenPassed * * @param string $testMarker The comment which prefaces the target token in the test file. * @param int|string $targetType Token type of the token to get as stackPtr. * * @return void */ - public function testGetDeclarationNameNull($testMarker, $targetType) + public function testInvalidTokenPassed($testMarker, $targetType) { $target = $this->getTargetToken($testMarker, $targetType); $result = ObjectDeclarations::getName(self::$phpcsFile, $target); $this->assertNull($result); } + /** + * Data provider. + * + * @see testInvalidTokenPassed() For the array format. + * + * @return array> + */ + public static function dataInvalidTokenPassed() + { + $data = parent::dataInvalidTokenPassed(); + unset($data['unsupported token T_STRING']); + + return $data; + } + /** * Test retrieving the name of a function or OO structure. * diff --git a/Tests/Utils/ObjectDeclarations/GetNameParseError1Test.php b/Tests/Utils/ObjectDeclarations/GetNameParseError1Test.php index c7e7a69a..d853bfcf 100644 --- a/Tests/Utils/ObjectDeclarations/GetNameParseError1Test.php +++ b/Tests/Utils/ObjectDeclarations/GetNameParseError1Test.php @@ -50,14 +50,17 @@ public static function setUpTestFile() /** * Test receiving "null" in case of a parse error. * - * @dataProvider dataGetDeclarationNameNull + * Note: the upstream and the BCFile method no longer returns `null`, but an empty string. + * For PHPCSUtils, this change needs to wait for the next major. + * + * @dataProvider dataGetDeclarationName * * @param string $testMarker The comment which prefaces the target token in the test file. * @param int|string $targetType Token type of the token to get as stackPtr. * * @return void */ - public function testGetDeclarationNameNull($testMarker, $targetType) + public function testGetDeclarationName($testMarker, $targetType) { $target = $this->getTargetToken($testMarker, $targetType); $result = ObjectDeclarations::getName(self::$phpcsFile, $target); diff --git a/Tests/Utils/ObjectDeclarations/GetNameParseError2Test.php b/Tests/Utils/ObjectDeclarations/GetNameParseError2Test.php index b3f53606..c42c8163 100644 --- a/Tests/Utils/ObjectDeclarations/GetNameParseError2Test.php +++ b/Tests/Utils/ObjectDeclarations/GetNameParseError2Test.php @@ -50,14 +50,17 @@ public static function setUpTestFile() /** * Test receiving "null" in case of a parse error. * - * @dataProvider dataGetDeclarationNameNull + * Note: the upstream and the BCFile method no longer returns `null`, but an empty string. + * For PHPCSUtils, this change needs to wait for the next major. + * + * @dataProvider dataGetDeclarationName * * @param string $testMarker The comment which prefaces the target token in the test file. * @param int|string $targetType Token type of the token to get as stackPtr. * * @return void */ - public function testGetDeclarationNameNull($testMarker, $targetType) + public function testGetDeclarationName($testMarker, $targetType) { $target = $this->getTargetToken($testMarker, $targetType); $result = ObjectDeclarations::getName(self::$phpcsFile, $target); diff --git a/Tests/Utils/ObjectDeclarations/GetNameTest.php b/Tests/Utils/ObjectDeclarations/GetNameTest.php index 95af9668..9fcd597f 100644 --- a/Tests/Utils/ObjectDeclarations/GetNameTest.php +++ b/Tests/Utils/ObjectDeclarations/GetNameTest.php @@ -52,7 +52,7 @@ public static function setUpTestFile() * * @return void */ - public function testInvalidTokenPassed() + public function testTrulyInvalidTokenPassed() { $this->expectException('PHPCSUtils\Exceptions\UnexpectedTokenType'); $this->expectExceptionMessage( @@ -66,22 +66,40 @@ public function testInvalidTokenPassed() /** * Test receiving "null" when passed an anonymous construct or in case of a parse error. * + * Note: the upstream and the BCFile method no longer returns `null`, but throws an exception. + * For PHPCSUtils, this change needs to wait for the next major. + * * {@internal Method name not adjusted as otherwise it wouldn't overload the parent method.} * - * @dataProvider dataGetDeclarationNameNull + * @dataProvider dataInvalidTokenPassed * * @param string $testMarker The comment which prefaces the target token in the test file. * @param int|string $targetType Token type of the token to get as stackPtr. * * @return void */ - public function testGetDeclarationNameNull($testMarker, $targetType) + public function testInvalidTokenPassed($testMarker, $targetType) { $target = $this->getTargetToken($testMarker, $targetType); $result = ObjectDeclarations::getName(self::$phpcsFile, $target); $this->assertNull($result); } + /** + * Data provider. + * + * @see testInvalidTokenPassed() For the array format. + * + * @return array> + */ + public static function dataInvalidTokenPassed() + { + $data = parent::dataInvalidTokenPassed(); + unset($data['unsupported token T_STRING']); + + return $data; + } + /** * Test retrieving the name of a function or OO structure. * From dc1f23eba8a4407244df0d5a3347f42b96ce1a00 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 2 Jun 2025 02:34:30 +0200 Subject: [PATCH 13/14] BCFile::findExtendedClassName(): sync with PHPCS 4.0 / handling of namespace relative parent classes The PHPCS `File::findExtendedClassName()` method did not handle namespace relative parent classes correctly prior to PHPCS 4.0. The PHPCSUtils native `ObjectDeclarations::findExtendedClassName()` method **_did_** already handle this correctly. This commit adds the PHPCS 4.x version of the `findExtendedClassName()` method to the `BCFile` class. Includes moving related tests from the "Diff" test file to the "normal" test file and updating the docs to annotate this is no longer a difference between the PHPCS native and PHPCSUtils versions of the method. --- PHPCSUtils/BackCompat/BCFile.php | 39 ++++++++++++++++++- PHPCSUtils/Utils/ObjectDeclarations.php | 1 - .../BCFile/FindExtendedClassNameTest.inc | 3 ++ .../BCFile/FindExtendedClassNameTest.php | 4 ++ .../FindExtendedClassNameDiffTest.inc | 3 -- .../FindExtendedClassNameDiffTest.php | 4 -- 6 files changed, 44 insertions(+), 10 deletions(-) diff --git a/PHPCSUtils/BackCompat/BCFile.php b/PHPCSUtils/BackCompat/BCFile.php index 393b9074..6d4bfe40 100644 --- a/PHPCSUtils/BackCompat/BCFile.php +++ b/PHPCSUtils/BackCompat/BCFile.php @@ -905,7 +905,7 @@ public static function getCondition(File $phpcsFile, $stackPtr, $type, $first = * * Changelog for the PHPCS native function: * - Introduced in PHPCS 1.2.0. - * - The upstream method has received no significant updates since PHPCS 3.13.0. + * - PHPCS 4.0.0: Handling of the namespace relative parent class using the namespace keyword as operator. * * @see \PHP_CodeSniffer\Files\File::findExtendedClassName() Original source. * @see \PHPCSUtils\Utils\ObjectDeclarations::findExtendedClassName() PHPCSUtils native improved version. @@ -920,7 +920,42 @@ public static function getCondition(File $phpcsFile, $stackPtr, $type, $first = */ public static function findExtendedClassName(File $phpcsFile, $stackPtr) { - return $phpcsFile->findExtendedClassName($stackPtr); + $tokens = $phpcsFile->getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false) { + return false; + } + + if ($tokens[$stackPtr]['code'] !== T_CLASS + && $tokens[$stackPtr]['code'] !== T_ANON_CLASS + && $tokens[$stackPtr]['code'] !== T_INTERFACE + ) { + return false; + } + + if (isset($tokens[$stackPtr]['scope_opener']) === false) { + return false; + } + + $classOpenerIndex = $tokens[$stackPtr]['scope_opener']; + $extendsIndex = $phpcsFile->findNext(T_EXTENDS, $stackPtr, $classOpenerIndex); + if ($extendsIndex === false) { + return false; + } + + $find = Collections::namespacedNameTokens(); + $find[] = T_WHITESPACE; + + $end = $phpcsFile->findNext($find, ($extendsIndex + 1), ($classOpenerIndex + 1), true); + $name = $phpcsFile->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1)); + $name = trim($name); + + if ($name === '') { + return false; + } + + return $name; } /** diff --git a/PHPCSUtils/Utils/ObjectDeclarations.php b/PHPCSUtils/Utils/ObjectDeclarations.php index 8b1cae71..bcd289d0 100644 --- a/PHPCSUtils/Utils/ObjectDeclarations.php +++ b/PHPCSUtils/Utils/ObjectDeclarations.php @@ -242,7 +242,6 @@ public static function getClassProperties(File $phpcsFile, $stackPtr) * - Bugs fixed: * - Handling of PHPCS annotations. * - Handling of comments. - * - Handling of the namespace keyword used as operator. * - Improved handling of parse errors. * - The returned name will be clean of superfluous whitespace and/or comments. * - Support for PHP 8.0 tokenization of identifier/namespaced names, cross-version PHP & PHPCS. diff --git a/Tests/BackCompat/BCFile/FindExtendedClassNameTest.inc b/Tests/BackCompat/BCFile/FindExtendedClassNameTest.inc index d9885d27..d8bb1b58 100644 --- a/Tests/BackCompat/BCFile/FindExtendedClassNameTest.inc +++ b/Tests/BackCompat/BCFile/FindExtendedClassNameTest.inc @@ -15,6 +15,9 @@ class testFECNNamespacedClass extends \PHP_CodeSniffer\Tests\Core\File\testFECNC /* testExtendsPartiallyQualifiedClass */ class testFECNQualifiedClass extends Core\File\RelativeClass {} +/* testExtendsNamespaceRelativeClass */ +class testWithNSOperator extends namespace\Bar {} + /* testNonExtendedInterface */ interface testFECNInterface {} diff --git a/Tests/BackCompat/BCFile/FindExtendedClassNameTest.php b/Tests/BackCompat/BCFile/FindExtendedClassNameTest.php index 254d99a8..2adc29cf 100644 --- a/Tests/BackCompat/BCFile/FindExtendedClassNameTest.php +++ b/Tests/BackCompat/BCFile/FindExtendedClassNameTest.php @@ -121,6 +121,10 @@ public static function dataExtendedClass() 'identifier' => '/* testExtendsPartiallyQualifiedClass */', 'expected' => 'Core\File\RelativeClass', ], + 'class extends namespace relative class' => [ + 'identifier' => '/* testExtendsNamespaceRelativeClass */', + 'expected' => 'namespace\Bar', + ], 'interface does not extend' => [ 'identifier' => '/* testNonExtendedInterface */', 'expected' => false, diff --git a/Tests/Utils/ObjectDeclarations/FindExtendedClassNameDiffTest.inc b/Tests/Utils/ObjectDeclarations/FindExtendedClassNameDiffTest.inc index 60220f03..c03aee8d 100644 --- a/Tests/Utils/ObjectDeclarations/FindExtendedClassNameDiffTest.inc +++ b/Tests/Utils/ObjectDeclarations/FindExtendedClassNameDiffTest.inc @@ -6,8 +6,5 @@ class testDeclarationWithComments // phpcs:ignore Stnd.Cat.Sniff -- For reasons. \Package\SubDir /* comment */ \ /* comment */ SomeClass /* comment */ {} -/* testExtendedClassUsingNamespaceOperator */ -class testWithNSOperator extends namespace\Bar {} - /* testExtendedClassStrayComma */ class testExtendedClassStrayComma extends , testClass {} // Intentional parse error. diff --git a/Tests/Utils/ObjectDeclarations/FindExtendedClassNameDiffTest.php b/Tests/Utils/ObjectDeclarations/FindExtendedClassNameDiffTest.php index c6b0d603..c71fc391 100644 --- a/Tests/Utils/ObjectDeclarations/FindExtendedClassNameDiffTest.php +++ b/Tests/Utils/ObjectDeclarations/FindExtendedClassNameDiffTest.php @@ -60,10 +60,6 @@ public static function dataFindExtendedClassName() 'testMarker' => '/* testDeclarationWithComments */', 'expected' => '\Package\SubDir\SomeClass', ], - 'namespace-operator' => [ - 'testMarker' => '/* testExtendedClassUsingNamespaceOperator */', - 'expected' => 'namespace\Bar', - ], 'parse-error-stray-comma' => [ 'testMarker' => '/* testExtendedClassStrayComma */', 'expected' => 'testClass', From 2adfb1242089031a95a467b043804ec28affaf6b Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 2 Jun 2025 02:34:58 +0200 Subject: [PATCH 14/14] BCFile::findImplementedInterfaceNames(): sync with PHPCS 4.0 / handling of namespace relative interfaces The PHPCS `File::findImplementedInterfaceNames()` method did not handle namespace relative interfaces correctly prior to PHPCS 4.0. The PHPCSUtils native `ObjectDeclarations::findImplementedInterfaceNames()` method **_did_** already handle this correctly. This commit adds the PHPCS 4.x version of the `findImplementedInterfaceNames()` method to the `BCFile` class. Includes moving related tests from the "Diff" test file to the "normal" test file and updating the docs to annotate this is no longer a difference between the PHPCS native and PHPCSUtils versions of the method. --- PHPCSUtils/BackCompat/BCFile.php | 42 ++++++++++++++++++- PHPCSUtils/Utils/ObjectDeclarations.php | 1 - .../FindImplementedInterfaceNamesTest.inc | 3 ++ .../FindImplementedInterfaceNamesTest.php | 7 ++++ .../FindImplementedInterfaceNamesDiffTest.inc | 3 -- .../FindImplementedInterfaceNamesDiffTest.php | 7 ---- 6 files changed, 50 insertions(+), 13 deletions(-) diff --git a/PHPCSUtils/BackCompat/BCFile.php b/PHPCSUtils/BackCompat/BCFile.php index 6d4bfe40..03bd5eed 100644 --- a/PHPCSUtils/BackCompat/BCFile.php +++ b/PHPCSUtils/BackCompat/BCFile.php @@ -965,7 +965,7 @@ public static function findExtendedClassName(File $phpcsFile, $stackPtr) * * Changelog for the PHPCS native function: * - Introduced in PHPCS 2.7.0. - * - The upstream method has received no significant updates since PHPCS 3.13.0. + * - PHPCS 4.0.0: Handling of the namespace relative parent class using the namespace keyword as operator. * * @see \PHP_CodeSniffer\Files\File::findImplementedInterfaceNames() Original source. * @see \PHPCSUtils\Utils\ObjectDeclarations::findImplementedInterfaceNames() PHPCSUtils native improved version. @@ -980,6 +980,44 @@ public static function findExtendedClassName(File $phpcsFile, $stackPtr) */ public static function findImplementedInterfaceNames(File $phpcsFile, $stackPtr) { - return $phpcsFile->findImplementedInterfaceNames($stackPtr); + $tokens = $phpcsFile->getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false) { + return false; + } + + if ($tokens[$stackPtr]['code'] !== T_CLASS + && $tokens[$stackPtr]['code'] !== T_ANON_CLASS + && $tokens[$stackPtr]['code'] !== T_ENUM + ) { + return false; + } + + if (isset($tokens[$stackPtr]['scope_closer']) === false) { + return false; + } + + $classOpenerIndex = $tokens[$stackPtr]['scope_opener']; + $implementsIndex = $phpcsFile->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex); + if ($implementsIndex === false) { + return false; + } + + $find = Collections::namespacedNameTokens(); + $find[] = T_WHITESPACE; + $find[] = T_COMMA; + + $end = $phpcsFile->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true); + $name = $phpcsFile->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1)); + $name = trim($name); + + if ($name === '') { + return false; + } else { + $names = explode(',', $name); + $names = array_map('trim', $names); + return $names; + } } } diff --git a/PHPCSUtils/Utils/ObjectDeclarations.php b/PHPCSUtils/Utils/ObjectDeclarations.php index bcd289d0..3a579261 100644 --- a/PHPCSUtils/Utils/ObjectDeclarations.php +++ b/PHPCSUtils/Utils/ObjectDeclarations.php @@ -277,7 +277,6 @@ public static function findExtendedClassName(File $phpcsFile, $stackPtr) * - Bugs fixed: * - Handling of PHPCS annotations. * - Handling of comments. - * - Handling of the namespace keyword used as operator. * - Improved handling of parse errors. * - The returned name(s) will be clean of superfluous whitespace and/or comments. * - Support for PHP 8.0 tokenization of identifier/namespaced names, cross-version PHP & PHPCS. diff --git a/Tests/BackCompat/BCFile/FindImplementedInterfaceNamesTest.inc b/Tests/BackCompat/BCFile/FindImplementedInterfaceNamesTest.inc index 3246efa2..50d38b88 100644 --- a/Tests/BackCompat/BCFile/FindImplementedInterfaceNamesTest.inc +++ b/Tests/BackCompat/BCFile/FindImplementedInterfaceNamesTest.inc @@ -21,6 +21,9 @@ class testFIINNamespacedClass implements \PHP_CodeSniffer\Tests\Core\File\testFI /* testImplementsPartiallyQualified */ class testFIINQualifiedClass implements Core\File\RelativeInterface {} +/* testImplementsMultipleNamespaceRelativeInterfaces */ +class testMultiImplementedNSOperator implements namespace\testInterfaceA, namespace\testInterfaceB {} + /* testClassThatExtendsAndImplements */ class testFECNClassThatExtendsAndImplements extends testFECNClass implements InterfaceA, \NameSpaced\Cat\InterfaceB {} diff --git a/Tests/BackCompat/BCFile/FindImplementedInterfaceNamesTest.php b/Tests/BackCompat/BCFile/FindImplementedInterfaceNamesTest.php index 7b24f254..8cf78798 100644 --- a/Tests/BackCompat/BCFile/FindImplementedInterfaceNamesTest.php +++ b/Tests/BackCompat/BCFile/FindImplementedInterfaceNamesTest.php @@ -130,6 +130,13 @@ public static function dataImplementedInterface() 'identifier' => '/* testImplementsPartiallyQualified */', 'expected' => ['Core\File\RelativeInterface'], ], + 'class implements multiple interfaces, namespace relative' => [ + 'identifier' => '/* testImplementsMultipleNamespaceRelativeInterfaces */', + 'expected' => [ + 'namespace\testInterfaceA', + 'namespace\testInterfaceB', + ], + ], 'class extends and implements' => [ 'identifier' => '/* testClassThatExtendsAndImplements */', 'expected' => [ diff --git a/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesDiffTest.inc b/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesDiffTest.inc index 46069654..0cd35a9d 100644 --- a/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesDiffTest.inc +++ b/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesDiffTest.inc @@ -13,8 +13,5 @@ class testDeclarationWithComments // comment InterfaceB {} -/* testDeclarationMultiImplementedNamespaceOperator */ -class testMultiImplementedNSOperator implements namespace\testInterfaceA, namespace\testInterfaceB {} - /* testMultiImplementedStrayComma */ class testMultiImplementedStrayComma implements testInterfaceA, , testInterfaceB {} // Intentional parse error. diff --git a/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesDiffTest.php b/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesDiffTest.php index 201f018d..db9c0dfd 100644 --- a/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesDiffTest.php +++ b/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesDiffTest.php @@ -63,13 +63,6 @@ public static function dataFindImplementedInterfaceNames() 'InterfaceB', ], ], - 'namespace-operator' => [ - 'testMarker' => '/* testDeclarationMultiImplementedNamespaceOperator */', - 'expected' => [ - 'namespace\testInterfaceA', - 'namespace\testInterfaceB', - ], - ], 'parse-error-stray-comma' => [ 'testMarker' => '/* testMultiImplementedStrayComma */', 'expected' => [