* Dariusz Rumiński * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace PhpCsFixer\Fixer\Whitespace; use PhpCsFixer\AbstractFixer; use PhpCsFixer\Fixer\ConfigurableFixerInterface; use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface; use PhpCsFixer\FixerConfiguration\AllowedValueSubset; use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface; use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; use PhpCsFixer\FixerDefinition\CodeSample; use PhpCsFixer\FixerDefinition\FixerDefinition; use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; use PhpCsFixer\Tokenizer\TokensAnalyzer; /** * @author Dariusz Rumiński * @author Andreas Möller */ final class BlankLineBeforeStatementFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface { /** * @var array */ private static $tokenMap = [ 'break' => T_BREAK, 'case' => T_CASE, 'continue' => T_CONTINUE, 'declare' => T_DECLARE, 'default' => T_DEFAULT, 'do' => T_DO, 'exit' => T_EXIT, 'for' => T_FOR, 'foreach' => T_FOREACH, 'goto' => T_GOTO, 'if' => T_IF, 'include' => T_INCLUDE, 'include_once' => T_INCLUDE_ONCE, 'require' => T_REQUIRE, 'require_once' => T_REQUIRE_ONCE, 'return' => T_RETURN, 'switch' => T_SWITCH, 'throw' => T_THROW, 'try' => T_TRY, 'while' => T_WHILE, 'yield' => T_YIELD, 'yield_from' => T_YIELD_FROM, ]; /** * @var array */ private $fixTokenMap = []; /** * {@inheritdoc} */ public function configure(array $configuration): void { parent::configure($configuration); $this->fixTokenMap = []; foreach ($this->configuration['statements'] as $key) { $this->fixTokenMap[$key] = self::$tokenMap[$key]; } $this->fixTokenMap = array_values($this->fixTokenMap); } /** * {@inheritdoc} */ public function getDefinition(): FixerDefinitionInterface { return new FixerDefinition( 'An empty line feed must precede any configured statement.', [ new CodeSample( 'process(); break; case 44: break; } ', [ 'statements' => ['break'], ] ), new CodeSample( 'isTired()) { $bar->sleep(); continue; } } ', [ 'statements' => ['continue'], ] ), new CodeSample( ' 0); ', [ 'statements' => ['do'], ] ), new CodeSample( ' ['exit'], ] ), new CodeSample( ' ['goto'], ] ), new CodeSample( ' ['if'], ] ), new CodeSample( ' ['return'], ] ), new CodeSample( ' ['switch'], ] ), new CodeSample( 'bar(); throw new \UnexpectedValueException("A cannot be null."); } ', [ 'statements' => ['throw'], ] ), new CodeSample( 'bar(); } catch (\Exception $exception) { $a = -1; } ', [ 'statements' => ['try'], ] ), new CodeSample( ' ['yield'], ] ), ] ); } /** * {@inheritdoc} * * Must run after NoExtraBlankLinesFixer, NoUselessReturnFixer, ReturnAssignmentFixer. */ public function getPriority(): int { return -21; } /** * {@inheritdoc} */ public function isCandidate(Tokens $tokens): bool { return $tokens->isAnyTokenKindsFound($this->fixTokenMap); } /** * {@inheritdoc} */ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void { $analyzer = new TokensAnalyzer($tokens); for ($index = $tokens->count() - 1; $index > 0; --$index) { $token = $tokens[$index]; if (!$token->isGivenKind($this->fixTokenMap)) { continue; } if ($token->isGivenKind(T_WHILE) && $analyzer->isWhilePartOfDoWhile($index)) { continue; } $prevNonWhitespace = $tokens->getPrevNonWhitespace($index); if ($this->shouldAddBlankLine($tokens, $prevNonWhitespace)) { $this->insertBlankLine($tokens, $index); } $index = $prevNonWhitespace; } } /** * {@inheritdoc} */ protected function createConfigurationDefinition(): FixerConfigurationResolverInterface { return new FixerConfigurationResolver([ (new FixerOptionBuilder('statements', 'List of statements which must be preceded by an empty line.')) ->setAllowedTypes(['array']) ->setAllowedValues([new AllowedValueSubset(array_keys(self::$tokenMap))]) ->setDefault([ 'break', 'continue', 'declare', 'return', 'throw', 'try', ]) ->getOption(), ]); } private function shouldAddBlankLine(Tokens $tokens, int $prevNonWhitespace): bool { $prevNonWhitespaceToken = $tokens[$prevNonWhitespace]; if ($prevNonWhitespaceToken->isComment()) { for ($j = $prevNonWhitespace - 1; $j >= 0; --$j) { if (str_contains($tokens[$j]->getContent(), "\n")) { return false; } if ($tokens[$j]->isWhitespace() || $tokens[$j]->isComment()) { continue; } return $tokens[$j]->equalsAny([';', '}']); } } return $prevNonWhitespaceToken->equalsAny([';', '}']); } private function insertBlankLine(Tokens $tokens, int $index): void { $prevIndex = $index - 1; $prevToken = $tokens[$prevIndex]; $lineEnding = $this->whitespacesConfig->getLineEnding(); if ($prevToken->isWhitespace()) { $newlinesCount = substr_count($prevToken->getContent(), "\n"); if (0 === $newlinesCount) { $tokens[$prevIndex] = new Token([T_WHITESPACE, rtrim($prevToken->getContent(), " \t").$lineEnding.$lineEnding]); } elseif (1 === $newlinesCount) { $tokens[$prevIndex] = new Token([T_WHITESPACE, $lineEnding.$prevToken->getContent()]); } } else { $tokens->insertAt($index, new Token([T_WHITESPACE, $lineEnding.$lineEnding])); } } }