mirror of
https://github.com/rectorphp/rector.git
synced 2025-02-24 19:53:14 +01:00
correct prefixing of class in case of multiple mutual classes
This commit is contained in:
parent
3d0d42dcc1
commit
dd90e06881
@ -50,7 +50,7 @@ final class FixtureSplitter
|
|||||||
public 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
|
// 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, 15);
|
$hash = Strings::substring(md5($smartFileInfo->getRealPath()), -15);
|
||||||
|
|
||||||
return sprintf($this->tempPath . '/%s_%s_%s', $prefix, $hash, $smartFileInfo->getBasename('.inc'));
|
return sprintf($this->tempPath . '/%s_%s_%s', $prefix, $hash, $smartFileInfo->getBasename('.inc'));
|
||||||
}
|
}
|
||||||
|
53
src/Testing/PHPUnit/Runnable/ClassLikeNamesSuffixer.php
Normal file
53
src/Testing/PHPUnit/Runnable/ClassLikeNamesSuffixer.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Rector\Core\Testing\PHPUnit\Runnable;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\NodeTraverser;
|
||||||
|
use PhpParser\Parser;
|
||||||
|
use PhpParser\ParserFactory;
|
||||||
|
use PhpParser\PrettyPrinter\Standard;
|
||||||
|
use Rector\Core\Testing\PHPUnit\Runnable\NodeVisitor\ClassLikeNameCollectingNodeVisitor;
|
||||||
|
use Rector\Core\Testing\PHPUnit\Runnable\NodeVisitor\PrefixingClassLikeNamesNodeVisitor;
|
||||||
|
|
||||||
|
final class ClassLikeNamesSuffixer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Parser
|
||||||
|
*/
|
||||||
|
private $parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Standard
|
||||||
|
*/
|
||||||
|
private $standardPrinter;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
|
||||||
|
$this->standardPrinter = new Standard();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function suffixContent(string $content, string $classSuffix): string
|
||||||
|
{
|
||||||
|
/** @var Node[] $nodes */
|
||||||
|
$nodes = $this->parser->parse($content);
|
||||||
|
|
||||||
|
// collect all class, trait, interface local names, e.g. class <name>, interface <name>
|
||||||
|
$classLikeNameCollectingNodeVisitor = new ClassLikeNameCollectingNodeVisitor();
|
||||||
|
$nodeTraverser = new NodeTraverser();
|
||||||
|
$nodeTraverser->addVisitor($classLikeNameCollectingNodeVisitor);
|
||||||
|
$nodeTraverser->traverse($nodes);
|
||||||
|
|
||||||
|
$classLikeNames = $classLikeNameCollectingNodeVisitor->getClassLikeNames();
|
||||||
|
|
||||||
|
// replace those class names in code
|
||||||
|
$nodeTraverser = new NodeTraverser();
|
||||||
|
$nodeTraverser->addVisitor(new PrefixingClassLikeNamesNodeVisitor($classLikeNames, $classSuffix));
|
||||||
|
$nodes = $nodeTraverser->traverse($nodes);
|
||||||
|
|
||||||
|
return $this->standardPrinter->prettyPrintFile($nodes);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Rector\Core\Testing\PHPUnit\Runnable\NodeVisitor;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt\ClassLike;
|
||||||
|
use PhpParser\NodeVisitorAbstract;
|
||||||
|
|
||||||
|
final class ClassLikeNameCollectingNodeVisitor extends NodeVisitorAbstract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
private $classLikeNames = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getClassLikeNames(): array
|
||||||
|
{
|
||||||
|
return $this->classLikeNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enterNode(Node $node)
|
||||||
|
{
|
||||||
|
if (! $node instanceof ClassLike) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($node->name === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->classLikeNames[] = $node->name->toString();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Rector\Core\Testing\PHPUnit\Runnable\NodeVisitor;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\New_;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
use PhpParser\Node\Name;
|
||||||
|
use PhpParser\Node\Stmt\Class_;
|
||||||
|
use PhpParser\Node\Stmt\ClassLike;
|
||||||
|
use PhpParser\NodeVisitorAbstract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Very dummy, use carefully and extend if needed
|
||||||
|
*/
|
||||||
|
final class PrefixingClassLikeNamesNodeVisitor extends NodeVisitorAbstract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
private $classLikeNames = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $suffix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $classLikeNames
|
||||||
|
*/
|
||||||
|
public function __construct(array $classLikeNames, string $suffix)
|
||||||
|
{
|
||||||
|
$this->classLikeNames = $classLikeNames;
|
||||||
|
$this->suffix = $suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enterNode(Node $node): ?Node
|
||||||
|
{
|
||||||
|
if ($node instanceof ClassLike) {
|
||||||
|
return $this->refactorClassLike($node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($node instanceof New_) {
|
||||||
|
return $this->refactorNew($node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function refactorClassLike(ClassLike $classLike): ?ClassLike
|
||||||
|
{
|
||||||
|
if ($classLike->name === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rename extends
|
||||||
|
if ($classLike instanceof Class_) {
|
||||||
|
$this->refactorClass($classLike);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->classLikeNames as $classLikeName) {
|
||||||
|
$className = $classLike->name->toString();
|
||||||
|
if ($className !== $classLikeName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$classLike->name = new Identifier($classLikeName . '_' . $this->suffix);
|
||||||
|
return $classLike;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function refactorNew(New_ $new): ?New_
|
||||||
|
{
|
||||||
|
if (! $new->class instanceof Name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->classLikeNames as $classLikeName) {
|
||||||
|
$className = $new->class->toString();
|
||||||
|
if ($className !== $classLikeName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$new->class = new Name($classLikeName . '_' . $this->suffix);
|
||||||
|
return $new;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function refactorClass(Class_ $class): void
|
||||||
|
{
|
||||||
|
if ($class->extends === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$extends = $class->extends->toString();
|
||||||
|
|
||||||
|
foreach ($this->classLikeNames as $classLikeName) {
|
||||||
|
if ($extends !== $classLikeName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$class->extends = new Name($extends . '_' . $this->suffix);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
83
src/Testing/PHPUnit/Runnable/RunnableClassFinder.php
Normal file
83
src/Testing/PHPUnit/Runnable/RunnableClassFinder.php
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Rector\Core\Testing\PHPUnit\Runnable;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt\Class_;
|
||||||
|
use PhpParser\NodeFinder;
|
||||||
|
use PhpParser\NodeTraverser;
|
||||||
|
use PhpParser\NodeVisitor\NameResolver;
|
||||||
|
use PhpParser\Parser;
|
||||||
|
use PhpParser\ParserFactory;
|
||||||
|
use Rector\Core\Exception\ShouldNotHappenException;
|
||||||
|
use Rector\Core\Testing\Contract\RunnableInterface;
|
||||||
|
|
||||||
|
final class RunnableClassFinder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Parser
|
||||||
|
*/
|
||||||
|
private $parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var NodeFinder
|
||||||
|
*/
|
||||||
|
private $nodeFinder;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
|
||||||
|
$this->nodeFinder = new NodeFinder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find(string $content): string
|
||||||
|
{
|
||||||
|
/** @var Node[] $nodes */
|
||||||
|
$nodes = $this->parser->parse($content);
|
||||||
|
$this->decorateNodesWithNames($nodes);
|
||||||
|
|
||||||
|
$class = $this->findClassThatImplementsRunnableInterface($nodes);
|
||||||
|
|
||||||
|
return (string) $class->namespacedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Node[] $nodes
|
||||||
|
*/
|
||||||
|
private function decorateNodesWithNames(array $nodes): void
|
||||||
|
{
|
||||||
|
$nodeTraverser = new NodeTraverser();
|
||||||
|
$nodeTraverser->addVisitor(new NameResolver(null, ['preserveOriginalNames' => true]));
|
||||||
|
$nodeTraverser->traverse($nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Node[] $nodes
|
||||||
|
*/
|
||||||
|
private function findClassThatImplementsRunnableInterface(array $nodes): Class_
|
||||||
|
{
|
||||||
|
$class = $this->nodeFinder->findFirst($nodes, function (Node $node) {
|
||||||
|
if (! $node instanceof Class_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ((array) $node->implements as $implement) {
|
||||||
|
if ((string) $implement !== RunnableInterface::class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (! $class instanceof Class_) {
|
||||||
|
throw new ShouldNotHappenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $class;
|
||||||
|
}
|
||||||
|
}
|
@ -6,9 +6,9 @@ namespace Rector\Core\Testing\PHPUnit;
|
|||||||
|
|
||||||
use Nette\Utils\FileSystem;
|
use Nette\Utils\FileSystem;
|
||||||
use Nette\Utils\Random;
|
use Nette\Utils\Random;
|
||||||
use Nette\Utils\Strings;
|
|
||||||
use Rector\Core\Exception\ShouldNotHappenException;
|
|
||||||
use Rector\Core\Testing\Contract\RunnableInterface;
|
use Rector\Core\Testing\Contract\RunnableInterface;
|
||||||
|
use Rector\Core\Testing\PHPUnit\Runnable\ClassLikeNamesSuffixer;
|
||||||
|
use Rector\Core\Testing\PHPUnit\Runnable\RunnableClassFinder;
|
||||||
use Symplify\SmartFileSystem\SmartFileInfo;
|
use Symplify\SmartFileSystem\SmartFileInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,33 +29,27 @@ trait RunnableRectorTrait
|
|||||||
$this->assertSame($expectedResult, $actualResult);
|
$this->assertSame($expectedResult, $actualResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTemporaryClassName(): string
|
private function getTemporaryClassSuffix(): string
|
||||||
{
|
{
|
||||||
return 'ClassName_' . Random::generate(20);
|
return Random::generate(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createRunnableClass(SmartFileInfo $classFileInfo): RunnableInterface
|
private function createRunnableClass(SmartFileInfo $classFileContent): RunnableInterface
|
||||||
{
|
{
|
||||||
$temporaryPath = $this->fixtureSplitter->createTemporaryPathWithPrefix($classFileInfo, 'runnable');
|
$temporaryPath = $this->fixtureSplitter->createTemporaryPathWithPrefix($classFileContent, 'runnable');
|
||||||
|
|
||||||
$fileContent = $classFileInfo->getContents();
|
$fileContent = $classFileContent->getContents();
|
||||||
|
$classNameSuffix = $this->getTemporaryClassSuffix();
|
||||||
|
|
||||||
// use unique class name for before and for after class, so both can be instantiated
|
$classNameSufixer = new ClassLikeNamesSuffixer();
|
||||||
$className = $this->getTemporaryClassName();
|
$suffixedFileContent = $classNameSufixer->suffixContent($fileContent, $classNameSuffix);
|
||||||
$classFileInfo = Strings::replace($fileContent, '#class\\s+(\\S*)\\s+#', sprintf('class %s ', $className));
|
|
||||||
|
|
||||||
FileSystem::write($temporaryPath, $classFileInfo);
|
FileSystem::write($temporaryPath, $suffixedFileContent);
|
||||||
include_once $temporaryPath;
|
include_once $temporaryPath;
|
||||||
|
|
||||||
$matches = Strings::match($classFileInfo, '#\bnamespace (?<namespace>.*?);#');
|
$runnableClassFinder = new RunnableClassFinder();
|
||||||
$namespace = $matches['namespace'] ?? '';
|
$runnableFullyQualifiedClassName = $runnableClassFinder->find($suffixedFileContent);
|
||||||
|
|
||||||
$fullyQualifiedClassName = $namespace . '\\' . $className;
|
return new $runnableFullyQualifiedClassName();
|
||||||
|
|
||||||
if (! is_a($fullyQualifiedClassName, RunnableInterface::class, true)) {
|
|
||||||
throw new ShouldNotHappenException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new $fullyQualifiedClassName();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user