class EarlyExitSniff
Hierarchy
- class \SlevomatCodingStandard\Sniffs\ControlStructures\EarlyExitSniff implements \PHP_CodeSniffer\Sniffs\Sniff
Expanded class hierarchy of EarlyExitSniff
File
-
vendor/
slevomat/ coding-standard/ SlevomatCodingStandard/ Sniffs/ ControlStructures/ EarlyExitSniff.php, line 34
Namespace
SlevomatCodingStandard\Sniffs\ControlStructuresView source
class EarlyExitSniff implements Sniff {
public const CODE_EARLY_EXIT_NOT_USED = 'EarlyExitNotUsed';
public const CODE_USELESS_ELSEIF = 'UselessElseIf';
public const CODE_USELESS_ELSE = 'UselessElse';
/** @var bool */
public $ignoreStandaloneIfInScope = false;
/** @var bool */
public $ignoreOneLineTrailingIf = false;
/** @var bool */
public $ignoreTrailingIfWithOneInstruction = false;
/**
* @return array<int, (int|string)>
*/
public function register() : array {
return [
T_IF,
T_ELSEIF,
T_ELSE,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $pointer
*/
public function process(File $phpcsFile, $pointer) : void {
$tokens = $phpcsFile->getTokens();
if ($tokens[$pointer]['code'] === T_IF) {
$this->processIf($phpcsFile, $pointer);
}
elseif ($tokens[$pointer]['code'] === T_ELSEIF) {
$this->processElseIf($phpcsFile, $pointer);
}
else {
$this->processElse($phpcsFile, $pointer);
}
}
private function processElse(File $phpcsFile, int $elsePointer) : void {
$tokens = $phpcsFile->getTokens();
if (!array_key_exists('scope_opener', $tokens[$elsePointer])) {
// Else without curly braces is not supported.
return;
}
try {
$allConditionsPointers = $this->getAllConditionsPointers($phpcsFile, $elsePointer);
} catch (Throwable $e) {
// Else without curly braces is not supported.
return;
}
if (TokenHelper::findNext($phpcsFile, T_FUNCTION, $tokens[$elsePointer]['scope_opener'] + 1, $tokens[$elsePointer]['scope_closer']) !== null) {
return;
}
$ifPointer = $allConditionsPointers[0];
$ifEarlyExitPointer = null;
$elseEarlyExitPointer = null;
$previousConditionPointer = null;
$previousConditionEarlyExitPointer = null;
foreach ($allConditionsPointers as $conditionPointer) {
$conditionEarlyExitPointer = $this->findEarlyExitInScope($phpcsFile, $tokens[$conditionPointer]['scope_opener'], $tokens[$conditionPointer]['scope_closer']);
if ($conditionPointer === $elsePointer) {
$elseEarlyExitPointer = $conditionEarlyExitPointer;
continue;
}
if (count($allConditionsPointers) > 2 && $conditionEarlyExitPointer === null) {
return;
}
$previousConditionPointer = $conditionPointer;
$previousConditionEarlyExitPointer = $conditionEarlyExitPointer;
if ($conditionPointer === $ifPointer) {
$ifEarlyExitPointer = $conditionEarlyExitPointer;
continue;
}
}
if ($ifEarlyExitPointer === null && $elseEarlyExitPointer === null) {
return;
}
if ($elseEarlyExitPointer !== null && $previousConditionEarlyExitPointer === null) {
$fix = $phpcsFile->addFixableError('Use early exit instead of "else".', $elsePointer, self::CODE_EARLY_EXIT_NOT_USED);
if (!$fix) {
return;
}
$ifCodePointers = $this->getScopeCodePointers($phpcsFile, $ifPointer);
$elseCode = $this->getScopeCode($phpcsFile, $elsePointer);
$negativeIfCondition = ConditionHelper::getNegativeCondition($phpcsFile, $tokens[$ifPointer]['parenthesis_opener'], $tokens[$ifPointer]['parenthesis_closer']);
$afterIfCode = IndentationHelper::fixIndentation($phpcsFile, $ifCodePointers, IndentationHelper::getIndentation($phpcsFile, $ifPointer));
$ifContent = sprintf('if %s {%s}%s%s', $negativeIfCondition, $elseCode, $phpcsFile->eolChar, $afterIfCode);
$phpcsFile->fixer
->beginChangeset();
FixerHelper::change($phpcsFile, $ifPointer, $tokens[$elsePointer]['scope_closer'], $ifContent);
$phpcsFile->fixer
->endChangeset();
return;
}
$fix = $phpcsFile->addFixableError('Remove useless "else" to reduce code nesting.', $elsePointer, self::CODE_USELESS_ELSE);
if (!$fix) {
return;
}
$elseCodePointers = $this->getScopeCodePointers($phpcsFile, $elsePointer);
$afterIfCode = IndentationHelper::fixIndentation($phpcsFile, $elseCodePointers, IndentationHelper::getIndentation($phpcsFile, $ifPointer));
$phpcsFile->fixer
->beginChangeset();
$previousConditionContent = sprintf('%s%s', $phpcsFile->eolChar, $afterIfCode);
FixerHelper::change($phpcsFile, $tokens[$previousConditionPointer]['scope_closer'] + 1, $tokens[$elsePointer]['scope_closer'], $previousConditionContent);
$phpcsFile->fixer
->endChangeset();
}
private function processElseIf(File $phpcsFile, int $elseIfPointer) : void {
$tokens = $phpcsFile->getTokens();
try {
$allConditionsPointers = $this->getAllConditionsPointers($phpcsFile, $elseIfPointer);
} catch (Throwable $e) {
// Elseif without curly braces is not supported.
return;
}
if (TokenHelper::findNext($phpcsFile, T_FUNCTION, $tokens[$elseIfPointer]['scope_opener'] + 1, $tokens[$elseIfPointer]['scope_closer']) !== null) {
return;
}
foreach ($allConditionsPointers as $conditionPointer) {
$conditionEarlyExitPointer = $this->findEarlyExitInScope($phpcsFile, $tokens[$conditionPointer]['scope_opener'], $tokens[$conditionPointer]['scope_closer']);
if ($conditionPointer === $elseIfPointer) {
break;
}
if ($conditionEarlyExitPointer === null) {
return;
}
}
$fix = $phpcsFile->addFixableError('Use "if" instead of "elseif".', $elseIfPointer, self::CODE_USELESS_ELSEIF);
if (!$fix) {
return;
}
/** @var int $pointerBeforeElseIfPointer */
$pointerBeforeElseIfPointer = TokenHelper::findPreviousNonWhitespace($phpcsFile, $elseIfPointer - 1);
$phpcsFile->fixer
->beginChangeset();
FixerHelper::removeBetween($phpcsFile, $pointerBeforeElseIfPointer, $elseIfPointer);
$phpcsFile->fixer
->addNewline($pointerBeforeElseIfPointer);
$phpcsFile->fixer
->addNewline($pointerBeforeElseIfPointer);
$phpcsFile->fixer
->replaceToken($elseIfPointer, sprintf('%sif', IndentationHelper::getIndentation($phpcsFile, $allConditionsPointers[0])));
$phpcsFile->fixer
->endChangeset();
}
private function processIf(File $phpcsFile, int $ifPointer) : void {
$tokens = $phpcsFile->getTokens();
if (!array_key_exists('scope_closer', $tokens[$ifPointer])) {
// If without curly braces is not supported.
return;
}
$nextPointer = TokenHelper::findNextNonWhitespace($phpcsFile, $tokens[$ifPointer]['scope_closer'] + 1);
if ($nextPointer === null || $tokens[$nextPointer]['code'] !== T_CLOSE_CURLY_BRACKET) {
return;
}
$previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $ifPointer - 1);
if ($this->ignoreStandaloneIfInScope && in_array($tokens[$previousPointer]['code'], [
T_OPEN_CURLY_BRACKET,
T_COLON,
], true)) {
return;
}
if ($this->ignoreOneLineTrailingIf && $tokens[$tokens[$ifPointer]['scope_opener']]['line'] + 2 === $tokens[$tokens[$ifPointer]['scope_closer']]['line']) {
return;
}
if ($this->ignoreTrailingIfWithOneInstruction) {
$pointerBeforeScopeCloser = TokenHelper::findPreviousEffective($phpcsFile, $tokens[$ifPointer]['scope_closer'] - 1);
if ($tokens[$pointerBeforeScopeCloser]['code'] === T_SEMICOLON) {
$ignore = true;
$searchStartPointer = $tokens[$ifPointer]['scope_opener'] + 1;
while (true) {
$anotherSemicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $searchStartPointer, $pointerBeforeScopeCloser);
if ($anotherSemicolonPointer === null) {
break;
}
if (ScopeHelper::isInSameScope($phpcsFile, $anotherSemicolonPointer, $pointerBeforeScopeCloser)) {
$ignore = false;
break;
}
$searchStartPointer = $anotherSemicolonPointer + 1;
}
if ($ignore) {
return;
}
}
}
$scopePointer = $tokens[$nextPointer]['scope_condition'];
if (!in_array($tokens[$scopePointer]['code'], [
T_FUNCTION,
T_CLOSURE,
T_WHILE,
T_DO,
T_FOREACH,
T_FOR,
], true)) {
return;
}
if ($this->isEarlyExitInScope($phpcsFile, $tokens[$ifPointer]['scope_opener'], $tokens[$ifPointer]['scope_closer'])) {
return;
}
$fix = $phpcsFile->addFixableError('Use early exit to reduce code nesting.', $ifPointer, self::CODE_EARLY_EXIT_NOT_USED);
if (!$fix) {
return;
}
$ifCodePointers = $this->getScopeCodePointers($phpcsFile, $ifPointer);
$ifIndentation = IndentationHelper::getIndentation($phpcsFile, $ifPointer);
$earlyExitCode = $this->getEarlyExitCode($tokens[$scopePointer]['code']);
$earlyExitCodeIndentation = IndentationHelper::addIndentation($ifIndentation);
$negativeIfCondition = ConditionHelper::getNegativeCondition($phpcsFile, $tokens[$ifPointer]['parenthesis_opener'], $tokens[$ifPointer]['parenthesis_closer']);
$afterIfCode = IndentationHelper::fixIndentation($phpcsFile, $ifCodePointers, $ifIndentation);
$ifContent = sprintf('if %s {%s%s%s;%s%s}%s%s', $negativeIfCondition, $phpcsFile->eolChar, $earlyExitCodeIndentation, $earlyExitCode, $phpcsFile->eolChar, $ifIndentation, $phpcsFile->eolChar, $afterIfCode);
$phpcsFile->fixer
->beginChangeset();
FixerHelper::change($phpcsFile, $ifPointer, $tokens[$ifPointer]['scope_closer'], $ifContent);
$phpcsFile->fixer
->endChangeset();
}
private function getScopeCode(File $phpcsFile, int $scopePointer) : string {
$tokens = $phpcsFile->getTokens();
return TokenHelper::getContent($phpcsFile, $tokens[$scopePointer]['scope_opener'] + 1, $tokens[$scopePointer]['scope_closer'] - 1);
}
/**
* @return list<int>
*/
private function getScopeCodePointers(File $phpcsFile, int $scopePointer) : array {
$tokens = $phpcsFile->getTokens();
return range($tokens[$scopePointer]['scope_opener'] + 1, $tokens[$scopePointer]['scope_closer'] - 1);
}
/**
* @param string|int $code
*/
private function getEarlyExitCode($code) : string {
if (in_array($code, [
T_WHILE,
T_DO,
T_FOREACH,
T_FOR,
], true)) {
return 'continue';
}
return 'return';
}
private function findEarlyExitInScope(File $phpcsFile, int $startPointer, int $endPointer) : ?int {
$tokens = $phpcsFile->getTokens();
$ifPointers = TokenHelper::findNextAll($phpcsFile, T_IF, $startPointer + 1, $endPointer);
foreach ($ifPointers as $ifPointer) {
if ($tokens[$ifPointer]['level'] - 1 !== $tokens[$startPointer]['level']) {
continue;
}
$conditionPointers = $this->getAllConditionsPointers($phpcsFile, $ifPointer);
foreach ($conditionPointers as $conditionPointer) {
if ($this->findEarlyExitInScope($phpcsFile, $tokens[$conditionPointer]['scope_opener'], $tokens[$conditionPointer]['scope_closer']) === null) {
return null;
}
}
}
$lastSemicolonInScopePointer = TokenHelper::findPreviousEffective($phpcsFile, $endPointer - 1, $startPointer);
return $tokens[$lastSemicolonInScopePointer]['code'] === T_SEMICOLON ? TokenHelper::findPreviousLocal($phpcsFile, TokenHelper::$earlyExitTokenCodes, $lastSemicolonInScopePointer - 1, $startPointer) : null;
}
private function isEarlyExitInScope(File $phpcsFile, int $startPointer, int $endPointer) : bool {
return $this->findEarlyExitInScope($phpcsFile, $startPointer, $endPointer) !== null;
}
/**
* @return list<int>
*/
private function getAllConditionsPointers(File $phpcsFile, int $conditionPointer) : array {
$tokens = $phpcsFile->getTokens();
$conditionsPointers = [
$conditionPointer,
];
if (isset($tokens[$conditionPointer]['scope_opener']) && $tokens[$tokens[$conditionPointer]['scope_opener']]['code'] === T_COLON) {
// Alternative control structure syntax.
throw new Exception(sprintf('"%s" without curly braces is not supported.', $tokens[$conditionPointer]['content']));
}
if ($tokens[$conditionPointer]['code'] !== T_IF) {
$currentConditionPointer = $conditionPointer;
do {
$previousConditionCloseParenthesisPointer = TokenHelper::findPreviousEffective($phpcsFile, $currentConditionPointer - 1);
$currentConditionPointer = $tokens[$previousConditionCloseParenthesisPointer]['scope_condition'];
$conditionsPointers[] = $currentConditionPointer;
} while ($tokens[$currentConditionPointer]['code'] !== T_IF);
}
if ($tokens[$conditionPointer]['code'] !== T_ELSE) {
if (!array_key_exists('scope_closer', $tokens[$conditionPointer])) {
throw new Exception(sprintf('"%s" without curly braces is not supported.', $tokens[$conditionPointer]['content']));
}
$currentConditionPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$conditionPointer]['scope_closer'] + 1);
if ($currentConditionPointer !== null) {
while (in_array($tokens[$currentConditionPointer]['code'], [
T_ELSEIF,
T_ELSE,
], true)) {
$conditionsPointers[] = $currentConditionPointer;
if (!array_key_exists('scope_closer', $tokens[$currentConditionPointer])) {
throw new Exception(sprintf('"%s" without curly braces is not supported.', $tokens[$currentConditionPointer]['content']));
}
$currentConditionPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$currentConditionPointer]['scope_closer'] + 1);
}
}
}
sort($conditionsPointers);
return $conditionsPointers;
}
}