diff --git a/docs/AllRectorsOverview.md b/docs/AllRectorsOverview.md index aa158e9926b..37d2386754f 100644 --- a/docs/AllRectorsOverview.md +++ b/docs/AllRectorsOverview.md @@ -1524,8 +1524,8 @@ Change assertContains()/assertNotContains() method to new string and iterable al - $this->assertNotContains('foo', 'foo bar'); - $this->assertContains('foo', ['foo', 'bar']); - $this->assertNotContains('foo', ['foo', 'bar']); -+ $this->assertStringContains('foo', 'foo bar'); -+ $this->assertStringNotContains('foo', 'foo bar'); ++ $this->assertStringContainsString('foo', 'foo bar'); ++ $this->assertStringNotContainsString('foo', 'foo bar'); + $this->assertIterableContains('foo', ['foo', 'bar']); + $this->assertIterableNotContains('foo', ['foo', 'bar']); } diff --git a/packages/ContributorTools/src/Command/CreateRectorCommand.php b/packages/ContributorTools/src/Command/CreateRectorCommand.php index 4e35157f8bb..429db118393 100644 --- a/packages/ContributorTools/src/Command/CreateRectorCommand.php +++ b/packages/ContributorTools/src/Command/CreateRectorCommand.php @@ -219,17 +219,18 @@ final class CreateRectorCommand extends Command implements ContributorCommandInt */ private function processComposerAutoload(array $templateVariables): void { - $composerJsonContent = FileSystem::read(getcwd() . '/composer.json'); - $composerJson = Json::decode($composerJsonContent, Json::FORCE_ARRAY); + $composerJsonFilePath = getcwd() . '/composer.json'; + $composerJson = $this->loadFileToJson($composerJsonFilePath); $package = $templateVariables['_Package_']; - // already autoloaded? + // skip core, already autoloaded if ($package === 'Rector') { return; } $package = $templateVariables['_Package_']; + $namespace = 'Rector\\' . $package . '\\'; $namespaceTest = 'Rector\\' . $package . '\\Tests\\'; @@ -241,13 +242,7 @@ final class CreateRectorCommand extends Command implements ContributorCommandInt $composerJson['autoload']['psr-4'][$namespace] = 'packages/' . $package . '/src'; $composerJson['autoload-dev']['psr-4'][$namespaceTest] = 'packages/' . $package . '/tests'; - $composerJsonContent = Json::encode($composerJson, Json::PRETTY); - - $composerJsonContent = $this->inlineSections($composerJsonContent, ['keywords', 'bin']); - - // inline short arrays - - FileSystem::write(getcwd() . '/composer.json', $composerJsonContent); + $this->saveJsonToFile($composerJsonFilePath, $composerJson); } /** @@ -267,4 +262,23 @@ final class CreateRectorCommand extends Command implements ContributorCommandInt return $jsonContent; } + + /** + * @return mixed[] + */ + private function loadFileToJson(string $filePath): array + { + $fileContent = FileSystem::read($filePath); + return Json::decode($fileContent, Json::FORCE_ARRAY); + } + + /** + * @param mixed[] $json + */ + private function saveJsonToFile(string $filePath, array $json): void + { + $content = Json::encode($json, Json::PRETTY); + $content = $this->inlineSections($content, ['keywords', 'bin']); + FileSystem::write($filePath, $content); + } } diff --git a/packages/NetteTesterToPHPUnit/src/Rector/Class_/NetteTesterClassToPHPUnitClassRector.php b/packages/NetteTesterToPHPUnit/src/Rector/Class_/NetteTesterClassToPHPUnitClassRector.php new file mode 100644 index 00000000000..7914f751579 --- /dev/null +++ b/packages/NetteTesterToPHPUnit/src/Rector/Class_/NetteTesterClassToPHPUnitClassRector.php @@ -0,0 +1,340 @@ + 'assertSame', + 'notSame' => 'assertNotSame', + 'equal' => 'assertEqual', + 'notEqual' => 'assertNotEqual', + 'true' => 'assertTrue', + 'false' => 'assertFalse', + 'null' => 'assertNull', + 'notNull' => 'assertNotNull', + 'count' => 'assertCount', + 'match' => 'assertStringMatchesFormat', + 'matchFile' => 'assertStringMatchesFormatFile', + 'contains' => 'assertContains', + 'notContains' => 'assertNotContains', + 'nan' => 'assertIsNumeric', + ]; + + /** + * @var DocBlockManipulator + */ + private $docBlockManipulator; + + public function __construct( + ClassManipulator $classManipulator, + CallableNodeTraverser $callableNodeTraverser, + DocBlockManipulator $docBlockManipulator, + string $netteTesterTestCaseClass = 'Tester\TestCase' + ) { + $this->classManipulator = $classManipulator; + $this->callableNodeTraverser = $callableNodeTraverser; + $this->netteTesterTestCaseClass = $netteTesterTestCaseClass; + $this->docBlockManipulator = $docBlockManipulator; + } + + public function getDefinition(): RectorDefinition + { + return new RectorDefinition('Migrate Nette Tester test case to PHPUnit', [ + new CodeSample( + <<<'CODE_SAMPLE' +namespace KdybyTests\Doctrine; + +use Tester\TestCase; +use Tester\Assert; + +require_once __DIR__ . '/../bootstrap.php'; + +class ExtensionTest extends TestCase +{ + public function setUp() + { + } + + public function testFunctionality() + { + Assert::true($default instanceof Kdyby\Doctrine\EntityManager); + Assert::true(5); + Assert::same($container->getService('kdyby.doctrine.default.entityManager'), $default); + } +} + +(new \ExtensionTest())->run(); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +namespace KdybyTests\Doctrine; + +use Tester\TestCase; +use Tester\Assert; + +class ExtensionTest extends \PHPUnit\Framework\TestCase +{ + protected function setUp() + { + } + + public function testFunctionality() + { + self::assertInstanceOf(\Kdyby\Doctrine\EntityManager::cllass, $default); + self::assertTrue(5); + self::same($container->getService('kdyby.doctrine.default.entityManager'), $default); + } +} +CODE_SAMPLE + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [Class_::class, Include_::class, MethodCall::class]; + } + + /** + * @param Class_|Include_|MethodCall $node + */ + public function refactor(Node $node): ?Node + { + if ($node instanceof Include_) { + $this->processAboveTestInclude($node); + return null; + } + + if (! $this->isType($node, $this->netteTesterTestCaseClass)) { + return null; + } + + if ($node instanceof MethodCall) { + $this->processUnderTestRun($node); + return null; + } + + $this->processExtends($node); + $this->processMethods($node); + + return $node; + } + + private function processExtends(Class_ $class): void + { + $class->extends = new FullyQualified('PHPUnit\Framework\TestCase'); + } + + private function processAboveTestInclude(Include_ $include): void + { + if ($include->getAttribute(Attribute::CLASS_NODE) === null) { + $this->removeNode($include); + } + + return; + } + + private function processUnderTestRun(MethodCall $methodCall): void + { + if ($this->isName($methodCall, 'run')) { + $this->removeNode($methodCall); + } + + return; + } + + private function processMethods(Class_ $class): void + { + $methods = $this->classManipulator->getMethods($class); + foreach ($methods as $method) { + if ($this->isNamesInsensitive($method, ['setUp', 'tearDown'])) { + $this->makeProtected($method); + } + + $this->processMethod($method); + } + } + + private function renameMethods(StaticCall $staticCall): void + { + // special cases + if ($this->isNames($staticCall, ['exception', 'throws'])) { + $this->processExceptionStaticCall($staticCall); + return; + } + + if ($this->isName($staticCall, 'type')) { + $this->processTypeStaticCall($staticCall); + return; + } + + if ($this->isName($staticCall, 'noError')) { + $this->processNoErrorStaticCall($staticCall); + return; + } + + if ($this->isNames($staticCall, ['truthy', 'falsey'])) { + $this->processTruthyOrFalseyStaticCall($staticCall); + return; + } + + foreach ($this->assertMethodsRemap as $oldMethod => $newMethod) { + if ($this->isName($staticCall, $oldMethod)) { + $staticCall->name = new Identifier($newMethod); + continue; + } + } + } + + private function processMethod(ClassMethod $classMethod): void + { + $this->callableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) { + if (! $node instanceof StaticCall) { + return null; + } + + if (! $this->isType($node->class, 'Tester\Assert')) { + return null; + } + + $node->class = new Name('self'); + $this->renameMethods($node); + }); + } + + private function processExceptionStaticCall(StaticCall $staticCall): void + { + // expect exception + $expectException = new StaticCall(new Name('self'), 'expectException'); + $expectException->args[] = $staticCall->args[1]; + $this->addNodeAfterNode($expectException, $staticCall); + + // expect message + if (isset($staticCall->args[2])) { + $expectExceptionMessage = new StaticCall(new Name('self'), 'expectExceptionMessage'); + $expectExceptionMessage->args[] = $staticCall->args[2]; + $this->addNodeAfterNode($expectExceptionMessage, $staticCall); + } + + // expect code + if (isset($staticCall->args[3])) { + $expectExceptionMessage = new StaticCall(new Name('self'), 'expectExceptionCode'); + $expectExceptionMessage->args[] = $staticCall->args[3]; + $this->addNodeAfterNode($expectExceptionMessage, $staticCall); + } + + /** @var Closure $callable */ + $callable = $staticCall->args[0]->value; + foreach ((array) $callable->stmts as $callableStmt) { + $this->addNodeAfterNode($callableStmt, $staticCall); + } + + $this->removeNode($staticCall); + } + + private function processNoErrorStaticCall(StaticCall $staticCall): void + { + /** @var Closure $callable */ + $callable = $staticCall->args[0]->value; + + foreach ((array) $callable->stmts as $callableStmt) { + $this->addNodeAfterNode($callableStmt, $staticCall); + } + + $this->removeNode($staticCall); + + $methodNode = $staticCall->getAttribute(Attribute::METHOD_NODE); + if ($methodNode === null) { + return; + } + + $phpDocTagNode = new PhpDocTextNode('@doesNotPerformAssertions'); + $this->docBlockManipulator->addTag($methodNode, $phpDocTagNode); + } + + private function processTruthyOrFalseyStaticCall(StaticCall $staticCall): void + { + if (! $this->isBoolType($staticCall->args[0]->value)) { + $staticCall->args[0]->value = new Bool_($staticCall->args[0]->value); + } + + if ($this->isName($staticCall, 'truthy')) { + $staticCall->name = new Identifier('assertTrue'); + } else { + $staticCall->name = new Identifier('assertFalse'); + } + } + + private function processTypeStaticCall(StaticCall $staticCall): void + { + $value = $this->getValue($staticCall->args[0]->value); + + $typeToMethod = [ + 'list' => 'assertIsArray', + 'array' => 'assertIsArray', + 'bool' => 'assertIsBool', + 'callable' => 'assertIsCallable', + 'float' => 'assertIsFloat', + 'int' => 'assertIsInt', + 'integer' => 'assertIsInt', + 'object' => 'assertIsObject', + 'resource' => 'assertIsResource', + 'string' => 'assertIsString', + 'scalar' => 'assertIsScalar', + ]; + + if (isset($typeToMethod[$value])) { + $staticCall->name = new Identifier($typeToMethod[$value]); + unset($staticCall->args[0]); + array_values($staticCall->args); + } elseif ($value === 'null') { + $staticCall->name = new Identifier('assertNull'); + unset($staticCall->args[0]); + array_values($staticCall->args); + } else { + $staticCall->name = new Identifier('assertInstanceOf'); + } + } +} diff --git a/packages/NetteTesterToPHPUnit/tests/Rector/Class_/NetteTesterClassToPHPUnitClassRector/Copy/bootstrap.php b/packages/NetteTesterToPHPUnit/tests/Rector/Class_/NetteTesterClassToPHPUnitClassRector/Copy/bootstrap.php new file mode 100644 index 00000000000..2b11bf37047 --- /dev/null +++ b/packages/NetteTesterToPHPUnit/tests/Rector/Class_/NetteTesterClassToPHPUnitClassRector/Copy/bootstrap.php @@ -0,0 +1,3 @@ + +----- + diff --git a/packages/NetteTesterToPHPUnit/tests/Rector/Class_/NetteTesterClassToPHPUnitClassRector/Fixture/fixture.php.inc b/packages/NetteTesterToPHPUnit/tests/Rector/Class_/NetteTesterClassToPHPUnitClassRector/Fixture/fixture.php.inc new file mode 100644 index 00000000000..1ea5d4d223a --- /dev/null +++ b/packages/NetteTesterToPHPUnit/tests/Rector/Class_/NetteTesterClassToPHPUnitClassRector/Fixture/fixture.php.inc @@ -0,0 +1,99 @@ +run(); + }, 'ExceptionClass', "Service 'one': Class or interface 'X' not found.", 200); + } + + public function testNoError() + { + Assert::noError(function () { + $value = 1; + }); + } + + public function testY() + { + Assert::falsey('value', 'some messsage'); + Assert::truthy(true); + } +} + +(new ExtensionTest())->run(); + +?> +----- +run(); + } + + /** + * @doesNotPerformAssertions + */ + public function testNoError() + { + $value = 1; + } + + public function testY() + { + self::assertFalse((bool) 'value', 'some messsage'); + self::assertTrue(true); + } +} + +?> diff --git a/packages/NetteTesterToPHPUnit/tests/Rector/Class_/NetteTesterClassToPHPUnitClassRector/NetteTesterClassToPHPUnitClassRectorTest.php b/packages/NetteTesterToPHPUnit/tests/Rector/Class_/NetteTesterClassToPHPUnitClassRector/NetteTesterClassToPHPUnitClassRectorTest.php new file mode 100644 index 00000000000..cd00cf6cb3f --- /dev/null +++ b/packages/NetteTesterToPHPUnit/tests/Rector/Class_/NetteTesterClassToPHPUnitClassRector/NetteTesterClassToPHPUnitClassRectorTest.php @@ -0,0 +1,34 @@ +getTempPath()); + + $this->doTestFiles([__DIR__ . '/Fixture/fixture.php.inc', __DIR__ . '/Fixture/assert_type.php.inc']); + } + + protected function getRectorClass(): string + { + return NetteTesterClassToPHPUnitClassRector::class; + } + + /** + * @return string[]|null + */ + protected function getRectorConfiguration(): ?array + { + return [ + '$netteTesterTestCaseClass' => NetteTesterTestCase::class, + ]; + } +} diff --git a/packages/NetteTesterToPHPUnit/tests/Rector/Class_/NetteTesterClassToPHPUnitClassRector/Source/NetteTesterTestCase.php b/packages/NetteTesterToPHPUnit/tests/Rector/Class_/NetteTesterClassToPHPUnitClassRector/Source/NetteTesterTestCase.php new file mode 100644 index 00000000000..dc75f0c745e --- /dev/null +++ b/packages/NetteTesterToPHPUnit/tests/Rector/Class_/NetteTesterClassToPHPUnitClassRector/Source/NetteTesterTestCase.php @@ -0,0 +1,10 @@ +assertStringContains('foo', 'foo bar'); - $this->assertStringNotContains('foo', 'foo bar'); + $this->assertStringContainsString('foo', 'foo bar'); + $this->assertStringNotContainsString('foo', 'foo bar'); } } CODE_SAMPLE diff --git a/packages/PhpSpecToPHPUnit/src/Naming/PhpSpecRenaming.php b/packages/PhpSpecToPHPUnit/src/Naming/PhpSpecRenaming.php index 938ce550628..678221ce02a 100644 --- a/packages/PhpSpecToPHPUnit/src/Naming/PhpSpecRenaming.php +++ b/packages/PhpSpecToPHPUnit/src/Naming/PhpSpecRenaming.php @@ -11,6 +11,7 @@ use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Namespace_; use Rector\Exception\ShouldNotHappenException; use Rector\NodeTypeResolver\Node\Attribute; +use Rector\PhpParser\Node\Resolver\NameResolver; use Rector\Util\RectorStrings; use Symplify\PackageBuilder\Strings\StringFormatConverter; @@ -21,13 +22,24 @@ final class PhpSpecRenaming */ private $stringFormatConverter; - public function __construct(StringFormatConverter $stringFormatConverter) + /** + * @var NameResolver + */ + private $nameResolver; + + public function __construct(StringFormatConverter $stringFormatConverter, NameResolver $nameResolver) { $this->stringFormatConverter = $stringFormatConverter; + $this->nameResolver = $nameResolver; } - public function renameMethod(ClassMethod $classMethod, string $name): void + public function renameMethod(ClassMethod $classMethod): void { + $name = $this->nameResolver->resolve($classMethod); + if ($name === null) { + return; + } + $name = RectorStrings::removePrefixes( $name, ['it_should_have_', 'it_should_be', 'it_should_', 'it_is_', 'it_', 'is_'] diff --git a/packages/PhpSpecToPHPUnit/src/Rector/Class_/PhpSpecClassToPHPUnitClassRector.php b/packages/PhpSpecToPHPUnit/src/Rector/Class_/PhpSpecClassToPHPUnitClassRector.php index b1e8ae3b3bf..7091bed9501 100644 --- a/packages/PhpSpecToPHPUnit/src/Rector/Class_/PhpSpecClassToPHPUnitClassRector.php +++ b/packages/PhpSpecToPHPUnit/src/Rector/Class_/PhpSpecClassToPHPUnitClassRector.php @@ -230,23 +230,21 @@ CODE_SAMPLE private function processMethods(Class_ $class): void { - $methods = $this->classManipulator->getMethodsByName($class); - // let → setUp - foreach ($methods as $name => $method) { - if ($name === 'let') { + foreach ($this->classManipulator->getMethods($class) as $method) { + if ($this->isName($method, 'let')) { $this->processLetMethod($method); } else { /** @var string $name */ - $this->processTestMethod($method, $name); + $this->processTestMethod($method); } } } - private function processTestMethod(ClassMethod $classMethod, string $name): void + private function processTestMethod(ClassMethod $classMethod): void { // change name to phpunit test case format - $this->phpSpecRenaming->renameMethod($classMethod, $name); + $this->phpSpecRenaming->renameMethod($classMethod); // replace "$this" with "$this->{testedObject}" $this->callableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) { diff --git a/src/PhpParser/Node/Manipulator/ClassManipulator.php b/src/PhpParser/Node/Manipulator/ClassManipulator.php index 1c18cafc045..9d8afd05d1d 100644 --- a/src/PhpParser/Node/Manipulator/ClassManipulator.php +++ b/src/PhpParser/Node/Manipulator/ClassManipulator.php @@ -2,6 +2,7 @@ namespace Rector\PhpParser\Node\Manipulator; +use PhpParser\Node; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Name; use PhpParser\Node\Stmt; @@ -232,15 +233,11 @@ final class ClassManipulator /** * @return ClassMethod[] */ - public function getMethodsByName(Class_ $classNode): array + public function getMethods(Class_ $class): array { - $methodsByName = []; - foreach ($classNode->stmts as $stmt) { - if ($stmt instanceof ClassMethod) { - $methodsByName[(string) $stmt->name] = $stmt; - } - } - return $methodsByName; + return array_filter($class->stmts, function (Node $node) { + return $node instanceof ClassMethod; + }); } private function tryInsertBeforeFirstMethod(Class_ $classNode, Stmt $stmt): bool diff --git a/src/Testing/PHPUnit/AbstractRectorTestCase.php b/src/Testing/PHPUnit/AbstractRectorTestCase.php index c65495aa6ca..dda12647104 100644 --- a/src/Testing/PHPUnit/AbstractRectorTestCase.php +++ b/src/Testing/PHPUnit/AbstractRectorTestCase.php @@ -66,12 +66,8 @@ abstract class AbstractRectorTestCase extends AbstractKernelTestCase protected function provideConfig(): string { if ($this->getRectorClass() !== '') { // use local if not overloaded - $hash = Strings::substring( - md5($this->getRectorClass() . Json::encode($this->getRectorConfiguration())), - 0, - 10 - ); - $configFileTempPath = sprintf(sys_get_temp_dir() . '/rector_temp_tests/config_%s.yaml', $hash); + $fixtureHash = $this->createFixtureHash(); + $configFileTempPath = sprintf(sys_get_temp_dir() . '/rector_temp_tests/config_%s.yaml', $fixtureHash); // cache for 2nd run, similar to original config one if (file_exists($configFileTempPath)) { @@ -121,6 +117,11 @@ abstract class AbstractRectorTestCase extends AbstractKernelTestCase $this->autoloadTestFixture = true; } + protected function getTempPath(): string + { + return sys_get_temp_dir() . '/rector_temp_tests'; + } + /** * @return string[] */ @@ -170,11 +171,15 @@ abstract class AbstractRectorTestCase extends AbstractKernelTestCase { $hash = Strings::substring(md5($smartFileInfo->getRealPath()), 0, 5); - return sprintf( - sys_get_temp_dir() . '/rector_temp_tests/%s_%s_%s', - $prefix, - $hash, - $smartFileInfo->getBasename('.inc') + return sprintf($this->getTempPath() . '/%s_%s_%s', $prefix, $hash, $smartFileInfo->getBasename('.inc')); + } + + private function createFixtureHash(): string + { + return Strings::substring( + md5($this->getRectorClass() . Json::encode($this->getRectorConfiguration())), + 0, + 10 ); } }