PhpUnitSizeClassFixer.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. <?php
  2. /*
  3. * This file is part of PHP CS Fixer.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  7. *
  8. * This source file is subject to the MIT license that is bundled
  9. * with this source code in the file LICENSE.
  10. */
  11. namespace PhpCsFixer\Fixer\PhpUnit;
  12. use PhpCsFixer\DocBlock\Annotation;
  13. use PhpCsFixer\DocBlock\DocBlock;
  14. use PhpCsFixer\DocBlock\Line;
  15. use PhpCsFixer\Fixer\AbstractPhpUnitFixer;
  16. use PhpCsFixer\Fixer\ConfigurationDefinitionFixerInterface;
  17. use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
  18. use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
  19. use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
  20. use PhpCsFixer\FixerDefinition\CodeSample;
  21. use PhpCsFixer\FixerDefinition\FixerDefinition;
  22. use PhpCsFixer\Tokenizer\Token;
  23. use PhpCsFixer\Tokenizer\Tokens;
  24. /**
  25. * @author Jefersson Nathan <malukenho.dev@gmail.com>
  26. */
  27. final class PhpUnitSizeClassFixer extends AbstractPhpUnitFixer implements WhitespacesAwareFixerInterface, ConfigurationDefinitionFixerInterface
  28. {
  29. /**
  30. * {@inheritdoc}
  31. */
  32. public function getDefinition()
  33. {
  34. return new FixerDefinition(
  35. 'All PHPUnit test cases should have `@small`, `@medium` or `@large` annotation to enable run time limits.',
  36. [
  37. new CodeSample("<?php\nclass MyTest extends TestCase {}\n"),
  38. new CodeSample("<?php\nclass MyTest extends TestCase {}\n", ['group' => 'medium']),
  39. ],
  40. 'The special groups [small, medium, large] provides a way to identify tests that are taking long to be executed.'
  41. );
  42. }
  43. /**
  44. * {@inheritdoc}
  45. */
  46. protected function createConfigurationDefinition()
  47. {
  48. return new FixerConfigurationResolver([
  49. (new FixerOptionBuilder('group', 'Define a specific group to be used in case no group is already in use'))
  50. ->setAllowedValues(['small', 'medium', 'large'])
  51. ->setDefault('small')
  52. ->getOption(),
  53. ]);
  54. }
  55. /**
  56. * {@inheritdoc}
  57. */
  58. protected function applyPhpUnitClassFix(Tokens $tokens, $startIndex, $endIndex)
  59. {
  60. $classIndex = $tokens->getPrevTokenOfKind($startIndex, [[T_CLASS]]);
  61. if ($this->isAbstractClass($tokens, $classIndex)) {
  62. return;
  63. }
  64. $docBlockIndex = $this->getDocBlockIndex($tokens, $classIndex);
  65. if ($this->isPHPDoc($tokens, $docBlockIndex)) {
  66. $this->updateDocBlockIfNeeded($tokens, $docBlockIndex);
  67. } else {
  68. $this->createDocBlock($tokens, $docBlockIndex);
  69. }
  70. }
  71. /**
  72. * @param int $i
  73. *
  74. * @return bool
  75. */
  76. private function isAbstractClass(Tokens $tokens, $i)
  77. {
  78. $typeIndex = $tokens->getPrevMeaningfulToken($i);
  79. return $tokens[$typeIndex]->isGivenKind(T_ABSTRACT);
  80. }
  81. private function createDocBlock(Tokens $tokens, $docBlockIndex)
  82. {
  83. $lineEnd = $this->whitespacesConfig->getLineEnding();
  84. $originalIndent = $this->detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
  85. $group = $this->configuration['group'];
  86. $toInsert = [
  87. new Token([T_DOC_COMMENT, '/**'.$lineEnd."{$originalIndent} * @".$group.$lineEnd."{$originalIndent} */"]),
  88. new Token([T_WHITESPACE, $lineEnd.$originalIndent]),
  89. ];
  90. $index = $tokens->getNextMeaningfulToken($docBlockIndex);
  91. $tokens->insertAt($index, $toInsert);
  92. }
  93. private function updateDocBlockIfNeeded(Tokens $tokens, $docBlockIndex)
  94. {
  95. $doc = new DocBlock($tokens[$docBlockIndex]->getContent());
  96. if (!empty($this->filterDocBlock($doc))) {
  97. return;
  98. }
  99. $doc = $this->makeDocBlockMultiLineIfNeeded($doc, $tokens, $docBlockIndex);
  100. $lines = $this->addSizeAnnotation($doc, $tokens, $docBlockIndex);
  101. $lines = implode('', $lines);
  102. $tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $lines]);
  103. }
  104. /**
  105. * @param int $index
  106. *
  107. * @return string
  108. */
  109. private function detectIndent(Tokens $tokens, $index)
  110. {
  111. if (!$tokens[$index - 1]->isWhitespace()) {
  112. return ''; // cannot detect indent
  113. }
  114. $explodedContent = explode($this->whitespacesConfig->getLineEnding(), $tokens[$index - 1]->getContent());
  115. return end($explodedContent);
  116. }
  117. /**
  118. * @param int $docBlockIndex
  119. *
  120. * @return Line[]
  121. */
  122. private function addSizeAnnotation(DocBlock $docBlock, Tokens $tokens, $docBlockIndex)
  123. {
  124. $lines = $docBlock->getLines();
  125. $originalIndent = $this->detectIndent($tokens, $docBlockIndex);
  126. $lineEnd = $this->whitespacesConfig->getLineEnding();
  127. $group = $this->configuration['group'];
  128. array_splice($lines, -1, 0, $originalIndent.' *'.$lineEnd.$originalIndent.' * @'.$group.$lineEnd);
  129. return $lines;
  130. }
  131. /**
  132. * @param int $docBlockIndex
  133. *
  134. * @return DocBlock
  135. */
  136. private function makeDocBlockMultiLineIfNeeded(DocBlock $doc, Tokens $tokens, $docBlockIndex)
  137. {
  138. $lines = $doc->getLines();
  139. if (1 === \count($lines) && empty($this->filterDocBlock($doc))) {
  140. $lines = $this->splitUpDocBlock($lines, $tokens, $docBlockIndex);
  141. return new DocBlock(implode('', $lines));
  142. }
  143. return $doc;
  144. }
  145. /**
  146. * Take a one line doc block, and turn it into a multi line doc block.
  147. *
  148. * @param Line[] $lines
  149. * @param int $docBlockIndex
  150. *
  151. * @return Line[]
  152. */
  153. private function splitUpDocBlock($lines, Tokens $tokens, $docBlockIndex)
  154. {
  155. $lineContent = $this->getSingleLineDocBlockEntry($lines);
  156. $lineEnd = $this->whitespacesConfig->getLineEnding();
  157. $originalIndent = $this->detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
  158. return [
  159. new Line('/**'.$lineEnd),
  160. new Line($originalIndent.' * '.$lineContent.$lineEnd),
  161. new Line($originalIndent.' */'),
  162. ];
  163. }
  164. /**
  165. * @param Line|Line[]|string $line
  166. *
  167. * @return string
  168. */
  169. private function getSingleLineDocBlockEntry($line)
  170. {
  171. $line = $line[0];
  172. $line = str_replace('*/', '', $line);
  173. $line = trim($line);
  174. $line = str_split($line);
  175. $i = \count($line);
  176. do {
  177. --$i;
  178. } while ('*' !== $line[$i] && '*' !== $line[$i - 1] && '/' !== $line[$i - 2]);
  179. if (' ' === $line[$i]) {
  180. ++$i;
  181. }
  182. $line = \array_slice($line, $i);
  183. return implode('', $line);
  184. }
  185. /**
  186. * @return Annotation[][]
  187. */
  188. private function filterDocBlock(DocBlock $doc)
  189. {
  190. return array_filter([
  191. $doc->getAnnotationsOfType('small'),
  192. $doc->getAnnotationsOfType('large'),
  193. $doc->getAnnotationsOfType('medium'),
  194. ]);
  195. }
  196. }