*/ 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::removeIndentation( $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::removeIndentation( $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); FixerHelper::replace( $phpcsFile, $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($phpcsFile, $ifIndentation); $negativeIfCondition = ConditionHelper::getNegativeCondition( $phpcsFile, $tokens[$ifPointer]['parenthesis_opener'], $tokens[$ifPointer]['parenthesis_closer'], ); $afterIfCode = IndentationHelper::removeIndentation($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 */ 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::EARLY_EXIT_TOKEN_CODES, $lastSemicolonInScopePointer - 1, $startPointer, ) : null; } private function isEarlyExitInScope(File $phpcsFile, int $startPointer, int $endPointer): bool { return $this->findEarlyExitInScope($phpcsFile, $startPointer, $endPointer) !== null; } /** * @return list */ 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; } }