diff --git a/composer.json b/composer.json
index a8e77408e756..3c3e67f07206 100644
--- a/composer.json
+++ b/composer.json
@@ -43,14 +43,14 @@
"friendsofphp/php-cs-fixer": "^2.16",
"ocramius/package-versions": "^1.4|^1.5",
"phpunit/phpunit": "^8.5|^9.0",
+ "psr/event-dispatcher": "^1.0",
"slam/phpstan-extensions": "^5.0",
+ "slevomat/coding-standard": "dev-master",
"symplify/changelog-linker": "^8.0",
"symplify/easy-coding-standard": "^8.0",
"symplify/monorepo-builder": "^8.0",
"symplify/phpstan-extensions": "^8.0",
- "thecodingmachine/phpstan-strict-rules": "^0.12",
- "psr/event-dispatcher": "^1.0",
- "slevomat/coding-standard": "dev-master"
+ "thecodingmachine/phpstan-strict-rules": "^0.12"
},
"replace": {
"rector/rector-prefixed": "self.version"
diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md
index e3a2cfd4b58e..39fd21df9118 100644
--- a/docs/rector_rules_overview.md
+++ b/docs/rector_rules_overview.md
@@ -7371,8 +7371,9 @@ each() function is deprecated, use key() and current() instead
```diff
-list($key, $callback) = each($callbacks);
-+$key = key($opt->option);
-+$val = current($opt->option);
++$key = key($callbacks);
++$callback = current($callbacks);
++next($callbacks);
```
diff --git a/rules/code-quality/tests/Rector/FuncCall/ArrayKeysAndInArrayToArrayKeyExistsRector/ArrayKeysAndInArrayToArrayKeyExistsRectorTest.php b/rules/code-quality/tests/Rector/FuncCall/ArrayKeysAndInArrayToArrayKeyExistsRector/ArrayKeysAndInArrayToArrayKeyExistsRectorTest.php
index 4ad8193d88d3..6899f6c75a90 100644
--- a/rules/code-quality/tests/Rector/FuncCall/ArrayKeysAndInArrayToArrayKeyExistsRector/ArrayKeysAndInArrayToArrayKeyExistsRectorTest.php
+++ b/rules/code-quality/tests/Rector/FuncCall/ArrayKeysAndInArrayToArrayKeyExistsRector/ArrayKeysAndInArrayToArrayKeyExistsRectorTest.php
@@ -6,9 +6,9 @@
use Iterator;
use Rector\CodeQuality\Rector\FuncCall\ArrayKeysAndInArrayToArrayKeyExistsRector;
-use Rector\Core\Testing\PHPUnit\AbstractRunnableRectorTestCase;
+use Rector\Core\Testing\PHPUnit\AbstractRectorTestCase;
-final class ArrayKeysAndInArrayToArrayKeyExistsRectorTest extends AbstractRunnableRectorTestCase
+final class ArrayKeysAndInArrayToArrayKeyExistsRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
@@ -16,7 +16,6 @@ final class ArrayKeysAndInArrayToArrayKeyExistsRectorTest extends AbstractRunnab
public function test(string $file): void
{
$this->doTestFile($file);
- $this->assertOriginalAndFixedFileResultEquals($file);
}
public function provideData(): Iterator
diff --git a/rules/code-quality/tests/Rector/FuncCall/ArrayKeysAndInArrayToArrayKeyExistsRector/Fixture/fixture.php.inc b/rules/code-quality/tests/Rector/FuncCall/ArrayKeysAndInArrayToArrayKeyExistsRector/Fixture/fixture.php.inc
index 46d6fbbb255e..c46f8a024bf9 100644
--- a/rules/code-quality/tests/Rector/FuncCall/ArrayKeysAndInArrayToArrayKeyExistsRector/Fixture/fixture.php.inc
+++ b/rules/code-quality/tests/Rector/FuncCall/ArrayKeysAndInArrayToArrayKeyExistsRector/Fixture/fixture.php.inc
@@ -2,16 +2,17 @@
namespace Rector\CodeQuality\Tests\Rector\FuncCall\ArrayKeysAndInArrayToArrayKeyExistsRector\Fixture;
-use Rector\Core\Testing\PHPUnit\RunnableInterface;
+use Rector\Core\Testing\Contract\RunnableInterface;
class SomeClass implements RunnableInterface
{
public function run()
{
$packageName = "foo";
- $values = ["foo" => "bar"];
-
+ $values = ["foo" => "bar"];
+
$keys = array_keys($values);
+
return in_array($packageName, $keys, true);
}
}
@@ -22,14 +23,15 @@ class SomeClass implements RunnableInterface
namespace Rector\CodeQuality\Tests\Rector\FuncCall\ArrayKeysAndInArrayToArrayKeyExistsRector\Fixture;
-use Rector\Core\Testing\PHPUnit\RunnableInterface;
+use Rector\Core\Testing\Contract\RunnableInterface;
class SomeClass implements RunnableInterface
{
public function run()
{
$packageName = "foo";
- $values = ["foo" => "bar"];
+ $values = ["foo" => "bar"];
+
return array_key_exists($packageName, $values);
}
}
diff --git a/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceEventManagerWithEventSubscriberRector/ReplaceEventManagerWithEventSubscriberRectorTest.php b/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceEventManagerWithEventSubscriberRector/ReplaceEventManagerWithEventSubscriberRectorTest.php
index 1f68af95835d..7d2a9bc83261 100644
--- a/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceEventManagerWithEventSubscriberRector/ReplaceEventManagerWithEventSubscriberRectorTest.php
+++ b/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceEventManagerWithEventSubscriberRector/ReplaceEventManagerWithEventSubscriberRectorTest.php
@@ -13,7 +13,7 @@ public function test(): void
{
$this->doTestFile(__DIR__ . '/Fixture/fixture.php.inc');
- $expectedEventFilePath = dirname($this->originalTempFile) . '/Event/SomeClassCopyEvent.php';
+ $expectedEventFilePath = $this->originalTempFileInfo->getPath() . '/Event/SomeClassCopyEvent.php';
$this->assertFileExists($expectedEventFilePath);
$this->assertFileEquals(__DIR__ . '/Source/ExpectedSomeClassCopyEvent.php', $expectedEventFilePath);
diff --git a/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/ReplaceMagicPropertyEventWithEventClassRectorTest.php b/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/ReplaceMagicPropertyEventWithEventClassRectorTest.php
index 28e286156ebf..f71fd63f7ab5 100644
--- a/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/ReplaceMagicPropertyEventWithEventClassRectorTest.php
+++ b/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/ReplaceMagicPropertyEventWithEventClassRectorTest.php
@@ -18,7 +18,7 @@ public function testSimpleEvent(): void
{
$this->doTestFile(__DIR__ . '/Fixture/simple_event.php.inc');
- $expectedEventFilePath = dirname($this->originalTempFile) . '/Event/FileManagerUploadEvent.php';
+ $expectedEventFilePath = $this->originalTempFileInfo->getPath() . '/Event/FileManagerUploadEvent.php';
$this->assertFileExists($expectedEventFilePath);
$this->assertFileEquals(__DIR__ . '/Source/ExpectedFileManagerUploadEvent.php', $expectedEventFilePath);
}
@@ -27,7 +27,7 @@ public function testDuplicatedEventParams(): void
{
$this->doTestFile(__DIR__ . '/Fixture/duplicated_event_params.php.inc');
- $expectedEventFilePath = dirname($this->originalTempFile) . '/Event/DuplicatedEventParamsUploadEvent.php';
+ $expectedEventFilePath = $this->originalTempFileInfo->getPath() . '/Event/DuplicatedEventParamsUploadEvent.php';
$this->assertFileExists($expectedEventFilePath);
$this->assertFileEquals(
__DIR__ . '/Source/ExpectedDuplicatedEventParamsUploadEvent.php',
diff --git a/rules/php71/tests/Rector/BinaryOp/IsIterableRector/Fixture/polyfill_function.php.inc b/rules/php71/tests/Rector/BinaryOp/IsIterableRector/Fixture/polyfill_function.php.inc
index a3eae8194ddb..e6afb08a3eb7 100644
--- a/rules/php71/tests/Rector/BinaryOp/IsIterableRector/Fixture/polyfill_function.php.inc
+++ b/rules/php71/tests/Rector/BinaryOp/IsIterableRector/Fixture/polyfill_function.php.inc
@@ -4,7 +4,7 @@ namespace Rector\Php71\Tests\Rector\BinaryOp\IsIterableRector\Fixture;
use Traversable;
-class PolyfillFunction
+class IsIterablePolyfillFunction
{
public function run($foo)
{
@@ -28,7 +28,7 @@ namespace Rector\Php71\Tests\Rector\BinaryOp\IsIterableRector\Fixture;
use Traversable;
-class PolyfillFunction
+class IsIterablePolyfillFunction
{
public function run($foo)
{
diff --git a/rules/php72/src/Rector/Each/ListEachRector.php b/rules/php72/src/Rector/Each/ListEachRector.php
index 94bea7a46027..f21b6ac7fe35 100644
--- a/rules/php72/src/Rector/Each/ListEachRector.php
+++ b/rules/php72/src/Rector/Each/ListEachRector.php
@@ -8,7 +8,6 @@
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\List_;
-use PhpParser\Node\Stmt\Do_;
use PhpParser\Node\Stmt\Expression;
use Rector\Core\PhpParser\Node\Manipulator\AssignManipulator;
use Rector\Core\Rector\AbstractRector;
@@ -44,8 +43,9 @@ public function getDefinition(): RectorDefinition
PHP
,
<<<'PHP'
-$key = key($opt->option);
-$val = current($opt->option);
+$key = key($callbacks);
+$callback = current($callbacks);
+next($callbacks);
PHP
),
]
@@ -94,15 +94,13 @@ public function refactor(Node $node): ?Node
// ↓
// $key = key($values);
// $value = current($values);
- // next($values); - only inside a loop
+ // next($values);
$currentFuncCall = $this->createFuncCall('current', $eachFuncCall->args);
$assignCurrentNode = new Assign($listNode->items[1]->value, $currentFuncCall);
$this->addNodeAfterNode($assignCurrentNode, $node);
- if ($this->isInsideDoWhile($node)) {
- $nextFuncCall = $this->createFuncCall('next', $eachFuncCall->args);
- $this->addNodeAfterNode($nextFuncCall, $node);
- }
+ $nextFuncCall = $this->createFuncCall('next', $eachFuncCall->args);
+ $this->addNodeAfterNode($nextFuncCall, $node);
$keyFuncCall = $this->createFuncCall('key', $eachFuncCall->args);
return new Assign($listNode->items[0]->value, $keyFuncCall);
@@ -130,19 +128,4 @@ private function shouldSkip(Assign $assign): bool
// empty list → cannot handle
return $listNode->items[0] === null && $listNode->items[1] === null;
}
-
- /**
- * Is inside the "do {} while ();" loop → need to add "next()"
- */
- private function isInsideDoWhile(Node $assignNode): bool
- {
- $parentNode = $assignNode->getAttribute(AttributeKey::PARENT_NODE);
- if (! $parentNode instanceof Expression) {
- return false;
- }
-
- $parentParentNode = $parentNode->getAttribute(AttributeKey::PARENT_NODE);
-
- return $parentParentNode instanceof Do_;
- }
}
diff --git a/rules/php72/tests/Rector/Each/Fixture/fixture2.php.inc b/rules/php72/tests/Rector/Each/Fixture/fixture2.php.inc
index 76151eee6d58..3fed9c3630ce 100644
--- a/rules/php72/tests/Rector/Each/Fixture/fixture2.php.inc
+++ b/rules/php72/tests/Rector/Each/Fixture/fixture2.php.inc
@@ -23,6 +23,7 @@ function each2
{
$key = key($opt->option);
$val = current($opt->option);
+ next($opt->option);
$tid = key($option->option);
diff --git a/rules/php72/tests/Rector/Each/Fixture/list_each_next.php.inc b/rules/php72/tests/Rector/Each/Fixture/list_each_next.php.inc
new file mode 100644
index 000000000000..8c485138e71d
--- /dev/null
+++ b/rules/php72/tests/Rector/Each/Fixture/list_each_next.php.inc
@@ -0,0 +1,47 @@
+ 1, 'b' => 2];
+
+ list($key, $value) = each($parentArray);
+
+ list($key2, $value2) = each($parentArray);
+
+ return [$key, $value, $parentArray, $key2, $value2];
+ }
+}
+
+?>
+-----
+ 1, 'b' => 2];
+
+ $key = key($parentArray);
+ $value = current($parentArray);
+ next($parentArray);
+
+ $key2 = key($parentArray);
+ $value2 = current($parentArray);
+ next($parentArray);
+
+ return [$key, $value, $parentArray, $key2, $value2];
+ }
+}
+
+?>
diff --git a/src/Testing/PHPUnit/RunnableInterface.php b/src/Testing/Contract/RunnableInterface.php
similarity index 69%
rename from src/Testing/PHPUnit/RunnableInterface.php
rename to src/Testing/Contract/RunnableInterface.php
index 7ffab39a7a16..c1a68e9aec82 100644
--- a/src/Testing/PHPUnit/RunnableInterface.php
+++ b/src/Testing/Contract/RunnableInterface.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Rector\Core\Testing\PHPUnit;
+namespace Rector\Core\Testing\Contract;
interface RunnableInterface
{
diff --git a/src/Testing/PHPUnit/AbstractRectorTestCase.php b/src/Testing/PHPUnit/AbstractRectorTestCase.php
index 552a60c3d274..035ff9a762c6 100644
--- a/src/Testing/PHPUnit/AbstractRectorTestCase.php
+++ b/src/Testing/PHPUnit/AbstractRectorTestCase.php
@@ -5,6 +5,7 @@
namespace Rector\Core\Testing\PHPUnit;
use Nette\Utils\FileSystem;
+use Nette\Utils\Strings;
use PHPStan\Analyser\NodeScopeResolver;
use PHPUnit\Framework\ExpectationFailedException;
use Psr\Container\ContainerInterface;
@@ -17,6 +18,7 @@
use Rector\Core\HttpKernel\RectorKernel;
use Rector\Core\Stubs\StubLoader;
use Rector\Core\Testing\Application\EnabledRectorsProvider;
+use Rector\Core\Testing\Contract\RunnableInterface;
use Rector\Core\Testing\Finder\RectorsFinder;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
@@ -27,6 +29,8 @@
abstract class AbstractRectorTestCase extends AbstractGenericRectorTestCase
{
+ use RunnableRectorTrait;
+
/**
* @var FileProcessor
*/
@@ -38,19 +42,19 @@ abstract class AbstractRectorTestCase extends AbstractGenericRectorTestCase
protected $parameterProvider;
/**
- * @var string
+ * @var SmartFileInfo
*/
- protected $originalTempFile;
+ protected $originalTempFileInfo;
/**
- * @var bool
+ * @var FixtureSplitter
*/
- private $autoloadTestFixture = true;
+ protected $fixtureSplitter;
/**
- * @var FixtureSplitter
+ * @var bool
*/
- private $fixtureSplitter;
+ private $autoloadTestFixture = true;
/**
* @var Container|ContainerInterface|null
@@ -130,21 +134,23 @@ protected function doTestFileWithoutAutoload(string $file): void
protected function doTestFile(string $fixtureFile): void
{
- $smartFileInfo = new SmartFileInfo($fixtureFile);
- [$originalFile, $changedFile] = $this->fixtureSplitter->splitContentToOriginalFileAndExpectedFile(
- $smartFileInfo,
+ $fixtureFileInfo = new SmartFileInfo($fixtureFile);
+
+ [$originalFileInfo, $expectedFileInfo] = $this->fixtureSplitter->splitContentToOriginalFileAndExpectedFile(
+ $fixtureFileInfo,
$this->autoloadTestFixture
);
- $this->nodeScopeResolver->setAnalysedFiles([$originalFile]);
+ $this->nodeScopeResolver->setAnalysedFiles([$originalFileInfo->getRealPath()]);
- $this->doTestFileMatchesExpectedContent(
- $originalFile,
- $changedFile,
- $smartFileInfo->getRelativeFilePathFromCwd()
- );
+ $this->doTestFileMatchesExpectedContent($originalFileInfo, $expectedFileInfo, $fixtureFileInfo);
+
+ $this->originalTempFileInfo = $originalFileInfo;
- $this->originalTempFile = $originalFile;
+ // runnable?
+ if (Strings::contains($originalFileInfo->getContents(), RunnableInterface::class)) {
+ $this->assertOriginalAndFixedFileResultEquals($originalFileInfo, $expectedFileInfo);
+ }
}
protected function getTempPath(): string
@@ -238,39 +244,32 @@ private function configurePhpVersionFeatures(): void
}
private function doTestFileMatchesExpectedContent(
- string $originalFile,
- string $expectedFile,
- string $fixtureFile
+ SmartFileInfo $originalFileInfo,
+ SmartFileInfo $expectedFileInfo,
+ SmartFileInfo $fixtureFileInfo
): void {
- $this->setParameter(Option::SOURCE, [$originalFile]);
-
- $smartFileInfo = new SmartFileInfo($originalFile);
+ $this->setParameter(Option::SOURCE, [$originalFileInfo->getRealPath()]);
// life-cycle trio :)
- $this->fileProcessor->parseFileInfoToLocalCache($smartFileInfo);
- $this->fileProcessor->refactor($smartFileInfo);
+ $this->fileProcessor->parseFileInfoToLocalCache($originalFileInfo);
+ $this->fileProcessor->refactor($originalFileInfo);
- $changedContent = $this->fileProcessor->printToString($smartFileInfo);
+ $changedContent = $this->fileProcessor->printToString($originalFileInfo);
- $causedByFixtureMessage = $this->createCausedByFixtureMessage($fixtureFile);
+ $causedByFixtureMessage = $fixtureFileInfo->getRelativeFilePathFromCwd();
$removedAndAddedFilesProcessor = self::$container->get(RemovedAndAddedFilesProcessor::class);
$removedAndAddedFilesProcessor->run();
try {
- $this->assertStringEqualsFile($expectedFile, $changedContent, $causedByFixtureMessage);
+ $this->assertStringEqualsFile($expectedFileInfo->getRealPath(), $changedContent, $causedByFixtureMessage);
} catch (ExpectationFailedException $expectationFailedException) {
- $expectedFileContent = FileSystem::read($expectedFile);
+ $expectedFileContent = $expectedFileInfo->getContents();
$this->assertStringMatchesFormat($expectedFileContent, $changedContent, $causedByFixtureMessage);
}
}
- private function createCausedByFixtureMessage(string $fixtureFile): string
- {
- return (new SmartFileInfo($fixtureFile))->getRelativeFilePathFromCwd();
- }
-
private function createRectorRepositoryContainer(): void
{
if (self::$allRectorContainer === null) {
diff --git a/src/Testing/PHPUnit/AbstractRunnableRectorTestCase.php b/src/Testing/PHPUnit/AbstractRunnableRectorTestCase.php
deleted file mode 100644
index 3710da738960..000000000000
--- a/src/Testing/PHPUnit/AbstractRunnableRectorTestCase.php
+++ /dev/null
@@ -1,68 +0,0 @@
- refactor in a method
- */
- $smartFileInfo = new SmartFileInfo($file);
- if (Strings::match($smartFileInfo->getContents(), SplitLine::SPLIT_LINE)) {
- [$originalContent, $expectedContent] = Strings::split($smartFileInfo->getContents(), SplitLine::SPLIT_LINE);
- } else {
- $originalContent = $smartFileInfo->getContents();
- $expectedContent = $originalContent;
- }
-
- $originalInstance = $this->loadClass($originalContent);
- if ($originalInstance !== null) {
- $expectedInstance = $this->loadClass($expectedContent);
- if ($expectedInstance !== null) {
- $actual = $originalInstance->run();
- $expected = $expectedInstance->run();
-
- $this->assertSame($actual, $expected);
- }
- }
- }
-
- protected function getTemporaryClassName(): string
- {
- $testName = (new ReflectionClass(static::class))->getShortName();
- // Todo - pull in Ramsey UUID to generate temporay class names?
-// $uuid = Uuid::uuid4()->toString();
- $uuid = md5((string) random_int(0, 100000000));
- $className = $testName . '_' . $uuid;
-
- return Strings::replace($className, '#[^0-9a-zA-Z]#', '_');
- }
-
- protected function loadClass(string $classContent): ?RunnableInterface
- {
- $className = $this->getTemporaryClassName();
- $loadable = Strings::replace($classContent, '#\\s*<\\?php#', '');
- $loadable = Strings::replace($loadable, '#\\s*namespace.*;#', '');
- $loadable = Strings::replace($loadable, '#class\\s+(\\S*)\\s+#', sprintf('class %s ', $className));
- eval($loadable);
- if (is_a($className, RunnableInterface::class, true)) {
- /**
- * @var RunnableInterface
- */
- return new $className();
- }
- return null;
- }
-}
diff --git a/src/Testing/PHPUnit/FixtureSplitter.php b/src/Testing/PHPUnit/FixtureSplitter.php
index 551641da893f..89728b568ff8 100644
--- a/src/Testing/PHPUnit/FixtureSplitter.php
+++ b/src/Testing/PHPUnit/FixtureSplitter.php
@@ -22,20 +22,13 @@ public function __construct(string $tempPath)
}
/**
- * @return string[]
+ * @return SmartFileInfo[]
*/
public function splitContentToOriginalFileAndExpectedFile(
SmartFileInfo $smartFileInfo,
bool $autoloadTestFixture
): array {
- if (Strings::match($smartFileInfo->getContents(), SplitLine::SPLIT_LINE)) {
- // original → expected
- [$originalContent, $expectedContent] = Strings::split($smartFileInfo->getContents(), SplitLine::SPLIT_LINE);
- } else {
- // no changes
- $originalContent = $smartFileInfo->getContents();
- $expectedContent = $originalContent;
- }
+ [$originalContent, $expectedContent] = $this->resolveBeforeAfterFixtureContent($smartFileInfo);
$originalFile = $this->createTemporaryPathWithPrefix($smartFileInfo, 'original');
$expectedFile = $this->createTemporaryPathWithPrefix($smartFileInfo, 'expected');
@@ -48,14 +41,34 @@ public function splitContentToOriginalFileAndExpectedFile(
require_once $originalFile;
}
- return [$originalFile, $expectedFile];
+ $originalFileInfo = new SmartFileInfo($originalFile);
+ $expectedFileInfo = new SmartFileInfo($expectedFile);
+
+ return [$originalFileInfo, $expectedFileInfo];
}
- private function createTemporaryPathWithPrefix(SmartFileInfo $smartFileInfo, string $prefix): string
+ public function createTemporaryPathWithPrefix(SmartFileInfo $smartFileInfo, string $prefix): string
{
// warning: if this hash is too short, the file can becom "identical"; took me 1 hour to find out
- $hash = Strings::substring(md5($smartFileInfo->getRealPath()), 0, 12);
+ $hash = Strings::substring(md5($smartFileInfo->getRealPath()), 0, 15);
return sprintf($this->tempPath . '/%s_%s_%s', $prefix, $hash, $smartFileInfo->getBasename('.inc'));
}
+
+ /**
+ * @return string[]
+ */
+ private function resolveBeforeAfterFixtureContent(SmartFileInfo $smartFileInfo): array
+ {
+ if (Strings::match($smartFileInfo->getContents(), SplitLine::SPLIT_LINE)) {
+ // original → expected
+ [$originalContent, $expectedContent] = Strings::split($smartFileInfo->getContents(), SplitLine::SPLIT_LINE);
+ } else {
+ // no changes
+ $originalContent = $smartFileInfo->getContents();
+ $expectedContent = $originalContent;
+ }
+
+ return [$originalContent, $expectedContent];
+ }
}
diff --git a/src/Testing/PHPUnit/RunnableRectorTrait.php b/src/Testing/PHPUnit/RunnableRectorTrait.php
new file mode 100644
index 000000000000..4946780ed00a
--- /dev/null
+++ b/src/Testing/PHPUnit/RunnableRectorTrait.php
@@ -0,0 +1,61 @@
+createRunnableClass($originalFileInfo);
+ $expectedInstance = $this->createRunnableClass($expectedFileInfo);
+
+ $actualResult = $originalInstance->run();
+ $expectedResult = $expectedInstance->run();
+
+ $this->assertSame($expectedResult, $actualResult);
+ }
+
+ private function getTemporaryClassName(): string
+ {
+ return 'ClassName_' . Random::generate(20);
+ }
+
+ private function createRunnableClass(SmartFileInfo $classFileInfo): RunnableInterface
+ {
+ $temporaryPath = $this->fixtureSplitter->createTemporaryPathWithPrefix($classFileInfo, 'runnable');
+
+ $fileContent = $classFileInfo->getContents();
+
+ // use unique class name for before and for after class, so both can be instantiated
+ $className = $this->getTemporaryClassName();
+ $classFileInfo = Strings::replace($fileContent, '#class\\s+(\\S*)\\s+#', sprintf('class %s ', $className));
+
+ FileSystem::write($temporaryPath, $classFileInfo);
+ include_once $temporaryPath;
+
+ $matches = Strings::match($classFileInfo, '#\bnamespace (?.*?);#');
+ $namespace = $matches['namespace'] ?? '';
+
+ $fullyQualifiedClassName = $namespace . '\\' . $className;
+
+ if (! is_a($fullyQualifiedClassName, RunnableInterface::class, true)) {
+ throw new ShouldNotHappenException();
+ }
+
+ return new $fullyQualifiedClassName();
+ }
+}