diff --git a/.github/workflows/csqa.yml b/.github/workflows/csqa.yml index 1fba44e..fb7724e 100644 --- a/.github/workflows/csqa.yml +++ b/.github/workflows/csqa.yml @@ -26,10 +26,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2 with: php-version: "latest" coverage: none @@ -42,14 +42,14 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") # Validate the XML file. - name: Validate ruleset against schema - uses: phpcsstandards/xmllint-validate@v1 + uses: phpcsstandards/xmllint-validate@0fd9c4a9046055f621fca4bbdccb8eab1fd59fdc # v1 with: pattern: "VariableAnalysis/ruleset.xml" xsd-file: "vendor/squizlabs/php_codesniffer/phpcs.xsd" @@ -74,10 +74,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2 with: php-version: "8.1" coverage: none @@ -86,7 +86,7 @@ jobs: # Dependencies need to be installed to make sure the PHPUnit classes are recognized. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") @@ -104,10 +104,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2 with: php-version: "8.1" coverage: none @@ -116,7 +116,7 @@ jobs: # Dependencies need to be installed to make sure the PHPUnit classes are recognized. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b0e8221..3c0e0e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: # # 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.7", "4.0.0", "4.x-dev"] + phpcs_version: ["3.13.5", "4.0.0", "4.x-dev"] exclude: # PHPCS 4.x requires PHP 7.2+ @@ -61,32 +61,32 @@ jobs: - php: "8.5" phpcs_version: "4.0.0" - php: "8.5" - phpcs_version: "3.13.4" + phpcs_version: "3.13.5" - php: "8.4" phpcs_version: "4.x-dev" - php: "8.4" - phpcs_version: "3.6.1" + phpcs_version: "3.13.5" - php: "8.3" phpcs_version: "4.x-dev" - php: "8.3" - phpcs_version: "3.6.1" + phpcs_version: "3.13.5" - php: "8.2" phpcs_version: "4.x-dev" - php: "8.2" - phpcs_version: "3.6.1" + phpcs_version: "3.13.5" - php: "8.1" phpcs_version: "4.x-dev" - php: "8.1" - phpcs_version: "3.6.1" + phpcs_version: "3.13.5" - php: "8.0" phpcs_version: "4.x-dev" - php: "8.0" - phpcs_version: "3.5.7" + phpcs_version: "3.13.5" - php: "7.4" phpcs_version: "4.x-dev" @@ -101,7 +101,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup ini config id: set_ini @@ -115,7 +115,7 @@ jobs: fi - name: Install PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2 with: php-version: ${{ matrix.php }} ini-values: ${{ steps.set_ini.outputs.PHP_INI }} @@ -131,7 +131,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: # For the PHP "nightly", we need to install with ignore platform reqs as not all dependencies may allow it yet. composer-options: ${{ matrix.php == '8.6' && '--ignore-platform-req=php+' || '' }} @@ -168,18 +168,18 @@ jobs: - php: "8.5" phpcs_version: "4.x-dev" - php: "7.4" - phpcs_version: "3.5.7" + phpcs_version: "3.13.5" - php: "7.2" phpcs_version: "4.x-dev" - php: "5.4" - phpcs_version: "3.5.7" + phpcs_version: "3.13.5" name: "Coverage: PHP ${{ matrix.php }} on PHPCS ${{ matrix.phpcs_version }}" steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup ini config id: set_ini @@ -193,7 +193,7 @@ jobs: fi - name: Install PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2 with: php-version: ${{ matrix.php }} ini-values: ${{ steps.set_ini.outputs.PHP_INI }} @@ -206,7 +206,7 @@ jobs: composer require --no-update squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}" - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") @@ -245,7 +245,7 @@ jobs: - name: Upload coverage results to Coveralls if: ${{ success() }} - uses: coverallsapp/github-action@v2 + uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # v2 with: format: clover file: build/logs/clover.xml @@ -258,6 +258,6 @@ jobs: steps: - name: Coveralls Finished - uses: coverallsapp/github-action@v2 + uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # v2 with: parallel-finished: true diff --git a/README.md b/README.md index 0eb6e07..7fab196 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Plugin for PHP_CodeSniffer static analysis tool that adds analysis of problemati ### Requirements -VariableAnalysis requires PHP 5.4 or higher and [PHP CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer) version 3.5.7 or higher. +VariableAnalysis requires PHP 5.4 or higher and [PHP CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer) version 3.13.5 or higher. ### With PHPCS Composer Installer diff --git a/VariableAnalysis/Lib/Helpers.php b/VariableAnalysis/Lib/Helpers.php index 8b67f11..dada61f 100644 --- a/VariableAnalysis/Lib/Helpers.php +++ b/VariableAnalysis/Lib/Helpers.php @@ -10,6 +10,12 @@ use VariableAnalysis\Lib\ScopeType; use VariableAnalysis\Lib\VariableInfo; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\Conditions; +use PHPCSUtils\Utils\Context; +use PHPCSUtils\Utils\FunctionDeclarations; +use PHPCSUtils\Utils\Lists; +use PHPCSUtils\Utils\Parentheses; +use PHPCSUtils\Utils\PassedParameters; class Helpers { @@ -90,36 +96,27 @@ public static function getPreviousStatementPtr(File $phpcsFile, $stackPtr) */ public static function findContainingOpeningBracket(File $phpcsFile, $stackPtr) { - $tokens = $phpcsFile->getTokens(); - if (isset($tokens[$stackPtr]['nested_parenthesis'])) { - /** - * @var list - */ - $openPtrs = array_keys($tokens[$stackPtr]['nested_parenthesis']); - return (int)end($openPtrs); - } - return null; + // Use PHPCSUtils to get the innermost parenthesis opener + $result = Parentheses::getLastOpener($phpcsFile, $stackPtr); + + // PHPCSUtils returns false on failure, but our code expects null + return $result !== false ? $result : null; } /** - * @param array{conditions: (int|string)[], content: string} $token + * @param File $phpcsFile + * @param int $stackPtr * * @return bool */ - public static function areAnyConditionsAClass(array $token) + public static function areAnyConditionsAClass(File $phpcsFile, $stackPtr) { - $conditions = $token['conditions']; $classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT]; if (defined('T_ENUM')) { $classlikeCodes[] = T_ENUM; } $classlikeCodes[] = 'PHPCS_T_ENUM'; - foreach (array_reverse($conditions, true) as $scopeCode) { - if (in_array($scopeCode, $classlikeCodes, true)) { - return true; - } - } - return false; + return Conditions::hasCondition($phpcsFile, $stackPtr, $classlikeCodes); } /** @@ -284,17 +281,14 @@ public static function getUseIndexForUseImport(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); - $nonUseTokenTypes = Tokens::$emptyTokens; - $nonUseTokenTypes[] = T_VARIABLE; - $nonUseTokenTypes[] = T_ELLIPSIS; - $nonUseTokenTypes[] = T_COMMA; - $nonUseTokenTypes[] = T_BITWISE_AND; - $openParenPtr = self::getIntOrNull($phpcsFile->findPrevious($nonUseTokenTypes, $stackPtr - 1, null, true, null, true)); - if (! is_int($openParenPtr) || $tokens[$openParenPtr]['code'] !== T_OPEN_PARENTHESIS) { + $openParenPtr = self::findContainingOpeningBracket($phpcsFile, $stackPtr); + if (! is_int($openParenPtr)) { return null; } - $usePtr = self::getIntOrNull($phpcsFile->findPrevious(array_values($nonUseTokenTypes), $openParenPtr - 1, null, true, null, true)); + $usePtr = self::getIntOrNull( + $phpcsFile->findPrevious(Tokens::$emptyTokens, $openParenPtr - 1, null, true, null, true) + ); if (! is_int($usePtr) || $tokens[$usePtr]['code'] !== T_USE) { return null; } @@ -340,7 +334,7 @@ public static function findFunctionCall(File $phpcsFile, $stackPtr) * @param File $phpcsFile * @param int $stackPtr * - * @return array> + * @return array> */ public static function findFunctionCallArguments(File $phpcsFile, $stackPtr) { @@ -355,38 +349,7 @@ public static function findFunctionCallArguments(File $phpcsFile, $stackPtr) } } - // $stackPtr is the function name, find our brackets after it - $openPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true); - if (($openPtr === false) || ($tokens[$openPtr]['code'] !== T_OPEN_PARENTHESIS)) { - return []; - } - - if (!isset($tokens[$openPtr]['parenthesis_closer'])) { - return []; - } - $closePtr = $tokens[$openPtr]['parenthesis_closer']; - - $argPtrs = []; - $lastPtr = $openPtr; - $lastArgComma = $openPtr; - $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr); - while (is_int($nextPtr)) { - if (self::findContainingOpeningBracket($phpcsFile, $nextPtr) === $openPtr) { - // Comma is at our level of brackets, it's an argument delimiter. - $range = range($lastArgComma + 1, $nextPtr - 1); - array_push($argPtrs, $range); - $lastArgComma = $nextPtr; - } - $lastPtr = $nextPtr; - $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr); - } - $range = range($lastArgComma + 1, $closePtr - 1); - $range = array_filter($range, function ($element) { - return is_int($element); - }); - array_push($argPtrs, $range); - - return $argPtrs; + return PassedParameters::getParameters($phpcsFile, $stackPtr); } /** @@ -461,9 +424,9 @@ public static function findVariableScope(File $phpcsFile, $stackPtr, $varName = /** * Return the variable names and positions of each variable targetted by a `compact()` call. * - * @param File $phpcsFile - * @param int $stackPtr - * @param array> $arguments The stack pointers of each argument; see findFunctionCallArguments + * @param File $phpcsFile + * @param int $stackPtr + * @param array> $arguments The parameters from PassedParameters::getParameters() * * @return array each variable's firstRead position and its name; other VariableInfo properties are not set! */ @@ -472,36 +435,50 @@ public static function getVariablesInsideCompact(File $phpcsFile, $stackPtr, $ar $tokens = $phpcsFile->getTokens(); $variablePositionsAndNames = []; - foreach ($arguments as $argumentPtrs) { - $argumentPtrs = array_values(array_filter($argumentPtrs, function ($argumentPtr) use ($tokens) { - return isset(Tokens::$emptyTokens[$tokens[$argumentPtr]['code']]) === false; - })); - if (empty($argumentPtrs)) { - continue; + foreach ($arguments as $param) { + // Find the first non-empty token in this argument's range. + $firstNonEmpty = null; + $nonEmptyCount = 0; + for ($i = (int)$param['start']; $i <= (int)$param['end']; $i++) { + if (!isset(Tokens::$emptyTokens[$tokens[$i]['code']])) { + if ($firstNonEmpty === null) { + $firstNonEmpty = $i; + } + $nonEmptyCount++; + } } - if (!isset($tokens[$argumentPtrs[0]])) { + + if ($firstNonEmpty === null) { continue; } - $argumentFirstToken = $tokens[$argumentPtrs[0]]; + + $argumentFirstToken = $tokens[$firstNonEmpty]; + if ($argumentFirstToken['code'] === T_ARRAY) { // It's an array argument, recurse. - $arrayArguments = self::findFunctionCallArguments($phpcsFile, $argumentPtrs[0]); - $variablePositionsAndNames = array_merge($variablePositionsAndNames, self::getVariablesInsideCompact($phpcsFile, $stackPtr, $arrayArguments)); + $arrayArguments = PassedParameters::getParameters($phpcsFile, $firstNonEmpty); + $variablePositionsAndNames = array_merge( + $variablePositionsAndNames, + self::getVariablesInsideCompact($phpcsFile, $stackPtr, $arrayArguments) + ); continue; } - if (count($argumentPtrs) > 1) { + + if ($nonEmptyCount > 1) { // Complex argument, we can't handle it, ignore. continue; } + if ($argumentFirstToken['code'] === T_CONSTANT_ENCAPSED_STRING) { // Single-quoted string literal, ie compact('whatever'). // Substr is to strip the enclosing single-quotes. $varName = substr($argumentFirstToken['content'], 1, -1); $variable = new VariableInfo($varName); - $variable->firstRead = $argumentPtrs[0]; + $variable->firstRead = $firstNonEmpty; $variablePositionsAndNames[] = $variable; continue; } + if ($argumentFirstToken['code'] === T_DOUBLE_QUOTED_STRING) { // Double-quoted string literal. $regexp = Constants::getDoubleQuotedVarRegexp(); @@ -512,9 +489,8 @@ public static function getVariablesInsideCompact(File $phpcsFile, $stackPtr, $ar // Substr is to strip the enclosing double-quotes. $varName = substr($argumentFirstToken['content'], 1, -1); $variable = new VariableInfo($varName); - $variable->firstRead = $argumentPtrs[0]; + $variable->firstRead = $firstNonEmpty; $variablePositionsAndNames[] = $variable; - continue; } } return $variablePositionsAndNames; @@ -649,7 +625,7 @@ public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPt // We found the closest arrow function before this token. If the token is // within the scope of that arrow function, then return it. - if ($stackPtr > $arrowFunctionInfo['scope_opener'] && $stackPtr < $arrowFunctionInfo['scope_closer']) { + if ($stackPtr >= $arrowFunctionInfo['scope_opener'] && $stackPtr <= $arrowFunctionInfo['scope_closer']) { return $arrowFunctionIndex; } @@ -699,28 +675,7 @@ private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr public static function isArrowFunction(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); - if (defined('T_FN') && $tokens[$stackPtr]['code'] === T_FN) { - return true; - } - if ($tokens[$stackPtr]['content'] !== 'fn') { - return false; - } - // Make sure next non-space token is an open parenthesis - $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); - if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) { - return false; - } - // Find the associated close parenthesis - $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer']; - // Make sure the next token is a fat arrow - $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true); - if (! is_int($fatArrowIndex)) { - return false; - } - if ($tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW') { - return false; - } - return true; + return $tokens[$stackPtr]['code'] === T_FN; } /** @@ -741,169 +696,21 @@ public static function isArrowFunction(File $phpcsFile, $stackPtr) public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); - if ($tokens[$stackPtr]['content'] !== 'fn') { - return null; - } - // Make sure next non-space token is an open parenthesis - $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); - if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) { - return null; - } - // Find the associated close parenthesis - $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer']; - // Make sure the next token is a fat arrow or a return type - $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true); - if (! is_int($fatArrowIndex)) { - return null; - } - if ( - $tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && - $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW' && - $tokens[$fatArrowIndex]['code'] !== T_COLON - ) { - return null; - } - // Find the scope closer - $scopeCloserIndex = null; - $foundCurlyPairs = 0; - $foundArrayPairs = 0; - $foundParenPairs = 0; - $arrowBodyStart = $tokens[$stackPtr]['parenthesis_closer'] + 1; - $lastToken = self::getLastNonEmptyTokenIndexInFile($phpcsFile); - for ($index = $arrowBodyStart; $index < $lastToken; $index++) { - $token = $tokens[$index]; - if (empty($token['code'])) { - $scopeCloserIndex = $index; - break; - } - - $code = $token['code']; - - // A semicolon is always a closer. - if ($code === T_SEMICOLON) { - $scopeCloserIndex = $index; - break; - } - - // Track pair opening tokens. - if ($code === T_OPEN_CURLY_BRACKET) { - $foundCurlyPairs += 1; - continue; - } - if ($code === T_OPEN_SHORT_ARRAY || $code === T_OPEN_SQUARE_BRACKET) { - $foundArrayPairs += 1; - continue; - } - if ($code === T_OPEN_PARENTHESIS) { - $foundParenPairs += 1; - continue; - } - - // A pair closing is only an arrow func closer if there was no matching opening token. - if ($code === T_CLOSE_CURLY_BRACKET) { - if ($foundCurlyPairs === 0) { - $scopeCloserIndex = $index; - break; - } - $foundCurlyPairs -= 1; - continue; - } - if ($code === T_CLOSE_SHORT_ARRAY || $code === T_CLOSE_SQUARE_BRACKET) { - if ($foundArrayPairs === 0) { - $scopeCloserIndex = $index; - break; - } - $foundArrayPairs -= 1; - continue; - } - if ($code === T_CLOSE_PARENTHESIS) { - if ($foundParenPairs === 0) { - $scopeCloserIndex = $index; - break; - } - $foundParenPairs -= 1; - continue; - } - - // A comma is a closer only if we are not inside an opening token. - if ($code === T_COMMA) { - if (empty($foundArrayPairs) && empty($foundParenPairs) && empty($foundCurlyPairs)) { - $scopeCloserIndex = $index; - break; - } - continue; - } + if ($tokens[$stackPtr]['code'] !== T_FN) { + return null; } - if (! is_int($scopeCloserIndex)) { + if (!isset($tokens[$stackPtr]['scope_closer'])) { return null; } return [ - 'scope_opener' => $stackPtr, - 'scope_closer' => $scopeCloserIndex, + 'scope_opener' => $tokens[$stackPtr]['scope_opener'], + 'scope_closer' => $tokens[$stackPtr]['scope_closer'], ]; } - /** - * Determine if a token is a list opener for list assignment/destructuring. - * - * The index provided can be either the opening square brace of a short list - * assignment like the first character of `[$a] = $b;` or the `list` token of - * an expression like `list($a) = $b;` or the opening parenthesis of that - * expression. - * - * @param File $phpcsFile - * @param int $listOpenerIndex - * - * @return bool - */ - private static function isListAssignment(File $phpcsFile, $listOpenerIndex) - { - $tokens = $phpcsFile->getTokens(); - // Match `[$a] = $b;` except for when the previous token is a parenthesis. - if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SHORT_ARRAY) { - return true; - } - // Match `list($a) = $b;` - if ($tokens[$listOpenerIndex]['code'] === T_LIST) { - return true; - } - - // If $listOpenerIndex is the open parenthesis of `list($a) = $b;`, then - // match that too. - if ($tokens[$listOpenerIndex]['code'] === T_OPEN_PARENTHESIS) { - $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true); - if ( - isset($tokens[$previousTokenPtr]) - && $tokens[$previousTokenPtr]['code'] === T_LIST - ) { - return true; - } - return true; - } - - // If the list opener token is a square bracket that is preceeded by a - // close parenthesis that has an owner which is a scope opener, then this - // is a list assignment and not an array access. - // - // Match `if (true) [$a] = $b;` - if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SQUARE_BRACKET) { - $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true); - if ( - isset($tokens[$previousTokenPtr]) - && $tokens[$previousTokenPtr]['code'] === T_CLOSE_PARENTHESIS - && isset($tokens[$previousTokenPtr]['parenthesis_owner']) - && isset(Tokens::$scopeOpeners[$tokens[$tokens[$previousTokenPtr]['parenthesis_owner']]['code']]) - ) { - return true; - } - } - - return false; - } - /** * Return a list of indices for variables assigned within a list assignment. * @@ -919,74 +726,44 @@ private static function isListAssignment(File $phpcsFile, $listOpenerIndex) */ public static function getListAssignments(File $phpcsFile, $listOpenerIndex) { - $tokens = $phpcsFile->getTokens(); - self::debug('getListAssignments', $listOpenerIndex, $tokens[$listOpenerIndex]); + self::debug('getListAssignments', $listOpenerIndex, $phpcsFile->getTokens()[$listOpenerIndex]); - // First find the end of the list - $closePtr = null; - if (isset($tokens[$listOpenerIndex]['parenthesis_closer'])) { - $closePtr = $tokens[$listOpenerIndex]['parenthesis_closer']; - } - if (isset($tokens[$listOpenerIndex]['bracket_closer'])) { - $closePtr = $tokens[$listOpenerIndex]['bracket_closer']; - } - if (! $closePtr) { + // Use PHPCSUtils to get detailed assignment information + try { + $assignments = \PHPCSUtils\Utils\Lists::getAssignments($phpcsFile, $listOpenerIndex); + } catch (\PHPCSUtils\Exceptions\UnexpectedTokenType $e) { + // Not a list token return null; } - // Find the assignment (equals sign) which, if this is a list assignment, should be the next non-space token - $assignPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $closePtr + 1, null, true); - - // If the next token isn't an assignment, check for nested brackets because we might be a nested assignment - if (! is_int($assignPtr) || $tokens[$assignPtr]['code'] !== T_EQUAL) { - // Collect the enclosing list open/close tokens ($parents is an assoc array keyed by opener index and the value is the closer index) - $parents = isset($tokens[$listOpenerIndex]['nested_parenthesis']) ? $tokens[$listOpenerIndex]['nested_parenthesis'] : []; - // There's no record of nested brackets for short lists; we'll have to find the parent ourselves - if (empty($parents)) { - $parentSquareBracketPtr = self::findContainingOpeningSquareBracket($phpcsFile, $listOpenerIndex); - if (is_int($parentSquareBracketPtr)) { - // Make sure that the parent is really a parent by checking that its - // closing index is outside of the current bracket's closing index. - $parentSquareBracketToken = $tokens[$parentSquareBracketPtr]; - $parentSquareBracketClosePtr = $parentSquareBracketToken['bracket_closer']; - if ($parentSquareBracketClosePtr && $parentSquareBracketClosePtr > $closePtr) { - self::debug("found enclosing bracket for {$listOpenerIndex}: {$parentSquareBracketPtr}"); - // Collect the opening index, but we don't actually need the closing paren index so just make that 0 - $parents = [$parentSquareBracketPtr => 0]; - } - } - } - // If we have no parents, this is not a nested assignment and therefore is not an assignment - if (empty($parents)) { - return null; - } - - // Recursively check to see if the parent is a list assignment (we only need to check one level due to the recursion) - $isNestedAssignment = null; - $parentListOpener = array_keys(array_reverse($parents, true))[0]; - $isNestedAssignment = self::getListAssignments($phpcsFile, $parentListOpener); - if ($isNestedAssignment === null) { - return null; - } + if (empty($assignments)) { + return null; } + // Extract just the variable token positions for backward compatibility $variablePtrs = []; + foreach ($assignments as $assignment) { + // Skip empty list items like in: list($a, , $b) + if ($assignment['is_empty']) { + continue; + } - $currentPtr = $listOpenerIndex; - $variablePtr = 0; - while ($currentPtr < $closePtr && is_int($variablePtr)) { - $variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr); - if (is_int($variablePtr)) { - $variablePtrs[] = $variablePtr; + // For nested lists, recursively get the assignments + if ($assignment['is_nested_list'] && $assignment['assignment_token'] !== false) { + $nestedVars = self::getListAssignments($phpcsFile, $assignment['assignment_token']); + if (is_array($nestedVars)) { + $variablePtrs = array_merge($variablePtrs, $nestedVars); + } + continue; } - ++$currentPtr; - } - if (! self::isListAssignment($phpcsFile, $listOpenerIndex)) { - return null; + // For regular variables, use the assignment_token which points to the T_VARIABLE + if ($assignment['assignment_token'] !== false && $assignment['variable'] !== false) { + $variablePtrs[] = $assignment['assignment_token']; + } } - return $variablePtrs; + return empty($variablePtrs) ? null : $variablePtrs; } /** @@ -1177,12 +954,6 @@ public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartInd { $tokens = $phpcsFile->getTokens(); $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0; - - if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) { - $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex); - $scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex; - } - if ($scopeStartIndex === 0) { $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile); } @@ -1333,22 +1104,8 @@ public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, */ public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr) { - $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr); - if (! is_int($functionIndex)) { - return false; - } - $tokens = $phpcsFile->getTokens(); - if (! isset($tokens[$functionIndex])) { - return false; - } - $allowedFunctionNames = [ - 'isset', - 'empty', - ]; - if (in_array($tokens[$functionIndex]['content'], $allowedFunctionNames, true)) { - return true; - } - return false; + // Use PHPCSUtils which handles all edge cases across PHP/PHPCS versions + return Context::inIsset($phpcsFile, $stackPtr) || Context::inEmpty($phpcsFile, $stackPtr); } /** @@ -1397,18 +1154,8 @@ public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr) */ public static function isVariableInsideUnset(File $phpcsFile, $stackPtr) { - $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr); - if (! is_int($functionIndex)) { - return false; - } - $tokens = $phpcsFile->getTokens(); - if (! isset($tokens[$functionIndex])) { - return false; - } - if ($tokens[$functionIndex]['content'] === 'unset') { - return true; - } - return false; + // Use PHPCSUtils which handles all edge cases across PHP/PHPCS versions + return Context::inUnset($phpcsFile, $stackPtr); } /** @@ -1622,31 +1369,15 @@ public static function getForLoopForIncrementVariable($stackPtr, $forLoops) */ public static function isConstructorPromotion(File $phpcsFile, $stackPtr) { - // If we are not in a function's parameters, this is not promotion. $functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr); if (! $functionIndex) { return false; } - - $tokens = $phpcsFile->getTokens(); - - // Move backwards from the token, ignoring whitespace, typehints, and the - // 'readonly' keyword, and return true if the previous token is a - // visibility keyword (eg: `public`). - for ($i = $stackPtr - 1; $i > $functionIndex; $i--) { - if (in_array($tokens[$i]['code'], Tokens::$scopeModifiers, true)) { - return true; - } - if (in_array($tokens[$i]['code'], Tokens::$emptyTokens, true)) { - continue; - } - if ($tokens[$i]['content'] === 'readonly') { - continue; - } - if (self::isTokenPartOfTypehint($phpcsFile, $i)) { - continue; + $params = FunctionDeclarations::getParameters($phpcsFile, $functionIndex); + foreach ($params as $param) { + if ($param['token'] === $stackPtr) { + return isset($param['property_visibility']); } - return false; } return false; } @@ -1704,85 +1435,6 @@ public static function getFunctionNameWithNamespace(File $phpcsFile, $stackPtr) return $functionName; } - /** - * Return false if the token is definitely not part of a typehint - * - * @param File $phpcsFile - * @param int $stackPtr - * - * @return bool - */ - private static function isTokenPossiblyPartOfTypehint(File $phpcsFile, $stackPtr) - { - $tokens = $phpcsFile->getTokens(); - $token = $tokens[$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; - } - if ($token['code'] === T_STRING) { - return true; - } - if ($token['code'] === T_TRUE) { - return true; - } - if ($token['code'] === T_FALSE) { - return true; - } - if ($token['code'] === T_NULL) { - return true; - } - if ($token['content'] === '|') { - return true; - } - if (in_array($token['code'], Tokens::$emptyTokens)) { - return true; - } - return false; - } - - /** - * Return true if the token is inside a typehint - * - * @param File $phpcsFile - * @param int $stackPtr - * - * @return bool - */ - public static function isTokenPartOfTypehint(File $phpcsFile, $stackPtr) - { - $tokens = $phpcsFile->getTokens(); - - if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $stackPtr)) { - return false; - } - - // Examine every following token, ignoring everything that might be part of - // a typehint. If we find a variable at the end, this is part of a - // typehint. - $i = $stackPtr; - while (true) { - $i += 1; - if (! isset($tokens[$i])) { - return false; - } - if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $i)) { - return ($tokens[$i]['code'] === T_VARIABLE); - } - } - } - /** * Return true if the token is inside an abstract class. * diff --git a/VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php b/VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php index b464c26..2ebbf67 100644 --- a/VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php +++ b/VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php @@ -170,6 +170,7 @@ public function __construct() * * @return (int|string)[] */ + #[\Override] public function register() { $types = [ @@ -240,6 +241,7 @@ private function getPassByReferenceFunctions() * * @return void */ + #[\Override] public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); @@ -890,7 +892,7 @@ protected function processVariableAsClassProperty(File $phpcsFile, $stackPtr) /** @var array{conditions?: (int|string)[], content?: string}|null */ $token = $tokens[$stackPtr]; if ($token && !empty($token['conditions']) && !empty($token['content']) && !Helpers::areConditionsWithinFunctionBeforeClass($token)) { - return Helpers::areAnyConditionsAClass($token); + return Helpers::areAnyConditionsAClass($phpcsFile, $stackPtr); } return false; } @@ -1103,7 +1105,7 @@ protected function processVariableAsStaticOutsideClass(File $phpcsFile, $stackPt } $errorClass = $code === T_SELF ? 'SelfOutsideClass' : 'StaticOutsideClass'; $staticRefType = $code === T_SELF ? 'self::' : 'static::'; - if (!empty($token['conditions']) && !empty($token['content']) && Helpers::areAnyConditionsAClass($token)) { + if ($token && !empty($token['content']) && Helpers::areAnyConditionsAClass($phpcsFile, $stackPtr)) { return false; } $phpcsFile->addError( @@ -1547,9 +1549,9 @@ protected function processVariableAsPassByReferenceFunctionCall(File $phpcsFile, // We're within a function call arguments list, find which arg we are. $argPos = false; - foreach ($argPtrs as $idx => $ptrs) { - if (in_array($stackPtr, $ptrs)) { - $argPos = $idx + 1; + foreach ($argPtrs as $idx => $param) { + if ($stackPtr >= $param['start'] && $stackPtr <= $param['end']) { + $argPos = $idx; break; } } @@ -1570,7 +1572,8 @@ protected function processVariableAsPassByReferenceFunctionCall(File $phpcsFile, // Our argument position matches that of a pass-by-ref argument, // check that we're the only part of the argument expression. - foreach ($argPtrs[$argPos - 1] as $ptr) { + $param = $argPtrs[$argPos]; + for ($ptr = (int)$param['start']; $ptr <= (int)$param['end']; $ptr++) { if ($ptr === $stackPtr) { continue; } diff --git a/composer.json b/composer.json index d94d0af..b4cf2fa 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,8 @@ }, "require": { "php": ">=5.4.0", - "squizlabs/php_codesniffer": "^3.5.7 || ^4.0.0" + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.0", + "phpcsstandards/phpcsutils": "^1.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", diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 3bf1ba8..93aaa90 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -15,12 +15,6 @@ - - - - - - @@ -28,9 +22,6 @@ - - - @@ -38,10 +29,6 @@ - - - -