* Dariusz RumiƄski * * 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\Annotation; use PhpCsFixer\DocBlock\DocBlock; use PhpCsFixer\DocBlock\Line; use PhpCsFixer\Fixer\AbstractPhpUnitFixer; use PhpCsFixer\Fixer\ConfigurableFixerInterface; 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\Tokenizer\Analyzer\WhitespacesAnalyzer; use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; /** * @author Jefersson Nathan */ final class PhpUnitSizeClassFixer extends AbstractPhpUnitFixer implements WhitespacesAwareFixerInterface, ConfigurableFixerInterface { /** * {@inheritdoc} */ public function getDefinition(): FixerDefinitionInterface { return new FixerDefinition( 'All PHPUnit test cases should have `@small`, `@medium` or `@large` annotation to enable run time limits.', [ new CodeSample(" 'medium']), ], 'The special groups [small, medium, large] provides a way to identify tests that are taking long to be executed.' ); } /** * {@inheritdoc} */ protected function createConfigurationDefinition(): FixerConfigurationResolverInterface { return new FixerConfigurationResolver([ (new FixerOptionBuilder('group', 'Define a specific group to be used in case no group is already in use')) ->setAllowedValues(['small', 'medium', 'large']) ->setDefault('small') ->getOption(), ]); } /** * {@inheritdoc} */ protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void { $classIndex = $tokens->getPrevTokenOfKind($startIndex, [[T_CLASS]]); if ($this->isAbstractClass($tokens, $classIndex)) { return; } $docBlockIndex = $this->getDocBlockIndex($tokens, $classIndex); if ($this->isPHPDoc($tokens, $docBlockIndex)) { $this->updateDocBlockIfNeeded($tokens, $docBlockIndex); } else { $this->createDocBlock($tokens, $docBlockIndex); } } private function isAbstractClass(Tokens $tokens, int $i): bool { $typeIndex = $tokens->getPrevMeaningfulToken($i); return $tokens[$typeIndex]->isGivenKind(T_ABSTRACT); } private function createDocBlock(Tokens $tokens, int $docBlockIndex): void { $lineEnd = $this->whitespacesConfig->getLineEnding(); $originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex)); $group = $this->configuration['group']; $toInsert = [ new Token([T_DOC_COMMENT, '/**'.$lineEnd."{$originalIndent} * @".$group.$lineEnd."{$originalIndent} */"]), new Token([T_WHITESPACE, $lineEnd.$originalIndent]), ]; $index = $tokens->getNextMeaningfulToken($docBlockIndex); $tokens->insertAt($index, $toInsert); } private function updateDocBlockIfNeeded(Tokens $tokens, int $docBlockIndex): void { $doc = new DocBlock($tokens[$docBlockIndex]->getContent()); if (0 !== \count($this->filterDocBlock($doc))) { return; } $doc = $this->makeDocBlockMultiLineIfNeeded($doc, $tokens, $docBlockIndex); $lines = $this->addSizeAnnotation($doc, $tokens, $docBlockIndex); $lines = implode('', $lines); $tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $lines]); } /** * @return Line[] */ private function addSizeAnnotation(DocBlock $docBlock, Tokens $tokens, int $docBlockIndex): array { $lines = $docBlock->getLines(); $originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $docBlockIndex); $lineEnd = $this->whitespacesConfig->getLineEnding(); $group = $this->configuration['group']; array_splice($lines, -1, 0, $originalIndent.' *'.$lineEnd.$originalIndent.' * @'.$group.$lineEnd); return $lines; } private function makeDocBlockMultiLineIfNeeded(DocBlock $doc, Tokens $tokens, int $docBlockIndex): DocBlock { $lines = $doc->getLines(); if (1 === \count($lines) && 0 === \count($this->filterDocBlock($doc))) { $lines = $this->splitUpDocBlock($lines, $tokens, $docBlockIndex); return new DocBlock(implode('', $lines)); } return $doc; } /** * Take a one line doc block, and turn it into a multi line doc block. * * @param Line[] $lines * * @return Line[] */ private function splitUpDocBlock(array $lines, Tokens $tokens, int $docBlockIndex): array { $lineContent = $this->getSingleLineDocBlockEntry($lines[0]); $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 */ private function getSingleLineDocBlockEntry(Line $line): string { $line = $line->getContent(); $line = str_replace('*/', '', $line); $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); } /** * @return Annotation[][] */ private function filterDocBlock(DocBlock $doc): array { return array_filter([ $doc->getAnnotationsOfType('small'), $doc->getAnnotationsOfType('large'), $doc->getAnnotationsOfType('medium'), ]); } }