* 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\Phpdoc; use PhpCsFixer\AbstractFixer; use PhpCsFixer\DocBlock\DocBlock; use PhpCsFixer\Fixer\ConfigurableFixerInterface; 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 Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; /** * @author Graham Campbell * @author Jakub Kwaśniewski */ final class PhpdocOrderFixer extends AbstractFixer implements ConfigurableFixerInterface { /** * @const string[] * * @TODO: 4.0 - change default to ['param', 'return', 'throws'] */ private const ORDER_DEFAULT = ['param', 'throws', 'return']; public function getDefinition(): FixerDefinitionInterface { $code = <<<'EOF' self::ORDER_DEFAULT]), new CodeSample($code, ['order' => ['param', 'return', 'throws']]), new CodeSample($code, ['order' => ['param', 'custom', 'throws', 'return']]), ], ); } public function isCandidate(Tokens $tokens): bool { return $tokens->isTokenKindFound(T_DOC_COMMENT); } /** * {@inheritdoc} * * Must run before PhpdocAlignFixer, PhpdocSeparationFixer, PhpdocTrimFixer. * Must run after AlignMultilineCommentFixer, CommentToPhpdocFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocIndentFixer, PhpdocNoEmptyReturnFixer, PhpdocScalarFixer, PhpdocToCommentFixer, PhpdocTypesFixer. */ public function getPriority(): int { return -2; } protected function createConfigurationDefinition(): FixerConfigurationResolverInterface { return new FixerConfigurationResolver([ (new FixerOptionBuilder('order', 'Sequence in which annotations in PHPDoc should be ordered.')) ->setAllowedTypes(['string[]']) ->setAllowedValues([static function (array $order): bool { if (\count($order) < 2) { throw new InvalidOptionsException('The option "order" value is invalid. Minimum two tags are required.'); } return true; }]) ->setDefault(self::ORDER_DEFAULT) ->getOption(), ]); } protected function applyFix(\SplFileInfo $file, Tokens $tokens): void { foreach ($tokens as $index => $token) { if (!$token->isGivenKind(T_DOC_COMMENT)) { continue; } // assuming annotations are already grouped by tags $content = $token->getContent(); // sort annotations $successors = $this->configuration['order']; while (\count($successors) >= 3) { $predecessor = array_shift($successors); $content = $this->moveAnnotationsBefore($predecessor, $successors, $content); } // we're parsing the content last time to make sure the internal // state of the docblock is correct after the modifications $predecessors = $this->configuration['order']; $last = array_pop($predecessors); $content = $this->moveAnnotationsAfter($last, $predecessors, $content); // persist the content at the end $tokens[$index] = new Token([T_DOC_COMMENT, $content]); } } /** * Move all given annotations in before given set of annotations. * * @param string $move Tag of annotations that should be moved * @param string[] $before Tags of annotations that should moved annotations be placed before */ private function moveAnnotationsBefore(string $move, array $before, string $content): string { $doc = new DocBlock($content); $toBeMoved = $doc->getAnnotationsOfType($move); // nothing to do if there are no annotations to be moved if (0 === \count($toBeMoved)) { return $content; } $others = $doc->getAnnotationsOfType($before); if (0 === \count($others)) { return $content; } // get the index of the final line of the final toBoMoved annotation $end = end($toBeMoved)->getEnd(); $line = $doc->getLine($end); // move stuff about if required foreach ($others as $other) { if ($other->getStart() < $end) { // we're doing this to maintain the original line indices $line->setContent($line->getContent().$other->getContent()); $other->remove(); } } return $doc->getContent(); } /** * Move all given annotations after given set of annotations. * * @param string $move Tag of annotations that should be moved * @param string[] $after Tags of annotations that should moved annotations be placed after */ private function moveAnnotationsAfter(string $move, array $after, string $content): string { $doc = new DocBlock($content); $toBeMoved = $doc->getAnnotationsOfType($move); // nothing to do if there are no annotations to be moved if (0 === \count($toBeMoved)) { return $content; } $others = $doc->getAnnotationsOfType($after); // nothing to do if there are no other annotations if (0 === \count($others)) { return $content; } // get the index of the first line of the first toBeMoved annotation $start = $toBeMoved[0]->getStart(); $line = $doc->getLine($start); // move stuff about if required foreach (array_reverse($others) as $other) { if ($other->getEnd() > $start) { // we're doing this to maintain the original line indices $line->setContent($other->getContent().$line->getContent()); $other->remove(); } } return $doc->getContent(); } }