correct prefixing of class in case of multiple mutual classes

This commit is contained in:
TomasVotruba 2020-06-21 13:19:47 +02:00
parent 3d0d42dcc1
commit dd90e06881
6 changed files with 302 additions and 21 deletions

View File

@ -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'));
}

View 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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View 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;
}
}

View File

@ -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();
}
}