Missing dependancies

This commit is contained in:
tokslaw7
2023-06-21 12:19:22 +00:00
parent fbf3f180c2
commit 421f25c80d
2356 changed files with 342670 additions and 4743 deletions
@@ -0,0 +1,210 @@
<?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\Fixer\AbstractPhpUnitFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
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\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class PhpUnitConstructFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface
{
/**
* @var array<string,string>
*/
private static array $assertionFixers = [
'assertSame' => 'fixAssertPositive',
'assertEquals' => 'fixAssertPositive',
'assertNotEquals' => 'fixAssertNegative',
'assertNotSame' => 'fixAssertNegative',
];
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'PHPUnit assertion method calls like `->assertSame(true, $foo)` should be written with dedicated method like `->assertTrue($foo)`.',
[
new CodeSample(
'<?php
final class FooTest extends \PHPUnit_Framework_TestCase {
public function testSomething() {
$this->assertEquals(false, $b);
$this->assertSame(true, $a);
$this->assertNotEquals(null, $c);
$this->assertNotSame(null, $d);
}
}
'
),
new CodeSample(
'<?php
final class FooTest extends \PHPUnit_Framework_TestCase {
public function testSomething() {
$this->assertEquals(false, $b);
$this->assertSame(true, $a);
$this->assertNotEquals(null, $c);
$this->assertNotSame(null, $d);
}
}
',
['assertions' => ['assertSame', 'assertNotSame']]
),
],
null,
'Fixer could be risky if one is overriding PHPUnit\'s native methods.'
);
}
/**
* {@inheritdoc}
*
* Must run before PhpUnitDedicateAssertFixer.
*/
public function getPriority(): int
{
return -8;
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
// no assertions to be fixed - fast return
if (empty($this->configuration['assertions'])) {
return;
}
foreach ($this->configuration['assertions'] as $assertionMethod) {
$assertionFixer = self::$assertionFixers[$assertionMethod];
for ($index = $startIndex; $index < $endIndex; ++$index) {
$index = $this->{$assertionFixer}($tokens, $index, $assertionMethod);
if (null === $index) {
break;
}
}
}
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('assertions', 'List of assertion methods to fix.'))
->setAllowedTypes(['array'])
->setAllowedValues([new AllowedValueSubset(array_keys(self::$assertionFixers))])
->setDefault([
'assertEquals',
'assertSame',
'assertNotEquals',
'assertNotSame',
])
->getOption(),
]);
}
private function fixAssertNegative(Tokens $tokens, int $index, string $method): ?int
{
static $map = [
'false' => 'assertNotFalse',
'null' => 'assertNotNull',
'true' => 'assertNotTrue',
];
return $this->fixAssert($map, $tokens, $index, $method);
}
private function fixAssertPositive(Tokens $tokens, int $index, string $method): ?int
{
static $map = [
'false' => 'assertFalse',
'null' => 'assertNull',
'true' => 'assertTrue',
];
return $this->fixAssert($map, $tokens, $index, $method);
}
/**
* @param array<string, string> $map
*/
private function fixAssert(array $map, Tokens $tokens, int $index, string $method): ?int
{
$functionsAnalyzer = new FunctionsAnalyzer();
$sequence = $tokens->findSequence(
[
[T_STRING, $method],
'(',
],
$index
);
if (null === $sequence) {
return null;
}
$sequenceIndices = array_keys($sequence);
if (!$functionsAnalyzer->isTheSameClassCall($tokens, $sequenceIndices[0])) {
return null;
}
$sequenceIndices[2] = $tokens->getNextMeaningfulToken($sequenceIndices[1]);
$firstParameterToken = $tokens[$sequenceIndices[2]];
if (!$firstParameterToken->isNativeConstant()) {
return $sequenceIndices[2];
}
$sequenceIndices[3] = $tokens->getNextMeaningfulToken($sequenceIndices[2]);
// return if first method argument is an expression, not value
if (!$tokens[$sequenceIndices[3]]->equals(',')) {
return $sequenceIndices[3];
}
$tokens[$sequenceIndices[0]] = new Token([T_STRING, $map[strtolower($firstParameterToken->getContent())]]);
$tokens->clearRange($sequenceIndices[2], $tokens->getNextNonWhitespace($sequenceIndices[3]) - 1);
return $sequenceIndices[3];
}
}
@@ -0,0 +1,643 @@
<?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\Fixer\AbstractPhpUnitFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
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\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class PhpUnitDedicateAssertFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface
{
/**
* @var array<string, array<string, bool|int|string>|true>
*/
private static array $fixMap = [
'array_key_exists' => [
'positive' => 'assertArrayHasKey',
'negative' => 'assertArrayNotHasKey',
'argument_count' => 2,
],
'empty' => [
'positive' => 'assertEmpty',
'negative' => 'assertNotEmpty',
],
'file_exists' => [
'positive' => 'assertFileExists',
'negative' => 'assertFileNotExists',
],
'is_array' => true,
'is_bool' => true,
'is_callable' => true,
'is_dir' => [
'positive' => 'assertDirectoryExists',
'negative' => 'assertDirectoryNotExists',
],
'is_double' => true,
'is_float' => true,
'is_infinite' => [
'positive' => 'assertInfinite',
'negative' => 'assertFinite',
],
'is_int' => true,
'is_integer' => true,
'is_long' => true,
'is_nan' => [
'positive' => 'assertNan',
'negative' => false,
],
'is_null' => [
'positive' => 'assertNull',
'negative' => 'assertNotNull',
],
'is_numeric' => true,
'is_object' => true,
'is_readable' => [
'positive' => 'assertIsReadable',
'negative' => 'assertNotIsReadable',
],
'is_real' => true,
'is_resource' => true,
'is_scalar' => true,
'is_string' => true,
'is_writable' => [
'positive' => 'assertIsWritable',
'negative' => 'assertNotIsWritable',
],
'str_contains' => [ // since 7.5
'positive' => 'assertStringContainsString',
'negative' => 'assertStringNotContainsString',
'argument_count' => 2,
'swap_arguments' => true,
],
'str_ends_with' => [ // since 3.4
'positive' => 'assertStringEndsWith',
'negative' => 'assertStringEndsNotWith',
'argument_count' => 2,
'swap_arguments' => true,
],
'str_starts_with' => [ // since 3.4
'positive' => 'assertStringStartsWith',
'negative' => 'assertStringStartsNotWith',
'argument_count' => 2,
'swap_arguments' => true,
],
];
/**
* @var string[]
*/
private array $functions = [];
/**
* {@inheritdoc}
*/
public function configure(array $configuration): void
{
parent::configure($configuration);
// assertions added in 3.0: assertArrayNotHasKey assertArrayHasKey assertFileNotExists assertFileExists assertNotNull, assertNull
$this->functions = [
'array_key_exists',
'file_exists',
'is_null',
'str_ends_with',
'str_starts_with',
];
if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_3_5)) {
// assertions added in 3.5: assertInternalType assertNotEmpty assertEmpty
$this->functions = array_merge($this->functions, [
'empty',
'is_array',
'is_bool',
'is_boolean',
'is_callable',
'is_double',
'is_float',
'is_int',
'is_integer',
'is_long',
'is_numeric',
'is_object',
'is_real',
'is_scalar',
'is_string',
]);
}
if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_5_0)) {
// assertions added in 5.0: assertFinite assertInfinite assertNan
$this->functions = array_merge($this->functions, [
'is_infinite',
'is_nan',
]);
}
if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_5_6)) {
// assertions added in 5.6: assertDirectoryExists assertDirectoryNotExists assertIsReadable assertNotIsReadable assertIsWritable assertNotIsWritable
$this->functions = array_merge($this->functions, [
'is_dir',
'is_readable',
'is_writable',
]);
}
if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_7_5)) {
$this->functions = array_merge($this->functions, [
'str_contains',
]);
}
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'PHPUnit assertions like `assertInternalType`, `assertFileExists`, should be used over `assertTrue`.',
[
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testSomeTest()
{
$this->assertTrue(is_float( $a), "my message");
$this->assertTrue(is_nan($a));
}
}
'
),
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testSomeTest()
{
$this->assertTrue(is_dir($a));
$this->assertTrue(is_writable($a));
$this->assertTrue(is_readable($a));
}
}
',
['target' => PhpUnitTargetVersion::VERSION_5_6]
),
],
null,
'Fixer could be risky if one is overriding PHPUnit\'s native methods.'
);
}
/**
* {@inheritdoc}
*
* Must run before NoUnusedImportsFixer, PhpUnitDedicateAssertInternalTypeFixer.
* Must run after ModernizeStrposFixer, NoAliasFunctionsFixer, PhpUnitConstructFixer.
*/
public function getPriority(): int
{
return -9;
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
$argumentsAnalyzer = new ArgumentsAnalyzer();
foreach ($this->getPreviousAssertCall($tokens, $startIndex, $endIndex) as $assertCall) {
// test and fix for assertTrue/False to dedicated asserts
if ('asserttrue' === $assertCall['loweredName'] || 'assertfalse' === $assertCall['loweredName']) {
$this->fixAssertTrueFalse($tokens, $argumentsAnalyzer, $assertCall);
continue;
}
if (
'assertsame' === $assertCall['loweredName']
|| 'assertnotsame' === $assertCall['loweredName']
|| 'assertequals' === $assertCall['loweredName']
|| 'assertnotequals' === $assertCall['loweredName']
) {
$this->fixAssertSameEquals($tokens, $assertCall);
continue;
}
}
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('target', 'Target version of PHPUnit.'))
->setAllowedTypes(['string'])
->setAllowedValues([
PhpUnitTargetVersion::VERSION_3_0,
PhpUnitTargetVersion::VERSION_3_5,
PhpUnitTargetVersion::VERSION_5_0,
PhpUnitTargetVersion::VERSION_5_6,
PhpUnitTargetVersion::VERSION_NEWEST,
])
->setDefault(PhpUnitTargetVersion::VERSION_NEWEST)
->getOption(),
]);
}
/**
* @param array{
* index: int,
* loweredName: string,
* openBraceIndex: int,
* closeBraceIndex: int,
* } $assertCall
*/
private function fixAssertTrueFalse(Tokens $tokens, ArgumentsAnalyzer $argumentsAnalyzer, array $assertCall): void
{
$testDefaultNamespaceTokenIndex = null;
$testIndex = $tokens->getNextMeaningfulToken($assertCall['openBraceIndex']);
if (!$tokens[$testIndex]->isGivenKind([T_EMPTY, T_STRING])) {
if ($this->fixAssertTrueFalseInstanceof($tokens, $assertCall, $testIndex)) {
return;
}
if (!$tokens[$testIndex]->isGivenKind(T_NS_SEPARATOR)) {
return;
}
$testDefaultNamespaceTokenIndex = $testIndex;
$testIndex = $tokens->getNextMeaningfulToken($testIndex);
}
$testOpenIndex = $tokens->getNextMeaningfulToken($testIndex);
if (!$tokens[$testOpenIndex]->equals('(')) {
return;
}
$testCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $testOpenIndex);
$assertCallCloseIndex = $tokens->getNextMeaningfulToken($testCloseIndex);
if (!$tokens[$assertCallCloseIndex]->equalsAny([')', ','])) {
return;
}
$content = strtolower($tokens[$testIndex]->getContent());
if (!\in_array($content, $this->functions, true)) {
return;
}
$arguments = $argumentsAnalyzer->getArguments($tokens, $testOpenIndex, $testCloseIndex);
$isPositive = 'asserttrue' === $assertCall['loweredName'];
if (\is_array(self::$fixMap[$content])) {
$expectedCount = self::$fixMap[$content]['argument_count'] ?? 1;
if ($expectedCount !== \count($arguments)) {
return;
}
$isPositive = $isPositive ? 'positive' : 'negative';
if (false === self::$fixMap[$content][$isPositive]) {
return;
}
$tokens[$assertCall['index']] = new Token([T_STRING, self::$fixMap[$content][$isPositive]]);
$this->removeFunctionCall($tokens, $testDefaultNamespaceTokenIndex, $testIndex, $testOpenIndex, $testCloseIndex);
if (self::$fixMap[$content]['swap_arguments'] ?? false) {
if (2 !== $expectedCount) {
throw new \RuntimeException('Can only swap two arguments, please update map or logic.');
}
$this->swapArguments($tokens, $arguments);
}
return;
}
if (1 !== \count($arguments)) {
return;
}
$type = substr($content, 3);
$tokens[$assertCall['index']] = new Token([T_STRING, $isPositive ? 'assertInternalType' : 'assertNotInternalType']);
$tokens[$testIndex] = new Token([T_CONSTANT_ENCAPSED_STRING, "'".$type."'"]);
$tokens[$testOpenIndex] = new Token(',');
$tokens->clearTokenAndMergeSurroundingWhitespace($testCloseIndex);
$commaIndex = $tokens->getPrevMeaningfulToken($testCloseIndex);
if ($tokens[$commaIndex]->equals(',')) {
$tokens->removeTrailingWhitespace($commaIndex);
$tokens->clearAt($commaIndex);
}
if (!$tokens[$testOpenIndex + 1]->isWhitespace()) {
$tokens->insertAt($testOpenIndex + 1, new Token([T_WHITESPACE, ' ']));
}
if (null !== $testDefaultNamespaceTokenIndex) {
$tokens->clearTokenAndMergeSurroundingWhitespace($testDefaultNamespaceTokenIndex);
}
}
/**
* @param array{
* index: int,
* loweredName: string,
* openBraceIndex: int,
* closeBraceIndex: int,
* } $assertCall
*/
private function fixAssertTrueFalseInstanceof(Tokens $tokens, array $assertCall, int $testIndex): bool
{
if ($tokens[$testIndex]->equals('!')) {
$variableIndex = $tokens->getNextMeaningfulToken($testIndex);
$positive = false;
} else {
$variableIndex = $testIndex;
$positive = true;
}
if (!$tokens[$variableIndex]->isGivenKind(T_VARIABLE)) {
return false;
}
$instanceOfIndex = $tokens->getNextMeaningfulToken($variableIndex);
if (!$tokens[$instanceOfIndex]->isGivenKind(T_INSTANCEOF)) {
return false;
}
$classEndIndex = $instanceOfIndex;
$classPartTokens = [];
do {
$classEndIndex = $tokens->getNextMeaningfulToken($classEndIndex);
$classPartTokens[] = $tokens[$classEndIndex];
} while ($tokens[$classEndIndex]->isGivenKind([T_STRING, T_NS_SEPARATOR, T_VARIABLE]));
if ($tokens[$classEndIndex]->equalsAny([',', ')'])) { // do the fixing
array_pop($classPartTokens);
$isInstanceOfVar = reset($classPartTokens)->isGivenKind(T_VARIABLE);
$insertIndex = $testIndex - 1;
$newTokens = [];
foreach ($classPartTokens as $token) {
$newTokens[++$insertIndex] = clone $token;
}
if (!$isInstanceOfVar) {
$newTokens[++$insertIndex] = new Token([T_DOUBLE_COLON, '::']);
$newTokens[++$insertIndex] = new Token([CT::T_CLASS_CONSTANT, 'class']);
}
$newTokens[++$insertIndex] = new Token(',');
$newTokens[++$insertIndex] = new Token([T_WHITESPACE, ' ']);
$newTokens[++$insertIndex] = clone $tokens[$variableIndex];
for ($i = $classEndIndex - 1; $i >= $testIndex; --$i) {
if (!$tokens[$i]->isComment()) {
$tokens->clearTokenAndMergeSurroundingWhitespace($i);
}
}
$tokens->insertSlices($newTokens);
$tokens[$assertCall['index']] = new Token([T_STRING, $positive ? 'assertInstanceOf' : 'assertNotInstanceOf']);
}
return true;
}
/**
* @param array{
* index: int,
* loweredName: string,
* openBraceIndex: int,
* closeBraceIndex: int,
* } $assertCall
*/
private function fixAssertSameEquals(Tokens $tokens, array $assertCall): void
{
// @ $this->/self::assertEquals/Same([$nextIndex])
$expectedIndex = $tokens->getNextMeaningfulToken($assertCall['openBraceIndex']);
// do not fix
// let $a = [1,2]; $b = "2";
// "$this->assertEquals("2", count($a)); $this->assertEquals($b, count($a)); $this->assertEquals(2.1, count($a));"
if ($tokens[$expectedIndex]->isGivenKind([T_VARIABLE])) {
if (!$tokens[$tokens->getNextMeaningfulToken($expectedIndex)]->equals(',')) {
return;
}
} elseif (!$tokens[$expectedIndex]->isGivenKind([T_LNUMBER, T_VARIABLE])) {
return;
}
// @ $this->/self::assertEquals/Same([$nextIndex,$commaIndex])
$commaIndex = $tokens->getNextMeaningfulToken($expectedIndex);
if (!$tokens[$commaIndex]->equals(',')) {
return;
}
// @ $this->/self::assertEquals/Same([$nextIndex,$commaIndex,$countCallIndex])
$countCallIndex = $tokens->getNextMeaningfulToken($commaIndex);
if ($tokens[$countCallIndex]->isGivenKind(T_NS_SEPARATOR)) {
$defaultNamespaceTokenIndex = $countCallIndex;
$countCallIndex = $tokens->getNextMeaningfulToken($countCallIndex);
} else {
$defaultNamespaceTokenIndex = null;
}
if (!$tokens[$countCallIndex]->isGivenKind(T_STRING)) {
return;
}
$lowerContent = strtolower($tokens[$countCallIndex]->getContent());
if ('count' !== $lowerContent && 'sizeof' !== $lowerContent) {
return; // not a call to "count" or "sizeOf"
}
// @ $this->/self::assertEquals/Same([$nextIndex,$commaIndex,[$defaultNamespaceTokenIndex,]$countCallIndex,$countCallOpenBraceIndex])
$countCallOpenBraceIndex = $tokens->getNextMeaningfulToken($countCallIndex);
if (!$tokens[$countCallOpenBraceIndex]->equals('(')) {
return;
}
$countCallCloseBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $countCallOpenBraceIndex);
$afterCountCallCloseBraceIndex = $tokens->getNextMeaningfulToken($countCallCloseBraceIndex);
if (!$tokens[$afterCountCallCloseBraceIndex]->equalsAny([')', ','])) {
return;
}
$this->removeFunctionCall(
$tokens,
$defaultNamespaceTokenIndex,
$countCallIndex,
$countCallOpenBraceIndex,
$countCallCloseBraceIndex
);
$tokens[$assertCall['index']] = new Token([
T_STRING,
false === strpos($assertCall['loweredName'], 'not', 6) ? 'assertCount' : 'assertNotCount',
]);
}
/**
* @return iterable<array{
* index: int,
* loweredName: string,
* openBraceIndex: int,
* closeBraceIndex: int,
* }>
*/
private 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 removeFunctionCall(Tokens $tokens, ?int $callNSIndex, int $callIndex, int $openIndex, int $closeIndex): void
{
$tokens->clearTokenAndMergeSurroundingWhitespace($callIndex);
if (null !== $callNSIndex) {
$tokens->clearTokenAndMergeSurroundingWhitespace($callNSIndex);
}
$tokens->clearTokenAndMergeSurroundingWhitespace($openIndex);
$commaIndex = $tokens->getPrevMeaningfulToken($closeIndex);
if ($tokens[$commaIndex]->equals(',')) {
$tokens->removeTrailingWhitespace($commaIndex);
$tokens->clearAt($commaIndex);
}
$tokens->clearTokenAndMergeSurroundingWhitespace($closeIndex);
}
/**
* @param array<int, int> $argumentsIndices
*/
private function swapArguments(Tokens $tokens, array $argumentsIndices): void
{
[$firstArgumentIndex, $secondArgumentIndex] = array_keys($argumentsIndices);
$firstArgumentEndIndex = $argumentsIndices[$firstArgumentIndex];
$secondArgumentEndIndex = $argumentsIndices[$secondArgumentIndex];
$firstClone = $this->cloneAndClearTokens($tokens, $firstArgumentIndex, $firstArgumentEndIndex);
$secondClone = $this->cloneAndClearTokens($tokens, $secondArgumentIndex, $secondArgumentEndIndex);
if (!$firstClone[0]->isWhitespace()) {
array_unshift($firstClone, new Token([T_WHITESPACE, ' ']));
}
$tokens->insertAt($secondArgumentIndex, $firstClone);
if ($secondClone[0]->isWhitespace()) {
array_shift($secondClone);
}
$tokens->insertAt($firstArgumentIndex, $secondClone);
}
/**
* @return list<Token>
*/
private function cloneAndClearTokens(Tokens $tokens, int $start, int $end): array
{
$clone = [];
for ($i = $start; $i <= $end; ++$i) {
if ('' === $tokens[$i]->getContent()) {
continue;
}
$clone[] = clone $tokens[$i];
$tokens->clearAt($i);
}
return $clone;
}
}
@@ -0,0 +1,203 @@
<?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\Fixer\AbstractPhpUnitFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
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\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class PhpUnitDedicateAssertInternalTypeFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface
{
/**
* @var array<string, string>
*/
private array $typeToDedicatedAssertMap = [
'array' => 'assertIsArray',
'boolean' => 'assertIsBool',
'bool' => 'assertIsBool',
'double' => 'assertIsFloat',
'float' => 'assertIsFloat',
'integer' => 'assertIsInt',
'int' => 'assertIsInt',
'null' => 'assertNull',
'numeric' => 'assertIsNumeric',
'object' => 'assertIsObject',
'real' => 'assertIsFloat',
'resource' => 'assertIsResource',
'string' => 'assertIsString',
'scalar' => 'assertIsScalar',
'callable' => 'assertIsCallable',
'iterable' => 'assertIsIterable',
];
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'PHPUnit assertions like `assertIsArray` should be used over `assertInternalType`.',
[
new CodeSample(
'<?php
final class MyTest extends \PHPUnit\Framework\TestCase
{
public function testMe()
{
$this->assertInternalType("array", $var);
$this->assertInternalType("boolean", $var);
}
}
'
),
new CodeSample(
'<?php
final class MyTest extends \PHPUnit\Framework\TestCase
{
public function testMe()
{
$this->assertInternalType("array", $var);
$this->assertInternalType("boolean", $var);
}
}
',
['target' => PhpUnitTargetVersion::VERSION_7_5]
),
],
null,
'Risky when PHPUnit methods are overridden or when project has PHPUnit incompatibilities.'
);
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*
* Must run after NoBinaryStringFixer, NoUselessConcatOperatorFixer, PhpUnitDedicateAssertFixer.
*/
public function getPriority(): int
{
return -16;
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('target', 'Target version of PHPUnit.'))
->setAllowedTypes(['string'])
->setAllowedValues([PhpUnitTargetVersion::VERSION_7_5, PhpUnitTargetVersion::VERSION_NEWEST])
->setDefault(PhpUnitTargetVersion::VERSION_NEWEST)
->getOption(),
]);
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
$anonymousClassIndices = [];
$tokenAnalyzer = new TokensAnalyzer($tokens);
for ($index = $startIndex; $index < $endIndex; ++$index) {
if (!$tokens[$index]->isGivenKind(T_CLASS) || !$tokenAnalyzer->isAnonymousClass($index)) {
continue;
}
$openingBraceIndex = $tokens->getNextTokenOfKind($index, ['{']);
$closingBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $openingBraceIndex);
$anonymousClassIndices[$closingBraceIndex] = $openingBraceIndex;
}
for ($index = $endIndex - 1; $index > $startIndex; --$index) {
if (isset($anonymousClassIndices[$index])) {
$index = $anonymousClassIndices[$index];
continue;
}
if (!$tokens[$index]->isGivenKind(T_STRING)) {
continue;
}
$functionName = strtolower($tokens[$index]->getContent());
if ('assertinternaltype' !== $functionName && 'assertnotinternaltype' !== $functionName) {
continue;
}
$bracketTokenIndex = $tokens->getNextMeaningfulToken($index);
if (!$tokens[$bracketTokenIndex]->equals('(')) {
continue;
}
$expectedTypeTokenIndex = $tokens->getNextMeaningfulToken($bracketTokenIndex);
$expectedTypeToken = $tokens[$expectedTypeTokenIndex];
if (!$expectedTypeToken->isGivenKind(T_CONSTANT_ENCAPSED_STRING)) {
continue;
}
$expectedType = trim($expectedTypeToken->getContent(), '\'"');
if (!isset($this->typeToDedicatedAssertMap[$expectedType])) {
continue;
}
$commaTokenIndex = $tokens->getNextMeaningfulToken($expectedTypeTokenIndex);
if (!$tokens[$commaTokenIndex]->equals(',')) {
continue;
}
$newAssertion = $this->typeToDedicatedAssertMap[$expectedType];
if ('assertnotinternaltype' === $functionName) {
$newAssertion = str_replace('Is', 'IsNot', $newAssertion);
$newAssertion = str_replace('Null', 'NotNull', $newAssertion);
}
$nextMeaningfulTokenIndex = $tokens->getNextMeaningfulToken($commaTokenIndex);
$tokens->overrideRange($index, $nextMeaningfulTokenIndex - 1, [
new Token([T_STRING, $newAssertion]),
new Token('('),
]);
}
}
}
@@ -0,0 +1,289 @@
<?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\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\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\WhitespacesAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class PhpUnitExpectationFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
{
/**
* @var array<string, string>
*/
private array $methodMap = [];
/**
* {@inheritdoc}
*/
public function configure(array $configuration): void
{
parent::configure($configuration);
$this->methodMap = [
'setExpectedException' => 'expectExceptionMessage',
];
if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_5_6)) {
$this->methodMap['setExpectedExceptionRegExp'] = 'expectExceptionMessageRegExp';
}
if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_8_4)) {
$this->methodMap['setExpectedExceptionRegExp'] = 'expectExceptionMessageMatches';
$this->methodMap['expectExceptionMessageRegExp'] = 'expectExceptionMessageMatches';
}
}
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Usages of `->setExpectedException*` methods MUST be replaced by `->expectException*` methods.',
[
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testFoo()
{
$this->setExpectedException("RuntimeException", "Msg", 123);
foo();
}
public function testBar()
{
$this->setExpectedExceptionRegExp("RuntimeException", "/Msg.*/", 123);
bar();
}
}
'
),
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testFoo()
{
$this->setExpectedException("RuntimeException", null, 123);
foo();
}
public function testBar()
{
$this->setExpectedExceptionRegExp("RuntimeException", "/Msg.*/", 123);
bar();
}
}
',
['target' => PhpUnitTargetVersion::VERSION_8_4]
),
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testFoo()
{
$this->setExpectedException("RuntimeException", null, 123);
foo();
}
public function testBar()
{
$this->setExpectedExceptionRegExp("RuntimeException", "/Msg.*/", 123);
bar();
}
}
',
['target' => PhpUnitTargetVersion::VERSION_5_6]
),
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testFoo()
{
$this->setExpectedException("RuntimeException", "Msg", 123);
foo();
}
public function testBar()
{
$this->setExpectedExceptionRegExp("RuntimeException", "/Msg.*/", 123);
bar();
}
}
',
['target' => PhpUnitTargetVersion::VERSION_5_2]
),
],
null,
'Risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.'
);
}
/**
* {@inheritdoc}
*
* Must run after PhpUnitNoExpectationAnnotationFixer.
*/
public function getPriority(): int
{
return 0;
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('target', 'Target version of PHPUnit.'))
->setAllowedTypes(['string'])
->setAllowedValues([PhpUnitTargetVersion::VERSION_5_2, PhpUnitTargetVersion::VERSION_5_6, PhpUnitTargetVersion::VERSION_8_4, PhpUnitTargetVersion::VERSION_NEWEST])
->setDefault(PhpUnitTargetVersion::VERSION_NEWEST)
->getOption(),
]);
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
foreach (Token::getObjectOperatorKinds() as $objectOperator) {
$this->applyPhpUnitClassFixWithObjectOperator($tokens, $startIndex, $endIndex, $objectOperator);
}
}
private function applyPhpUnitClassFixWithObjectOperator(Tokens $tokens, int $startIndex, int $endIndex, int $objectOperator): void
{
$argumentsAnalyzer = new ArgumentsAnalyzer();
$oldMethodSequence = [
[T_VARIABLE, '$this'],
[$objectOperator],
[T_STRING],
];
for ($index = $startIndex; $startIndex < $endIndex; ++$index) {
$match = $tokens->findSequence($oldMethodSequence, $index);
if (null === $match) {
return;
}
[$thisIndex, , $index] = array_keys($match);
if (!isset($this->methodMap[$tokens[$index]->getContent()])) {
continue;
}
$openIndex = $tokens->getNextTokenOfKind($index, ['(']);
$closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
$commaIndex = $tokens->getPrevMeaningfulToken($closeIndex);
if ($tokens[$commaIndex]->equals(',')) {
$tokens->removeTrailingWhitespace($commaIndex);
$tokens->clearAt($commaIndex);
}
$arguments = $argumentsAnalyzer->getArguments($tokens, $openIndex, $closeIndex);
$argumentsCnt = \count($arguments);
$argumentsReplacements = ['expectException', $this->methodMap[$tokens[$index]->getContent()], 'expectExceptionCode'];
$indent = $this->whitespacesConfig->getLineEnding().WhitespacesAnalyzer::detectIndent($tokens, $thisIndex);
$isMultilineWhitespace = false;
for ($cnt = $argumentsCnt - 1; $cnt >= 1; --$cnt) {
$argStart = array_keys($arguments)[$cnt];
$argBefore = $tokens->getPrevMeaningfulToken($argStart);
if ('expectExceptionMessage' === $argumentsReplacements[$cnt]) {
$paramIndicatorIndex = $tokens->getNextMeaningfulToken($argBefore);
$afterParamIndicatorIndex = $tokens->getNextMeaningfulToken($paramIndicatorIndex);
if (
$tokens[$paramIndicatorIndex]->equals([T_STRING, 'null'], false)
&& $tokens[$afterParamIndicatorIndex]->equals(')')
) {
if ($tokens[$argBefore + 1]->isWhitespace()) {
$tokens->clearTokenAndMergeSurroundingWhitespace($argBefore + 1);
}
$tokens->clearTokenAndMergeSurroundingWhitespace($argBefore);
$tokens->clearTokenAndMergeSurroundingWhitespace($paramIndicatorIndex);
continue;
}
}
$isMultilineWhitespace = $isMultilineWhitespace || ($tokens[$argStart]->isWhitespace() && !$tokens[$argStart]->isWhitespace(" \t"));
$tokensOverrideArgStart = [
new Token([T_WHITESPACE, $indent]),
new Token([T_VARIABLE, '$this']),
new Token([T_OBJECT_OPERATOR, '->']),
new Token([T_STRING, $argumentsReplacements[$cnt]]),
new Token('('),
];
$tokensOverrideArgBefore = [
new Token(')'),
new Token(';'),
];
if ($isMultilineWhitespace) {
$tokensOverrideArgStart[] = new Token([T_WHITESPACE, $indent.$this->whitespacesConfig->getIndent()]);
array_unshift($tokensOverrideArgBefore, new Token([T_WHITESPACE, $indent]));
}
if ($tokens[$argStart]->isWhitespace()) {
$tokens->overrideRange($argStart, $argStart, $tokensOverrideArgStart);
} else {
$tokens->insertAt($argStart, $tokensOverrideArgStart);
}
$tokens->overrideRange($argBefore, $argBefore, $tokensOverrideArgBefore);
}
$methodName = 'expectException';
if ('expectExceptionMessageRegExp' === $tokens[$index]->getContent()) {
$methodName = $this->methodMap[$tokens[$index]->getContent()];
}
$tokens[$index] = new Token([T_STRING, $methodName]);
}
}
}
@@ -0,0 +1,92 @@
<?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\Fixer\AbstractPhpUnitFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Roland Franssen <franssen.roland@gmail.com>
*/
final class PhpUnitFqcnAnnotationFixer extends AbstractPhpUnitFixer
{
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'PHPUnit annotations should be a FQCNs including a root namespace.',
[new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException InvalidArgumentException
* @covers Project\NameSpace\Something
* @coversDefaultClass Project\Default
* @uses Project\Test\Util
*/
public function testSomeTest()
{
}
}
'
)]
);
}
/**
* {@inheritdoc}
*
* Must run before NoUnusedImportsFixer, PhpdocOrderByValueFixer.
*/
public function getPriority(): int
{
return -9;
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
$prevDocCommentIndex = $tokens->getPrevTokenOfKind($startIndex, [[T_DOC_COMMENT]]);
if (null !== $prevDocCommentIndex) {
$startIndex = $prevDocCommentIndex;
}
$this->fixPhpUnitClass($tokens, $startIndex, $endIndex);
}
private function fixPhpUnitClass(Tokens $tokens, int $startIndex, int $endIndex): void
{
for ($index = $startIndex; $index < $endIndex; ++$index) {
if ($tokens[$index]->isGivenKind(T_DOC_COMMENT)) {
$tokens[$index] = new Token([T_DOC_COMMENT, Preg::replace(
'~^(\s*\*\s*@(?:expectedException|covers|coversDefaultClass|uses)\h+)(?!(?:self|static)::)(\w.*)$~m',
'$1\\\\$2',
$tokens[$index]->getContent()
)]);
}
}
}
}
@@ -0,0 +1,165 @@
<?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\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
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 Gert de Pagter <BackEndTea@gmail.com>
*/
final class PhpUnitInternalClassFixer extends AbstractPhpUnitFixer implements WhitespacesAwareFixerInterface, ConfigurableFixerInterface
{
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'All PHPUnit test classes should be marked as internal.',
[
new CodeSample("<?php\nclass MyTest extends TestCase {}\n"),
new CodeSample(
"<?php\nclass MyTest extends TestCase {}\nfinal class FinalTest extends TestCase {}\nabstract class AbstractTest extends TestCase {}\n",
['types' => ['final']]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before FinalInternalClassFixer.
*/
public function getPriority(): int
{
return 68;
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
$types = ['normal', 'final', 'abstract'];
return new FixerConfigurationResolver([
(new FixerOptionBuilder('types', 'What types of classes to mark as internal'))
->setAllowedValues([new AllowedValueSubset($types)])
->setAllowedTypes(['array'])
->setDefault(['normal', 'final'])
->getOption(),
]);
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
$classIndex = $tokens->getPrevTokenOfKind($startIndex, [[T_CLASS]]);
if (!$this->isAllowedByConfiguration($tokens, $classIndex)) {
return;
}
$docBlockIndex = $this->getDocBlockIndex($tokens, $classIndex);
if ($this->isPHPDoc($tokens, $docBlockIndex)) {
$this->updateDocBlockIfNeeded($tokens, $docBlockIndex);
} else {
$this->createDocBlock($tokens, $docBlockIndex);
}
}
private function isAllowedByConfiguration(Tokens $tokens, int $i): bool
{
$typeIndex = $tokens->getPrevMeaningfulToken($i);
if ($tokens[$typeIndex]->isGivenKind(T_FINAL)) {
return \in_array('final', $this->configuration['types'], true);
}
if ($tokens[$typeIndex]->isGivenKind(T_ABSTRACT)) {
return \in_array('abstract', $this->configuration['types'], true);
}
return \in_array('normal', $this->configuration['types'], true);
}
private function createDocBlock(Tokens $tokens, int $docBlockIndex): void
{
$lineEnd = $this->whitespacesConfig->getLineEnding();
$originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
$toInsert = [
new Token([T_DOC_COMMENT, '/**'.$lineEnd."{$originalIndent} * @internal".$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 (!empty($doc->getAnnotationsOfType('internal'))) {
return;
}
$doc = $this->makeDocBlockMultiLineIfNeeded($doc, $tokens, $docBlockIndex);
$lines = $this->addInternalAnnotation($doc, $tokens, $docBlockIndex);
$lines = implode('', $lines);
$tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $lines]);
}
/**
* @return Line[]
*/
private function addInternalAnnotation(DocBlock $docBlock, Tokens $tokens, int $docBlockIndex): array
{
$lines = $docBlock->getLines();
$originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $docBlockIndex);
$lineEnd = $this->whitespacesConfig->getLineEnding();
array_splice($lines, -1, 0, $originalIndent.' *'.$lineEnd.$originalIndent.' * @internal'.$lineEnd);
return $lines;
}
private function makeDocBlockMultiLineIfNeeded(DocBlock $doc, Tokens $tokens, int $docBlockIndex): DocBlock
{
$lines = $doc->getLines();
if (1 === \count($lines) && empty($doc->getAnnotationsOfType('internal'))) {
$indent = WhitespacesAnalyzer::detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
$doc->makeMultiLine($indent, $this->whitespacesConfig->getLineEnding());
return $doc;
}
return $doc;
}
}
@@ -0,0 +1,208 @@
<?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\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\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
use PhpCsFixer\Utils;
/**
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class PhpUnitMethodCasingFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface
{
/**
* @internal
*/
public const CAMEL_CASE = 'camel_case';
/**
* @internal
*/
public const SNAKE_CASE = 'snake_case';
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Enforce camel (or snake) case for PHPUnit test methods, following configuration.',
[
new CodeSample(
'<?php
class MyTest extends \\PhpUnit\\FrameWork\\TestCase
{
public function test_my_code() {}
}
'
),
new CodeSample(
'<?php
class MyTest extends \\PhpUnit\\FrameWork\\TestCase
{
public function testMyCode() {}
}
',
['case' => self::SNAKE_CASE]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run after PhpUnitTestAnnotationFixer.
*/
public function getPriority(): int
{
return 0;
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('case', 'Apply camel or snake case to test methods'))
->setAllowedValues([self::CAMEL_CASE, self::SNAKE_CASE])
->setDefault(self::CAMEL_CASE)
->getOption(),
]);
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
for ($index = $endIndex - 1; $index > $startIndex; --$index) {
if (!$this->isTestMethod($tokens, $index)) {
continue;
}
$functionNameIndex = $tokens->getNextMeaningfulToken($index);
$functionName = $tokens[$functionNameIndex]->getContent();
$newFunctionName = $this->updateMethodCasing($functionName);
if ($newFunctionName !== $functionName) {
$tokens[$functionNameIndex] = new Token([T_STRING, $newFunctionName]);
}
$docBlockIndex = $this->getDocBlockIndex($tokens, $index);
if ($this->isPHPDoc($tokens, $docBlockIndex)) {
$this->updateDocBlock($tokens, $docBlockIndex);
}
}
}
private function updateMethodCasing(string $functionName): string
{
$parts = explode('::', $functionName);
$functionNamePart = array_pop($parts);
if (self::CAMEL_CASE === $this->configuration['case']) {
$newFunctionNamePart = $functionNamePart;
$newFunctionNamePart = ucwords($newFunctionNamePart, '_');
$newFunctionNamePart = str_replace('_', '', $newFunctionNamePart);
$newFunctionNamePart = lcfirst($newFunctionNamePart);
} else {
$newFunctionNamePart = Utils::camelCaseToUnderscore($functionNamePart);
}
$parts[] = $newFunctionNamePart;
return implode('::', $parts);
}
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's a test
$functionNameIndex = $tokens->getNextMeaningfulToken($index);
$functionName = $tokens[$functionNameIndex]->getContent();
if (str_starts_with($functionName, 'test')) {
return true;
}
$docBlockIndex = $this->getDocBlockIndex($tokens, $index);
return
$this->isPHPDoc($tokens, $docBlockIndex) // If the function doesn't have test in its name, and no doc block, it's not a test
&& 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 updateDocBlock(Tokens $tokens, int $docBlockIndex): void
{
$doc = new DocBlock($tokens[$docBlockIndex]->getContent());
$lines = $doc->getLines();
$docBlockNeedsUpdate = false;
for ($inc = 0; $inc < \count($lines); ++$inc) {
$lineContent = $lines[$inc]->getContent();
if (!str_contains($lineContent, '@depends')) {
continue;
}
$newLineContent = Preg::replaceCallback('/(@depends\s+)(.+)(\b)/', function (array $matches): string {
return sprintf(
'%s%s%s',
$matches[1],
$this->updateMethodCasing($matches[2]),
$matches[3]
);
}, $lineContent);
if ($newLineContent !== $lineContent) {
$lines[$inc] = new Line($newLineContent);
$docBlockNeedsUpdate = true;
}
}
if ($docBlockNeedsUpdate) {
$lines = implode('', $lines);
$tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $lines]);
}
}
}
@@ -0,0 +1,142 @@
<?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\Fixer\AbstractPhpUnitFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
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\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class PhpUnitMockFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface
{
/**
* @var bool
*/
private $fixCreatePartialMock;
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Usages of `->getMock` and `->getMockWithoutInvokingTheOriginalConstructor` methods MUST be replaced by `->createMock` or `->createPartialMock` methods.',
[
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testFoo()
{
$mock = $this->getMockWithoutInvokingTheOriginalConstructor("Foo");
$mock1 = $this->getMock("Foo");
$mock1 = $this->getMock("Bar", ["aaa"]);
$mock1 = $this->getMock("Baz", ["aaa"], ["argument"]); // version with more than 2 params is not supported
}
}
'
),
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testFoo()
{
$mock1 = $this->getMock("Foo");
$mock1 = $this->getMock("Bar", ["aaa"]); // version with multiple params is not supported
}
}
',
['target' => PhpUnitTargetVersion::VERSION_5_4]
),
],
null,
'Risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.'
);
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function configure(array $configuration): void
{
parent::configure($configuration);
$this->fixCreatePartialMock = PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_5_5);
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
$argumentsAnalyzer = new ArgumentsAnalyzer();
for ($index = $startIndex; $index < $endIndex; ++$index) {
if (!$tokens[$index]->isObjectOperator()) {
continue;
}
$index = $tokens->getNextMeaningfulToken($index);
if ($tokens[$index]->equals([T_STRING, 'getMockWithoutInvokingTheOriginalConstructor'], false)) {
$tokens[$index] = new Token([T_STRING, 'createMock']);
} elseif ($tokens[$index]->equals([T_STRING, 'getMock'], false)) {
$openingParenthesis = $tokens->getNextMeaningfulToken($index);
$closingParenthesis = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openingParenthesis);
$argumentsCount = $argumentsAnalyzer->countArguments($tokens, $openingParenthesis, $closingParenthesis);
if (1 === $argumentsCount) {
$tokens[$index] = new Token([T_STRING, 'createMock']);
} elseif (2 === $argumentsCount && true === $this->fixCreatePartialMock) {
$tokens[$index] = new Token([T_STRING, 'createPartialMock']);
}
}
}
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('target', 'Target version of PHPUnit.'))
->setAllowedTypes(['string'])
->setAllowedValues([PhpUnitTargetVersion::VERSION_5_4, PhpUnitTargetVersion::VERSION_5_5, PhpUnitTargetVersion::VERSION_NEWEST])
->setDefault(PhpUnitTargetVersion::VERSION_NEWEST)
->getOption(),
]);
}
}
@@ -0,0 +1,131 @@
<?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\Fixer\AbstractPhpUnitFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Michał Adamski <michal.adamski@gmail.com>
* @author Kuba Werłos <werlos@gmail.com>
*/
final class PhpUnitMockShortWillReturnFixer extends AbstractPhpUnitFixer
{
private const RETURN_METHODS_MAP = [
'returnargument' => 'willReturnArgument',
'returncallback' => 'willReturnCallback',
'returnself' => 'willReturnSelf',
'returnvalue' => 'willReturn',
'returnvaluemap' => 'willReturnMap',
];
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Usage of PHPUnit\'s mock e.g. `->will($this->returnValue(..))` must be replaced by its shorter equivalent such as `->willReturn(...)`.',
[
new CodeSample('<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testSomeTest()
{
$someMock = $this->createMock(Some::class);
$someMock->method("some")->will($this->returnSelf());
$someMock->method("some")->will($this->returnValue("example"));
$someMock->method("some")->will($this->returnArgument(2));
$someMock->method("some")->will($this->returnCallback("str_rot13"));
$someMock->method("some")->will($this->returnValueMap(["a","b","c"]));
}
}
'),
],
null,
'Risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.'
);
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
$functionsAnalyzer = new FunctionsAnalyzer();
for ($index = $startIndex; $index < $endIndex; ++$index) {
if (!$tokens[$index]->isObjectOperator()) {
continue;
}
$functionToReplaceIndex = $tokens->getNextMeaningfulToken($index);
if (!$tokens[$functionToReplaceIndex]->equals([T_STRING, 'will'], false)) {
continue;
}
$functionToReplaceOpeningBraceIndex = $tokens->getNextMeaningfulToken($functionToReplaceIndex);
if (!$tokens[$functionToReplaceOpeningBraceIndex]->equals('(')) {
continue;
}
$classReferenceIndex = $tokens->getNextMeaningfulToken($functionToReplaceOpeningBraceIndex);
$objectOperatorIndex = $tokens->getNextMeaningfulToken($classReferenceIndex);
$functionToRemoveIndex = $tokens->getNextMeaningfulToken($objectOperatorIndex);
if (!$functionsAnalyzer->isTheSameClassCall($tokens, $functionToRemoveIndex)) {
continue;
}
if (!\array_key_exists(strtolower($tokens[$functionToRemoveIndex]->getContent()), self::RETURN_METHODS_MAP)) {
continue;
}
$openingBraceIndex = $tokens->getNextMeaningfulToken($functionToRemoveIndex);
if (!$tokens[$openingBraceIndex]->equals('(')) {
continue;
}
if ($tokens[$tokens->getNextMeaningfulToken($openingBraceIndex)]->isGivenKind(CT::T_FIRST_CLASS_CALLABLE)) {
continue;
}
$closingBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openingBraceIndex);
$tokens[$functionToReplaceIndex] = new Token([T_STRING, self::RETURN_METHODS_MAP[strtolower($tokens[$functionToRemoveIndex]->getContent())]]);
$tokens->clearTokenAndMergeSurroundingWhitespace($classReferenceIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($objectOperatorIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($functionToRemoveIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($openingBraceIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($closingBraceIndex);
}
}
}
@@ -0,0 +1,230 @@
<?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\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
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\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class PhpUnitNamespacedFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/**
* @var string
*/
private $originalClassRegEx;
/**
* Class Mappings.
*
* * [original classname => new classname] Some classes which match the
* original class regular expression do not have a same-compound name-
* space class and need a dedicated translation table. This trans-
* lation table is defined in @see configure.
*
* @var array|string[] Class Mappings
*/
private $classMap;
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
$codeSample = '<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testSomething()
{
PHPUnit_Framework_Assert::assertTrue(true);
}
}
';
return new FixerDefinition(
'PHPUnit classes MUST be used in namespaced version, e.g. `\PHPUnit\Framework\TestCase` instead of `\PHPUnit_Framework_TestCase`.',
[
new CodeSample($codeSample),
new CodeSample($codeSample, ['target' => PhpUnitTargetVersion::VERSION_4_8]),
],
"PHPUnit v6 has finally fully switched to namespaces.\n"
."You could start preparing the upgrade by switching from non-namespaced TestCase to namespaced one.\n"
.'Forward compatibility layer (`\PHPUnit\Framework\TestCase` class) was backported to PHPUnit v4.8.35 and PHPUnit v5.4.0.'."\n"
.'Extended forward compatibility layer (`PHPUnit\Framework\Assert`, `PHPUnit\Framework\BaseTestListener`, `PHPUnit\Framework\TestListener` classes) was introduced in v5.7.0.'."\n",
'Risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.'
);
}
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_STRING);
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function configure(array $configuration): void
{
parent::configure($configuration);
if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_6_0)) {
$this->originalClassRegEx = '/^PHPUnit_\w+$/i';
// @noinspection ClassConstantCanBeUsedInspection
$this->classMap = [
'PHPUnit_Extensions_PhptTestCase' => 'PHPUnit\Runner\PhptTestCase',
'PHPUnit_Framework_Constraint' => 'PHPUnit\Framework\Constraint\Constraint',
'PHPUnit_Framework_Constraint_StringMatches' => 'PHPUnit\Framework\Constraint\StringMatchesFormatDescription',
'PHPUnit_Framework_Constraint_JsonMatches_ErrorMessageProvider' => 'PHPUnit\Framework\Constraint\JsonMatchesErrorMessageProvider',
'PHPUnit_Framework_Constraint_PCREMatch' => 'PHPUnit\Framework\Constraint\RegularExpression',
'PHPUnit_Framework_Constraint_ExceptionMessageRegExp' => 'PHPUnit\Framework\Constraint\ExceptionMessageRegularExpression',
'PHPUnit_Framework_Constraint_And' => 'PHPUnit\Framework\Constraint\LogicalAnd',
'PHPUnit_Framework_Constraint_Or' => 'PHPUnit\Framework\Constraint\LogicalOr',
'PHPUnit_Framework_Constraint_Not' => 'PHPUnit\Framework\Constraint\LogicalNot',
'PHPUnit_Framework_Constraint_Xor' => 'PHPUnit\Framework\Constraint\LogicalXor',
'PHPUnit_Framework_Error' => 'PHPUnit\Framework\Error\Error',
'PHPUnit_Framework_TestSuite_DataProvider' => 'PHPUnit\Framework\DataProviderTestSuite',
'PHPUnit_Framework_MockObject_Invocation_Static' => 'PHPUnit\Framework\MockObject\Invocation\StaticInvocation',
'PHPUnit_Framework_MockObject_Invocation_Object' => 'PHPUnit\Framework\MockObject\Invocation\ObjectInvocation',
'PHPUnit_Framework_MockObject_Stub_Return' => 'PHPUnit\Framework\MockObject\Stub\ReturnStub',
'PHPUnit_Runner_Filter_Group_Exclude' => 'PHPUnit\Runner\Filter\ExcludeGroupFilterIterator',
'PHPUnit_Runner_Filter_Group_Include' => 'PHPUnit\Runner\Filter\IncludeGroupFilterIterator',
'PHPUnit_Runner_Filter_Test' => 'PHPUnit\Runner\Filter\NameFilterIterator',
'PHPUnit_Util_PHP' => 'PHPUnit\Util\PHP\AbstractPhpProcess',
'PHPUnit_Util_PHP_Default' => 'PHPUnit\Util\PHP\DefaultPhpProcess',
'PHPUnit_Util_PHP_Windows' => 'PHPUnit\Util\PHP\WindowsPhpProcess',
'PHPUnit_Util_Regex' => 'PHPUnit\Util\RegularExpression',
'PHPUnit_Util_TestDox_ResultPrinter_XML' => 'PHPUnit\Util\TestDox\XmlResultPrinter',
'PHPUnit_Util_TestDox_ResultPrinter_HTML' => 'PHPUnit\Util\TestDox\HtmlResultPrinter',
'PHPUnit_Util_TestDox_ResultPrinter_Text' => 'PHPUnit\Util\TestDox\TextResultPrinter',
'PHPUnit_Util_TestSuiteIterator' => 'PHPUnit\Framework\TestSuiteIterator',
'PHPUnit_Util_XML' => 'PHPUnit\Util\Xml',
];
} elseif (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_5_7)) {
$this->originalClassRegEx = '/^PHPUnit_Framework_TestCase|PHPUnit_Framework_Assert|PHPUnit_Framework_BaseTestListener|PHPUnit_Framework_TestListener$/i';
$this->classMap = [];
} else {
$this->originalClassRegEx = '/^PHPUnit_Framework_TestCase$/i';
$this->classMap = [];
}
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$importedOriginalClassesMap = [];
$currIndex = 0;
while (true) {
$currIndex = $tokens->getNextTokenOfKind($currIndex, [[T_STRING]]);
if (null === $currIndex) {
break;
}
$prevIndex = $tokens->getPrevMeaningfulToken($currIndex);
if ($tokens[$prevIndex]->isGivenKind([T_CONST, T_DOUBLE_COLON])) {
continue;
}
$originalClass = $tokens[$currIndex]->getContent();
if (1 !== Preg::match($this->originalClassRegEx, $originalClass)) {
++$currIndex;
continue;
}
$substituteTokens = $this->generateReplacement($originalClass);
$tokens->clearAt($currIndex);
$tokens->insertAt(
$currIndex,
isset($importedOriginalClassesMap[$originalClass]) ? $substituteTokens[$substituteTokens->getSize() - 1] : $substituteTokens
);
$prevIndex = $tokens->getPrevMeaningfulToken($currIndex);
if ($tokens[$prevIndex]->isGivenKind(T_USE)) {
$importedOriginalClassesMap[$originalClass] = true;
} elseif ($tokens[$prevIndex]->isGivenKind(T_NS_SEPARATOR)) {
$prevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
if ($tokens[$prevIndex]->isGivenKind(T_USE)) {
$importedOriginalClassesMap[$originalClass] = true;
}
}
}
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('target', 'Target version of PHPUnit.'))
->setAllowedTypes(['string'])
->setAllowedValues([PhpUnitTargetVersion::VERSION_4_8, PhpUnitTargetVersion::VERSION_5_7, PhpUnitTargetVersion::VERSION_6_0, PhpUnitTargetVersion::VERSION_NEWEST])
->setDefault(PhpUnitTargetVersion::VERSION_NEWEST)
->getOption(),
]);
}
private function generateReplacement(string $originalClassName): Tokens
{
$delimiter = '_';
$string = $originalClassName;
if (isset($this->classMap[$originalClassName])) {
$delimiter = '\\';
$string = $this->classMap[$originalClassName];
}
$parts = explode($delimiter, $string);
$tokensArray = [];
while (!empty($parts)) {
$tokensArray[] = new Token([T_STRING, array_shift($parts)]);
if (!empty($parts)) {
$tokensArray[] = new Token([T_NS_SEPARATOR, '\\']);
}
}
return Tokens::fromArray($tokensArray);
}
}
@@ -0,0 +1,283 @@
<?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\Annotation;
use PhpCsFixer\DocBlock\DocBlock;
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\Preg;
use PhpCsFixer\Tokenizer\Analyzer\WhitespacesAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class PhpUnitNoExpectationAnnotationFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
{
/**
* @var bool
*/
private $fixMessageRegExp;
/**
* {@inheritdoc}
*/
public function configure(array $configuration): void
{
parent::configure($configuration);
$this->fixMessageRegExp = PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_4_3);
}
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Usages of `@expectedException*` annotations MUST be replaced by `->setExpectedException*` methods.',
[
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException FooException
* @expectedExceptionMessageRegExp /foo.*$/
* @expectedExceptionCode 123
*/
function testAaa()
{
aaa();
}
}
'
),
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException FooException
* @expectedExceptionCode 123
*/
function testBbb()
{
bbb();
}
/**
* @expectedException FooException
* @expectedExceptionMessageRegExp /foo.*$/
*/
function testCcc()
{
ccc();
}
}
',
['target' => PhpUnitTargetVersion::VERSION_3_2]
),
],
null,
'Risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.'
);
}
/**
* {@inheritdoc}
*
* Must run before NoEmptyPhpdocFixer, PhpUnitExpectationFixer.
*/
public function getPriority(): int
{
return 10;
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('target', 'Target version of PHPUnit.'))
->setAllowedTypes(['string'])
->setAllowedValues([PhpUnitTargetVersion::VERSION_3_2, PhpUnitTargetVersion::VERSION_4_3, PhpUnitTargetVersion::VERSION_NEWEST])
->setDefault(PhpUnitTargetVersion::VERSION_NEWEST)
->getOption(),
(new FixerOptionBuilder('use_class_const', 'Use ::class notation.'))
->setAllowedTypes(['bool'])
->setDefault(true)
->getOption(),
]);
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
for ($i = $endIndex - 1; $i > $startIndex; --$i) {
if (!$tokens[$i]->isGivenKind(T_FUNCTION) || $tokensAnalyzer->isLambda($i)) {
continue;
}
$functionIndex = $i;
$docBlockIndex = $i;
// ignore abstract functions
$braceIndex = $tokens->getNextTokenOfKind($functionIndex, [';', '{']);
if (!$tokens[$braceIndex]->equals('{')) {
continue;
}
do {
$docBlockIndex = $tokens->getPrevNonWhitespace($docBlockIndex);
} while ($tokens[$docBlockIndex]->isGivenKind([T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_COMMENT]));
if (!$tokens[$docBlockIndex]->isGivenKind(T_DOC_COMMENT)) {
continue;
}
$doc = new DocBlock($tokens[$docBlockIndex]->getContent());
$annotations = [];
foreach ($doc->getAnnotationsOfType([
'expectedException',
'expectedExceptionCode',
'expectedExceptionMessage',
'expectedExceptionMessageRegExp',
]) as $annotation) {
$tag = $annotation->getTag()->getName();
$content = $this->extractContentFromAnnotation($annotation);
$annotations[$tag] = $content;
$annotation->remove();
}
if (!isset($annotations['expectedException'])) {
continue;
}
if (!$this->fixMessageRegExp && isset($annotations['expectedExceptionMessageRegExp'])) {
continue;
}
$originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $docBlockIndex);
$paramList = $this->annotationsToParamList($annotations);
$newMethodsCode = '<?php $this->'
.(isset($annotations['expectedExceptionMessageRegExp']) ? 'setExpectedExceptionRegExp' : 'setExpectedException')
.'('
.implode(', ', $paramList)
.');';
$newMethods = Tokens::fromCode($newMethodsCode);
$newMethods[0] = new Token([
T_WHITESPACE,
$this->whitespacesConfig->getLineEnding().$originalIndent.$this->whitespacesConfig->getIndent(),
]);
// apply changes
$docContent = $doc->getContent();
if ('' === $docContent) {
$docContent = '/** */';
}
$tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $docContent]);
$tokens->insertAt($braceIndex + 1, $newMethods);
$whitespaceIndex = $braceIndex + $newMethods->getSize() + 1;
$tokens[$whitespaceIndex] = new Token([
T_WHITESPACE,
$this->whitespacesConfig->getLineEnding().$tokens[$whitespaceIndex]->getContent(),
]);
$i = $docBlockIndex;
}
}
private function extractContentFromAnnotation(Annotation $annotation): string
{
$tag = $annotation->getTag()->getName();
if (1 !== Preg::match('/@'.$tag.'\s+(.+)$/s', $annotation->getContent(), $matches)) {
return '';
}
$content = Preg::replace('/\*+\/$/', '', $matches[1]);
if (Preg::match('/\R/u', $content)) {
$content = Preg::replace('/\s*\R+\s*\*\s*/u', ' ', $content);
}
return rtrim($content);
}
/**
* @param array<string, string> $annotations
*
* @return list<string>
*/
private function annotationsToParamList(array $annotations): array
{
$params = [];
$exceptionClass = ltrim($annotations['expectedException'], '\\');
if (str_contains($exceptionClass, '*')) {
$exceptionClass = substr($exceptionClass, 0, strpos($exceptionClass, '*'));
}
$exceptionClass = trim($exceptionClass);
if (true === $this->configuration['use_class_const']) {
$params[] = "\\{$exceptionClass}::class";
} else {
$params[] = "'{$exceptionClass}'";
}
if (isset($annotations['expectedExceptionMessage'])) {
$params[] = var_export($annotations['expectedExceptionMessage'], true);
} elseif (isset($annotations['expectedExceptionMessageRegExp'])) {
$params[] = var_export($annotations['expectedExceptionMessageRegExp'], true);
} elseif (isset($annotations['expectedExceptionCode'])) {
$params[] = 'null';
}
if (isset($annotations['expectedExceptionCode'])) {
$params[] = $annotations['expectedExceptionCode'];
}
return $params;
}
}
@@ -0,0 +1,117 @@
<?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\Fixer\AbstractPhpUnitFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @author Gert de Pagter
*/
final class PhpUnitSetUpTearDownVisibilityFixer extends AbstractPhpUnitFixer
{
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Changes the visibility of the `setUp()` and `tearDown()` functions of PHPUnit to `protected`, to match the PHPUnit TestCase.',
[
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
private $hello;
public function setUp()
{
$this->hello = "hello";
}
public function tearDown()
{
$this->hello = null;
}
}
'
),
],
null,
'This fixer may change functions named `setUp()` or `tearDown()` outside of PHPUnit tests, '.
'when a class is wrongly seen as a PHPUnit test.'
);
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
$counter = 0;
$tokensAnalyzer = new TokensAnalyzer($tokens);
for ($i = $endIndex - 1; $i > $startIndex; --$i) {
if (2 === $counter) {
break; // we've seen both method we are interested in, so stop analyzing this class
}
if (!$this->isSetupOrTearDownMethod($tokens, $i)) {
continue;
}
++$counter;
$visibility = $tokensAnalyzer->getMethodAttributes($i)['visibility'];
if (T_PUBLIC === $visibility) {
$index = $tokens->getPrevTokenOfKind($i, [[T_PUBLIC]]);
$tokens[$index] = new Token([T_PROTECTED, 'protected']);
continue;
}
if (null === $visibility) {
$tokens->insertAt($i, [new Token([T_PROTECTED, 'protected']), new Token([T_WHITESPACE, ' '])]);
}
}
}
private function isSetupOrTearDownMethod(Tokens $tokens, int $index): bool
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
$isMethod = $tokens[$index]->isGivenKind(T_FUNCTION) && !$tokensAnalyzer->isLambda($index);
if (!$isMethod) {
return false;
}
$functionNameIndex = $tokens->getNextMeaningfulToken($index);
$functionName = strtolower($tokens[$functionNameIndex]->getContent());
return 'setup' === $functionName || 'teardown' === $functionName;
}
}
@@ -0,0 +1,200 @@
<?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\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 <malukenho.dev@gmail.com>
*/
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("<?php\nclass MyTest extends TestCase {}\n"),
new CodeSample("<?php\nclass MyTest extends TestCase {}\n", ['group' => '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'),
]);
}
}
@@ -0,0 +1,153 @@
<?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\Fixer\AbstractPhpUnitFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
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\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class PhpUnitStrictFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface
{
/**
* @var array<string,string>
*/
private static array $assertionMap = [
'assertAttributeEquals' => 'assertAttributeSame',
'assertAttributeNotEquals' => 'assertAttributeNotSame',
'assertEquals' => 'assertSame',
'assertNotEquals' => 'assertNotSame',
];
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'PHPUnit methods like `assertSame` should be used instead of `assertEquals`.',
[
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testSomeTest()
{
$this->assertAttributeEquals(a(), b());
$this->assertAttributeNotEquals(a(), b());
$this->assertEquals(a(), b());
$this->assertNotEquals(a(), b());
}
}
'
),
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testSomeTest()
{
$this->assertAttributeEquals(a(), b());
$this->assertAttributeNotEquals(a(), b());
$this->assertEquals(a(), b());
$this->assertNotEquals(a(), b());
}
}
',
['assertions' => ['assertEquals']]
),
],
null,
'Risky when any of the functions are overridden or when testing object equality.'
);
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
$argumentsAnalyzer = new ArgumentsAnalyzer();
$functionsAnalyzer = new FunctionsAnalyzer();
foreach ($this->configuration['assertions'] as $methodBefore) {
$methodAfter = self::$assertionMap[$methodBefore];
for ($index = $startIndex; $index < $endIndex; ++$index) {
$methodIndex = $tokens->getNextTokenOfKind($index, [[T_STRING, $methodBefore]]);
if (null === $methodIndex) {
break;
}
if (!$functionsAnalyzer->isTheSameClassCall($tokens, $methodIndex)) {
continue;
}
$openingParenthesisIndex = $tokens->getNextMeaningfulToken($methodIndex);
$argumentsCount = $argumentsAnalyzer->countArguments(
$tokens,
$openingParenthesisIndex,
$tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openingParenthesisIndex)
);
if (2 === $argumentsCount || 3 === $argumentsCount) {
$tokens[$methodIndex] = new Token([T_STRING, $methodAfter]);
}
$index = $methodIndex;
}
}
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('assertions', 'List of assertion methods to fix.'))
->setAllowedTypes(['array'])
->setAllowedValues([new AllowedValueSubset(array_keys(self::$assertionMap))])
->setDefault([
'assertAttributeEquals',
'assertAttributeNotEquals',
'assertEquals',
'assertNotEquals',
])
->getOption(),
]);
}
}
@@ -0,0 +1,58 @@
<?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 Composer\Semver\Comparator;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class PhpUnitTargetVersion
{
public const VERSION_3_0 = '3.0';
public const VERSION_3_2 = '3.2';
public const VERSION_3_5 = '3.5';
public const VERSION_4_3 = '4.3';
public const VERSION_4_8 = '4.8';
public const VERSION_5_0 = '5.0';
public const VERSION_5_2 = '5.2';
public const VERSION_5_4 = '5.4';
public const VERSION_5_5 = '5.5';
public const VERSION_5_6 = '5.6';
public const VERSION_5_7 = '5.7';
public const VERSION_6_0 = '6.0';
public const VERSION_7_5 = '7.5';
public const VERSION_8_4 = '8.4';
public const VERSION_NEWEST = 'newest';
private function __construct()
{
}
public static function fulfills(string $candidate, string $target): bool
{
if (self::VERSION_NEWEST === $target) {
throw new \LogicException(sprintf('Parameter `target` shall not be provided as "%s", determine proper target for tested PHPUnit feature instead.', self::VERSION_NEWEST));
}
if (self::VERSION_NEWEST === $candidate) {
return true;
}
return Comparator::greaterThanOrEqualTo($candidate, $target);
}
}
@@ -0,0 +1,415 @@
<?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\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;
/**
* @author Gert de Pagter
*/
final class PhpUnitTestAnnotationFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
{
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Adds or removes @test annotations from tests, following configuration.',
[
new CodeSample('<?php
class Test extends \\PhpUnit\\FrameWork\\TestCase
{
/**
* @test
*/
public function itDoesSomething() {} }'.$this->whitespacesConfig->getLineEnding()),
new CodeSample('<?php
class Test extends \\PhpUnit\\FrameWork\\TestCase
{
public function testItDoesSomething() {}}'.$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;
}
/**
* {@inheritdoc}
*/
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);
}
}
/**
* {@inheritdoc}
*/
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 ($this->isPHPDoc($tokens, $docBlockIndex)) {
$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);
}
}
}
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 (!$this->isPHPDoc($tokens, $docBlockIndex)) {
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
$this->isPHPDoc($tokens, $docBlockIndex)
&& 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 1 === 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);
}
private function createDocBlock(Tokens $tokens, int $docBlockIndex): void
{
$lineEnd = $this->whitespacesConfig->getLineEnding();
$originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
$toInsert = [
new Token([T_DOC_COMMENT, '/**'.$lineEnd."{$originalIndent} * @test".$lineEnd."{$originalIndent} */"]),
new Token([T_WHITESPACE, $lineEnd.$originalIndent]),
];
$index = $tokens->getNextMeaningfulToken($docBlockIndex);
$tokens->insertAt($index, $toInsert);
}
/**
* @return 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 Line[] $lines
*
* @return Line[]
*/
private function updateLines(array $lines, Tokens $tokens, int $docBlockIndex): array
{
$needsAnnotation = 'annotation' === $this->configuration['style'];
$doc = new DocBlock($tokens[$docBlockIndex]->getContent());
for ($i = 0; $i < \count($lines); ++$i) {
// If we need to add test annotation and it is a single line comment we need to deal with that separately
if ($needsAnnotation && ($lines[$i]->isTheStart() && $lines[$i]->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($lines[$i]->getContent(), ' @test')
&& !str_contains($lines[$i]->getContent(), '@testWith')
&& !str_contains($lines[$i]->getContent(), '@testdox')
) {
// We remove @test from the doc block
$lines[$i] = new Line(str_replace(' @test', '', $lines[$i]->getContent()));
}
// ignore the line if it isn't @depends
if (!str_contains($lines[$i]->getContent(), '@depends')) {
continue;
}
$lines[$i] = $this->updateDependsAnnotation($lines[$i]);
}
return $lines;
}
/**
* 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);
$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 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
{
$counter = \count($line);
do {
--$counter;
} while (' ' !== $line[$counter]);
return $counter + 1;
}
/**
* @param Line[] $lines
*
* @return 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, $originalIndent.' *'.$lineEnd.$originalIndent.' * @test'.$lineEnd);
}
return $lines;
}
private function doesDocBlockContainTest(DocBlock $doc): bool
{
return 0 !== \count($doc->getAnnotationsOfType('test'));
}
}
@@ -0,0 +1,491 @@
<?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\Fixer\AbstractPhpUnitFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
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\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
/**
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class PhpUnitTestCaseStaticMethodCallsFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface
{
/**
* @internal
*/
public const CALL_TYPE_THIS = 'this';
/**
* @internal
*/
public const CALL_TYPE_SELF = 'self';
/**
* @internal
*/
public const CALL_TYPE_STATIC = 'static';
/**
* @var array<string,bool>
*/
private array $allowedValues = [
self::CALL_TYPE_THIS => true,
self::CALL_TYPE_SELF => true,
self::CALL_TYPE_STATIC => true,
];
/**
* @var array<string,true>
*/
private array $staticMethods = [
// Assert methods
'anything' => true,
'arrayHasKey' => true,
'assertArrayHasKey' => true,
'assertArrayNotHasKey' => true,
'assertArraySubset' => true,
'assertAttributeContains' => true,
'assertAttributeContainsOnly' => true,
'assertAttributeCount' => true,
'assertAttributeEmpty' => true,
'assertAttributeEquals' => true,
'assertAttributeGreaterThan' => true,
'assertAttributeGreaterThanOrEqual' => true,
'assertAttributeInstanceOf' => true,
'assertAttributeInternalType' => true,
'assertAttributeLessThan' => true,
'assertAttributeLessThanOrEqual' => true,
'assertAttributeNotContains' => true,
'assertAttributeNotContainsOnly' => true,
'assertAttributeNotCount' => true,
'assertAttributeNotEmpty' => true,
'assertAttributeNotEquals' => true,
'assertAttributeNotInstanceOf' => true,
'assertAttributeNotInternalType' => true,
'assertAttributeNotSame' => true,
'assertAttributeSame' => true,
'assertClassHasAttribute' => true,
'assertClassHasStaticAttribute' => true,
'assertClassNotHasAttribute' => true,
'assertClassNotHasStaticAttribute' => true,
'assertContains' => true,
'assertContainsEquals' => true,
'assertContainsOnly' => true,
'assertContainsOnlyInstancesOf' => true,
'assertCount' => true,
'assertDirectoryDoesNotExist' => true,
'assertDirectoryExists' => true,
'assertDirectoryIsNotReadable' => true,
'assertDirectoryIsNotWritable' => true,
'assertDirectoryIsReadable' => true,
'assertDirectoryIsWritable' => true,
'assertDirectoryNotExists' => true,
'assertDirectoryNotIsReadable' => true,
'assertDirectoryNotIsWritable' => true,
'assertDoesNotMatchRegularExpression' => true,
'assertEmpty' => true,
'assertEqualXMLStructure' => true,
'assertEquals' => true,
'assertEqualsCanonicalizing' => true,
'assertEqualsIgnoringCase' => true,
'assertEqualsWithDelta' => true,
'assertFalse' => true,
'assertFileDoesNotExist' => true,
'assertFileEquals' => true,
'assertFileEqualsCanonicalizing' => true,
'assertFileEqualsIgnoringCase' => true,
'assertFileExists' => true,
'assertFileIsNotReadable' => true,
'assertFileIsNotWritable' => true,
'assertFileIsReadable' => true,
'assertFileIsWritable' => true,
'assertFileNotEquals' => true,
'assertFileNotEqualsCanonicalizing' => true,
'assertFileNotEqualsIgnoringCase' => true,
'assertFileNotExists' => true,
'assertFileNotIsReadable' => true,
'assertFileNotIsWritable' => true,
'assertFinite' => true,
'assertGreaterThan' => true,
'assertGreaterThanOrEqual' => true,
'assertInfinite' => true,
'assertInstanceOf' => true,
'assertInternalType' => true,
'assertIsArray' => true,
'assertIsBool' => true,
'assertIsCallable' => true,
'assertIsClosedResource' => true,
'assertIsFloat' => true,
'assertIsInt' => true,
'assertIsIterable' => true,
'assertIsNotArray' => true,
'assertIsNotBool' => true,
'assertIsNotCallable' => true,
'assertIsNotClosedResource' => true,
'assertIsNotFloat' => true,
'assertIsNotInt' => true,
'assertIsNotIterable' => true,
'assertIsNotNumeric' => true,
'assertIsNotObject' => true,
'assertIsNotReadable' => true,
'assertIsNotResource' => true,
'assertIsNotScalar' => true,
'assertIsNotString' => true,
'assertIsNotWritable' => true,
'assertIsNumeric' => true,
'assertIsObject' => true,
'assertIsReadable' => true,
'assertIsResource' => true,
'assertIsScalar' => true,
'assertIsString' => true,
'assertIsWritable' => true,
'assertJson' => true,
'assertJsonFileEqualsJsonFile' => true,
'assertJsonFileNotEqualsJsonFile' => true,
'assertJsonStringEqualsJsonFile' => true,
'assertJsonStringEqualsJsonString' => true,
'assertJsonStringNotEqualsJsonFile' => true,
'assertJsonStringNotEqualsJsonString' => true,
'assertLessThan' => true,
'assertLessThanOrEqual' => true,
'assertMatchesRegularExpression' => true,
'assertNan' => true,
'assertNotContains' => true,
'assertNotContainsEquals' => true,
'assertNotContainsOnly' => true,
'assertNotCount' => true,
'assertNotEmpty' => true,
'assertNotEquals' => true,
'assertNotEqualsCanonicalizing' => true,
'assertNotEqualsIgnoringCase' => true,
'assertNotEqualsWithDelta' => true,
'assertNotFalse' => true,
'assertNotInstanceOf' => true,
'assertNotInternalType' => true,
'assertNotIsReadable' => true,
'assertNotIsWritable' => true,
'assertNotNull' => true,
'assertNotRegExp' => true,
'assertNotSame' => true,
'assertNotSameSize' => true,
'assertNotTrue' => true,
'assertNull' => true,
'assertObjectEquals' => true,
'assertObjectHasAttribute' => true,
'assertObjectNotHasAttribute' => true,
'assertRegExp' => true,
'assertSame' => true,
'assertSameSize' => true,
'assertStringContainsString' => true,
'assertStringContainsStringIgnoringCase' => true,
'assertStringEndsNotWith' => true,
'assertStringEndsWith' => true,
'assertStringEqualsFile' => true,
'assertStringEqualsFileCanonicalizing' => true,
'assertStringEqualsFileIgnoringCase' => true,
'assertStringMatchesFormat' => true,
'assertStringMatchesFormatFile' => true,
'assertStringNotContainsString' => true,
'assertStringNotContainsStringIgnoringCase' => true,
'assertStringNotEqualsFile' => true,
'assertStringNotEqualsFileCanonicalizing' => true,
'assertStringNotEqualsFileIgnoringCase' => true,
'assertStringNotMatchesFormat' => true,
'assertStringNotMatchesFormatFile' => true,
'assertStringStartsNotWith' => true,
'assertStringStartsWith' => true,
'assertThat' => true,
'assertTrue' => true,
'assertXmlFileEqualsXmlFile' => true,
'assertXmlFileNotEqualsXmlFile' => true,
'assertXmlStringEqualsXmlFile' => true,
'assertXmlStringEqualsXmlString' => true,
'assertXmlStringNotEqualsXmlFile' => true,
'assertXmlStringNotEqualsXmlString' => true,
'attribute' => true,
'attributeEqualTo' => true,
'callback' => true,
'classHasAttribute' => true,
'classHasStaticAttribute' => true,
'contains' => true,
'containsEqual' => true,
'containsIdentical' => true,
'containsOnly' => true,
'containsOnlyInstancesOf' => true,
'countOf' => true,
'directoryExists' => true,
'equalTo' => true,
'equalToCanonicalizing' => true,
'equalToIgnoringCase' => true,
'equalToWithDelta' => true,
'fail' => true,
'fileExists' => true,
'getCount' => true,
'getObjectAttribute' => true,
'getStaticAttribute' => true,
'greaterThan' => true,
'greaterThanOrEqual' => true,
'identicalTo' => true,
'isEmpty' => true,
'isFalse' => true,
'isFinite' => true,
'isInfinite' => true,
'isInstanceOf' => true,
'isJson' => true,
'isNan' => true,
'isNull' => true,
'isReadable' => true,
'isTrue' => true,
'isType' => true,
'isWritable' => true,
'lessThan' => true,
'lessThanOrEqual' => true,
'logicalAnd' => true,
'logicalNot' => true,
'logicalOr' => true,
'logicalXor' => true,
'markTestIncomplete' => true,
'markTestSkipped' => true,
'matches' => true,
'matchesRegularExpression' => true,
'objectEquals' => true,
'objectHasAttribute' => true,
'readAttribute' => true,
'resetCount' => true,
'stringContains' => true,
'stringEndsWith' => true,
'stringStartsWith' => true,
// TestCase methods
'any' => true,
'at' => true,
'atLeast' => true,
'atLeastOnce' => true,
'atMost' => true,
'exactly' => true,
'never' => true,
'once' => true,
'onConsecutiveCalls' => true,
'returnArgument' => true,
'returnCallback' => true,
'returnSelf' => true,
'returnValue' => true,
'returnValueMap' => true,
'setUpBeforeClass' => true,
'tearDownAfterClass' => true,
'throwException' => true,
];
/**
* @var array<string, list<list<int|string>>>
*/
private array $conversionMap = [
self::CALL_TYPE_THIS => [[T_OBJECT_OPERATOR, '->'], [T_VARIABLE, '$this']],
self::CALL_TYPE_SELF => [[T_DOUBLE_COLON, '::'], [T_STRING, 'self']],
self::CALL_TYPE_STATIC => [[T_DOUBLE_COLON, '::'], [T_STATIC, 'static']],
];
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
$codeSample = '<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testMe()
{
$this->assertSame(1, 2);
self::assertSame(1, 2);
static::assertSame(1, 2);
}
}
';
return new FixerDefinition(
'Calls to `PHPUnit\Framework\TestCase` static methods must all be of the same type, either `$this->`, `self::` or `static::`.',
[
new CodeSample($codeSample),
new CodeSample($codeSample, ['call_type' => self::CALL_TYPE_THIS]),
],
null,
'Risky when PHPUnit methods are overridden or not accessible, or when project has PHPUnit incompatibilities.'
);
}
/**
* {@inheritdoc}
*
* Must run before SelfStaticAccessorFixer.
*/
public function getPriority(): int
{
return 0;
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
$thisFixer = $this;
return new FixerConfigurationResolver([
(new FixerOptionBuilder('call_type', 'The call type to use for referring to PHPUnit methods.'))
->setAllowedTypes(['string'])
->setAllowedValues(array_keys($this->allowedValues))
->setDefault('static')
->getOption(),
(new FixerOptionBuilder('methods', 'Dictionary of `method` => `call_type` values that differ from the default strategy.'))
->setAllowedTypes(['array'])
->setAllowedValues([static function (array $option) use ($thisFixer): bool {
foreach ($option as $method => $value) {
if (!isset($thisFixer->staticMethods[$method])) {
throw new InvalidOptionsException(
sprintf(
'Unexpected "methods" key, expected any of "%s", got "%s".',
implode('", "', array_keys($thisFixer->staticMethods)),
\gettype($method).'#'.$method
)
);
}
if (!isset($thisFixer->allowedValues[$value])) {
throw new InvalidOptionsException(
sprintf(
'Unexpected value for method "%s", expected any of "%s", got "%s".',
$method,
implode('", "', array_keys($thisFixer->allowedValues)),
\is_object($value) ? \get_class($value) : (null === $value ? 'null' : \gettype($value).'#'.$value)
)
);
}
}
return true;
}])
->setDefault([])
->getOption(),
]);
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
$analyzer = new TokensAnalyzer($tokens);
for ($index = $startIndex; $index < $endIndex; ++$index) {
// skip anonymous classes
if ($tokens[$index]->isGivenKind(T_CLASS)) {
$index = $this->findEndOfNextBlock($tokens, $index);
continue;
}
$callType = $this->configuration['call_type'];
if ($tokens[$index]->isGivenKind(T_FUNCTION)) {
// skip lambda
if ($analyzer->isLambda($index)) {
$index = $this->findEndOfNextBlock($tokens, $index);
continue;
}
// do not change `self` to `this` in static methods
if ('this' === $callType) {
$attributes = $analyzer->getMethodAttributes($index);
if (false !== $attributes['static']) {
$index = $this->findEndOfNextBlock($tokens, $index);
continue;
}
}
}
if (!$tokens[$index]->isGivenKind(T_STRING) || !isset($this->staticMethods[$tokens[$index]->getContent()])) {
continue;
}
$nextIndex = $tokens->getNextMeaningfulToken($index);
if (!$tokens[$nextIndex]->equals('(')) {
$index = $nextIndex;
continue;
}
if ($tokens[$tokens->getNextMeaningfulToken($nextIndex)]->isGivenKind(CT::T_FIRST_CLASS_CALLABLE)) {
continue;
}
$methodName = $tokens[$index]->getContent();
if (isset($this->configuration['methods'][$methodName])) {
$callType = $this->configuration['methods'][$methodName];
}
$operatorIndex = $tokens->getPrevMeaningfulToken($index);
$referenceIndex = $tokens->getPrevMeaningfulToken($operatorIndex);
if (!$this->needsConversion($tokens, $index, $referenceIndex, $callType)) {
continue;
}
$tokens[$operatorIndex] = new Token($this->conversionMap[$callType][0]);
$tokens[$referenceIndex] = new Token($this->conversionMap[$callType][1]);
}
}
private function needsConversion(Tokens $tokens, int $index, int $referenceIndex, string $callType): bool
{
$functionsAnalyzer = new FunctionsAnalyzer();
return $functionsAnalyzer->isTheSameClassCall($tokens, $index)
&& !$tokens[$referenceIndex]->equals($this->conversionMap[$callType][1], false);
}
private function findEndOfNextBlock(Tokens $tokens, int $index): int
{
$nextIndex = $tokens->getNextTokenOfKind($index, [';', '{']);
return $tokens[$nextIndex]->equals('{')
? $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $nextIndex)
: $nextIndex;
}
}
@@ -0,0 +1,135 @@
<?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\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class PhpUnitTestClassRequiresCoversFixer extends AbstractPhpUnitFixer implements WhitespacesAwareFixerInterface
{
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Adds a default `@coversNothing` annotation to PHPUnit test classes that have no `@covers*` annotation.',
[
new CodeSample(
'<?php
final class MyTest extends \PHPUnit_Framework_TestCase
{
public function testSomeTest()
{
$this->assertSame(a(), b());
}
}
'
),
]
);
}
/**
* {@inheritdoc}
*/
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
{
$classIndex = $tokens->getPrevTokenOfKind($startIndex, [[T_CLASS]]);
$prevIndex = $tokens->getPrevMeaningfulToken($classIndex);
// don't add `@covers` annotation for abstract base classes
if ($tokens[$prevIndex]->isGivenKind(T_ABSTRACT)) {
return;
}
$index = $tokens[$prevIndex]->isGivenKind(T_FINAL) ? $prevIndex : $classIndex;
$indent = $tokens[$index - 1]->isGivenKind(T_WHITESPACE)
? Preg::replace('/^.*\R*/', '', $tokens[$index - 1]->getContent())
: '';
$prevIndex = $tokens->getPrevNonWhitespace($index);
if ($tokens[$prevIndex]->isGivenKind(T_DOC_COMMENT)) {
$docIndex = $prevIndex;
$docContent = $tokens[$docIndex]->getContent();
// ignore one-line phpdocs like `/** foo */`, as there is no place to put new annotations
if (!str_contains($docContent, "\n")) {
return;
}
$doc = new DocBlock($docContent);
// skip if already has annotation
if (0 !== \count($doc->getAnnotationsOfType([
'covers',
'coversDefaultClass',
'coversNothing',
]))) {
return;
}
} else {
$docIndex = $index;
$tokens->insertAt($docIndex, [
new Token([T_DOC_COMMENT, sprintf('/**%s%s */', $this->whitespacesConfig->getLineEnding(), $indent)]),
new Token([T_WHITESPACE, sprintf('%s%s', $this->whitespacesConfig->getLineEnding(), $indent)]),
]);
if (!$tokens[$docIndex - 1]->isGivenKind(T_WHITESPACE)) {
$extraNewLines = $this->whitespacesConfig->getLineEnding();
if (!$tokens[$docIndex - 1]->isGivenKind(T_OPEN_TAG)) {
$extraNewLines .= $this->whitespacesConfig->getLineEnding();
}
$tokens->insertAt($docIndex, [
new Token([T_WHITESPACE, $extraNewLines.$indent]),
]);
++$docIndex;
}
$doc = new DocBlock($tokens[$docIndex]->getContent());
}
$lines = $doc->getLines();
array_splice(
$lines,
\count($lines) - 1,
0,
[
new Line(sprintf(
'%s * @coversNothing%s',
$indent,
$this->whitespacesConfig->getLineEnding()
)),
]
);
$tokens[$docIndex] = new Token([T_DOC_COMMENT, implode('', $lines)]);
}
}