Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ee87618
TemplateAnnotationRector init
TomasVotruba Jan 14, 2018
40d494c
isCandidate()
TomasVotruba Jan 14, 2018
a4630f9
temove annotation
TomasVotruba Jan 14, 2018
88aa989
determine template method from action method name
TomasVotruba Jan 14, 2018
f3036f6
wip
TomasVotruba Jan 14, 2018
e799cdc
pass
TomasVotruba Jan 14, 2018
666dc03
add NodeFactory, fix cs
TomasVotruba Jan 14, 2018
4f30c8f
simplify if
TomasVotruba Jan 14, 2018
105fff2
add second test case with return args
TomasVotruba Jan 14, 2018
3de9c64
cs
TomasVotruba Jan 14, 2018
6a5b8aa
improve 1st test case
TomasVotruba Jan 14, 2018
ac5b455
add test case for filename in annotation
TomasVotruba Jan 14, 2018
238519e
add test case for filename in annotation
TomasVotruba Jan 14, 2018
0ff8358
phpstan fixes
TomasVotruba Jan 14, 2018
8f507ba
decoupling
TomasVotruba Jan 14, 2018
a877312
uppercase @Template annotation
TomasVotruba Jan 14, 2018
03b16b2
remove unused interface
TomasVotruba Jan 14, 2018
af4d893
uppercase @Template
TomasVotruba Jan 14, 2018
1acc38c
better tests
mssimi Jan 14, 2018
441efc7
better tests
mssimi Jan 14, 2018
aab3c28
[monorepo] init build script
TomasVotruba Jan 14, 2018
9ddbe54
add own TemplateGuesser
TomasVotruba Jan 15, 2018
cc3f557
tests pass
TomasVotruba Jan 15, 2018
9db9da7
added tests for more use cases
mssimi Jan 15, 2018
5f3633d
added tests for more use cases
mssimi Jan 15, 2018
5d2e73f
Merge pull request #293 from mssimi/temlate-symfony-rector
Jan 15, 2018
77647de
fix test
TomasVotruba Jan 15, 2018
f4b7529
BetterNodeFinder: add findLastInstanceOf
TomasVotruba Jan 15, 2018
ae47518
TemplateAnnotationRector: add support for existing render() method
TomasVotruba Jan 15, 2018
e7683a9
decrease complexity
TomasVotruba Jan 15, 2018
70c8fd3
added tests and fixed code
mssimi Jan 16, 2018
77df023
fix cs
TomasVotruba Jan 17, 2018
7d2bf31
move TemplateAnnotationRector to Sensio\FrameworkExtraBundle
TomasVotruba Jan 17, 2018
ba6c9ec
add return to docs example
TomasVotruba Jan 17, 2018
f1e4680
TemplateGuesser: prepare for version decoupling, v5
TomasVotruba Jan 17, 2018
4b4bbc9
add configuration 'version' option
TomasVotruba Jan 17, 2018
899cc35
split tests to cover both version
TomasVotruba Jan 17, 2018
065f772
cleanup
TomasVotruba Jan 17, 2018
5382f7a
split test case temp files
TomasVotruba Jan 17, 2018
2f60ea2
TemplateGuesser - add Version5
TomasVotruba Jan 17, 2018
9c10d98
fix for Version5
TomasVotruba Jan 17, 2018
6140f1a
add test case with route, less pass for now
TomasVotruba Jan 17, 2018
4fb5179
move TemplateGuesser to related Helper dir
TomasVotruba Jan 17, 2018
bf61bed
decouple setConfig() method
TomasVotruba Jan 17, 2018
11c1814
cleanup
TomasVotruba Jan 17, 2018
c1fd7d6
use single wrong version case for all
TomasVotruba Jan 17, 2018
02a955a
fix @Route ( indent [closes #295]
TomasVotruba Jan 17, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions build/subtree-split-master-and-last-tag.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
git subsplit init git@github.com:rectorphp/rector.git

LAST_TAG=$(git tag -l --sort=committerdate | tail -n1);

git subsplit publish --heads="master" --tags=$LAST_TAG packages/NodeTypeResolver:git@github.com:rectorphp/node-type-resolver.git

rm -rf .subsplit/

# inspired by laravel: https://github.com/laravel/framework/blob/5.4/build/illuminate-split-full.sh
# they use SensioLabs now though: https://github.com/laravel/framework/pull/17048#issuecomment-269915319
13 changes: 13 additions & 0 deletions packages/NodeTraverserQueue/src/BetterNodeFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ public function findFirstInstanceOf($nodes, string $type): ?Node
return $this->nodeFinder->findFirstInstanceOf($nodes, $type);
}

/**
* @param Node|Node[] $nodes
*/
public function findLastInstanceOf($nodes, string $type): ?Node
{
$foundInstances = $this->nodeFinder->findInstanceOf($nodes, $type);
if (! $foundInstances) {
return null;
}

return array_pop($foundInstances);
}

/**
* @param Node|Node[] $nodes
* @return Node[]
Expand Down

This file was deleted.

16 changes: 15 additions & 1 deletion packages/ReflectionDocBlock/src/DocBlock/TidingSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Rector\ReflectionDocBlock\DocBlock;

use Nette\Utils\Strings;
use phpDocumentor\Reflection\DocBlock;
use Symplify\BetterReflectionDocBlock\FixedSerializer;

Expand All @@ -21,7 +22,9 @@ public function getDocComment(DocBlock $docBlock): string
{
$docComment = $this->fixedSerializer->getDocComment($docBlock);

return $this->clearUnnededPreslashes($docComment);
$docComment = $this->clearUnnededPreslashes($docComment);

return $this->clearUnnededSpaces($docComment);
}

/**
Expand All @@ -34,4 +37,15 @@ private function clearUnnededPreslashes(string $content): string

return str_replace('@param \\', '@param ', $content);
}

/**
* phpDocumentor adds extra spaces before Doctrine-Annotation based annotations
* starting with uppercase and followed by (, e.g. @Route('value')
*/
private function clearUnnededSpaces(string $content): string
{
return Strings::replace($content, '#@[A-Z][a-z]+\s\(#', function (array $match) {
return str_replace(' ', '', $match[0]);
});
}
}
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ parameters:
- '#Property Rector\\NodeTypeResolver\\Tests\\PerNodeCallerTypeResolver\\MethodCallCallerTypeResolver\\NestedMethodCallTest::\$(formChainMethodCallNodes|nestedMethodCallNodes) \(array<PhpParser\\Node\\Expr\\MethodCall>\) does not accept array<PhpParser\\Node>#'
- '#Cannot call method toString\(\) on string#'
- '#Parameter \#1 \$node of method Rector\\NodeTypeResolver\\NodeTypeResolver::resolve\(\) expects PhpParser\\Node, PhpParser\\Node\\Expr|string given#'
- '#Cannot call method render\(\) on phpDocumentor\\Reflection\\DocBlock\\Tag\|string#'

# known value of Name of MethodCall
- '#Call to an undefined method PhpParser\\Node\\Expr\|PhpParser\\Node\\Name::toString\(\)#'
Expand Down
9 changes: 9 additions & 0 deletions src/Exception/Rector/InvalidRectorConfigurationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types=1);

namespace Rector\Exception\Rector;

use Exception;

final class InvalidRectorConfigurationException extends Exception
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<?php declare(strict_types=1);

namespace Rector\Rector\Contrib\Sensio\FrameworkExtraBundle;

use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Return_;
use Rector\Exception\Rector\InvalidRectorConfigurationException;
use Rector\Node\MethodCallNodeFactory;
use Rector\Node\NodeFactory;
use Rector\NodeTraverserQueue\BetterNodeFinder;
use Rector\Rector\AbstractRector;
use Rector\Rector\Contrib\Sensio\Helper\TemplateGuesser;
use Rector\ReflectionDocBlock\NodeAnalyzer\DocBlockAnalyzer;

/**
* Converts all:
* - @Template()
* - public function indexAction() { }
*
* into:
* - public function indexAction() {
* - return $this->render('index.html.twig');
* - }
*/
final class TemplateAnnotationRector extends AbstractRector
{
/**
* @var DocBlockAnalyzer
*/
private $docBlockAnalyzer;

/**
* @var MethodCallNodeFactory
*/
private $methodCallNodeFactory;

/**
* @var NodeFactory
*/
private $nodeFactory;

/**
* @var BetterNodeFinder
*/
private $betterNodeFinder;

/**
* @var TemplateGuesser
*/
private $templateGuesser;

/**
* @var int
*/
private $version;

/**
* @param mixed[] $config
*/
public function __construct(
array $config,
DocBlockAnalyzer $docBlockAnalyzer,
MethodCallNodeFactory $methodCallNodeFactory,
NodeFactory $nodeFactory,
BetterNodeFinder $betterNodeFinder,
TemplateGuesser $templateGuesser
) {
$this->setConfig($config);
$this->docBlockAnalyzer = $docBlockAnalyzer;
$this->methodCallNodeFactory = $methodCallNodeFactory;
$this->nodeFactory = $nodeFactory;
$this->betterNodeFinder = $betterNodeFinder;
$this->templateGuesser = $templateGuesser;
}

public function isCandidate(Node $node): bool
{
if (! $node instanceof ClassMethod) {
return false;
}

return $this->docBlockAnalyzer->hasAnnotation($node, 'Template');
}

/**
* @param ClassMethod $classMethodNode
*/
public function refactor(Node $classMethodNode): ?Node
{
/** @var Return_|null $returnNode */
$returnNode = $this->betterNodeFinder->findLastInstanceOf((array) $classMethodNode->stmts, Return_::class);

// create "$this->render('template.file.twig.html', ['key' => 'value']);" method call
$renderArguments = $this->resolveRenderArguments($classMethodNode, $returnNode);
$thisRenderMethodCall = $this->methodCallNodeFactory->createWithVariableNameMethodNameAndArguments(
'this',
'render',
$renderArguments
);

if (! $returnNode) {
// or add as last statement in the method
$classMethodNode->stmts[] = new Return_($thisRenderMethodCall);
}

// replace Return_ node value if exists and is not already in correct format
if ($returnNode && ! $returnNode->expr instanceof MethodCall) {
$returnNode->expr = $thisRenderMethodCall;
}

// remove annotation
$this->docBlockAnalyzer->removeAnnotationFromNode($classMethodNode, 'Template');

return $classMethodNode;
}

/**
* @return Arg[]
*/
private function resolveRenderArguments(ClassMethod $classMethodNode, ?Return_ $returnNode): array
{
$arguments = [$this->resolveTemplateName($classMethodNode)];
if (! $returnNode) {
return $this->nodeFactory->createArgs($arguments);
}

if ($returnNode->expr instanceof Array_ && count($returnNode->expr->items)) {
$arguments[] = $returnNode->expr;
}

$arguments = array_merge($arguments, $this->resolveArgumentsFromMethodCall($returnNode));

return $this->nodeFactory->createArgs($arguments);
}

private function resolveTemplateName(ClassMethod $classMethodNode): string
{
$templateAnnotation = $this->docBlockAnalyzer->getTagsByName($classMethodNode, 'Template')[0];
$content = $templateAnnotation->render();

// @todo consider using sth similar to offical parsing
$annotationContent = Strings::match($content, '#\("(?<filename>.*?)"\)#');

if (isset($annotationContent['filename'])) {
return $annotationContent['filename'];
}

return $this->templateGuesser->resolveFromClassMethodNode($classMethodNode, $this->version);
}

/**
* Already existing method call
* @return mixed[]
*/
private function resolveArgumentsFromMethodCall(Return_ $returnNode): array
{
$arguments = [];
if ($returnNode->expr instanceof MethodCall) {
foreach ($returnNode->expr->args as $arg) {
if ($arg->value instanceof Array_) {
$arguments[] = $arg->value;
}
}
}

return $arguments;
}

/**
* @param mixed[] $config
*/
private function setConfig(array $config): void
{
$this->ensureConfigHasVersion($config);
$this->version = $config['version'];
}

/**
* @param mixed[] $config
*/
private function ensureConfigHasVersion(array $config): void
{
if (isset($config['version'])) {
return;
}

throw new InvalidRectorConfigurationException(sprintf(
'Rector "%s" is missing "%s" configuration. Add it as "%s" to config.yml under its key"',
self::class,
'version',
'version: <value>'
));
}
}
78 changes: 78 additions & 0 deletions src/Rector/Contrib/Sensio/Helper/TemplateGuesser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php declare(strict_types=1);

namespace Rector\Rector\Contrib\Sensio\Helper;

use Nette\Utils\Strings;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\Exception\ShouldNotHappenException;
use Rector\Node\Attribute;

final class TemplateGuesser
{
public function resolveFromClassMethodNode(ClassMethod $classMethodNode, int $version = 5): string
{
$namespace = (string) $classMethodNode->getAttribute(Attribute::NAMESPACE_NAME);
$class = (string) $classMethodNode->getAttribute(Attribute::CLASS_NAME);
$method = $classMethodNode->name->toString();

if ($version === 3) {
return $this->resolveForVersion3($namespace, $class, $method);
}

if ($version === 5) {
return $this->resolveForVersion5($namespace, $class, $method);
}

throw new ShouldNotHappenException(sprintf(
'Version "%d" is not supported in "%s". Add it.',
$version,
self::class
));
}

/**
* Mimics https://github.com/sensiolabs/SensioFrameworkExtraBundle/blob/v3.0.0/Templating/TemplateGuesser.php
*/
private function resolveForVersion3(string $namespace, string $class, string $method): string
{
// AppBundle\SomeNamespace\ => AppBundle
// App\OtherBundle\SomeNamespace\ => OtherBundle
$bundle = Strings::match($namespace, '/(?<bundle>[A-Za-z]*Bundle)/')['bundle'] ?? '';

// SomeSuper\ControllerClass => ControllerClass
$controller = Strings::match($class, '/(?<controller>[A-Za-z0-9]*)Controller$/')['controller'] ?? '';

// indexAction => index
$action = Strings::match($method, '/(?<method>[A-Za-z]*)Action$/')['method'] ?? '';

return sprintf('%s:%s:%s.html.twig', $bundle, $controller, $action);
}

/**
* Mimics https://github.com/sensiolabs/SensioFrameworkExtraBundle/blob/v5.0.0/Templating/TemplateGuesser.php
*/
private function resolveForVersion5(string $namespace, string $class, string $method): string
{
$bundle = Strings::match($namespace, '/(?<bundle>[A-Za-z]*Bundle)/')['bundle'] ?? '';
$bundle = preg_replace('/Bundle$/', '', $bundle);
$bundle = $bundle ? '@' . $bundle . '/' : '';

$controller = $this->resolveControllerVersion5($class);
$action = preg_replace('/Action$/', '', $method);

return sprintf('%s%s%s.html.twig', $bundle, $controller, $action);
}

private function resolveControllerVersion5(string $class): string
{
if (! preg_match('/Controller\\\(.+)Controller$/', $class, $tempMatch)) {
return '';
}

$controller = str_replace('\\', '/', strtolower(
preg_replace('/([a-z\d])([A-Z])/', '\\1_\\2', $tempMatch[1])
));

return $controller ? $controller . '/' : '';
}
}
3 changes: 3 additions & 0 deletions src/config/level/sensio/framework-extra-bundle-30.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
rectors:
Rector\Rector\Contrib\Sensio\FrameworkExtraBundle\TemplateAnnotationRector:
version: 3
3 changes: 3 additions & 0 deletions src/config/level/sensio/framework-extra-bundle-50.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
rectors:
Rector\Rector\Contrib\Sensio\FrameworkExtraBundle\TemplateAnnotationRector:
version: 5
2 changes: 1 addition & 1 deletion src/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ services:

Rector\:
resource: '../'
exclude: '../{Node/Attribute.php,Rector/Contrib,Rector/Dynamic,Rector/MagicDisclosure,Testing}'
exclude: '../{Node/Attribute.php,Rector/Contrib/**/*Rector.php,Rector/Dynamic,Rector/MagicDisclosure,Testing}'

Rector\Rector\Contrib\Symfony\Form\Helper\FormTypeStringToTypeProvider: ~

Expand Down
Loading