Merge pull request #3654 from rectorphp/template-fix

[Sension] improve template annotation
This commit is contained in:
kodiakhq[bot] 2020-07-05 19:20:01 +00:00 committed by GitHub
commit 771b2dc85d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 261 additions and 21 deletions

View File

@ -335,3 +335,5 @@ parameters:
- '#Parameter \#1 \$expr of method Rector\\MagicDisclosure\\Matcher\\ClassNameTypeMatcher\:\:doesExprMatchNames\(\) expects PhpParser\\Node\\Expr, PhpParser\\Node\\Expr\|null given#' - '#Parameter \#1 \$expr of method Rector\\MagicDisclosure\\Matcher\\ClassNameTypeMatcher\:\:doesExprMatchNames\(\) expects PhpParser\\Node\\Expr, PhpParser\\Node\\Expr\|null given#'
- '#Parameter \#1 \$objectType of method Rector\\Naming\\Naming\\PropertyNaming\:\:fqnToVariableName\(\) expects PHPStan\\Type\\ObjectType\|string, PHPStan\\Type\\Type given#' - '#Parameter \#1 \$objectType of method Rector\\Naming\\Naming\\PropertyNaming\:\:fqnToVariableName\(\) expects PHPStan\\Type\\ObjectType\|string, PHPStan\\Type\\Type given#'
- '#Method Rector\\Core\\PhpParser\\Node\\NodeFactory\:\:createConcat\(\) should return PhpParser\\Node\\Expr\\BinaryOp\\Concat\|null but returns PhpParser\\Node\\Expr#' - '#Method Rector\\Core\\PhpParser\\Node\\NodeFactory\:\:createConcat\(\) should return PhpParser\\Node\\Expr\\BinaryOp\\Concat\|null but returns PhpParser\\Node\\Expr#'
- '#Method Rector\\Core\\PhpParser\\Node\\BetterNodeFinder\:\:findFirstNonAnonymousClass\(\) should return PhpParser\\Node\\Stmt\\Class_\|null but returns PhpParser\\Node\|null#'

View File

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Rector\Sensio;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use Rector\CodingStyle\Naming\ClassNaming;
use Rector\Core\PhpParser\Node\BetterNodeFinder;
use Rector\Core\PhpParser\Parser\Parser;
use Rector\NodeNameResolver\NodeNameResolver;
use ReflectionClass;
use Symplify\SmartFileSystem\SmartFileInfo;
final class BundleClassResolver
{
/**
* @var Parser
*/
private $parser;
/**
* @var BetterNodeFinder
*/
private $betterNodeFinder;
/**
* @var ClassNaming
*/
private $classNaming;
/**
* @var NodeNameResolver
*/
private $nodeNameResolver;
public function __construct(
Parser $parser,
BetterNodeFinder $betterNodeFinder,
ClassNaming $classNaming,
NodeNameResolver $nodeNameResolver
) {
$this->parser = $parser;
$this->betterNodeFinder = $betterNodeFinder;
$this->classNaming = $classNaming;
$this->nodeNameResolver = $nodeNameResolver;
}
public function resolveShortBundleClassFromControllerClass(string $class): ?string
{
// resolve bundle from existing ones
$classReflection = new ReflectionClass($class);
$fileName = $classReflection->getFileName();
if (! $fileName) {
return null;
}
$controllerDirectory = dirname($fileName);
$rootFolder = getenv('SystemDrive', true) . DIRECTORY_SEPARATOR;
// traverse up, un-till first bundle class appears
$bundleFiles = [];
while ($bundleFiles === [] && $controllerDirectory !== $rootFolder) {
$bundleFiles = (array) glob($controllerDirectory . '/**Bundle.php');
$controllerDirectory = dirname($controllerDirectory);
}
if (count($bundleFiles) === 0) {
return null;
}
/** @var string $bundleFile */
$bundleFile = $bundleFiles[0];
$bundleClassName = $this->resolveClassNameFromFilePath($bundleFile);
if ($bundleClassName !== null) {
return $this->classNaming->getShortName($bundleClassName);
}
return null;
}
private function resolveClassNameFromFilePath(string $filePath): ?string
{
$fileInfo = new SmartFileInfo($filePath);
$nodes = $this->parser->parseFileInfo($fileInfo);
$this->addFullyQualifiedNamesToNodes($nodes);
$class = $this->betterNodeFinder->findFirstNonAnonymousClass($nodes);
if ($class === null) {
return null;
}
return $this->nodeNameResolver->getName($class);
}
/**
* @param Node[] $nodes
*/
private function addFullyQualifiedNamesToNodes(array $nodes): void
{
$nodeTraverser = new NodeTraverser();
$nameResolverNodeVisitor = new NameResolver();
$nodeTraverser->addVisitor($nameResolverNodeVisitor);
$nodeTraverser->traverse($nodes);
}
}

View File

@ -9,9 +9,10 @@ use PhpParser\Node\Stmt\ClassMethod;
use Rector\Core\Exception\ShouldNotHappenException; use Rector\Core\Exception\ShouldNotHappenException;
use Rector\NodeNameResolver\NodeNameResolver; use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\Sensio\BundleClassResolver;
/** /**
* @see \Rector\Sensio\Tests\Rector\FrameworkExtraBundle\TemplateAnnotationRector\TemplateAnnotationRectorTest * @see \Rector\Sensio\Tests\Rector\FrameworkExtraBundle\TemplateAnnotationToThisRenderRector\TemplateAnnotationToThisRenderRectorTest
*/ */
final class TemplateGuesser final class TemplateGuesser
{ {
@ -20,9 +21,15 @@ final class TemplateGuesser
*/ */
private $nodeNameResolver; private $nodeNameResolver;
public function __construct(NodeNameResolver $nodeNameResolver) /**
* @var BundleClassResolver
*/
private $bundleClassResolver;
public function __construct(NodeNameResolver $nodeNameResolver, BundleClassResolver $bundleClassResolver)
{ {
$this->nodeNameResolver = $nodeNameResolver; $this->nodeNameResolver = $nodeNameResolver;
$this->bundleClassResolver = $bundleClassResolver;
} }
public function resolveFromClassMethodNode(ClassMethod $classMethod): string public function resolveFromClassMethodNode(ClassMethod $classMethod): string
@ -50,17 +57,24 @@ final class TemplateGuesser
*/ */
private function resolve(string $namespace, string $class, string $method): string private function resolve(string $namespace, string $class, string $method): string
{ {
$bundle = Strings::match($namespace, '#(?<bundle>[\w]*Bundle)#')['bundle'] ?? ''; $bundle = $this->resolveBundle($class, $namespace);
$bundle = Strings::replace($bundle, '#Bundle$#'); $controller = $this->resolveController($class);
$bundle = $bundle !== '' ? '@' . $bundle . '/' : '';
$controller = $this->resolveControllerVersion5($class);
$action = Strings::replace($method, '#Action$#'); $action = Strings::replace($method, '#Action$#');
return sprintf('%s%s%s.html.twig', $bundle, $controller, $action); $fullPath = '';
if ($bundle !== '') {
$fullPath .= $bundle . '/';
}
if ($controller !== '') {
$fullPath .= $controller . '/';
}
return $fullPath . $action . '.html.twig';
} }
private function resolveControllerVersion5(string $class): string private function resolveController(string $class): string
{ {
$match = Strings::match($class, '#Controller\\\(.+)Controller$#'); $match = Strings::match($class, '#Controller\\\(.+)Controller$#');
if (! $match) { if (! $match) {
@ -68,9 +82,18 @@ final class TemplateGuesser
} }
$controller = Strings::replace($match[1], '#([a-z\d])([A-Z])#', '\\1_\\2'); $controller = Strings::replace($match[1], '#([a-z\d])([A-Z])#', '\\1_\\2');
$controller = strtolower($controller); return str_replace('\\', '/', $controller);
$controller = str_replace('\\', '/', $controller); }
return $controller !== '' ? $controller . '/' : ''; private function resolveBundle(string $class, string $namespace): string
{
$shortBundleClass = $this->bundleClassResolver->resolveShortBundleClassFromControllerClass($class);
if ($shortBundleClass !== null) {
return '@' . $shortBundleClass;
}
$bundle = Strings::match($namespace, '#(?<bundle>[\w]*Bundle)#')['bundle'] ?? '';
$bundle = Strings::replace($bundle, '#Bundle$#');
return $bundle !== '' ? '@' . $bundle : '';
} }
} }

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Rector\Sensio\Tests\Rector\FrameworkExtraBundle\TemplateAnnotationToThisRenderRector;
use Iterator;
use Nette\Utils\FileSystem;
use Rector\Core\Testing\PHPUnit\AbstractRectorTestCase;
use Rector\Sensio\Rector\FrameworkExtraBundle\TemplateAnnotationToThisRenderRector;
use Symplify\SmartFileSystem\SmartFileInfo;
final class DifferentBundleNameRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(SmartFileInfo $fileInfo): void
{
// prepare bundle path
$originalBundleFilePath = __DIR__ . '/FixtureDifferentBundleName/SomeActionBundle/DifferentNameBundle.php';
$temporaryBundleFilePath = $this->getTempPath() . '/DifferentNameBundle.php';
FileSystem::copy($originalBundleFilePath, $temporaryBundleFilePath, true);
$this->doTestFileInfo($fileInfo);
FileSystem::delete($temporaryBundleFilePath);
}
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/FixtureDifferentBundleName');
}
protected function getRectorClass(): string
{
return TemplateAnnotationToThisRenderRector::class;
}
}

View File

@ -51,7 +51,7 @@ final class ClassWithArrayAndManyResponseController extends AbstractController
if ($responseOrData instanceof \Symfony\Component\HttpFoundation\Response) { if ($responseOrData instanceof \Symfony\Component\HttpFoundation\Response) {
return $responseOrData; return $responseOrData;
} }
return $this->render('@App/class_with_array_and_many_response/index.html.twig', $responseOrData); return $this->render('@App/Class_With_Array_And_Many_Response/index.html.twig', $responseOrData);
} }
/** /**

View File

@ -45,12 +45,12 @@ class ClassWithNamedService15Controller extends AbstractController
{ {
public function indexAction(): \Symfony\Component\HttpFoundation\Response public function indexAction(): \Symfony\Component\HttpFoundation\Response
{ {
return $this->render('@App/class_with_named_service15/index.html.twig'); return $this->render('@App/Class_With_Named_Service15/index.html.twig');
} }
public function index2Action(): \Symfony\Component\HttpFoundation\Response public function index2Action(): \Symfony\Component\HttpFoundation\Response
{ {
return $this->render('@App/class_with_named_service15/index2.html.twig', ['someKey' => 'someValue']); return $this->render('@App/Class_With_Named_Service15/index2.html.twig', ['someKey' => 'someValue']);
} }
public function index3Action(): \Symfony\Component\HttpFoundation\Response public function index3Action(): \Symfony\Component\HttpFoundation\Response

View File

@ -28,7 +28,7 @@ class EmptyBodyController extends AbstractController
{ {
public function indexAction(): \Symfony\Component\HttpFoundation\Response public function indexAction(): \Symfony\Component\HttpFoundation\Response
{ {
return $this->render('@App/empty_body/index.html.twig'); return $this->render('@App/Empty_Body/index.html.twig');
} }
} }

View File

@ -49,7 +49,7 @@ class ClassWithNamedService35Controller extends AbstractController
return $this->redirectToRoute('rector_is_cool'); return $this->redirectToRoute('rector_is_cool');
} }
return $this->render('@App/class_with_named_service35/index.html.twig', array( return $this->render('@App/Class_With_Named_Service35/index.html.twig', array(
'form' => $form->createView() 'form' => $form->createView()
)); ));
} }

View File

@ -29,7 +29,7 @@ class ClassWithNamedService55Controller extends AbstractController
{ {
public function index(): \Symfony\Component\HttpFoundation\Response public function index(): \Symfony\Component\HttpFoundation\Response
{ {
return $this->render('class_with_named_service55/index.html.twig'); return $this->render('Class_With_Named_Service55/index.html.twig');
} }
} }

View File

@ -44,11 +44,11 @@ class ClassIfElseArrayController extends AbstractController
public function indexAction(): \Symfony\Component\HttpFoundation\Response public function indexAction(): \Symfony\Component\HttpFoundation\Response
{ {
if (mt_rand(0, 100)) { if (mt_rand(0, 100)) {
return $this->render('@App/class_if_else_array/index.html.twig', [ return $this->render('@App/Class_If_Else_Array/index.html.twig', [
'key' => 'value' 'key' => 'value'
]); ]);
} elseif (mt_rand(0, 200)) { } elseif (mt_rand(0, 200)) {
return $this->render('@App/class_if_else_array/index.html.twig', [ return $this->render('@App/Class_If_Else_Array/index.html.twig', [
'key' => 'value2' 'key' => 'value2'
]); ]);
} else { } else {

View File

@ -40,7 +40,7 @@ class ClassTryCatchArrayResponseController extends AbstractController
public function indexAction(): \Symfony\Component\HttpFoundation\Response public function indexAction(): \Symfony\Component\HttpFoundation\Response
{ {
try { try {
return $this->render('@App/class_try_catch_array_response/index.html.twig', [ return $this->render('@App/Class_Try_Catch_Array_Response/index.html.twig', [
'key' => 'value' 'key' => 'value'
]); ]);
} catch (Throwable $throwable) { } catch (Throwable $throwable) {

View File

@ -28,7 +28,7 @@ class WithOnlyVarsController extends AbstractController
{ {
public function index(Post $post): \Symfony\Component\HttpFoundation\Response public function index(Post $post): \Symfony\Component\HttpFoundation\Response
{ {
return $this->render('with_only_vars/index.html.twig', ['post' => $post]); return $this->render('With_Only_Vars/index.html.twig', ['post' => $post]);
} }
} }

View File

@ -0,0 +1,36 @@
<?php
namespace Rector\Sensio\Tests\Rector\FrameworkExtraBundle\TemplateAnnotationToThisRenderRector\FixtureDifferentBundleName\SomeActionBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class BasicController extends AbstractController
{
/**
* @Template()
*/
public function someAction()
{
return [];
}
}
?>
-----
<?php
namespace Rector\Sensio\Tests\Rector\FrameworkExtraBundle\TemplateAnnotationToThisRenderRector\FixtureDifferentBundleName\SomeActionBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class BasicController extends AbstractController
{
public function someAction(): \Symfony\Component\HttpFoundation\Response
{
return $this->render('@DifferentNameBundle/Basic/some.html.twig');
}
}
?>

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Rector\Sensio\Tests\Rector\FrameworkExtraBundle\TemplateAnnotationToThisRenderRector\FixtureDifferentBundleName\SomeActionBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
final class DifferentNameBundle extends Bundle
{
}

View File

@ -162,6 +162,21 @@ final class BetterNodeFinder
}); });
} }
/**
* @param Node[] $nodes
*/
public function findFirstNonAnonymousClass(array $nodes): ?Class_
{
return $this->findFirst($nodes, function (Node $node): bool {
if (! $node instanceof ClassLike) {
return false;
}
// skip anonymous classes
return ! ($node instanceof Class_ && $node->isAnonymous());
});
}
/** /**
* @param Node|Node[] $nodes * @param Node|Node[] $nodes
*/ */