diff --git a/.github/workflows/csqa.yml b/.github/workflows/csqa.yml index 6ec8edc..2387332 100644 --- a/.github/workflows/csqa.yml +++ b/.github/workflows/csqa.yml @@ -5,7 +5,7 @@ on: # Prevent the build from running when there are only irrelevant changes. push: paths-ignore: - - '**.md' + - "**.md" pull_request: # Allow manually triggering the workflow. workflow_dispatch: @@ -18,11 +18,11 @@ concurrency: jobs: checkcs: - name: 'Basic CS and QA checks' + name: "Basic CS and QA checks" runs-on: ubuntu-latest env: - XMLLINT_INDENT: ' ' + XMLLINT_INDENT: " " steps: - name: Checkout code @@ -31,13 +31,13 @@ jobs: - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: 'latest' + php-version: "latest" coverage: none tools: cs2pr # Using PHPCS `master` as an early detection system for bugs upstream. - - name: 'Composer: adjust dependencies' - run: composer require --no-update squizlabs/php_codesniffer:"dev-master" + - name: "Composer: adjust dependencies" + run: composer require --no-update squizlabs/php_codesniffer:"4.x-dev" # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 919932e..e625782 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,40 +34,60 @@ jobs: # - PHP 8.4 needs PHPCS 3.11.0+ to run without errors (though the errors don't affect this package). # # The matrix is set up so as not to duplicate the builds which are run for code coverage. - php: ['5.5', '5.6', '7.0', '7.1', '7.2', '7.3'] - phpcs_version: ['3.5.6', 'dev-master'] + php: ["5.5", "5.6", "7.0", "7.1", "7.2", "7.3"] + phpcs_version: ["3.5.7", "4.0.0", "4.x-dev"] + + exclude: + # PHPCS 4.x requires PHP 7.2+ + - php: "5.5" + phpcs_version: "4.0.0" + - php: "5.5" + phpcs_version: "4.x-dev" + - php: "5.6" + phpcs_version: "4.0.0" + - php: "5.6" + phpcs_version: "4.x-dev" + - php: "7.0" + phpcs_version: "4.0.0" + - php: "7.0" + phpcs_version: "4.x-dev" + - php: "7.1" + phpcs_version: "4.0.0" + - php: "7.1" + phpcs_version: "4.x-dev" include: - # Make the matrix complete without duplicating builds run in code coverage. - - php: '8.4' - phpcs_version: '3.6.1' - - - php: '8.3' - phpcs_version: 'dev-master' - - php: '8.3' - phpcs_version: '3.6.1' - - - php: '8.2' - phpcs_version: 'dev-master' - - php: '8.2' - phpcs_version: '3.6.1' - - - php: '8.1' - phpcs_version: 'dev-master' - - php: '8.1' - phpcs_version: '3.6.1' - - - php: '8.0' - phpcs_version: 'dev-master' - - php: '8.0' - phpcs_version: '3.5.7' - - - php: '7.4' - phpcs_version: 'dev-master' + - php: "8.4" + phpcs_version: "4.0.0" + - php: "8.4" + phpcs_version: "3.6.1" + + - php: "8.3" + phpcs_version: "4.x-dev" + - php: "8.3" + phpcs_version: "3.6.1" + + - php: "8.2" + phpcs_version: "4.x-dev" + - php: "8.2" + phpcs_version: "3.6.1" + + - php: "8.1" + phpcs_version: "4.x-dev" + - php: "8.1" + phpcs_version: "3.6.1" + + - php: "8.0" + phpcs_version: "4.x-dev" + - php: "8.0" + phpcs_version: "3.5.7" + + - php: "7.4" + phpcs_version: "4.x-dev" # Experimental builds. - - php: '8.5' # Nightly. - phpcs_version: 'dev-master' + - php: "8.5" # Nightly. + phpcs_version: "4.x-dev" name: "Test: PHP ${{ matrix.php }} on PHPCS ${{ matrix.phpcs_version }}" @@ -82,7 +102,7 @@ jobs: run: | # On stable PHPCS versions, allow for PHP deprecation notices. # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore. - if [ "${{ matrix.phpcs_version }}" != "dev-master" ]; then + if [ "${{ matrix.phpcs_version }}" != "4.x-dev" ]; then echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On, zend.assertions=1' >> $GITHUB_OUTPUT else echo 'PHP_INI=error_reporting=-1, display_errors=On, zend.assertions=1' >> $GITHUB_OUTPUT @@ -95,7 +115,7 @@ jobs: ini-values: ${{ steps.set_ini.outputs.PHP_INI }} coverage: none - - name: 'Composer: adjust dependencies' + - name: "Composer: adjust dependencies" run: | # Remove dev dependencies which are not compatible with all supported PHP versions. composer remove --dev --no-update sirbrillig/phpcs-import-detection phpstan/phpstan @@ -146,15 +166,15 @@ jobs: strategy: matrix: include: - - php: '8.4' - phpcs_version: 'dev-master' - - php: '7.4' - phpcs_version: '3.5.6' + - php: "8.4" + phpcs_version: "4.x-dev" + - php: "7.4" + phpcs_version: "3.5.7" - - php: '5.4' - phpcs_version: 'dev-master' - - php: '5.4' - phpcs_version: '3.5.6' + - php: "7.2" + phpcs_version: "4.x-dev" + - php: "5.4" + phpcs_version: "3.5.7" name: "Coverage: PHP ${{ matrix.php }} on PHPCS ${{ matrix.phpcs_version }}" @@ -167,7 +187,7 @@ jobs: run: | # On stable PHPCS versions, allow for PHP deprecation notices. # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore. - if [ "${{ matrix.phpcs_version }}" != "dev-master" ]; then + if [ "${{ matrix.phpcs_version }}" != "4.x-dev" ]; then echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On, zend.assertions=1' >> $GITHUB_OUTPUT else echo 'PHP_INI=error_reporting=-1, display_errors=On, zend.assertions=1' >> $GITHUB_OUTPUT @@ -180,7 +200,7 @@ jobs: ini-values: ${{ steps.set_ini.outputs.PHP_INI }} coverage: xdebug - - name: 'Composer: adjust dependencies' + - name: "Composer: adjust dependencies" run: | # Remove dev dependencies which are not compatible with all supported PHP/PHPCS versions. composer remove --dev --no-update phpcsstandards/phpcsdevcs sirbrillig/phpcs-import-detection phpstan/phpstan diff --git a/VariableAnalysis/Lib/Helpers.php b/VariableAnalysis/Lib/Helpers.php index 4a9c162..8b67f11 100644 --- a/VariableAnalysis/Lib/Helpers.php +++ b/VariableAnalysis/Lib/Helpers.php @@ -93,7 +93,7 @@ public static function findContainingOpeningBracket(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); if (isset($tokens[$stackPtr]['nested_parenthesis'])) { /** - * @var array + * @var list */ $openPtrs = array_keys($tokens[$stackPtr]['nested_parenthesis']); return (int)end($openPtrs); @@ -319,8 +319,18 @@ public static function findFunctionCall(File $phpcsFile, $stackPtr) if (is_int($openPtr)) { // First non-whitespace thing and see if it's a T_STRING function name $functionPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true); - if (is_int($functionPtr) && $tokens[$functionPtr]['code'] === T_STRING) { - return $functionPtr; + if (is_int($functionPtr)) { + $functionTokenCode = $tokens[$functionPtr]['code']; + // In PHPCS 4.x, function names can be T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, or T_NAME_RELATIVE + $validFunctionTokens = [ + T_STRING, + T_NAME_FULLY_QUALIFIED, + T_NAME_QUALIFIED, + T_NAME_RELATIVE, + ]; + if (in_array($functionTokenCode, $validFunctionTokens, true)) { + return $functionPtr; + } } } return null; @@ -364,9 +374,6 @@ public static function findFunctionCallArguments(File $phpcsFile, $stackPtr) if (self::findContainingOpeningBracket($phpcsFile, $nextPtr) === $openPtr) { // Comma is at our level of brackets, it's an argument delimiter. $range = range($lastArgComma + 1, $nextPtr - 1); - $range = array_filter($range, function ($element) { - return is_int($element); - }); array_push($argPtrs, $range); $lastArgComma = $nextPtr; } @@ -394,7 +401,8 @@ public static function getNextAssignPointer(File $phpcsFile, $stackPtr) // Is the next non-whitespace an assignment? $nextPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true); - if (is_int($nextPtr) + if ( + is_int($nextPtr) && isset(Tokens::$assignmentTokens[$tokens[$nextPtr]['code']]) // Ignore double arrow to prevent triggering on `foreach ( $array as $k => $v )`. && $tokens[$nextPtr]['code'] !== T_DOUBLE_ARROW @@ -548,6 +556,9 @@ public static function findVariableScopeExceptArrowFunctions(File $phpcsFile, $s T_DOUBLE_QUOTED_STRING, T_HEREDOC, T_STRING, + T_NAME_FULLY_QUALIFIED, + T_NAME_QUALIFIED, + T_NAME_RELATIVE, ]; if (! in_array($tokens[$stackPtr]['code'], $allowedTypes, true)) { throw new \Exception("Cannot find variable scope for non-variable {$tokens[$stackPtr]['type']}"); @@ -618,24 +629,6 @@ private static function getStartOfTokenScope(File $phpcsFile, $stackPtr) return 0; } - /** - * @param File $phpcsFile - * @param int $stackPtr - * - * @return bool - */ - public static function isTokenInsideArrowFunctionDefinition(File $phpcsFile, $stackPtr) - { - $tokens = $phpcsFile->getTokens(); - $token = $tokens[$stackPtr]; - $openParenIndices = isset($token['nested_parenthesis']) ? $token['nested_parenthesis'] : []; - if (empty($openParenIndices)) { - return false; - } - $openParenPtr = $openParenIndices[0]; - return self::isArrowFunction($phpcsFile, $openParenPtr - 1); - } - /** * @param File $phpcsFile * @param int $stackPtr @@ -1290,7 +1283,7 @@ public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, return null; } /** - * @var array + * @var list */ $startingParenthesis = array_keys($token['nested_parenthesis']); $startOfArguments = end($startingParenthesis); @@ -1681,9 +1674,27 @@ public static function getFunctionNameWithNamespace(File $phpcsFile, $stackPtr) $startOfScope = self::findVariableScope($phpcsFile, $stackPtr); $functionName = $tokens[$stackPtr]['content']; + // In PHPCS 4.x, T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, and T_NAME_RELATIVE + // tokens already contain the full namespaced name, so we can return early. + if ($tokens[$stackPtr]['code'] === T_NAME_FULLY_QUALIFIED) { + return $functionName; + } + if ($tokens[$stackPtr]['code'] === T_NAME_QUALIFIED) { + return $functionName; + } + if ($tokens[$stackPtr]['code'] === T_NAME_RELATIVE) { + return $functionName; + } + // Move backwards from the token, collecting namespace separators and // strings, until we encounter whitespace or something else. - $partOfNamespace = [T_NS_SEPARATOR, T_STRING]; + $partOfNamespace = [ + T_NS_SEPARATOR, + T_STRING, + T_NAME_QUALIFIED, + T_NAME_RELATIVE, + T_NAME_FULLY_QUALIFIED, + ]; for ($i = $stackPtr - 1; $i > $startOfScope; $i--) { if (! in_array($tokens[$i]['code'], $partOfNamespace, true)) { break; @@ -1708,6 +1719,15 @@ private static function isTokenPossiblyPartOfTypehint(File $phpcsFile, $stackPtr if ($token['code'] === 'PHPCS_T_NULLABLE') { return true; } + if ($token['code'] === T_NAME_QUALIFIED) { + return true; + } + if ($token['code'] === T_NAME_RELATIVE) { + return true; + } + if ($token['code'] === T_NAME_FULLY_QUALIFIED) { + return true; + } if ($token['code'] === T_NS_SEPARATOR) { return true; } diff --git a/VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php b/VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php index b1d7e46..b464c26 100644 --- a/VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php +++ b/VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php @@ -1503,7 +1503,34 @@ protected function processVariableAsPassByReferenceFunctionCall(File $phpcsFile, // Is our function a known pass-by-reference function? $functionName = $tokens[$functionPtr]['content']; - $refArgs = $this->getPassByReferenceFunction($functionName); + + // In PHPCS 4.x, T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, and T_NAME_RELATIVE + // tokens contain the full namespaced name. Extract just the base name for the + // first check so that 'my_function' in the config can match '\My\Namespace\my_function'. + $functionBaseName = $functionName; + if ($tokens[$functionPtr]['code'] === T_NAME_FULLY_QUALIFIED) { + $lastBackslashPos = strrpos($functionName, '\\'); + if ($lastBackslashPos !== false) { + $functionBaseName = substr($functionName, $lastBackslashPos + 1); + } + } elseif ($tokens[$functionPtr]['code'] === T_NAME_QUALIFIED) { + $lastBackslashPos = strrpos($functionName, '\\'); + if ($lastBackslashPos !== false) { + $functionBaseName = substr($functionName, $lastBackslashPos + 1); + } + } elseif ($tokens[$functionPtr]['code'] === T_NAME_RELATIVE) { + $lastBackslashPos = strrpos($functionName, '\\'); + if ($lastBackslashPos !== false) { + $functionBaseName = substr($functionName, $lastBackslashPos + 1); + } + } + + // Ensure we have a string (should always be true, but helps static analyzers). + if (! is_string($functionBaseName) || $functionBaseName === '') { + return false; + } + + $refArgs = $this->getPassByReferenceFunction($functionBaseName); if (! $refArgs) { // Check again with the fully namespaced function name. $functionName = Helpers::getFunctionNameWithNamespace($phpcsFile, $functionPtr); diff --git a/composer.json b/composer.json index b11fae4..d94d0af 100644 --- a/composer.json +++ b/composer.json @@ -51,13 +51,12 @@ }, "require": { "php": ">=5.4.0", - "squizlabs/php_codesniffer": "^3.5.6" + "squizlabs/php_codesniffer": "^3.5.7 || ^4.0.0" }, "require-dev": { "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0 || ^10.5.32 || ^11.3.3", - "phpcsstandards/phpcsdevcs": "^1.1", - "phpstan/phpstan": "^1.7", + "phpstan/phpstan": "^1.7 || ^2.0", "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", - "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0" + "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0 || ^6.0 || ^7.0" } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index ff63b21..42482de 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -30,33 +30,26 @@ - + - - - - - - - - - - - - - + + + + + + + diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000..3bf1ba8 --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/psalm.xml b/psalm.xml index 5223a5e..abd0443 100644 --- a/psalm.xml +++ b/psalm.xml @@ -6,6 +6,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" + errorBaseline="psalm-baseline.xml" >