Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x
  2. EarlyExitSniff.php

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\ControlStructures
View 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;
    }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title
EarlyExitSniff::$ignoreOneLineTrailingIf public property @var bool
EarlyExitSniff::$ignoreStandaloneIfInScope public property @var bool
EarlyExitSniff::$ignoreTrailingIfWithOneInstruction public property @var bool
EarlyExitSniff::CODE_EARLY_EXIT_NOT_USED public constant
EarlyExitSniff::CODE_USELESS_ELSE public constant
EarlyExitSniff::CODE_USELESS_ELSEIF public constant
EarlyExitSniff::findEarlyExitInScope private function
EarlyExitSniff::getAllConditionsPointers private function *
EarlyExitSniff::getEarlyExitCode private function *
EarlyExitSniff::getScopeCode private function
EarlyExitSniff::getScopeCodePointers private function *
EarlyExitSniff::isEarlyExitInScope private function
EarlyExitSniff::process public function * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
*
Overrides Sniff::process
EarlyExitSniff::processElse private function
EarlyExitSniff::processElseIf private function
EarlyExitSniff::processIf private function
EarlyExitSniff::register public function * Overrides Sniff::register
RSS feed
Powered by Drupal