239 lines
8.1 KiB
PHP
239 lines
8.1 KiB
PHP
<?php
|
|
/**
|
|
* PHPCompatibility, an external standard for PHP_CodeSniffer.
|
|
*
|
|
* @package PHPCompatibility
|
|
* @copyright 2012-2019 PHPCompatibility Contributors
|
|
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
|
|
* @link https://github.com/PHPCompatibility/PHPCompatibility
|
|
*/
|
|
|
|
namespace PHPCompatibility\Sniffs\ControlStructures;
|
|
|
|
use PHPCompatibility\Sniff;
|
|
use PHP_CodeSniffer_File as File;
|
|
use PHP_CodeSniffer_Tokens as Tokens;
|
|
|
|
/**
|
|
* Detect use of `continue` in `switch` control structures.
|
|
*
|
|
* As of PHP 7.3, PHP will throw a warning when `continue` is used to target a `switch`
|
|
* control structure.
|
|
* The sniff takes numeric arguments used with `continue` into account.
|
|
*
|
|
* PHP version 7.3
|
|
*
|
|
* @link https://www.php.net/manual/en/migration73.incompatible.php#migration73.incompatible.core.continue-targeting-switch
|
|
* @link https://wiki.php.net/rfc/continue_on_switch_deprecation
|
|
* @link https://github.com/php/php-src/commit/04e3523b7d095341f65ed5e71a3cac82fca690e4
|
|
* (actual implementation which is different from the RFC).
|
|
* @link https://www.php.net/manual/en/control-structures.switch.php
|
|
*
|
|
* @since 8.2.0
|
|
*/
|
|
class DiscouragedSwitchContinueSniff extends Sniff
|
|
{
|
|
|
|
/**
|
|
* Token codes of control structures which can be targeted using continue.
|
|
*
|
|
* @since 8.2.0
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $loopStructures = array(
|
|
\T_FOR => \T_FOR,
|
|
\T_FOREACH => \T_FOREACH,
|
|
\T_WHILE => \T_WHILE,
|
|
\T_DO => \T_DO,
|
|
\T_SWITCH => \T_SWITCH,
|
|
);
|
|
|
|
/**
|
|
* Tokens which start a new case within a switch.
|
|
*
|
|
* @since 8.2.0
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $caseTokens = array(
|
|
\T_CASE => \T_CASE,
|
|
\T_DEFAULT => \T_DEFAULT,
|
|
);
|
|
|
|
/**
|
|
* Token codes which are accepted to determine the level for the continue.
|
|
*
|
|
* This array is enriched with the arithmetic operators in the register() method.
|
|
*
|
|
* @since 8.2.0
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $acceptedLevelTokens = array(
|
|
\T_LNUMBER => \T_LNUMBER,
|
|
\T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS,
|
|
\T_CLOSE_PARENTHESIS => \T_CLOSE_PARENTHESIS,
|
|
);
|
|
|
|
|
|
/**
|
|
* Returns an array of tokens this test wants to listen for.
|
|
*
|
|
* @since 8.2.0
|
|
*
|
|
* @return array
|
|
*/
|
|
public function register()
|
|
{
|
|
$this->acceptedLevelTokens += Tokens::$arithmeticTokens;
|
|
$this->acceptedLevelTokens += Tokens::$emptyTokens;
|
|
|
|
return array(\T_SWITCH);
|
|
}
|
|
|
|
/**
|
|
* Processes this test, when one of its tokens is encountered.
|
|
*
|
|
* @since 8.2.0
|
|
*
|
|
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
|
|
* @param int $stackPtr The position of the current token in the
|
|
* stack passed in $tokens.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function process(File $phpcsFile, $stackPtr)
|
|
{
|
|
if ($this->supportsAbove('7.3') === false) {
|
|
return;
|
|
}
|
|
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
|
|
return;
|
|
}
|
|
|
|
$switchOpener = $tokens[$stackPtr]['scope_opener'];
|
|
$switchCloser = $tokens[$stackPtr]['scope_closer'];
|
|
|
|
// Quick check whether we need to bother with the more complex logic.
|
|
$hasContinue = $phpcsFile->findNext(\T_CONTINUE, ($switchOpener + 1), $switchCloser);
|
|
if ($hasContinue === false) {
|
|
return;
|
|
}
|
|
|
|
$caseDefault = $switchOpener;
|
|
|
|
do {
|
|
$caseDefault = $phpcsFile->findNext($this->caseTokens, ($caseDefault + 1), $switchCloser);
|
|
if ($caseDefault === false) {
|
|
break;
|
|
}
|
|
|
|
if (isset($tokens[$caseDefault]['scope_opener']) === false) {
|
|
// Unknown start of the case, skip.
|
|
continue;
|
|
}
|
|
|
|
$caseOpener = $tokens[$caseDefault]['scope_opener'];
|
|
$nextCaseDefault = $phpcsFile->findNext($this->caseTokens, ($caseDefault + 1), $switchCloser);
|
|
if ($nextCaseDefault === false) {
|
|
$caseCloser = $switchCloser;
|
|
} else {
|
|
$caseCloser = $nextCaseDefault;
|
|
}
|
|
|
|
// Check for unscoped control structures within the case.
|
|
$controlStructure = $caseOpener;
|
|
$doCount = 0;
|
|
while (($controlStructure = $phpcsFile->findNext($this->loopStructures, ($controlStructure + 1), $caseCloser)) !== false) {
|
|
if ($tokens[$controlStructure]['code'] === \T_DO) {
|
|
$doCount++;
|
|
}
|
|
|
|
if (isset($tokens[$controlStructure]['scope_opener'], $tokens[$controlStructure]['scope_closer']) === false) {
|
|
if ($tokens[$controlStructure]['code'] === \T_WHILE && $doCount > 0) {
|
|
// While in a do-while construct.
|
|
$doCount--;
|
|
continue;
|
|
}
|
|
|
|
// Control structure without braces found within the case, ignore this case.
|
|
continue 2;
|
|
}
|
|
}
|
|
|
|
// Examine the contents of the case.
|
|
$continue = $caseOpener;
|
|
|
|
do {
|
|
$continue = $phpcsFile->findNext(\T_CONTINUE, ($continue + 1), $caseCloser);
|
|
if ($continue === false) {
|
|
break;
|
|
}
|
|
|
|
$nextSemicolon = $phpcsFile->findNext(array(\T_SEMICOLON, \T_CLOSE_TAG), ($continue + 1), $caseCloser);
|
|
$codeString = '';
|
|
for ($i = ($continue + 1); $i < $nextSemicolon; $i++) {
|
|
if (isset($this->acceptedLevelTokens[$tokens[$i]['code']]) === false) {
|
|
// Function call/variable or other token which make numeric level impossible to determine.
|
|
continue 2;
|
|
}
|
|
|
|
if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
|
|
continue;
|
|
}
|
|
|
|
$codeString .= $tokens[$i]['content'];
|
|
}
|
|
|
|
$level = null;
|
|
if ($codeString !== '') {
|
|
if (is_numeric($codeString)) {
|
|
$level = (int) $codeString;
|
|
} else {
|
|
// With the above logic, the string can only contain digits and operators, eval!
|
|
$level = eval("return ( $codeString );");
|
|
}
|
|
}
|
|
|
|
if (isset($level) === false || $level === 0) {
|
|
$level = 1;
|
|
}
|
|
|
|
// Examine which control structure is being targeted by the continue statement.
|
|
if (isset($tokens[$continue]['conditions']) === false) {
|
|
continue;
|
|
}
|
|
|
|
$conditions = array_reverse($tokens[$continue]['conditions'], true);
|
|
// PHPCS adds more structures to the conditions array than we want to take into
|
|
// consideration, so clean up the array.
|
|
foreach ($conditions as $tokenPtr => $tokenCode) {
|
|
if (isset($this->loopStructures[$tokenCode]) === false) {
|
|
unset($conditions[$tokenPtr]);
|
|
}
|
|
}
|
|
|
|
$targetCondition = \array_slice($conditions, ($level - 1), 1, true);
|
|
if (empty($targetCondition)) {
|
|
continue;
|
|
}
|
|
|
|
$conditionToken = key($targetCondition);
|
|
if ($conditionToken === $stackPtr) {
|
|
$phpcsFile->addWarning(
|
|
"Targeting a 'switch' control structure with a 'continue' statement is strongly discouraged and will throw a warning as of PHP 7.3.",
|
|
$continue,
|
|
'Found'
|
|
);
|
|
}
|
|
|
|
} while ($continue < $caseCloser);
|
|
|
|
} while ($caseDefault < $switchCloser);
|
|
}
|
|
}
|