Revisit BlackLineAroundClassBodyFixer

This commit is contained in:
ErickSkrauch 2023-03-24 00:39:56 +01:00
parent b85d0d5c01
commit 050dfe6399
No known key found for this signature in database
GPG Key ID: 669339FCBB30EE0E

View File

@ -16,14 +16,26 @@ use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens; use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer; use PhpCsFixer\Tokenizer\TokensAnalyzer;
use SplFileInfo;
/** /**
* This is copy of the PR https://github.com/FriendsOfPHP/PHP-CS-Fixer/pull/3688 * This fixer conflicts with the CurlyBracesPositionFixer (which is part of the BracesFixer),
* because CurlyBracesPositionFixer always tries to remove any new lines between class beginning
* and the first meaningful statement. And then this fixer restores those spaces back.
* *
* @author ErickSkrauch <erickskrauch@ely.by> * That is the reason, why you always see a "braces, Ely/blank_line_around_class_body" in verbose output.
*/ */
final class BlankLineAroundClassBodyFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface { final class BlankLineAroundClassBodyFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface {
/**
* @internal
*/
public const C_BLANK_LINES_COUNT = 'blank_lines_count';
/**
* @internal
*/
public const C_APPLY_TO_ANONYMOUS_CLASSES = 'apply_to_anonymous_classes';
public function getDefinition(): FixerDefinitionInterface { public function getDefinition(): FixerDefinitionInterface {
return new FixerDefinition( return new FixerDefinition(
'Ensure that class body contains one blank line after class definition and before its end.', 'Ensure that class body contains one blank line after class definition and before its end.',
@ -48,7 +60,7 @@ new class extends Foo {
}; };
', ',
['apply_to_anonymous_classes' => false], [self::C_APPLY_TO_ANONYMOUS_CLASSES => false],
), ),
new CodeSample( new CodeSample(
'<?php '<?php
@ -58,74 +70,75 @@ new class extends Foo {
} }
}; };
', ',
['apply_to_anonymous_classes' => true], [self::C_APPLY_TO_ANONYMOUS_CLASSES => true],
), ),
], ],
); );
} }
public function getPriority(): int {
// should be run after the BracesFixer
return -26;
}
public function isCandidate(Tokens $tokens): bool { public function isCandidate(Tokens $tokens): bool {
return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds()); return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
} }
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void { protected function applyFix(SplFileInfo $file, Tokens $tokens): void {
$analyzer = new TokensAnalyzer($tokens); $analyzer = new TokensAnalyzer($tokens);
/** @var Token $token */
foreach ($tokens as $index => $token) { foreach ($tokens as $index => $token) {
if (!$token->isClassy()) { if (!$token->isClassy()) {
continue; continue;
} }
$countLines = $this->configuration['blank_lines_count']; $countLines = $this->configuration[self::C_BLANK_LINES_COUNT];
if (!$this->configuration['apply_to_anonymous_classes'] && $analyzer->isAnonymousClass($index)) { if (!$this->configuration[self::C_APPLY_TO_ANONYMOUS_CLASSES] && $analyzer->isAnonymousClass($index)) {
$countLines = 0; $countLines = 0;
} }
$startBraceIndex = $tokens->getNextTokenOfKind($index, ['{']); $startBraceIndex = $tokens->getNextTokenOfKind($index, ['{']);
if ($tokens[$startBraceIndex + 1]->isWhitespace()) { /** @var Token $nextAfterBraceToken */
$nextAfterBraceToken = $tokens[$startBraceIndex + 1];
if ($nextAfterBraceToken->isWhitespace()) {
$nextStatementIndex = $tokens->getNextMeaningfulToken($startBraceIndex); $nextStatementIndex = $tokens->getNextMeaningfulToken($startBraceIndex);
// Traits should be placed right after a class opening brace, /** @var Token $nextStatementToken */
if ($tokens[$nextStatementIndex]->getContent() !== 'use') { $nextStatementToken = $tokens[$nextStatementIndex];
$this->fixBlankLines($tokens, $startBraceIndex + 1, $countLines); // Traits should be placed right after a class opening brace
if ($nextStatementToken->getContent() !== 'use') {
$this->ensureBlankLines($tokens, $startBraceIndex + 1, $countLines);
} }
} }
$endBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $startBraceIndex); $endBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $startBraceIndex);
if ($tokens[$endBraceIndex - 1]->isWhitespace()) { if ($tokens[$endBraceIndex - 1]->isWhitespace()) {
$this->fixBlankLines($tokens, $endBraceIndex - 1, $countLines); $this->ensureBlankLines($tokens, $endBraceIndex - 1, $countLines);
} }
} }
} }
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface { protected function createConfigurationDefinition(): FixerConfigurationResolverInterface {
return new FixerConfigurationResolver([ return new FixerConfigurationResolver([
(new FixerOptionBuilder('blank_lines_count', 'adjusts an amount of the blank lines.')) (new FixerOptionBuilder(self::C_BLANK_LINES_COUNT, 'adjusts the number of blank lines.'))
->setAllowedTypes(['int']) ->setAllowedTypes(['int'])
->setDefault(1) ->setDefault(1)
->getOption(), ->getOption(),
(new FixerOptionBuilder('apply_to_anonymous_classes', 'whether this fixer should be applied to anonymous classes.')) (new FixerOptionBuilder(self::C_APPLY_TO_ANONYMOUS_CLASSES, 'whether this fixer should be applied to anonymous classes.'))
->setAllowedTypes(['bool']) ->setAllowedTypes(['bool'])
->setDefault(true) ->setDefault(true)
->getOption(), ->getOption(),
]); ]);
} }
private function fixBlankLines(Tokens $tokens, int $index, int $countLines): void { private function ensureBlankLines(Tokens $tokens, int $index, int $countLines): void {
$content = $tokens[$index]->getContent(); $content = $tokens[$index]->getContent();
// Apply fix only in the case when the count lines do not equals to expected // Apply fix only when the lines count doesn't equal to expected
// Don't check for \r\n sequence since it's still contains \n part
if (substr_count($content, "\n") === $countLines + 1) { if (substr_count($content, "\n") === $countLines + 1) {
return; return;
} }
// The final bit of the whitespace must be the next statement's indentation // Use regexp to extract contents between line breaks
Preg::matchAll('/[^\n\r]+[\r\n]*/', $content, $matches); Preg::matchAll('/[^\n\r]+[\r\n]*/', $content, $matches);
$lines = $matches[0]; $lines = $matches[0];
$eol = $this->whitespacesConfig->getLineEnding(); $eol = $this->whitespacesConfig->getLineEnding();
$tokens[$index] = new Token([T_WHITESPACE, str_repeat($eol, $countLines + 1) . end($lines)]); $tokens->ensureWhitespaceAtIndex($index, 0, str_repeat($eol, $countLines + 1) . end($lines));
} }
} }