Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
peripherad
/
back
/
vendor
/
friendsofphp
/
php-cs-fixer
/
src
/
Fixer
/
ClassNotation
:
ClassAttributesSeparationFixer.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php declare(strict_types=1); /* * This file is part of PHP CS Fixer. * * (c) Fabien Potencier <fabien@symfony.com> * Dariusz RumiĆski <dariusz.ruminski@gmail.com> * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace PhpCsFixer\Fixer\ClassNotation; use PhpCsFixer\AbstractFixer; use PhpCsFixer\Fixer\ConfigurableFixerInterface; use PhpCsFixer\Fixer\ConfigurableFixerTrait; use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface; 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\FixerDefinition\VersionSpecification; use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample; use PhpCsFixer\Preg; use PhpCsFixer\Tokenizer\CT; use PhpCsFixer\Tokenizer\FCT; use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; use PhpCsFixer\Tokenizer\TokensAnalyzer; use PhpCsFixer\Utils; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; /** * Make sure there is one blank line above and below class elements. * * The exception is when an element is the first or last item in a 'classy'. * * @phpstan-type _Class array{ * index: int, * open: int, * close: int, * elements: non-empty-list<_Element> * } * @phpstan-type _Element array{token: Token, type: string, index: int, start: int, end: int} * @phpstan-type _AutogeneratedInputConfiguration array{ * elements?: array<string, string>, * } * @phpstan-type _AutogeneratedComputedConfiguration array{ * elements: array<string, string>, * } * * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> * * @no-named-arguments Parameter names are not covered by the backward compatibility promise. */ final class ClassAttributesSeparationFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface { /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */ use ConfigurableFixerTrait; /** * @internal */ public const SPACING_NONE = 'none'; /** * @internal */ public const SPACING_ONE = 'one'; private const SPACING_ONLY_IF_META = 'only_if_meta'; private const MODIFIER_TYPES = [\T_PRIVATE, \T_PROTECTED, \T_PUBLIC, \T_ABSTRACT, \T_FINAL, \T_STATIC, \T_STRING, \T_NS_SEPARATOR, \T_VAR, CT::T_NULLABLE_TYPE, CT::T_ARRAY_TYPEHINT, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE, FCT::T_READONLY, FCT::T_PRIVATE_SET, FCT::T_PROTECTED_SET, FCT::T_PUBLIC_SET]; /** * @var array<string, string> */ private array $classElementTypes = []; public function getDefinition(): FixerDefinitionInterface { return new FixerDefinition( 'Class, trait and interface elements must be separated with one or none blank line.', [ new CodeSample( <<<'PHP' <?php final class Sample { protected function foo() { } protected function bar() { } } PHP, ), new CodeSample( <<<'PHP' <?php class Sample {private $a; // foo /** second in a hour */ private $b; } PHP, ['elements' => ['property' => self::SPACING_ONE]], ), new CodeSample( <<<'PHP' <?php class Sample { const A = 1; /** seconds in some hours */ const B = 3600; } PHP, ['elements' => ['const' => self::SPACING_ONE]], ), new CodeSample( <<<'PHP' <?php class Sample { /** @var int */ const SECOND = 1; /** @var int */ const MINUTE = 60; const HOUR = 3600; const DAY = 86400; } PHP, ['elements' => ['const' => self::SPACING_ONLY_IF_META]], ), new VersionSpecificCodeSample( <<<'PHP' <?php class Sample { public $a; #[SetUp] public $b; /** @var string */ public $c; /** @internal */ #[Assert\String()] public $d; public $e; } PHP, new VersionSpecification(8_00_00), ['elements' => ['property' => self::SPACING_ONLY_IF_META]], ), ], ); } /** * {@inheritdoc} * * Must run before BracesFixer, IndentationTypeFixer, NoExtraBlankLinesFixer, StatementIndentationFixer. * Must run after ModifierKeywordsFixer, OrderedClassElementsFixer, PhpUnitDataProviderMethodOrderFixer, SingleClassElementPerStatementFixer. */ public function getPriority(): int { return 55; } public function isCandidate(Tokens $tokens): bool { return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds()); } protected function configurePostNormalisation(): void { $this->classElementTypes = []; // reset previous configuration foreach ($this->configuration['elements'] as $elementType => $spacing) { $this->classElementTypes[$elementType] = $spacing; } } protected function applyFix(\SplFileInfo $file, Tokens $tokens): void { foreach ($this->getElementsByClass($tokens) as $class) { $elements = $class['elements']; if (0 === \count($elements)) { continue; } if (isset($this->classElementTypes[$elements[0]['type']])) { $this->fixSpaceBelowClassElement($tokens, $class); } foreach ($elements as $index => $element) { if (isset($this->classElementTypes[$element['type']])) { $this->fixSpaceAboveClassElement($tokens, $class, $index); } } } } protected function createConfigurationDefinition(): FixerConfigurationResolverInterface { return new FixerConfigurationResolver([ (new FixerOptionBuilder('elements', 'Dictionary of `const|method|property|trait_import|case` => `none|one|only_if_meta` values.')) ->setAllowedTypes(['array<string, string>']) ->setAllowedValues([static function (array $option): bool { foreach ($option as $type => $spacing) { $supportedTypes = ['const', 'method', 'property', 'trait_import', 'case']; if (!\in_array($type, $supportedTypes, true)) { throw new InvalidOptionsException( \sprintf( 'Unexpected element type, expected any of %s, got "%s".', Utils::naturalLanguageJoin($supportedTypes), \gettype($type).'#'.$type, ), ); } $supportedSpacings = [self::SPACING_NONE, self::SPACING_ONE, self::SPACING_ONLY_IF_META]; if (!\in_array($spacing, $supportedSpacings, true)) { throw new InvalidOptionsException( \sprintf( 'Unexpected spacing for element type "%s", expected any of %s, got "%s".', $spacing, Utils::naturalLanguageJoin($supportedSpacings), \is_object($spacing) ? \get_class($spacing) : (null === $spacing ? 'null' : \gettype($spacing).'#'.$spacing), ), ); } } return true; }]) ->setDefault([ 'const' => self::SPACING_ONE, 'method' => self::SPACING_ONE, 'property' => self::SPACING_ONE, 'trait_import' => self::SPACING_NONE, 'case' => self::SPACING_NONE, ]) ->getOption(), ]); } /** * Fix spacing above an element of a class, interface or trait. * * Deals with comments, PHPDocs and spaces above the element with respect to the position of the * element within the class, interface or trait. * * @param _Class $class */ private function fixSpaceAboveClassElement(Tokens $tokens, array $class, int $elementIndex): void { \assert(isset($class['elements'][$elementIndex])); $element = $class['elements'][$elementIndex]; $elementAboveEnd = isset($class['elements'][$elementIndex + 1]) ? $class['elements'][$elementIndex + 1]['end'] : 0; $nonWhiteAbove = $tokens->getPrevNonWhitespace($element['start']); // element is directly after class open brace if ($nonWhiteAbove === $class['open']) { $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 1); return; } // deal with comments above an element if ($tokens[$nonWhiteAbove]->isGivenKind(\T_COMMENT)) { // check if the comment belongs to the previous element if ($elementAboveEnd === $nonWhiteAbove) { $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], $this->determineRequiredLineCount($tokens, $class, $elementIndex)); return; } // more than one line break, always bring it back to 2 line breaks between the element start and what is above it if ($tokens[$nonWhiteAbove + 1]->isWhitespace() && substr_count($tokens[$nonWhiteAbove + 1]->getContent(), "\n") > 1) { $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 2); return; } // there are 2 cases: if ( 1 === $element['start'] - $nonWhiteAbove || $tokens[$nonWhiteAbove - 1]->isWhitespace() && substr_count($tokens[$nonWhiteAbove - 1]->getContent(), "\n") > 0 || $tokens[$nonWhiteAbove + 1]->isWhitespace() && substr_count($tokens[$nonWhiteAbove + 1]->getContent(), "\n") > 0 ) { // 1. The comment is meant for the element (although not a PHPDoc), // make sure there is one line break between the element and the comment... $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 1); // ... and make sure there is blank line above the comment (with the exception when it is directly after a class opening) $nonWhiteAbove = $this->findCommentBlockStart($tokens, $nonWhiteAbove, $elementAboveEnd); $nonWhiteAboveComment = $tokens->getPrevNonWhitespace($nonWhiteAbove); if ($nonWhiteAboveComment === $class['open']) { if ($tokens[$nonWhiteAboveComment - 1]->isWhitespace() && substr_count($tokens[$nonWhiteAboveComment - 1]->getContent(), "\n") > 0) { $this->correctLineBreaks($tokens, $nonWhiteAboveComment, $nonWhiteAbove, 1); } } else { $this->correctLineBreaks($tokens, $nonWhiteAboveComment, $nonWhiteAbove, 2); } } else { // 2. The comment belongs to the code above the element, // make sure there is a blank line above the element (i.e. 2 line breaks) $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 2); } return; } // deal with element with a PHPDoc/attribute above it if ($tokens[$nonWhiteAbove]->isGivenKind([\T_DOC_COMMENT, CT::T_ATTRIBUTE_CLOSE])) { // there should be one linebreak between the element and the attribute above it $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 1); // make sure there is blank line above the comment (with the exception when it is directly after a class opening) $nonWhiteAbove = $this->findCommentBlockStart($tokens, $nonWhiteAbove, $elementAboveEnd); $nonWhiteAboveComment = $tokens->getPrevNonWhitespace($nonWhiteAbove); $this->correctLineBreaks($tokens, $nonWhiteAboveComment, $nonWhiteAbove, $nonWhiteAboveComment === $class['open'] ? 1 : 2); return; } $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], $this->determineRequiredLineCount($tokens, $class, $elementIndex)); } /** * @param _Class $class */ private function determineRequiredLineCount(Tokens $tokens, array $class, int $elementIndex): int { \assert(isset($class['elements'][$elementIndex])); $type = $class['elements'][$elementIndex]['type']; $spacing = $this->classElementTypes[$type]; if (self::SPACING_ONE === $spacing) { return 2; } if (self::SPACING_NONE === $spacing) { if (!isset($class['elements'][$elementIndex + 1])) { return 1; } $aboveElement = $class['elements'][$elementIndex + 1]; if ($aboveElement['type'] !== $type) { return 2; } $aboveElementDocCandidateIndex = $tokens->getPrevNonWhitespace($aboveElement['start']); return $tokens[$aboveElementDocCandidateIndex]->isGivenKind([\T_DOC_COMMENT, CT::T_ATTRIBUTE_CLOSE]) ? 2 : 1; } if (self::SPACING_ONLY_IF_META === $spacing) { $aboveElementDocCandidateIndex = $tokens->getPrevNonWhitespace($class['elements'][$elementIndex]['start']); return $tokens[$aboveElementDocCandidateIndex]->isGivenKind([\T_DOC_COMMENT, CT::T_ATTRIBUTE_CLOSE]) ? 2 : 1; } throw new \RuntimeException(\sprintf('Unknown spacing "%s".', $spacing)); } /** * @param _Class $class */ private function fixSpaceBelowClassElement(Tokens $tokens, array $class): void { $element = $class['elements'][0]; // if this is last element fix; fix to the class end `}` here if appropriate if ($class['close'] === $tokens->getNextNonWhitespace($element['end'])) { $this->correctLineBreaks($tokens, $element['end'], $class['close'], 1); } } private function correctLineBreaks(Tokens $tokens, int $startIndex, int $endIndex, int $reqLineCount): void { $lineEnding = $this->whitespacesConfig->getLineEnding(); ++$startIndex; $numbOfWhiteTokens = $endIndex - $startIndex; if (0 === $numbOfWhiteTokens) { $tokens->insertAt($startIndex, new Token([\T_WHITESPACE, str_repeat($lineEnding, $reqLineCount)])); return; } $lineBreakCount = $this->getLineBreakCount($tokens, $startIndex, $endIndex); if ($reqLineCount === $lineBreakCount) { return; } if ($lineBreakCount < $reqLineCount) { $tokens[$startIndex] = new Token([ \T_WHITESPACE, str_repeat($lineEnding, $reqLineCount - $lineBreakCount).$tokens[$startIndex]->getContent(), ]); return; } // $lineCount = > $reqLineCount : check the one Token case first since this one will be true most of the time if (1 === $numbOfWhiteTokens) { $tokens[$startIndex] = new Token([ \T_WHITESPACE, Preg::replace('/\r\n|\n/', '', $tokens[$startIndex]->getContent(), $lineBreakCount - $reqLineCount), ]); return; } // $numbOfWhiteTokens = > 1 $toReplaceCount = $lineBreakCount - $reqLineCount; for ($i = $startIndex; $i < $endIndex && $toReplaceCount > 0; ++$i) { $tokenLineCount = substr_count($tokens[$i]->getContent(), "\n"); if ($tokenLineCount > 0) { $tokens[$i] = new Token([ \T_WHITESPACE, Preg::replace('/\r\n|\n/', '', $tokens[$i]->getContent(), min($toReplaceCount, $tokenLineCount)), ]); $toReplaceCount -= $tokenLineCount; } } } private function getLineBreakCount(Tokens $tokens, int $startIndex, int $endIndex): int { $lineCount = 0; for ($i = $startIndex; $i < $endIndex; ++$i) { $lineCount += substr_count($tokens[$i]->getContent(), "\n"); } return $lineCount; } private function findCommentBlockStart(Tokens $tokens, int $start, int $elementAboveEnd): int { for ($i = $start; $i > $elementAboveEnd; --$i) { if ($tokens[$i]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) { $start = $i = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $i); continue; } if ($tokens[$i]->isComment()) { $start = $i; continue; } if (!$tokens[$i]->isWhitespace() || $this->getLineBreakCount($tokens, $i, $i + 1) > 1) { break; } } return $start; } /** * @TODO Introduce proper DTO instead of an array * * @return \Generator<_Class> */ private function getElementsByClass(Tokens $tokens): \Generator { $tokensAnalyzer = new TokensAnalyzer($tokens); $class = null; foreach (array_reverse($tokensAnalyzer->getClassyElements(), true) as $index => $element) { $element['index'] = $index; if (null === $class || $element['classIndex'] !== $class['index']) { if (null !== $class) { yield $class; } $classIndex = $element['classIndex']; $classOpen = $tokens->getNextTokenOfKind($classIndex, ['{']); $classEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpen); $class = [ 'index' => $element['classIndex'], 'open' => $classOpen, 'close' => $classEnd, 'elements' => [], ]; } unset($element['classIndex']); $element['start'] = $this->getFirstTokenIndexOfClassElement($tokens, $class['open'], $index); $element['end'] = $this->getLastTokenIndexOfClassElement($tokens, $class['index'], $index, $element['type'], $tokensAnalyzer); $class['elements'][] = $element; // reset the key by design } if (null !== $class) { yield $class; } } /** * including trailing single line comments if belonging to the class element. */ private function getFirstTokenIndexOfClassElement(Tokens $tokens, int $classOpen, int $elementIndex): int { $firstElementAttributeIndex = $elementIndex; do { $nonWhiteAbove = $tokens->getPrevMeaningfulToken($firstElementAttributeIndex); if (null !== $nonWhiteAbove && $tokens[$nonWhiteAbove]->isGivenKind(self::MODIFIER_TYPES)) { $firstElementAttributeIndex = $nonWhiteAbove; } else { break; } } while ($firstElementAttributeIndex > $classOpen); return $firstElementAttributeIndex; } /** * including trailing single line comments if belonging to the class element. */ private function getLastTokenIndexOfClassElement(Tokens $tokens, int $classIndex, int $elementIndex, string $elementType, TokensAnalyzer $tokensAnalyzer): int { // find last token of the element if ('method' === $elementType && !$tokens[$classIndex]->isGivenKind(\T_INTERFACE)) { $attributes = $tokensAnalyzer->getMethodAttributes($elementIndex); if (true === $attributes['abstract']) { $elementEndIndex = $tokens->getNextTokenOfKind($elementIndex, [';']); } else { $elementEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $tokens->getNextTokenOfKind($elementIndex, ['{'])); } } elseif ('trait_import' === $elementType) { $elementEndIndex = $elementIndex; do { $elementEndIndex = $tokens->getNextMeaningfulToken($elementEndIndex); } while ($tokens[$elementEndIndex]->isGivenKind([\T_STRING, \T_NS_SEPARATOR]) || $tokens[$elementEndIndex]->equals(',')); if (!$tokens[$elementEndIndex]->equals(';')) { $elementEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $tokens->getNextTokenOfKind($elementIndex, ['{'])); } } else { // 'const', 'property', enum-'case', or 'method' of an interface $elementEndIndex = $tokens->getNextTokenOfKind($elementIndex, [';', '{']); } $singleLineElement = true; for ($i = $elementIndex + 1; $i < $elementEndIndex; ++$i) { if (str_contains($tokens[$i]->getContent(), "\n")) { $singleLineElement = false; break; } } if ($singleLineElement) { while (true) { $nextToken = $tokens[$elementEndIndex + 1]; if (($nextToken->isComment() || $nextToken->isWhitespace()) && !str_contains($nextToken->getContent(), "\n")) { ++$elementEndIndex; } else { break; } } if ($tokens[$elementEndIndex]->isWhitespace()) { $elementEndIndex = $tokens->getPrevNonWhitespace($elementEndIndex); } } return $elementEndIndex; } }