Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
tipuloidea
/
back
/
vendor
/
friendsofphp
/
php-cs-fixer
/
src
/
Fixer
/
PhpUnit
:
PhpUnitTestAnnotationFixer.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?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\PhpUnit; use PhpCsFixer\DocBlock\DocBlock; use PhpCsFixer\DocBlock\Line; use PhpCsFixer\Fixer\AbstractPhpUnitFixer; use PhpCsFixer\Fixer\ConfigurableFixerInterface; use PhpCsFixer\Fixer\ConfigurableFixerTrait; use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface; 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\Preg; use PhpCsFixer\Tokenizer\Analyzer\WhitespacesAnalyzer; use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; use PhpCsFixer\Tokenizer\TokensAnalyzer; /** * @phpstan-type _AutogeneratedInputConfiguration array{ * style?: 'annotation'|'prefix', * } * @phpstan-type _AutogeneratedComputedConfiguration array{ * style: 'annotation'|'prefix', * } * * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> * * @author Gert de Pagter * * @no-named-arguments Parameter names are not covered by the backward compatibility promise. */ final class PhpUnitTestAnnotationFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface { /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */ use ConfigurableFixerTrait; public function isRisky(): bool { return true; } public function getDefinition(): FixerDefinitionInterface { return new FixerDefinition( 'Adds or removes @test annotations from tests, following configuration.', [ new CodeSample( <<<'PHP' <?php class Test extends \PhpUnit\FrameWork\TestCase { /** * @test */ public function itDoesSomething() {} } PHP.$this->whitespacesConfig->getLineEnding(), ), new CodeSample( <<<'PHP' <?php class Test extends \PhpUnit\FrameWork\TestCase { public function testItDoesSomething() {}} PHP.$this->whitespacesConfig->getLineEnding(), ['style' => 'annotation'], ), ], null, 'This fixer may change the name of your tests, and could cause incompatibility with' .' abstract classes or interfaces.', ); } /** * {@inheritdoc} * * Must run before NoEmptyPhpdocFixer, PhpUnitMethodCasingFixer, PhpdocTrimFixer. */ public function getPriority(): int { return 10; } protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void { if ('annotation' === $this->configuration['style']) { $this->applyTestAnnotation($tokens, $startIndex, $endIndex); } else { $this->applyTestPrefix($tokens, $startIndex, $endIndex); } } protected function createConfigurationDefinition(): FixerConfigurationResolverInterface { return new FixerConfigurationResolver([ (new FixerOptionBuilder('style', 'Whether to use the @test annotation or not.')) ->setAllowedValues(['prefix', 'annotation']) ->setDefault('prefix') ->getOption(), ]); } private function applyTestAnnotation(Tokens $tokens, int $startIndex, int $endIndex): void { for ($i = $endIndex - 1; $i > $startIndex; --$i) { if (!$this->isTestMethod($tokens, $i)) { continue; } $functionNameIndex = $tokens->getNextMeaningfulToken($i); $functionName = $tokens[$functionNameIndex]->getContent(); if ($this->hasTestPrefix($functionName) && !$this->hasProperTestAnnotation($tokens, $i)) { $newFunctionName = $this->removeTestPrefix($functionName); $tokens[$functionNameIndex] = new Token([\T_STRING, $newFunctionName]); } $docBlockIndex = $this->getDocBlockIndex($tokens, $i); if ($tokens[$docBlockIndex]->isGivenKind(\T_DOC_COMMENT)) { $lines = $this->updateDocBlock($tokens, $docBlockIndex); $lines = $this->addTestAnnotation($lines, $tokens, $docBlockIndex); $lines = implode('', $lines); $tokens[$docBlockIndex] = new Token([\T_DOC_COMMENT, $lines]); } else { // Create a new docblock if it didn't have one before; $this->createDocBlock($tokens, $docBlockIndex, 'test'); } } } private function applyTestPrefix(Tokens $tokens, int $startIndex, int $endIndex): void { for ($i = $endIndex - 1; $i > $startIndex; --$i) { // We explicitly check again if the function has a doc block to save some time. if (!$this->isTestMethod($tokens, $i)) { continue; } $docBlockIndex = $this->getDocBlockIndex($tokens, $i); if (!$tokens[$docBlockIndex]->isGivenKind(\T_DOC_COMMENT)) { continue; } $lines = $this->updateDocBlock($tokens, $docBlockIndex); $lines = implode('', $lines); $tokens[$docBlockIndex] = new Token([\T_DOC_COMMENT, $lines]); $functionNameIndex = $tokens->getNextMeaningfulToken($i); $functionName = $tokens[$functionNameIndex]->getContent(); if ($this->hasTestPrefix($functionName)) { continue; } $newFunctionName = $this->addTestPrefix($functionName); $tokens[$functionNameIndex] = new Token([\T_STRING, $newFunctionName]); } } private function isTestMethod(Tokens $tokens, int $index): bool { // Check if we are dealing with a (non-abstract, non-lambda) function if (!$this->isMethod($tokens, $index)) { return false; } // if the function name starts with test it is a test $functionNameIndex = $tokens->getNextMeaningfulToken($index); $functionName = $tokens[$functionNameIndex]->getContent(); if ($this->hasTestPrefix($functionName)) { return true; } $docBlockIndex = $this->getDocBlockIndex($tokens, $index); // If the function doesn't have test in its name, and no doc block, it is not a test return $tokens[$docBlockIndex]->isGivenKind(\T_DOC_COMMENT) && str_contains($tokens[$docBlockIndex]->getContent(), '@test'); } private function isMethod(Tokens $tokens, int $index): bool { $tokensAnalyzer = new TokensAnalyzer($tokens); return $tokens[$index]->isGivenKind(\T_FUNCTION) && !$tokensAnalyzer->isLambda($index); } private function hasTestPrefix(string $functionName): bool { return str_starts_with($functionName, 'test'); } private function hasProperTestAnnotation(Tokens $tokens, int $index): bool { $docBlockIndex = $this->getDocBlockIndex($tokens, $index); $doc = $tokens[$docBlockIndex]->getContent(); return Preg::match('/\*\s+@test\b/', $doc); } private function removeTestPrefix(string $functionName): string { $remainder = Preg::replace('/^test(?=[A-Z_])_?/', '', $functionName); if ('' === $remainder) { return $functionName; } return lcfirst($remainder); } private function addTestPrefix(string $functionName): string { return 'test'.ucfirst($functionName); } /** * @return list<Line> */ private function updateDocBlock(Tokens $tokens, int $docBlockIndex): array { $doc = new DocBlock($tokens[$docBlockIndex]->getContent()); $lines = $doc->getLines(); return $this->updateLines($lines, $tokens, $docBlockIndex); } /** * @param list<Line> $lines * * @return list<Line> */ private function updateLines(array $lines, Tokens $tokens, int $docBlockIndex): array { $needsAnnotation = 'annotation' === $this->configuration['style']; $doc = new DocBlock($tokens[$docBlockIndex]->getContent()); foreach ($lines as $i => $line) { // If we need to add test annotation and it is a single line comment we need to deal with that separately if ($needsAnnotation && ($line->isTheStart() && $line->isTheEnd())) { if (!$this->doesDocBlockContainTest($doc)) { $lines = $this->splitUpDocBlock($lines, $tokens, $docBlockIndex); return $this->updateLines($lines, $tokens, $docBlockIndex); } // One we split it up, we run the function again, so we deal with other things in a proper way } if (!$needsAnnotation && str_contains($line->getContent(), ' @test') && !str_contains($line->getContent(), '@testWith') && !str_contains($line->getContent(), '@testdox') ) { // We remove @test from the doc block $lines[$i] = $line = new Line(str_replace(' @test', '', $line->getContent())); } // ignore the line if it isn't @depends if (!str_contains($line->getContent(), '@depends')) { continue; } $lines[$i] = $this->updateDependsAnnotation($line); } return $lines; } /** * Take a one line doc block, and turn it into a multi line doc block. * * @param non-empty-list<Line> $lines * * @return non-empty-list<Line> */ private function splitUpDocBlock(array $lines, Tokens $tokens, int $docBlockIndex): array { $lineContent = $this->getSingleLineDocBlockEntry($lines); $lineEnd = $this->whitespacesConfig->getLineEnding(); $originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex)); return [ new Line('/**'.$lineEnd), new Line($originalIndent.' * '.$lineContent.$lineEnd), new Line($originalIndent.' */'), ]; } /** * @TODO check whether it's doable to use \PhpCsFixer\DocBlock\DocBlock::getSingleLineDocBlockEntry instead * * @param non-empty-list<Line> $lines */ private function getSingleLineDocBlockEntry(array $lines): string { $line = $lines[0]; $line = str_replace('*/', '', $line->getContent()); $line = trim($line); $line = str_split($line); $i = \count($line); do { --$i; } while ('*' !== $line[$i] && '*' !== $line[$i - 1] && '/' !== $line[$i - 2]); if (' ' === $line[$i]) { ++$i; } $line = \array_slice($line, $i); return implode('', $line); } /** * Updates the depends tag on the current doc block. */ private function updateDependsAnnotation(Line $line): Line { if ('annotation' === $this->configuration['style']) { return $this->removeTestPrefixFromDependsAnnotation($line); } return $this->addTestPrefixToDependsAnnotation($line); } private function removeTestPrefixFromDependsAnnotation(Line $line): Line { $line = str_split($line->getContent()); $dependsIndex = $this->findWhereDependsFunctionNameStarts($line); $dependsFunctionName = implode('', \array_slice($line, $dependsIndex)); if ($this->hasTestPrefix($dependsFunctionName)) { $dependsFunctionName = $this->removeTestPrefix($dependsFunctionName); } array_splice($line, $dependsIndex); return new Line(implode('', $line).$dependsFunctionName); } private function addTestPrefixToDependsAnnotation(Line $line): Line { $line = str_split($line->getContent()); $dependsIndex = $this->findWhereDependsFunctionNameStarts($line); $dependsFunctionName = implode('', \array_slice($line, $dependsIndex)); if (!$this->hasTestPrefix($dependsFunctionName)) { $dependsFunctionName = $this->addTestPrefix($dependsFunctionName); } array_splice($line, $dependsIndex); return new Line(implode('', $line).$dependsFunctionName); } /** * Helps to find where the function name in the doc block starts. * * @param list<string> $line */ private function findWhereDependsFunctionNameStarts(array $line): int { $index = (int) stripos(implode('', $line), '@depends') + 8; while (' ' === $line[$index]) { ++$index; } return $index; } /** * @param list<Line> $lines * * @return list<Line> */ private function addTestAnnotation(array $lines, Tokens $tokens, int $docBlockIndex): array { $doc = new DocBlock($tokens[$docBlockIndex]->getContent()); if (!$this->doesDocBlockContainTest($doc)) { $originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $docBlockIndex); $lineEnd = $this->whitespacesConfig->getLineEnding(); array_splice($lines, -1, 0, [new Line($originalIndent.' *'.$lineEnd.$originalIndent.' * @test'.$lineEnd)]); \assert(array_is_list($lines)); // we know it's list, but we need to tell PHPStan } return $lines; } private function doesDocBlockContainTest(DocBlock $doc): bool { return 0 !== \count($doc->getAnnotationsOfType('test')); } }