123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- <?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;
- use PhpCsFixer\AbstractFixer;
- use PhpCsFixer\DocBlock\DocBlock;
- use PhpCsFixer\DocBlock\Line;
- use PhpCsFixer\Indicator\PhpUnitTestCaseIndicator;
- use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer;
- use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
- use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
- use PhpCsFixer\Tokenizer\Analyzer\WhitespacesAnalyzer;
- use PhpCsFixer\Tokenizer\CT;
- use PhpCsFixer\Tokenizer\Token;
- use PhpCsFixer\Tokenizer\Tokens;
- /**
- * @internal
- */
- abstract class AbstractPhpUnitFixer extends AbstractFixer
- {
- public function isCandidate(Tokens $tokens): bool
- {
- return $tokens->isAllTokenKindsFound([T_CLASS, T_STRING]);
- }
- final protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
- {
- $phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
- foreach ($phpUnitTestCaseIndicator->findPhpUnitClasses($tokens) as $indices) {
- $this->applyPhpUnitClassFix($tokens, $indices[0], $indices[1]);
- }
- }
- abstract protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void;
- final protected function getDocBlockIndex(Tokens $tokens, int $index): int
- {
- $modifiers = [T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_COMMENT];
- if (\defined('T_ATTRIBUTE')) { // @TODO: drop condition when PHP 8.0+ is required
- $modifiers[] = T_ATTRIBUTE;
- }
- if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.2+ is required
- $modifiers[] = T_READONLY;
- }
- do {
- $index = $tokens->getPrevNonWhitespace($index);
- if ($tokens[$index]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
- $index = $tokens->getPrevTokenOfKind($index, [[T_ATTRIBUTE]]);
- }
- } while ($tokens[$index]->isGivenKind($modifiers));
- return $index;
- }
- /**
- * @param list<string> $preventingAnnotations
- * @param list<class-string> $preventingAttributes
- */
- final protected function ensureIsDocBlockWithAnnotation(
- Tokens $tokens,
- int $index,
- string $annotation,
- array $preventingAnnotations,
- array $preventingAttributes
- ): void {
- $docBlockIndex = $this->getDocBlockIndex($tokens, $index);
- if (self::isPreventedByAttribute($tokens, $index, $preventingAttributes)) {
- return;
- }
- if ($this->isPHPDoc($tokens, $docBlockIndex)) {
- $this->updateDocBlockIfNeeded($tokens, $docBlockIndex, $annotation, $preventingAnnotations);
- } else {
- $this->createDocBlock($tokens, $docBlockIndex, $annotation);
- }
- }
- final protected function isPHPDoc(Tokens $tokens, int $index): bool
- {
- return $tokens[$index]->isGivenKind(T_DOC_COMMENT);
- }
- /**
- * @return iterable<array{
- * index: int,
- * loweredName: string,
- * openBraceIndex: int,
- * closeBraceIndex: int,
- * }>
- */
- protected function getPreviousAssertCall(Tokens $tokens, int $startIndex, int $endIndex): iterable
- {
- $functionsAnalyzer = new FunctionsAnalyzer();
- for ($index = $endIndex; $index > $startIndex; --$index) {
- $index = $tokens->getPrevTokenOfKind($index, [[T_STRING]]);
- if (null === $index) {
- return;
- }
- // test if "assert" something call
- $loweredContent = strtolower($tokens[$index]->getContent());
- if (!str_starts_with($loweredContent, 'assert')) {
- continue;
- }
- // test candidate for simple calls like: ([\]+'some fixable call'(...))
- $openBraceIndex = $tokens->getNextMeaningfulToken($index);
- if (!$tokens[$openBraceIndex]->equals('(')) {
- continue;
- }
- if (!$functionsAnalyzer->isTheSameClassCall($tokens, $index)) {
- continue;
- }
- yield [
- 'index' => $index,
- 'loweredName' => $loweredContent,
- 'openBraceIndex' => $openBraceIndex,
- 'closeBraceIndex' => $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openBraceIndex),
- ];
- }
- }
- private function createDocBlock(Tokens $tokens, int $docBlockIndex, string $annotation): void
- {
- $lineEnd = $this->whitespacesConfig->getLineEnding();
- $originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
- $toInsert = [
- new Token([T_DOC_COMMENT, "/**{$lineEnd}{$originalIndent} * @{$annotation}{$lineEnd}{$originalIndent} */"]),
- new Token([T_WHITESPACE, $lineEnd.$originalIndent]),
- ];
- $index = $tokens->getNextMeaningfulToken($docBlockIndex);
- $tokens->insertAt($index, $toInsert);
- if (!$tokens[$index - 1]->isGivenKind(T_WHITESPACE)) {
- $extraNewLines = $this->whitespacesConfig->getLineEnding();
- if (!$tokens[$index - 1]->isGivenKind(T_OPEN_TAG)) {
- $extraNewLines .= $this->whitespacesConfig->getLineEnding();
- }
- $tokens->insertAt($index, [
- new Token([T_WHITESPACE, $extraNewLines.WhitespacesAnalyzer::detectIndent($tokens, $index)]),
- ]);
- }
- }
- /**
- * @param list<string> $preventingAnnotations
- */
- private function updateDocBlockIfNeeded(
- Tokens $tokens,
- int $docBlockIndex,
- string $annotation,
- array $preventingAnnotations
- ): void {
- $doc = new DocBlock($tokens[$docBlockIndex]->getContent());
- foreach ($preventingAnnotations as $preventingAnnotation) {
- if ([] !== $doc->getAnnotationsOfType($preventingAnnotation)) {
- return;
- }
- }
- $doc = $this->makeDocBlockMultiLineIfNeeded($doc, $tokens, $docBlockIndex, $annotation);
- $lines = $this->addInternalAnnotation($doc, $tokens, $docBlockIndex, $annotation);
- $lines = implode('', $lines);
- $tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $lines]);
- }
- /**
- * @param list<class-string> $preventingAttributes
- */
- private static function isPreventedByAttribute(Tokens $tokens, int $index, array $preventingAttributes): bool
- {
- if ([] === $preventingAttributes) {
- return false;
- }
- $modifiers = [T_FINAL];
- if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.2+ is required
- $modifiers[] = T_READONLY;
- }
- do {
- $index = $tokens->getPrevMeaningfulToken($index);
- } while ($tokens[$index]->isGivenKind($modifiers));
- if (!$tokens[$index]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
- return false;
- }
- $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $index);
- foreach (AttributeAnalyzer::collect($tokens, $index) as $attributeAnalysis) {
- foreach ($attributeAnalysis->getAttributes() as $attribute) {
- if (\in_array(ltrim(self::getFullyQualifiedName($tokens, $attribute['name']), '\\'), $preventingAttributes, true)) {
- return true;
- }
- }
- }
- return false;
- }
- private static function getFullyQualifiedName(Tokens $tokens, string $name): string
- {
- $name = strtolower($name);
- $names = [];
- foreach ((new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens) as $namespaceUseAnalysis) {
- $names[strtolower($namespaceUseAnalysis->getShortName())] = strtolower($namespaceUseAnalysis->getFullName());
- }
- foreach ($names as $shortName => $fullName) {
- if ($name === $shortName) {
- return $fullName;
- }
- if (!str_starts_with($name, $shortName.'\\')) {
- continue;
- }
- return $fullName.substr($name, \strlen($shortName));
- }
- return $name;
- }
- /**
- * @return list<Line>
- */
- private function addInternalAnnotation(DocBlock $docBlock, Tokens $tokens, int $docBlockIndex, string $annotation): array
- {
- $lines = $docBlock->getLines();
- $originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $docBlockIndex);
- $lineEnd = $this->whitespacesConfig->getLineEnding();
- array_splice($lines, -1, 0, $originalIndent.' * @'.$annotation.$lineEnd);
- return $lines;
- }
- private function makeDocBlockMultiLineIfNeeded(DocBlock $doc, Tokens $tokens, int $docBlockIndex, string $annotation): DocBlock
- {
- $lines = $doc->getLines();
- if (1 === \count($lines) && [] === $doc->getAnnotationsOfType($annotation)) {
- $indent = WhitespacesAnalyzer::detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
- $doc->makeMultiLine($indent, $this->whitespacesConfig->getLineEnding());
- return $doc;
- }
- return $doc;
- }
- }
|