AbstractPhpUnitFixer.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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;
  13. use PhpCsFixer\AbstractFixer;
  14. use PhpCsFixer\DocBlock\DocBlock;
  15. use PhpCsFixer\DocBlock\Line;
  16. use PhpCsFixer\Indicator\PhpUnitTestCaseIndicator;
  17. use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer;
  18. use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
  19. use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
  20. use PhpCsFixer\Tokenizer\Analyzer\WhitespacesAnalyzer;
  21. use PhpCsFixer\Tokenizer\CT;
  22. use PhpCsFixer\Tokenizer\Token;
  23. use PhpCsFixer\Tokenizer\Tokens;
  24. /**
  25. * @internal
  26. */
  27. abstract class AbstractPhpUnitFixer extends AbstractFixer
  28. {
  29. public function isCandidate(Tokens $tokens): bool
  30. {
  31. return $tokens->isAllTokenKindsFound([T_CLASS, T_STRING]);
  32. }
  33. final protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
  34. {
  35. $phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
  36. foreach ($phpUnitTestCaseIndicator->findPhpUnitClasses($tokens) as $indices) {
  37. $this->applyPhpUnitClassFix($tokens, $indices[0], $indices[1]);
  38. }
  39. }
  40. abstract protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void;
  41. final protected function getDocBlockIndex(Tokens $tokens, int $index): int
  42. {
  43. $modifiers = [T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_COMMENT];
  44. if (\defined('T_ATTRIBUTE')) { // @TODO: drop condition when PHP 8.0+ is required
  45. $modifiers[] = T_ATTRIBUTE;
  46. }
  47. if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.2+ is required
  48. $modifiers[] = T_READONLY;
  49. }
  50. do {
  51. $index = $tokens->getPrevNonWhitespace($index);
  52. if ($tokens[$index]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
  53. $index = $tokens->getPrevTokenOfKind($index, [[T_ATTRIBUTE]]);
  54. }
  55. } while ($tokens[$index]->isGivenKind($modifiers));
  56. return $index;
  57. }
  58. /**
  59. * @param list<string> $preventingAnnotations
  60. * @param list<class-string> $preventingAttributes
  61. */
  62. final protected function ensureIsDocBlockWithAnnotation(
  63. Tokens $tokens,
  64. int $index,
  65. string $annotation,
  66. array $preventingAnnotations,
  67. array $preventingAttributes
  68. ): void {
  69. $docBlockIndex = $this->getDocBlockIndex($tokens, $index);
  70. if (self::isPreventedByAttribute($tokens, $index, $preventingAttributes)) {
  71. return;
  72. }
  73. if ($this->isPHPDoc($tokens, $docBlockIndex)) {
  74. $this->updateDocBlockIfNeeded($tokens, $docBlockIndex, $annotation, $preventingAnnotations);
  75. } else {
  76. $this->createDocBlock($tokens, $docBlockIndex, $annotation);
  77. }
  78. }
  79. final protected function isPHPDoc(Tokens $tokens, int $index): bool
  80. {
  81. return $tokens[$index]->isGivenKind(T_DOC_COMMENT);
  82. }
  83. /**
  84. * @return iterable<array{
  85. * index: int,
  86. * loweredName: string,
  87. * openBraceIndex: int,
  88. * closeBraceIndex: int,
  89. * }>
  90. */
  91. protected function getPreviousAssertCall(Tokens $tokens, int $startIndex, int $endIndex): iterable
  92. {
  93. $functionsAnalyzer = new FunctionsAnalyzer();
  94. for ($index = $endIndex; $index > $startIndex; --$index) {
  95. $index = $tokens->getPrevTokenOfKind($index, [[T_STRING]]);
  96. if (null === $index) {
  97. return;
  98. }
  99. // test if "assert" something call
  100. $loweredContent = strtolower($tokens[$index]->getContent());
  101. if (!str_starts_with($loweredContent, 'assert')) {
  102. continue;
  103. }
  104. // test candidate for simple calls like: ([\]+'some fixable call'(...))
  105. $openBraceIndex = $tokens->getNextMeaningfulToken($index);
  106. if (!$tokens[$openBraceIndex]->equals('(')) {
  107. continue;
  108. }
  109. if (!$functionsAnalyzer->isTheSameClassCall($tokens, $index)) {
  110. continue;
  111. }
  112. yield [
  113. 'index' => $index,
  114. 'loweredName' => $loweredContent,
  115. 'openBraceIndex' => $openBraceIndex,
  116. 'closeBraceIndex' => $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openBraceIndex),
  117. ];
  118. }
  119. }
  120. private function createDocBlock(Tokens $tokens, int $docBlockIndex, string $annotation): void
  121. {
  122. $lineEnd = $this->whitespacesConfig->getLineEnding();
  123. $originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
  124. $toInsert = [
  125. new Token([T_DOC_COMMENT, "/**{$lineEnd}{$originalIndent} * @{$annotation}{$lineEnd}{$originalIndent} */"]),
  126. new Token([T_WHITESPACE, $lineEnd.$originalIndent]),
  127. ];
  128. $index = $tokens->getNextMeaningfulToken($docBlockIndex);
  129. $tokens->insertAt($index, $toInsert);
  130. if (!$tokens[$index - 1]->isGivenKind(T_WHITESPACE)) {
  131. $extraNewLines = $this->whitespacesConfig->getLineEnding();
  132. if (!$tokens[$index - 1]->isGivenKind(T_OPEN_TAG)) {
  133. $extraNewLines .= $this->whitespacesConfig->getLineEnding();
  134. }
  135. $tokens->insertAt($index, [
  136. new Token([T_WHITESPACE, $extraNewLines.WhitespacesAnalyzer::detectIndent($tokens, $index)]),
  137. ]);
  138. }
  139. }
  140. /**
  141. * @param list<string> $preventingAnnotations
  142. */
  143. private function updateDocBlockIfNeeded(
  144. Tokens $tokens,
  145. int $docBlockIndex,
  146. string $annotation,
  147. array $preventingAnnotations
  148. ): void {
  149. $doc = new DocBlock($tokens[$docBlockIndex]->getContent());
  150. foreach ($preventingAnnotations as $preventingAnnotation) {
  151. if ([] !== $doc->getAnnotationsOfType($preventingAnnotation)) {
  152. return;
  153. }
  154. }
  155. $doc = $this->makeDocBlockMultiLineIfNeeded($doc, $tokens, $docBlockIndex, $annotation);
  156. $lines = $this->addInternalAnnotation($doc, $tokens, $docBlockIndex, $annotation);
  157. $lines = implode('', $lines);
  158. $tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $lines]);
  159. }
  160. /**
  161. * @param list<class-string> $preventingAttributes
  162. */
  163. private static function isPreventedByAttribute(Tokens $tokens, int $index, array $preventingAttributes): bool
  164. {
  165. if ([] === $preventingAttributes) {
  166. return false;
  167. }
  168. $modifiers = [T_FINAL];
  169. if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.2+ is required
  170. $modifiers[] = T_READONLY;
  171. }
  172. do {
  173. $index = $tokens->getPrevMeaningfulToken($index);
  174. } while ($tokens[$index]->isGivenKind($modifiers));
  175. if (!$tokens[$index]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
  176. return false;
  177. }
  178. $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $index);
  179. foreach (AttributeAnalyzer::collect($tokens, $index) as $attributeAnalysis) {
  180. foreach ($attributeAnalysis->getAttributes() as $attribute) {
  181. if (\in_array(ltrim(self::getFullyQualifiedName($tokens, $attribute['name']), '\\'), $preventingAttributes, true)) {
  182. return true;
  183. }
  184. }
  185. }
  186. return false;
  187. }
  188. private static function getFullyQualifiedName(Tokens $tokens, string $name): string
  189. {
  190. $name = strtolower($name);
  191. $names = [];
  192. foreach ((new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens) as $namespaceUseAnalysis) {
  193. $names[strtolower($namespaceUseAnalysis->getShortName())] = strtolower($namespaceUseAnalysis->getFullName());
  194. }
  195. foreach ($names as $shortName => $fullName) {
  196. if ($name === $shortName) {
  197. return $fullName;
  198. }
  199. if (!str_starts_with($name, $shortName.'\\')) {
  200. continue;
  201. }
  202. return $fullName.substr($name, \strlen($shortName));
  203. }
  204. return $name;
  205. }
  206. /**
  207. * @return list<Line>
  208. */
  209. private function addInternalAnnotation(DocBlock $docBlock, Tokens $tokens, int $docBlockIndex, string $annotation): array
  210. {
  211. $lines = $docBlock->getLines();
  212. $originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $docBlockIndex);
  213. $lineEnd = $this->whitespacesConfig->getLineEnding();
  214. array_splice($lines, -1, 0, $originalIndent.' * @'.$annotation.$lineEnd);
  215. return $lines;
  216. }
  217. private function makeDocBlockMultiLineIfNeeded(DocBlock $doc, Tokens $tokens, int $docBlockIndex, string $annotation): DocBlock
  218. {
  219. $lines = $doc->getLines();
  220. if (1 === \count($lines) && [] === $doc->getAnnotationsOfType($annotation)) {
  221. $indent = WhitespacesAnalyzer::detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
  222. $doc->makeMultiLine($indent, $this->whitespacesConfig->getLineEnding());
  223. return $doc;
  224. }
  225. return $doc;
  226. }
  227. }