PhpdocTrimConsecutiveBlankLineSeparationFixer.php 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Fixer\Phpdoc;
  13. use PhpCsFixer\AbstractFixer;
  14. use PhpCsFixer\DocBlock\DocBlock;
  15. use PhpCsFixer\DocBlock\Line;
  16. use PhpCsFixer\DocBlock\ShortDescription;
  17. use PhpCsFixer\FixerDefinition\CodeSample;
  18. use PhpCsFixer\FixerDefinition\FixerDefinition;
  19. use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
  20. use PhpCsFixer\Tokenizer\Token;
  21. use PhpCsFixer\Tokenizer\Tokens;
  22. /**
  23. * @author Nobu Funaki <nobu.funaki@gmail.com>
  24. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  25. */
  26. final class PhpdocTrimConsecutiveBlankLineSeparationFixer extends AbstractFixer
  27. {
  28. public function getDefinition(): FixerDefinitionInterface
  29. {
  30. return new FixerDefinition(
  31. 'Removes extra blank lines after summary and after description in PHPDoc.',
  32. [
  33. new CodeSample(
  34. '<?php
  35. /**
  36. * Summary.
  37. *
  38. *
  39. * Description that contain 4 lines,
  40. *
  41. *
  42. * while 2 of them are blank!
  43. *
  44. *
  45. * @param string $foo
  46. *
  47. *
  48. * @dataProvider provideFixCases
  49. */
  50. function fnc($foo) {}
  51. '
  52. ),
  53. ]
  54. );
  55. }
  56. /**
  57. * {@inheritdoc}
  58. *
  59. * Must run before PhpdocAlignFixer.
  60. * Must run after AlignMultilineCommentFixer, CommentToPhpdocFixer, PhpUnitAttributesFixer, PhpdocIndentFixer, PhpdocScalarFixer, PhpdocToCommentFixer, PhpdocTypesFixer.
  61. */
  62. public function getPriority(): int
  63. {
  64. return -41;
  65. }
  66. public function isCandidate(Tokens $tokens): bool
  67. {
  68. return $tokens->isTokenKindFound(T_DOC_COMMENT);
  69. }
  70. protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
  71. {
  72. foreach ($tokens as $index => $token) {
  73. if (!$token->isGivenKind(T_DOC_COMMENT)) {
  74. continue;
  75. }
  76. $doc = new DocBlock($token->getContent());
  77. $summaryEnd = (new ShortDescription($doc))->getEnd();
  78. if (null !== $summaryEnd) {
  79. $this->fixSummary($doc, $summaryEnd);
  80. $this->fixDescription($doc, $summaryEnd);
  81. }
  82. $this->fixAllTheRest($doc);
  83. $tokens[$index] = new Token([T_DOC_COMMENT, $doc->getContent()]);
  84. }
  85. }
  86. private function fixSummary(DocBlock $doc, int $summaryEnd): void
  87. {
  88. $nonBlankLineAfterSummary = $this->findNonBlankLine($doc, $summaryEnd);
  89. $this->removeExtraBlankLinesBetween($doc, $summaryEnd, $nonBlankLineAfterSummary);
  90. }
  91. private function fixDescription(DocBlock $doc, int $summaryEnd): void
  92. {
  93. $annotationStart = $this->findFirstAnnotationOrEnd($doc);
  94. // assuming the end of the Description appears before the first Annotation
  95. $descriptionEnd = $this->reverseFindLastUsefulContent($doc, $annotationStart);
  96. if (null === $descriptionEnd || $summaryEnd === $descriptionEnd) {
  97. return; // no Description
  98. }
  99. if ($annotationStart === \count($doc->getLines()) - 1) {
  100. return; // no content after Description
  101. }
  102. $this->removeExtraBlankLinesBetween($doc, $descriptionEnd, $annotationStart);
  103. }
  104. private function fixAllTheRest(DocBlock $doc): void
  105. {
  106. $annotationStart = $this->findFirstAnnotationOrEnd($doc);
  107. $lastLine = $this->reverseFindLastUsefulContent($doc, \count($doc->getLines()) - 1);
  108. if (null !== $lastLine && $annotationStart !== $lastLine) {
  109. $this->removeExtraBlankLinesBetween($doc, $annotationStart, $lastLine);
  110. }
  111. }
  112. private function removeExtraBlankLinesBetween(DocBlock $doc, int $from, int $to): void
  113. {
  114. for ($index = $from + 1; $index < $to; ++$index) {
  115. $line = $doc->getLine($index);
  116. $next = $doc->getLine($index + 1);
  117. $this->removeExtraBlankLine($line, $next);
  118. }
  119. }
  120. private function removeExtraBlankLine(Line $current, Line $next): void
  121. {
  122. if (!$current->isTheEnd() && !$current->containsUsefulContent()
  123. && !$next->isTheEnd() && !$next->containsUsefulContent()) {
  124. $current->remove();
  125. }
  126. }
  127. private function findNonBlankLine(DocBlock $doc, int $after): ?int
  128. {
  129. foreach ($doc->getLines() as $index => $line) {
  130. if ($index <= $after) {
  131. continue;
  132. }
  133. if ($line->containsATag() || $line->containsUsefulContent() || $line->isTheEnd()) {
  134. return $index;
  135. }
  136. }
  137. return null;
  138. }
  139. private function findFirstAnnotationOrEnd(DocBlock $doc): int
  140. {
  141. foreach ($doc->getLines() as $index => $line) {
  142. if ($line->containsATag()) {
  143. return $index;
  144. }
  145. }
  146. if (!isset($index)) {
  147. throw new \LogicException('PHPDoc has empty lines collection.');
  148. }
  149. return $index; // no Annotation, return the last line
  150. }
  151. private function reverseFindLastUsefulContent(DocBlock $doc, int $from): ?int
  152. {
  153. for ($index = $from - 1; $index >= 0; --$index) {
  154. if ($doc->getLine($index)->containsUsefulContent()) {
  155. return $index;
  156. }
  157. }
  158. return null;
  159. }
  160. }