|null */ private ?array $tokensToCheck = null; /** * @return list */ abstract protected function getSupportedKeywords(): array; /** * @return list */ abstract protected function getKeywordsToCheck(): array; abstract protected function getLinesCountBefore(): int; abstract protected function getLinesCountBeforeFirst(File $phpcsFile, int $controlStructurePointer): int; abstract protected function getLinesCountAfter(): int; abstract protected function getLinesCountAfterLast(File $phpcsFile, int $controlStructurePointer, int $controlStructureEndPointer): int; /** * @return array */ public function register(): array { return $this->getTokensToCheck(); } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $controlStructurePointer */ public function process(File $phpcsFile, $controlStructurePointer): void { $this->checkLinesBefore($phpcsFile, $controlStructurePointer); try { $this->checkLinesAfter($phpcsFile, $controlStructurePointer); } catch (Throwable $e) { // Unsupported syntax without curly braces. return; } } protected function checkLinesBefore(File $phpcsFile, int $controlStructurePointer): void { $tokens = $phpcsFile->getTokens(); if (in_array($tokens[$controlStructurePointer]['code'], [T_CASE, T_DEFAULT], true)) { $pointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $controlStructurePointer - 1); if ($tokens[$pointerBefore]['code'] === T_COLON) { return; } } $nonWhitespacePointerBefore = TokenHelper::findPreviousNonWhitespace($phpcsFile, $controlStructurePointer - 1); $controlStructureStartPointer = $controlStructurePointer; $pointerBefore = $nonWhitespacePointerBefore; $pointerToCheckFirst = $pointerBefore; if (in_array($tokens[$nonWhitespacePointerBefore]['code'], Tokens::$commentTokens, true)) { $effectivePointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $pointerBefore - 1); if ($tokens[$effectivePointerBefore]['line'] === $tokens[$nonWhitespacePointerBefore]['line']) { $pointerToCheckFirst = $effectivePointerBefore; } elseif ($tokens[$nonWhitespacePointerBefore]['line'] + 1 === $tokens[$controlStructurePointer]['line']) { if ($tokens[$effectivePointerBefore]['line'] !== $tokens[$nonWhitespacePointerBefore]['line']) { $controlStructureStartPointer = array_key_exists('comment_opener', $tokens[$nonWhitespacePointerBefore]) ? $tokens[$nonWhitespacePointerBefore]['comment_opener'] : CommentHelper::getMultilineCommentStartPointer($phpcsFile, $nonWhitespacePointerBefore); $pointerBefore = TokenHelper::findPreviousNonWhitespace($phpcsFile, $controlStructureStartPointer - 1); } $pointerToCheckFirst = $pointerBefore; } } $isFirstControlStructure = in_array($tokens[$pointerToCheckFirst]['code'], [T_OPEN_CURLY_BRACKET, T_COLON], true); $whitespaceBefore = ''; if ($tokens[$pointerBefore]['code'] === T_OPEN_TAG) { $whitespaceBefore .= substr($tokens[$pointerBefore]['content'], strlen('eolChar)) === $phpcsFile->eolChar; if ($hasCommentWithLineEndBefore) { $whitespaceBefore .= $phpcsFile->eolChar; } if ($pointerBefore + 1 !== $controlStructurePointer) { $whitespaceBefore .= TokenHelper::getContent($phpcsFile, $pointerBefore + 1, $controlStructureStartPointer - 1); } $requiredLinesCountBefore = $isFirstControlStructure ? $this->getLinesCountBeforeFirst($phpcsFile, $controlStructurePointer) : $this->getLinesCountBefore(); $actualLinesCountBefore = substr_count($whitespaceBefore, $phpcsFile->eolChar) - 1; if ($requiredLinesCountBefore === $actualLinesCountBefore) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s before "%s", found %d.', $requiredLinesCountBefore, $requiredLinesCountBefore === 1 ? '' : 's', $tokens[$controlStructurePointer]['content'], $actualLinesCountBefore, ), $controlStructurePointer, $isFirstControlStructure ? self::CODE_INCORRECT_LINES_COUNT_BEFORE_FIRST_CONTROL_STRUCTURE : self::CODE_INCORRECT_LINES_COUNT_BEFORE_CONTROL_STRUCTURE, ); if (!$fix) { return; } $endOfLineBeforePointer = TokenHelper::findPreviousContent( $phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $controlStructureStartPointer - 1, ); $phpcsFile->fixer->beginChangeset(); if ($tokens[$pointerBefore]['code'] === T_OPEN_TAG) { FixerHelper::replace($phpcsFile, $pointerBefore, 'fixer->addNewline($pointerBefore); } $phpcsFile->fixer->endChangeset(); } protected function checkLinesAfter(File $phpcsFile, int $controlStructurePointer): void { $tokens = $phpcsFile->getTokens(); if (in_array($tokens[$controlStructurePointer]['code'], [T_CASE, T_DEFAULT], true)) { $colonPointer = TokenHelper::findNext($phpcsFile, T_COLON, $controlStructurePointer + 1); $pointerAfterColon = TokenHelper::findNextEffective($phpcsFile, $colonPointer + 1); if (in_array($tokens[$pointerAfterColon]['code'], [T_CASE, T_DEFAULT], true)) { return; } } $controlStructureEndPointer = $this->findControlStructureEnd($phpcsFile, $controlStructurePointer); $pointerAfterControlStructureEnd = TokenHelper::findNextEffective($phpcsFile, $controlStructureEndPointer + 1); if ( $pointerAfterControlStructureEnd !== null && $tokens[$pointerAfterControlStructureEnd]['code'] === T_SEMICOLON ) { $controlStructureEndPointer = $pointerAfterControlStructureEnd; } $notWhitespacePointerAfter = TokenHelper::findNextNonWhitespace($phpcsFile, $controlStructureEndPointer + 1); if ($notWhitespacePointerAfter === null) { return; } $hasCommentAfter = in_array($tokens[$notWhitespacePointerAfter]['code'], Tokens::$commentTokens, true); $isCommentAfterOnSameLine = false; $pointerAfter = $notWhitespacePointerAfter; $isControlStructureEndAfterPointer = static fn (int $pointer): bool => in_array( $tokens[$controlStructurePointer]['code'], [T_CASE, T_DEFAULT], true, ) ? $tokens[$pointer]['code'] === T_CLOSE_CURLY_BRACKET : in_array($tokens[$pointer]['code'], [T_CLOSE_CURLY_BRACKET, T_CASE, T_DEFAULT], true); if ($hasCommentAfter) { if ($tokens[$notWhitespacePointerAfter]['line'] === $tokens[$controlStructureEndPointer]['line'] + 1) { $commentEndPointer = CommentHelper::getCommentEndPointer($phpcsFile, $notWhitespacePointerAfter); $pointerAfterComment = TokenHelper::findNextNonWhitespace($phpcsFile, $commentEndPointer + 1); if ($isControlStructureEndAfterPointer($pointerAfterComment)) { $controlStructureEndPointer = $commentEndPointer; $pointerAfter = $pointerAfterComment; } } elseif ($tokens[$notWhitespacePointerAfter]['line'] === $tokens[$controlStructureEndPointer]['line']) { $isCommentAfterOnSameLine = true; $pointerAfter = TokenHelper::findNextNonWhitespace($phpcsFile, $notWhitespacePointerAfter + 1); } } $isLastControlStructure = $isControlStructureEndAfterPointer($pointerAfter); $requiredLinesCountAfter = $isLastControlStructure ? $this->getLinesCountAfterLast($phpcsFile, $controlStructurePointer, $controlStructureEndPointer) : $this->getLinesCountAfter(); $actualLinesCountAfter = $tokens[$pointerAfter]['line'] - $tokens[$controlStructureEndPointer]['line'] - 1; if ($requiredLinesCountAfter === $actualLinesCountAfter) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s after "%s", found %d.', $requiredLinesCountAfter, $requiredLinesCountAfter === 1 ? '' : 's', $tokens[$controlStructurePointer]['content'], $actualLinesCountAfter, ), $controlStructurePointer, $isLastControlStructure ? self::CODE_INCORRECT_LINES_COUNT_AFTER_LAST_CONTROL_STRUCTURE : self::CODE_INCORRECT_LINES_COUNT_AFTER_CONTROL_STRUCTURE, ); if (!$fix) { return; } $replaceStartPointer = $isCommentAfterOnSameLine ? $notWhitespacePointerAfter : $controlStructureEndPointer; $endOfLineBeforeAfterPointer = TokenHelper::findLastTokenOnPreviousLine($phpcsFile, $pointerAfter); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $replaceStartPointer + 1, $endOfLineBeforeAfterPointer); if ($isCommentAfterOnSameLine) { for ($i = 0; $i < $requiredLinesCountAfter; $i++) { $phpcsFile->fixer->addNewline($notWhitespacePointerAfter); } } else { $linesToAdd = substr($tokens[$controlStructureEndPointer]['content'], -strlen($phpcsFile->eolChar)) === $phpcsFile->eolChar ? $requiredLinesCountAfter - 1 : $requiredLinesCountAfter; for ($i = 0; $i <= $linesToAdd; $i++) { $phpcsFile->fixer->addNewline($controlStructureEndPointer); } } $phpcsFile->fixer->endChangeset(); } /** * @return array */ private function getTokensToCheck(): array { if ($this->tokensToCheck === null) { $supportedKeywords = $this->getSupportedKeywords(); $supportedTokens = [ self::KEYWORD_IF => T_IF, self::KEYWORD_DO => T_DO, self::KEYWORD_WHILE => T_WHILE, self::KEYWORD_FOR => T_FOR, self::KEYWORD_FOREACH => T_FOREACH, self::KEYWORD_SWITCH => T_SWITCH, self::KEYWORD_CASE => T_CASE, self::KEYWORD_DEFAULT => T_DEFAULT, self::KEYWORD_TRY => T_TRY, self::KEYWORD_PARENT => T_PARENT, self::KEYWORD_GOTO => T_GOTO, self::KEYWORD_BREAK => T_BREAK, self::KEYWORD_CONTINUE => T_CONTINUE, self::KEYWORD_RETURN => T_RETURN, self::KEYWORD_THROW => T_THROW, self::KEYWORD_YIELD => T_YIELD, self::KEYWORD_YIELD_FROM => T_YIELD_FROM, ]; $this->tokensToCheck = array_map( static function (string $keyword) use ($supportedKeywords, $supportedTokens) { if (!in_array($keyword, $supportedKeywords, true)) { throw new UnsupportedKeywordException($keyword); } return $supportedTokens[$keyword]; }, SniffSettingsHelper::normalizeArray($this->getKeywordsToCheck()), ); if (count($this->tokensToCheck) === 0) { $this->tokensToCheck = array_map(static fn (string $keyword) => $supportedTokens[$keyword], $supportedKeywords); } } return $this->tokensToCheck; } private function findControlStructureEnd(File $phpcsFile, int $controlStructurePointer): int { $tokens = $phpcsFile->getTokens(); if ($tokens[$controlStructurePointer]['code'] === T_IF) { if (!array_key_exists('scope_closer', $tokens[$controlStructurePointer])) { throw new Exception('"if" without curly braces is not supported.'); } $pointerAfterParenthesisCloser = TokenHelper::findNextEffective( $phpcsFile, $tokens[$controlStructurePointer]['parenthesis_closer'] + 1, ); if ($pointerAfterParenthesisCloser !== null && $tokens[$pointerAfterParenthesisCloser]['code'] === T_COLON) { throw new Exception('"if" without curly braces is not supported.'); } $controlStructureEndPointer = $tokens[$controlStructurePointer]['scope_closer']; do { $nextPointer = TokenHelper::findNextEffective($phpcsFile, $controlStructureEndPointer + 1); if ($nextPointer === null) { return $controlStructureEndPointer; } if ($tokens[$nextPointer]['code'] === T_ELSE) { if (!array_key_exists('scope_closer', $tokens[$nextPointer])) { throw new Exception('"else" without curly braces is not supported.'); } return $tokens[$nextPointer]['scope_closer']; } if ($tokens[$nextPointer]['code'] !== T_ELSEIF) { return $controlStructureEndPointer; } $controlStructureEndPointer = $tokens[$nextPointer]['scope_closer']; } while (true); } if ($tokens[$controlStructurePointer]['code'] === T_DO) { $whilePointer = TokenHelper::findNext($phpcsFile, T_WHILE, $tokens[$controlStructurePointer]['scope_closer'] + 1); return (int) TokenHelper::findNext($phpcsFile, T_SEMICOLON, $tokens[$whilePointer]['parenthesis_closer'] + 1); } if ($tokens[$controlStructurePointer]['code'] === T_TRY) { $controlStructureEndPointer = $tokens[$controlStructurePointer]['scope_closer']; do { $nextPointer = TokenHelper::findNextEffective($phpcsFile, $controlStructureEndPointer + 1); if ($nextPointer === null) { return $controlStructureEndPointer; } if (!in_array($tokens[$nextPointer]['code'], [T_CATCH, T_FINALLY], true)) { return $controlStructureEndPointer; } $controlStructureEndPointer = $tokens[$nextPointer]['scope_closer']; } while (true); } if (in_array($tokens[$controlStructurePointer]['code'], [T_WHILE, T_FOR, T_FOREACH, T_SWITCH], true)) { return $tokens[$controlStructurePointer]['scope_closer']; } if (in_array($tokens[$controlStructurePointer]['code'], [T_CASE, T_DEFAULT], true)) { $switchPointer = TokenHelper::findPrevious($phpcsFile, T_SWITCH, $controlStructurePointer - 1); $pointers = TokenHelper::findNextAll( $phpcsFile, [T_CASE, T_DEFAULT], $controlStructurePointer + 1, $tokens[$switchPointer]['scope_closer'], ); foreach ($pointers as $pointer) { if (TokenHelper::findPrevious($phpcsFile, T_SWITCH, $pointer - 1) === $switchPointer) { $pointerBeforeCaseOrDefault = TokenHelper::findPreviousNonWhitespace($phpcsFile, $pointer - 1); if ( in_array($tokens[$pointerBeforeCaseOrDefault]['code'], Tokens::$commentTokens, true) && $tokens[$pointerBeforeCaseOrDefault]['line'] + 1 === $tokens[$pointer]['line'] ) { $pointerBeforeCaseOrDefault = TokenHelper::findPreviousExcluding( $phpcsFile, T_WHITESPACE, $pointerBeforeCaseOrDefault - 1, ); } return $pointerBeforeCaseOrDefault; } } return TokenHelper::findPreviousNonWhitespace($phpcsFile, $tokens[$switchPointer]['scope_closer'] - 1); } $nextPointer = TokenHelper::findNext( $phpcsFile, [T_SEMICOLON, T_ANON_CLASS, T_CLOSURE, T_FN, T_OPEN_SHORT_ARRAY], $controlStructurePointer + 1, ); if ($tokens[$nextPointer]['code'] === T_SEMICOLON) { return $nextPointer; } $scopeCloserPointer = $tokens[$nextPointer]['code'] === T_OPEN_SHORT_ARRAY ? $tokens[$nextPointer]['bracket_closer'] : $tokens[$nextPointer]['scope_closer']; if ($tokens[$scopeCloserPointer]['code'] === T_SEMICOLON) { return $scopeCloserPointer; } $nextPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $scopeCloserPointer + 1); $level = $tokens[$controlStructurePointer]['level']; while ($level !== $tokens[$nextPointer]['level']) { $nextPointer = (int) TokenHelper::findNext($phpcsFile, T_SEMICOLON, $nextPointer + 1); } return $nextPointer; } }