From bcb9c56d0075cc69b1456163ad4ac41fbe7d15b5 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 10 Nov 2020 17:15:42 -0500 Subject: [PATCH] Backport all of 3.0 to 2.x without phpcsutils (#213) * Replace isArrowFunction/getArrowFunctionOpenClose with local version * Replace Lists::getAssignments with local getListAssignments * Remove PHPCSUtils imports * Remove PHPCSUtils from README * Remove phpcsutils from requirements * Fix return type of getListAssignments * Add phpcodesniffer-composer-installer as dev dependency --- README.md | 14 +- VariableAnalysis/Lib/Helpers.php | 154 +++++++++++++++++- .../CodeAnalysis/VariableAnalysisSniff.php | 22 +-- VariableAnalysis/ruleset.xml | 2 - composer.circleci.json | 3 +- composer.json | 6 +- 6 files changed, 162 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index d1df6b2b..2d331777 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ Plugin for PHP_CodeSniffer static analysis tool that adds analysis of problematic variable use. -**Please note that this README is for VariableAnalysis v3. For documentation about v2, [see this page](https://github.com/sirbrillig/phpcs-variable-analysis/blob/2.x-legacy/README.md).** - - Warns if variables are used without being defined. (Sniff code: `VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable`) - Warns if variables are used for an array push shortcut without being defined. (Sniff code: `VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedArrayVariable`) - Warns if variables are used inside `unset()` without being defined. (Sniff code: `VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedUnsetVariable`) @@ -20,8 +18,6 @@ Plugin for PHP_CodeSniffer static analysis tool that adds analysis of problemati VariableAnalysis requires PHP 5.4 or higher and [PHP CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) version 3.5.0 or higher. -It also requires [PHPCSUtils](https://phpcsutils.com/) which must be installed as a PHPCS standard. If you are using composer, this will be done automatically (see below). - ### With PHPCS Composer Installer This is the easiest method. @@ -52,17 +48,15 @@ It should just work after that! Do ensure that PHP_CodeSniffer's version matches our [requirements](#requirements). -2. Install PHPCSUtils (required by this sniff). Download either the zip or tar.gz file from [the PHPCSUtils latest release page](https://github.com/PHPCSStandards/PHPCSUtils/releases/latest). Expand the file and rename the resulting directory to `phpcsutils`. Move the directory to a place where you'd like to keep all your PHPCS standards. - -3. Install VariableAnalysis. Download either the zip or tar.gz file from [the VariableAnalysis latest release page](https://github.com/sirbrillig/phpcs-variable-analysis/releases/latest). Expand the file and rename the resulting directory to `phpcs-variable-analysis`. Move the directory to a place where you'd like to keep all your PHPCS standards. +2. Install VariableAnalysis. Download either the zip or tar.gz file from [the VariableAnalysis latest release page](https://github.com/sirbrillig/phpcs-variable-analysis/releases/latest). Expand the file and rename the resulting directory to `phpcs-variable-analysis`. Move the directory to a place where you'd like to keep all your PHPCS standards. -4. Add the paths of the newly installed standards to the [PHP_CodeSniffer installed_paths configuration](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Configuration-Options#setting-the-installed-standard-paths). The following command should append the new standards to your existing standards (be sure to supply the actual paths to the directories you created above). +3. Add the paths of the newly installed standards to the [PHP_CodeSniffer installed_paths configuration](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Configuration-Options#setting-the-installed-standard-paths). The following command should append the new standards to your existing standards (be sure to supply the actual paths to the directories you created above). - phpcs --config-set installed_paths "$(phpcs --config-show|grep installed_paths|awk '{ print $2 }'),/path/to/phpcsutils,/path/to/phpcs-variable-analysis" + phpcs --config-set installed_paths "$(phpcs --config-show|grep installed_paths|awk '{ print $2 }'),/path/to/phpcs-variable-analysis" If you do not have any other standards installed, you can do this more easily (again, be sure to supply the actual paths): - phpcs --config-set installed_paths /path/to/phpcsutils,/path/to/phpcs-variable-analysis + phpcs --config-set installed_paths /path/to/phpcs-variable-analysis ## Customization diff --git a/VariableAnalysis/Lib/Helpers.php b/VariableAnalysis/Lib/Helpers.php index f4016dfa..5bde2274 100644 --- a/VariableAnalysis/Lib/Helpers.php +++ b/VariableAnalysis/Lib/Helpers.php @@ -7,7 +7,6 @@ use VariableAnalysis\Lib\ScopeType; use VariableAnalysis\Lib\VariableInfo; use PHP_CodeSniffer\Util\Tokens; -use PHPCSUtils\Utils\FunctionDeclarations; class Helpers { /** @@ -160,7 +159,7 @@ public static function getFunctionIndexForFunctionArgument(File $phpcsFile, $sta T_FUNCTION, T_CLOSURE, ]; - if (!in_array($functionToken['code'], $functionTokenTypes, true) && ! FunctionDeclarations::isArrowFunction($phpcsFile, $functionPtr)) { + if (!in_array($functionToken['code'], $functionTokenTypes, true) && ! self::isArrowFunction($phpcsFile, $functionPtr)) { return null; } return $functionPtr; @@ -412,7 +411,7 @@ private static function getStartOfTokenScope(File $phpcsFile, $stackPtr) { T_CLOSURE, ]; foreach (array_reverse($conditions, true) as $scopePtr => $scopeCode) { - if (in_array($scopeCode, $functionTokenTypes, true) || FunctionDeclarations::isArrowFunction($phpcsFile, $scopePtr)) { + if (in_array($scopeCode, $functionTokenTypes, true) || self::isArrowFunction($phpcsFile, $scopePtr)) { return $scopePtr; } if (isset(Tokens::$ooScopeTokens[$scopeCode]) === true) { @@ -447,7 +446,7 @@ public static function isTokenInsideArrowFunctionDefinition(File $phpcsFile, $st return false; } $openParenPtr = $openParenIndices[0]; - return FunctionDeclarations::isArrowFunction($phpcsFile, $openParenPtr - 1); + return self::isArrowFunction($phpcsFile, $openParenPtr - 1); } /** @@ -461,7 +460,7 @@ public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPt if (! is_int($arrowFunctionIndex)) { return null; } - $arrowFunctionInfo = FunctionDeclarations::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionIndex); + $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionIndex); if (! $arrowFunctionInfo) { return null; } @@ -484,13 +483,150 @@ private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr); for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) { $token = $tokens[$index]; - if ($token['content'] === 'fn' && FunctionDeclarations::isArrowFunction($phpcsFile, $index)) { + if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) { return $index; } } return null; } + /** + * @param File $phpcsFile + * @param int $stackPtr + * + * @return bool + */ + 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; + } + + /** + * @param File $phpcsFile + * @param int $stackPtr + * + * @return ?array + */ + public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr) { + $tokens = $phpcsFile->getTokens(); + if (defined('T_FN') && $tokens[$stackPtr]['code'] === T_FN) { + return [ + 'scope_opener' => $tokens[$stackPtr]['scope_opener'], + 'scope_closer' => $tokens[$stackPtr]['scope_closer'], + ]; + } + 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 + $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') { + return null; + } + // Find the scope closer + $endScopeTokens = [ + T_COMMA, + T_SEMICOLON, + T_CLOSE_PARENTHESIS, + T_CLOSE_CURLY_BRACKET, + T_CLOSE_SHORT_ARRAY, + ]; + $scopeCloserIndex = $phpcsFile->findNext($endScopeTokens, $fatArrowIndex + 1); + if (! is_int($scopeCloserIndex)) { + return null; + } + return [ + 'scope_opener' => $stackPtr, + 'scope_closer' => $scopeCloserIndex, + ]; + } + + /** + * Return a list of indices for variables assigned within a list assignment + * + * @param File $phpcsFile + * @param int $listOpenerIndex + * + * @return ?array + */ + public static function getListAssignments(File $phpcsFile, $listOpenerIndex) { + $tokens = $phpcsFile->getTokens(); + self::debug('getListAssignments', $listOpenerIndex, $tokens[$listOpenerIndex]); + $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) { + return null; + } + + $assignPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $closePtr + 1, null, true); + if (! is_int($assignPtr) || $tokens[$assignPtr]['code'] !== T_EQUAL) { + // If we are nested inside a destructured assignment, we are also an assignment + $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 + $parentSquareBracket = Helpers::findContainingOpeningSquareBracket($phpcsFile, $listOpenerIndex); + if (is_int($parentSquareBracket)) { + $parents[$parentSquareBracket] = 0; // We don't actually need the closing paren + } + $nestedAssignments = null; + foreach (array_reverse($parents, true) as $openParen => $closeParen) { + $nestedAssignments = self::getListAssignments($phpcsFile, $openParen); + } + if ($nestedAssignments === null) { + return null; + } + } + + $variablePtrs = []; + + $currentPtr = $listOpenerIndex; + $variablePtr = 0; + while ($currentPtr < $closePtr && is_int($variablePtr)) { + $variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr); + if (is_int($variablePtr)) { + $variablePtrs[] = $variablePtr; + } + $currentPtr++; + } + + return $variablePtrs; + } + /** * @param File $phpcsFile * @param int $stackPtr @@ -665,8 +801,8 @@ public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartInd $tokens = $phpcsFile->getTokens(); $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : null; - if (FunctionDeclarations::isArrowFunction($phpcsFile, $scopeStartIndex)) { - $arrowFunctionInfo = FunctionDeclarations::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex); + if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) { + $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex); $scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex; } @@ -773,7 +909,7 @@ public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) { return null; } - if ($tokens[$functionPtr]['code'] === 'function' || ($tokens[$functionPtr]['content'] === 'fn' && FunctionDeclarations::isArrowFunction($phpcsFile, $functionPtr))) { + if ($tokens[$functionPtr]['code'] === 'function' || ($tokens[$functionPtr]['content'] === 'fn' && self::isArrowFunction($phpcsFile, $functionPtr))) { return null; } return $functionPtr; diff --git a/VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php b/VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php index 10cc10fe..195cf789 100644 --- a/VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php +++ b/VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php @@ -10,8 +10,6 @@ use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Util\Tokens; -use PHPCSUtils\Utils\Lists; -use PHPCSUtils\Utils\FunctionDeclarations; class VariableAnalysisSniff implements Sniff { /** @@ -239,7 +237,7 @@ public function process(File $phpcsFile, $stackPtr) { return; } if (in_array($token['code'], $scopeStartTokenTypes, true) - || FunctionDeclarations::isArrowFunction($phpcsFile, $stackPtr) + || Helpers::isArrowFunction($phpcsFile, $stackPtr) ) { Helpers::debug('found scope condition', $token); $this->recordScopeStartAndEnd($phpcsFile, $stackPtr); @@ -987,13 +985,12 @@ protected function processVariableAsListShorthandAssignment(File $phpcsFile, $st } // OK, we're a [ ... ] construct... are we being assigned to? - try { - $assignments = Lists::getAssignments($phpcsFile, $openPtr); - } catch (\Exception $error) { + $assignments = Helpers::getListAssignments($phpcsFile, $openPtr); + if (! $assignments) { return false; } - $matchingAssignment = array_reduce($assignments, function ($thisAssignment, array $assignment) use ($stackPtr) { - if (isset($assignment['assignment_token']) && $assignment['assignment_token'] === $stackPtr) { + $matchingAssignment = array_reduce($assignments, function ($thisAssignment, $assignment) use ($stackPtr) { + if ($assignment === $stackPtr) { return $assignment; } return $thisAssignment; @@ -1030,13 +1027,12 @@ protected function processVariableAsListAssignment(File $phpcsFile, $stackPtr, $ } // OK, we're a list (...) construct... are we being assigned to? - try { - $assignments = Lists::getAssignments($phpcsFile, $prevPtr); - } catch (\Exception $error) { + $assignments = Helpers::getListAssignments($phpcsFile, $prevPtr); + if (! $assignments) { return false; } - $matchingAssignment = array_reduce($assignments, function ($thisAssignment, array $assignment) use ($stackPtr) { - if (isset($assignment['assignment_token']) && $assignment['assignment_token'] === $stackPtr) { + $matchingAssignment = array_reduce($assignments, function ($thisAssignment, $assignment) use ($stackPtr) { + if ($assignment === $stackPtr) { return $assignment; } return $thisAssignment; diff --git a/VariableAnalysis/ruleset.xml b/VariableAnalysis/ruleset.xml index 001d5ca3..29e7a014 100644 --- a/VariableAnalysis/ruleset.xml +++ b/VariableAnalysis/ruleset.xml @@ -2,6 +2,4 @@ Plugin for PHP_CodeSniffer static analysis tool that adds analysis of problematic variable use. - - diff --git a/composer.circleci.json b/composer.circleci.json index 066a5546..675ee84e 100644 --- a/composer.circleci.json +++ b/composer.circleci.json @@ -38,8 +38,7 @@ }, "require" : { "php" : ">=5.4.0", - "squizlabs/php_codesniffer": "^3.1", - "phpcsstandards/phpcsutils": "^1.0" + "squizlabs/php_codesniffer": "^3.1" }, "require-dev": { "phpunit/phpunit": "^4.0 || ^5.0 || ^6.5 || ^7.0 || ^8.0" diff --git a/composer.json b/composer.json index a0224240..2458e62a 100644 --- a/composer.json +++ b/composer.json @@ -40,13 +40,13 @@ }, "require" : { "php" : ">=5.4.0", - "squizlabs/php_codesniffer": "^3.5", - "phpcsstandards/phpcsutils": "^1.0" + "squizlabs/php_codesniffer": "^3.5" }, "require-dev": { "phpunit/phpunit": "^5.0 || ^6.5 || ^7.0 || ^8.0", "sirbrillig/phpcs-import-detection": "^1.1", "limedeck/phpunit-detailed-printer": "^3.1 || ^4.0 || ^5.0", - "phpstan/phpstan": "^0.11.8" + "phpstan/phpstan": "^0.11.8", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0" } }