mirror of
https://github.com/rectorphp/rector.git
synced 2025-02-24 11:44: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
|
||||
{
|
||||
// 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'));
|
||||
}
|
||||
|
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\Random;
|
||||
use Nette\Utils\Strings;
|
||||
use Rector\Core\Exception\ShouldNotHappenException;
|
||||
use Rector\Core\Testing\Contract\RunnableInterface;
|
||||
use Rector\Core\Testing\PHPUnit\Runnable\ClassLikeNamesSuffixer;
|
||||
use Rector\Core\Testing\PHPUnit\Runnable\RunnableClassFinder;
|
||||
use Symplify\SmartFileSystem\SmartFileInfo;
|
||||
|
||||
/**
|
||||
@ -29,33 +29,27 @@ trait RunnableRectorTrait
|
||||
$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
|
||||
$className = $this->getTemporaryClassName();
|
||||
$classFileInfo = Strings::replace($fileContent, '#class\\s+(\\S*)\\s+#', sprintf('class %s ', $className));
|
||||
$classNameSufixer = new ClassLikeNamesSuffixer();
|
||||
$suffixedFileContent = $classNameSufixer->suffixContent($fileContent, $classNameSuffix);
|
||||
|
||||
FileSystem::write($temporaryPath, $classFileInfo);
|
||||
FileSystem::write($temporaryPath, $suffixedFileContent);
|
||||
include_once $temporaryPath;
|
||||
|
||||
$matches = Strings::match($classFileInfo, '#\bnamespace (?<namespace>.*?);#');
|
||||
$namespace = $matches['namespace'] ?? '';
|
||||
$runnableClassFinder = new RunnableClassFinder();
|
||||
$runnableFullyQualifiedClassName = $runnableClassFinder->find($suffixedFileContent);
|
||||
|
||||
$fullyQualifiedClassName = $namespace . '\\' . $className;
|
||||
|
||||
if (! is_a($fullyQualifiedClassName, RunnableInterface::class, true)) {
|
||||
throw new ShouldNotHappenException();
|
||||
}
|
||||
|
||||
return new $fullyQualifiedClassName();
|
||||
return new $runnableFullyQualifiedClassName();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user