Merge pull request #995 from rectorphp/nts-routing

Add RouterListToControllerAnnotationsRector
This commit is contained in:
Tomáš Votruba 2019-02-02 11:41:06 -08:00 committed by GitHub
commit f29aae36e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1483 additions and 115 deletions

View File

@ -38,6 +38,7 @@
"Rector\\DeadCode\\": "packages/DeadCode/src",
"Rector\\Guzzle\\": "packages/Guzzle/src",
"Rector\\CodeQuality\\": "packages/CodeQuality/src",
"Rector\\NetteToSymfony\\": "packages/NetteToSymfony/src",
"Rector\\DomainDrivenDesign\\": "packages/DomainDrivenDesign/src",
"Rector\\NodeTypeResolver\\": "packages/NodeTypeResolver/src",
"Rector\\Symfony\\": "packages/Symfony/src",
@ -63,6 +64,7 @@
"Rector\\CakePHP\\Tests\\": "packages/CakePHP/tests",
"Rector\\CodeQuality\\Tests\\": "packages/CodeQuality/tests",
"Rector\\DeadCode\\Tests\\": "packages/DeadCode/tests",
"Rector\\NetteToSymfony\\Tests\\": "packages/NetteToSymfony/tests",
"Rector\\CodingStyle\\Tests\\": "packages/CodingStyle/tests",
"Rector\\DomainDrivenDesign\\Tests\\": "packages/DomainDrivenDesign/tests",
"Rector\\Guzzle\\Tests\\": "packages/Guzzle/tests",

View File

@ -36,6 +36,7 @@
- [DomainDrivenDesign\ObjectToScalar](#domaindrivendesignobjecttoscalar)
- [Guzzle\MethodCall](#guzzlemethodcall)
- [Jms\Property](#jmsproperty)
- [NetteToSymfony](#nettetosymfony)
- [PHPStan\Assign](#phpstanassign)
- [PHPStan\Cast](#phpstancast)
- [PHPUnit](#phpunit)
@ -1014,6 +1015,41 @@ Changes properties with `@JMS\DiExtraBundle\Annotation\Inject` to constructor in
<br>
## NetteToSymfony
### `RouterListToControllerAnnotationsRector`
- class: `Rector\NetteToSymfony\Rector\RouterListToControllerAnnotationsRector`
Change new Route() from RouteFactory to @Route annotation above controller method
```diff
final class RouterFactory
{
public function create(): RouteList
{
$routeList = new RouteList();
+
+ // case of single action controller, usually get() or __invoke() method
$routeList[] = new Route('some-path', SomePresenter::class);
return $routeList;
}
}
final class SomePresenter
{
+ /**
+ * @Symfony\Component\Routing\Annotation\Route(path="some-path")
+ */
public function run()
{
}
}
```
<br>
## PHPStan\Assign
### `PHPStormVarAnnotationRector`

View File

@ -77,7 +77,6 @@ parameters:
- '*/packages/NodeTypeResolver/**/PerNodeTypeResolver/**TypeResolver.php'
- '*/packages/NodeTypeResolver/**/PerNodeTypeResolver/**TypeResolver/*Test.php'
- '*RectorTest.php'
- 'tests/PhpParser/Node/ConstExprEvaluatorFactoryTest.php'
- 'src/Rector/AbstractPHPUnitRector.php'
- 'src/Rector/Class_/ParentClassToTraitsRector.php'
# required for exact string match with "\"
@ -106,6 +105,8 @@ parameters:
- 'src/PhpParser/Node/Resolver/NameResolver.php'
- 'src/Rector/MethodBody/NormalToFluentRector.php'
- 'packages/CodingStyle/src/Rector/Use_/RemoveUnusedAliasRector.php'
- 'packages/NetteToSymfony/src/Route/RouteInfoFactory.php'
# copied 3rd party logic
- 'packages/Php/src/EregToPcreTransformer.php'
# dev
@ -140,3 +141,7 @@ parameters:
Symplify\CodingStandard\Sniffs\Debug\CommentedOutCodeSniff.Found:
# notes
- 'packages/Php/src/Rector/Each/ListEachRector.php'
Symplify\CodingStandard\Sniffs\DependencyInjection\NoClassInstantiationSniff:
# 3rd party api
- 'src/PhpParser/Node/Value/ValueResolver.php'

View File

@ -0,0 +1,8 @@
services:
_defaults:
public: true
autowire: true
Rector\NetteToSymfony\:
resource: '../src'
exclude: '../src/{Annotation,Route/RouteInfo.php}'

View File

@ -0,0 +1,7 @@
services:
_defaults:
public: true
autowire: true
Rector\NetteToSymfony\:
resource: '../../src'

View File

@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace Rector\NetteToSymfony\Annotation;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
final class RouteTagValueNode implements PhpDocChildNode
{
/**
* @var string
*/
private $path;
/**
* @var string|null
*/
private $name;
/**
* @var string
*/
private $routeClass;
/**
* @var string[]
*/
private $methods = [];
/**
* @param string[] $methods
*/
public function __construct(string $routeClass, string $path, ?string $name = null, array $methods = [])
{
$this->path = $path;
$this->name = $name;
$this->routeClass = $routeClass;
$this->methods = $methods;
}
public function __toString(): string
{
$string = sprintf('@\\%s(', $this->routeClass);
$string .= sprintf('path="%s"', $this->path);
if ($this->name) {
$string .= sprintf(', name="%s"', $this->name);
}
if ($this->methods) {
$string .= sprintf(', methods={"%s"}', implode('", "', $this->methods));
}
$string .= ')' . PHP_EOL . ' ';
return $string;
}
}

View File

@ -0,0 +1,355 @@
<?php declare(strict_types=1);
namespace Rector\NetteToSymfony\Rector;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\NetteToSymfony\Annotation\RouteTagValueNode;
use Rector\NetteToSymfony\Route\RouteInfo;
use Rector\NetteToSymfony\Route\RouteInfoFactory;
use Rector\NodeTypeResolver\Application\ClassLikeNodeCollector;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockAnalyzer;
use Rector\PhpParser\Node\BetterNodeFinder;
use Rector\PhpParser\Node\Maintainer\ClassMaintainer;
use Rector\PhpParser\Node\Maintainer\ClassMethodMaintainer;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
use Rector\Util\RectorStrings;
use ReflectionMethod;
/**
* @see https://doc.nette.org/en/2.4/routing
* @see https://symfony.com/doc/current/routing.html
*/
final class RouterListToControllerAnnotationsRector extends AbstractRector
{
/**
* @var string
*/
private $routeListClass;
/**
* @var string
*/
private $routerClass;
/**
* @var string
*/
private $routeAnnotationClass;
/**
* @var BetterNodeFinder
*/
private $betterNodeFinder;
/**
* @var ClassLikeNodeCollector
*/
private $classLikeNodeCollector;
/**
* @var ClassMaintainer
*/
private $classMaintainer;
/**
* @var DocBlockAnalyzer
*/
private $docBlockAnalyzer;
/**
* @var RouteInfoFactory
*/
private $routeInfoFactory;
/**
* @var ClassMethodMaintainer
*/
private $classMethodMaintainer;
public function __construct(
BetterNodeFinder $betterNodeFinder,
ClassLikeNodeCollector $classLikeNodeCollector,
ClassMaintainer $classMaintainer,
ClassMethodMaintainer $classMethodMaintainer,
DocBlockAnalyzer $docBlockAnalyzer,
RouteInfoFactory $routeInfoFactory,
string $routeListClass = 'Nette\Application\Routers\RouteList',
string $routerClass = 'Nette\Application\IRouter',
string $routeAnnotationClass = 'Symfony\Component\Routing\Annotation\Route'
) {
$this->routeListClass = $routeListClass;
$this->routerClass = $routerClass;
$this->betterNodeFinder = $betterNodeFinder;
$this->classLikeNodeCollector = $classLikeNodeCollector;
$this->classMaintainer = $classMaintainer;
$this->docBlockAnalyzer = $docBlockAnalyzer;
$this->routeAnnotationClass = $routeAnnotationClass;
$this->routeInfoFactory = $routeInfoFactory;
$this->classMethodMaintainer = $classMethodMaintainer;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition(
'Change new Route() from RouteFactory to @Route annotation above controller method',
[
new CodeSample(
<<<'CODE_SAMPLE'
final class RouterFactory
{
public function create(): RouteList
{
$routeList = new RouteList();
$routeList[] = new Route('some-path', SomePresenter::class);
return $routeList;
}
}
final class SomePresenter
{
public function run()
{
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
final class RouterFactory
{
public function create(): RouteList
{
$routeList = new RouteList();
// case of single action controller, usually get() or __invoke() method
$routeList[] = new Route('some-path', SomePresenter::class);
return $routeList;
}
}
final class SomePresenter
{
/**
* @Symfony\Component\Routing\Annotation\Route(path="some-path")
*/
public function run()
{
}
}
CODE_SAMPLE
),
]
);
}
/**
* List of nodes this class checks, classes that implement @see \PhpParser\Node
* @return string[]
*/
public function getNodeTypes(): array
{
return [ClassMethod::class];
}
/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
if (empty($node->stmts)) {
return null;
}
$nodeReturnTypes = $this->classMethodMaintainer->resolveReturnType($node);
if ($nodeReturnTypes === []) {
return null;
}
if (! in_array($this->routeListClass, $nodeReturnTypes, true)) {
return null;
}
$assignNodes = $this->resolveAssignRouteNodes($node);
if ($assignNodes === []) {
return null;
}
$routeInfos = $this->createRouteInfosFromAssignNodes($assignNodes);
/** @var RouteInfo $routeInfo */
foreach ($routeInfos as $routeInfo) {
$classMethod = $this->resolveControllerClassMethod($routeInfo);
if ($classMethod === null) {
continue;
}
$phpDocTagNode = new RouteTagValueNode(
$this->routeAnnotationClass,
$routeInfo->getPath(),
null,
$routeInfo->getHttpMethods()
);
$this->docBlockAnalyzer->addTag($classMethod, $phpDocTagNode);
}
// complete all other non-explicit methods, from "<presenter>/<action>"
$this->completeImplicitRoutes();
// remove routes
$this->removeNodes($assignNodes);
return null;
}
/**
* @return Assign[]
*/
private function resolveAssignRouteNodes(ClassMethod $node): array
{
// look for <...>[] = IRoute<Type>
return $this->betterNodeFinder->find($node->stmts, function (Node $node) {
if (! $node instanceof Assign) {
return false;
}
// $routeList[] =
if (! $node->var instanceof ArrayDimFetch) {
return false;
}
if ($this->isType($node->expr, $this->routerClass)) {
return true;
}
if ($node->expr instanceof StaticCall) {
// for custom static route factories
return $this->isRouteStaticCallMatch($node->expr);
}
return false;
});
}
/**
* @param Assign[] $assignNodes
* @return RouteInfo[]
*/
private function createRouteInfosFromAssignNodes(array $assignNodes): array
{
$routeInfos = [];
// collect annotations and target controllers
foreach ($assignNodes as $assignNode) {
$routeNameToControllerMethod = $this->routeInfoFactory->createFromNode($assignNode->expr);
if ($routeNameToControllerMethod === null) {
continue;
}
$routeInfos[] = $routeNameToControllerMethod;
}
return $routeInfos;
}
private function resolveControllerClassMethod(RouteInfo $routeInfo): ?ClassMethod
{
$classNode = $this->classLikeNodeCollector->findClass($routeInfo->getClass());
if ($classNode === null) {
return null;
}
return $this->classMaintainer->getMethodByName($classNode, $routeInfo->getMethod());
}
private function completeImplicitRoutes(): void
{
$presenterClassNodes = $this->classLikeNodeCollector->findClassesBySuffix('Presenter');
foreach ($presenterClassNodes as $presenterClassNode) {
foreach ((array) $presenterClassNode->stmts as $classStmt) {
if ($this->shouldSkipClassStmt($classStmt)) {
continue;
}
/** @var ClassMethod $classStmt */
$path = $this->resolvePathFromClassAndMethodNodes($presenterClassNode, $classStmt);
$phpDocTagNode = new RouteTagValueNode($this->routeAnnotationClass, $path);
$this->docBlockAnalyzer->addTag($classStmt, $phpDocTagNode);
}
}
}
/**
* @todo allow extension with custom resolvers
*/
private function isRouteStaticCallMatch(StaticCall $node): bool
{
$className = $this->getName($node->class);
if ($className === null) {
return false;
}
$methodName = $this->getName($node->name);
if ($methodName === null) {
return false;
}
// @todo decouple - resolve method return type
if (! method_exists($className, $methodName)) {
return false;
}
$methodReflection = new ReflectionMethod($className, $methodName);
if ($methodReflection->getReturnType()) {
$staticCallReturnType = (string) $methodReflection->getReturnType();
if (is_a($staticCallReturnType, $this->routerClass, true)) {
return true;
}
}
return false;
}
private function shouldSkipClassStmt(Node $node): bool
{
if (! $node instanceof ClassMethod) {
return true;
}
// not an action method
if (! $node->isPublic()) {
return true;
}
if (! $this->matchName($node, '#^(render|action)#')) {
return true;
}
// already has Route tag
return $this->docBlockAnalyzer->hasTag($node, $this->routeAnnotationClass);
}
private function resolvePathFromClassAndMethodNodes(Class_ $classNode, ClassMethod $classMethodNode): string
{
$presenterName = $this->getName($classNode);
$presenterPart = Strings::after($presenterName, '\\', -1);
$presenterPart = Strings::substring($presenterPart, 0, -Strings::length('Presenter'));
$presenterPart = RectorStrings::camelCaseToDashes($presenterPart);
$match = Strings::match($this->getName($classMethodNode), '#^(action|render)(?<short_action_name>.*?$)#sm');
$actionPart = lcfirst($match['short_action_name']);
return $presenterPart . '/' . $actionPart;
}
}

View File

@ -0,0 +1,76 @@
<?php declare(strict_types=1);
namespace Rector\NetteToSymfony\Route;
final class RouteInfo
{
/**
* @var string
*/
private $class;
/**
* @var string
*/
private $method;
/**
* @var string
*/
private $path;
/**
* @var string|null
*/
private $name;
/**
* @var string[]
*/
private $httpMethods = [];
/**
* @param string[] $httpMethods
*/
public function __construct(
string $class,
string $method,
string $path,
?string $name = null,
array $httpMethods = []
) {
$this->class = $class;
$this->method = $method;
$this->path = $path;
$this->name = $name;
$this->httpMethods = $httpMethods;
}
public function getClass(): string
{
return $this->class;
}
public function getMethod(): string
{
return $this->method;
}
public function getPath(): string
{
return $this->path;
}
public function getName(): ?string
{
return $this->name;
}
/**
* @return string[]
*/
public function getHttpMethods(): array
{
return $this->httpMethods;
}
}

View File

@ -0,0 +1,157 @@
<?php declare(strict_types=1);
namespace Rector\NetteToSymfony\Route;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Scalar\String_;
use Rector\NodeTypeResolver\Application\ClassLikeNodeCollector;
use Rector\PhpParser\Node\Resolver\NameResolver;
use Rector\PhpParser\Node\Value\ValueResolver;
final class RouteInfoFactory
{
/**
* @var NameResolver
*/
private $nameResolver;
/**
* @var ValueResolver
*/
private $valueResolver;
/**
* @var ClassLikeNodeCollector
*/
private $classLikeNodeCollector;
public function __construct(
NameResolver $nameResolver,
ValueResolver $valueResolver,
ClassLikeNodeCollector $classLikeNodeCollector
) {
$this->nameResolver = $nameResolver;
$this->valueResolver = $valueResolver;
$this->classLikeNodeCollector = $classLikeNodeCollector;
}
public function createFromNode(Node $expr): ?RouteInfo
{
if ($expr instanceof New_) {
if (! isset($expr->args[0]) || ! isset($expr->args[1])) {
return null;
}
return $this->createRouteInfoFromArgs($expr);
}
// Route::create()
if ($expr instanceof StaticCall) {
if (! isset($expr->args[0]) || ! isset($expr->args[1])) {
return null;
}
$method = $this->nameResolver->matchNameInsensitiveInMap($expr, [
'get' => 'GET',
'head' => 'HEAD',
'post' => 'POST',
'put' => 'PUT',
'patch' => 'PATCH',
'delete' => 'DELETE',
]);
$methods = [];
if ($method !== null) {
$methods[] = $method;
}
return $this->createRouteInfoFromArgs($expr, $methods);
}
return null;
}
/**
* @param New_|StaticCall $expr
* @param string[] $methods
*/
private function createRouteInfoFromArgs(Node $expr, array $methods = []): ?RouteInfo
{
$pathArgument = $expr->args[0]->value;
$routePath = $this->valueResolver->resolve($pathArgument);
// route path is needed
if ($routePath === null || ! is_string($routePath)) {
return null;
}
$targetNode = $expr->args[1]->value;
if ($targetNode instanceof ClassConstFetch) {
/** @var ClassConstFetch $controllerMethodNode */
$controllerMethodNode = $expr->args[1]->value;
// SomePresenter::class
if ($this->nameResolver->isName($controllerMethodNode->name, 'class')) {
$presenterClass = $this->nameResolver->resolve($controllerMethodNode->class);
if ($presenterClass === null) {
return null;
}
if (class_exists($presenterClass) === false) {
return null;
}
if (method_exists($presenterClass, 'run')) {
return new RouteInfo($presenterClass, 'run', $routePath, null, $methods);
}
}
// @todo method specific route
}
if ($targetNode instanceof String_) {
$targetValue = $targetNode->value;
if (! Strings::contains($targetValue, ':')) {
return null;
}
[$controller, $method] = explode(':', $targetValue);
// detect class by controller name?
// foreach all instance and try to match a name $controller . 'Presenter/Controller'
$classNode = $this->classLikeNodeCollector->findByShortName($controller . 'Presenter');
if ($classNode === null) {
$classNode = $this->classLikeNodeCollector->findByShortName($controller . 'Controller');
}
// unable to find here
if ($classNode === null) {
return null;
}
$controllerClass = $this->nameResolver->resolve($classNode);
if ($controllerClass === null) {
return null;
}
$methodName = null;
if (method_exists($controllerClass, 'render' . ucfirst($method))) {
$methodName = 'render' . ucfirst($method);
} elseif (method_exists($controllerClass, 'action' . ucfirst($method))) {
$methodName = 'action' . ucfirst($method);
}
if ($methodName === null) {
return null;
}
return new RouteInfo($controllerClass, $methodName, $routePath, null, []);
}
return null;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Fixture;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\Route;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\RouteList;
final class ConstantReferenceRouterFactory
{
const SOME_PATH = '/some-path';
public function create(): RouteList
{
$routeList = new RouteList();
// case of single action controller, usually get() or __invoke() method
$routeList[] = Route::get(self::SOME_PATH, ConstantReferenceSomePresenter::class);
return $routeList;
}
}
final class ConstantReferenceSomePresenter
{
public function run()
{
}
}
?>
-----
<?php
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Fixture;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\Route;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\RouteList;
final class ConstantReferenceRouterFactory
{
const SOME_PATH = '/some-path';
public function create(): RouteList
{
$routeList = new RouteList();
return $routeList;
}
}
final class ConstantReferenceSomePresenter
{
/**
* @\Symfony\Component\Routing\Annotation\Route(path="/some-path", methods={"GET"})
*/
public function run()
{
}
}
?>

View File

@ -0,0 +1,66 @@
<?php
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Fixture;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\Route;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\RouteList;
final class GeneralMethodNamedRoutesRouterFactory
{
public function create(): RouteList
{
$routeList = new RouteList();
$routeList[] = new Route('<presenter>/<action>', 'Homepage:default');
return $routeList;
}
}
final class GeneralMethodNamedRoutesSomePresenter
{
public function actionFirst()
{
}
public function actionSecond()
{
}
}
?>
-----
<?php
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Fixture;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\Route;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\RouteList;
final class GeneralMethodNamedRoutesRouterFactory
{
public function create(): RouteList
{
$routeList = new RouteList();
return $routeList;
}
}
final class GeneralMethodNamedRoutesSomePresenter
{
/**
* @\Symfony\Component\Routing\Annotation\Route(path="general-method-named-routes-some/first")
*/
public function actionFirst()
{
}
/**
* @\Symfony\Component\Routing\Annotation\Route(path="general-method-named-routes-some/second")
*/
public function actionSecond()
{
}
}
?>

View File

@ -0,0 +1,68 @@
<?php
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Fixture;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\Route;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\RouteList;
final class MethodNamedRoutesRouterFactory
{
public function create(): RouteList
{
$routeList = new RouteList();
$routeList[] = new Route('hi', 'MethodNamedRoutesSome:first');
$routeList[] = new Route('hello', 'MethodNamedRoutesSome:second');
$routeList[] = new Route('<presenter>/<action>', 'Homepage:default');
return $routeList;
}
}
final class MethodNamedRoutesSomePresenter
{
public function actionFirst()
{
}
public function actionSecond()
{
}
}
?>
-----
<?php
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Fixture;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\Route;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\RouteList;
final class MethodNamedRoutesRouterFactory
{
public function create(): RouteList
{
$routeList = new RouteList();
return $routeList;
}
}
final class MethodNamedRoutesSomePresenter
{
/**
* @\Symfony\Component\Routing\Annotation\Route(path="hi")
*/
public function actionFirst()
{
}
/**
* @\Symfony\Component\Routing\Annotation\Route(path="hello")
*/
public function actionSecond()
{
}
}
?>

View File

@ -0,0 +1,57 @@
<?php
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Fixture;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\Route;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\RouteList;
final class NewRouterFactory
{
public function create(): RouteList
{
$routeList = new RouteList();
// case of single action controller, usually get() or __invoke() method
$routeList[] = new Route('some-path', NewSomePresenter::class);
return $routeList;
}
}
final class NewSomePresenter
{
public function run()
{
}
}
?>
-----
<?php
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Fixture;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\Route;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\RouteList;
final class NewRouterFactory
{
public function create(): RouteList
{
$routeList = new RouteList();
return $routeList;
}
}
final class NewSomePresenter
{
/**
* @\Symfony\Component\Routing\Annotation\Route(path="some-path")
*/
public function run()
{
}
}
?>

View File

@ -0,0 +1,57 @@
<?php
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Fixture;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\RouteFactory;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\RouteList;
final class StaticRouterFactory
{
public function create(): RouteList
{
$routeList = new RouteList();
// case of single action controller, usually get() or __invoke() method
$routeList[] = RouteFactory::get('some-path', StaticSomePresenter::class);
return $routeList;
}
}
final class StaticSomePresenter
{
public function run()
{
}
}
?>
-----
<?php
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Fixture;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\RouteFactory;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\RouteList;
final class StaticRouterFactory
{
public function create(): RouteList
{
$routeList = new RouteList();
return $routeList;
}
}
final class StaticSomePresenter
{
/**
* @\Symfony\Component\Routing\Annotation\Route(path="some-path", methods={"GET"})
*/
public function run()
{
}
}
?>

View File

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor;
use Rector\NetteToSymfony\Rector\RouterListToControllerAnnotationsRector;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\Route;
use Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source\RouteList;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class RouterListToControllerAnnotationsRetorTest extends AbstractRectorTestCase
{
public function test(): void
{
$this->doTestFiles([
__DIR__ . '/Fixture/new_route_to_annotation.php.inc',
__DIR__ . '/Fixture/static_route_to_annotation.php.inc',
__DIR__ . '/Fixture/constant_reference_route_to_annotation.php.inc',
__DIR__ . '/Fixture/method_named_routes.php.inc',
__DIR__ . '/Fixture/general_method_named_routes.php.inc',
]);
}
protected function getRectorClass(): string
{
return RouterListToControllerAnnotationsRector::class;
}
/**
* @return mixed[]
*/
protected function getRectorConfiguration(): array
{
return [
'$routeListClass' => RouteList::class,
'$routerClass' => Route::class,
];
}
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source;
final class Route
{
public function __construct(string $path, string $target)
{
}
}

View File

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source;
final class RouteFactory
{
public static function get(string $path, string $presenterClass): Route
{
return new Route($path, $presenterClass);
}
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source;
final class RouteList
{
}

View File

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace Rector\NetteToSymfony\Tests\Rector\MethodCall\RouterListToControllerAnnotationsRetor\Source;
final class SymfonyRouteAnnotation
{
}

View File

@ -2,6 +2,7 @@
namespace Rector\NodeTypeResolver\Application;
use Nette\Utils\Strings;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Interface_;
@ -170,4 +171,33 @@ final class ClassLikeNodeCollector
return $traits;
}
public function findByShortName(string $shortName): ?Class_
{
foreach ($this->classes as $className => $classNode) {
if (Strings::endsWith($className, '\\' . $shortName)) {
return $classNode;
}
}
return null;
}
/**
* @return Class_[]
*/
public function findClassesBySuffix(string $suffix): array
{
$classNodes = [];
foreach ($this->classes as $className => $classNode) {
if (! Strings::endsWith($className, $suffix)) {
continue;
}
$classNodes[] = $classNode;
}
return $classNodes;
}
}

View File

@ -2,6 +2,7 @@
namespace Rector\NodeTypeResolver\Application;
use Nette\Utils\Strings;
use PhpParser\Node\Stmt\ClassConst;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\Attribute;
@ -38,6 +39,10 @@ final class ConstantNodeCollector
public function findConstant(string $constantName, string $className): ?ClassConst
{
if (Strings::contains($constantName, '\\')) {
throw new ShouldNotHappenException(sprintf('Switched arguments in "%s"', __METHOD__));
}
return $this->constantsByType[$className][$constantName] ?? null;
}
}

View File

@ -68,8 +68,16 @@ final class DocBlockAnalyzer
return false;
}
// normalize tag name
$name = ltrim($name, '@');
// simple check
if (Strings::contains($node->getDocComment()->getText(), '@' . $name)) {
if (Strings::contains($node->getDocComment()->getText(), '@' . ltrim($name, '@'))) {
return true;
}
// fqn class annotation
if (Strings::contains($node->getDocComment()->getText(), '@\\' . ltrim($name, '@'))) {
return true;
}
@ -100,6 +108,7 @@ final class DocBlockAnalyzer
if ($node->getDocComment()) {
$phpDocInfo = $this->createPhpDocInfoFromNode($node);
$phpDocNode = $phpDocInfo->getPhpDocNode();
$phpDocNode->children[] = $phpDocChildNode;
$this->updateNodeWithPhpDocInfo($node, $phpDocInfo);
} else {
$phpDocNode = new PhpDocNode([$phpDocChildNode]);

View File

@ -26,8 +26,10 @@ parameters:
ignoreErrors:
# false positive
- '#Call to function method_exists\(\) with string and (.*?) will always evaluate to false#'
- '#PHPDoc tag \@param for parameter \$node with type float is incompatible with native type PhpParser\\Node#'
- '#Result of && is always true#'
- '#Parameter \#2 \$classMethodNode of method Rector\\NetteToSymfony\\Rector\\RouterListToControllerAnnotationsRector\:\:resolvePathFromClassAndMethodNodes\(\) expects PhpParser\\Node\\Stmt\\ClassMethod, PhpParser\\Node\\Stmt given#'
# missuse of interface and class
- '#Parameter \#1 (.*?) expects Symfony\\Component\\DependencyInjection\\ContainerBuilder, Symfony\\Component\\DependencyInjection\\ContainerInterface given#'
@ -103,3 +105,6 @@ parameters:
# console argument/option
- '#Cannot cast array<string\>\|string\|null to string#'
- '#Parameter \#1 \$nodes of method Rector\\PhpParser\\Node\\BetterNodeFinder\:\:find\(\) expects array<PhpParser\\Node\>\|PhpParser\\Node, array<PhpParser\\Node\\Stmt\>\|null given#'
- '#Method Rector\\NetteToSymfony\\Rector\\RouterListToControllerAnnotationsRector\:\:resolveAssignRouteNodes\(\) should return array<PhpParser\\Node\\Expr\\Assign\> but returns array<PhpParser\\Node\>#'

View File

@ -1,52 +0,0 @@
<?php declare(strict_types=1);
namespace Rector\PhpParser\Node;
use PhpParser\ConstExprEvaluator;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar\MagicConst\Dir;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\Attribute;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;
final class ConstExprEvaluatorFactory
{
public function create(): ConstExprEvaluator
{
return new ConstExprEvaluator(function (Expr $expr): ?string {
// resolve "__DIR__"
if ($expr instanceof Dir) {
$fileInfo = $expr->getAttribute(Attribute::FILE_INFO);
if (! $fileInfo instanceof SmartFileInfo) {
throw new ShouldNotHappenException();
}
return $fileInfo->getPath();
}
// resolve "SomeClass::SOME_CONST"
if ($expr instanceof ClassConstFetch) {
return $this->resolveClassConstFetch($expr);
}
return null;
});
}
private function resolveClassConstFetch(ClassConstFetch $classConstFetchNode): string
{
$class = $classConstFetchNode->class->getAttribute(Attribute::RESOLVED_NAME);
if ($class === null) {
return '';
}
/** @var Identifier $identifierNode */
$identifierNode = $classConstFetchNode->name;
$constant = $identifierNode->toString();
return $class->toString() . '::' . $constant;
}
}

View File

@ -222,6 +222,21 @@ final class ClassMaintainer
return in_array($methodName, $methodNames, true);
}
public function getMethodByName(Class_ $classNode, string $methodName): ?ClassMethod
{
foreach ($classNode->stmts as $stmt) {
if (! $stmt instanceof ClassMethod) {
continue;
}
if ($this->nameResolver->isName($stmt, $methodName)) {
return $stmt;
}
}
return null;
}
private function tryInsertBeforeFirstMethod(Class_ $classNode, Stmt $node): bool
{
foreach ($classNode->stmts as $key => $classElementNode) {

View File

@ -8,7 +8,9 @@ use PhpParser\Node\Param;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Return_;
use Rector\NodeTypeResolver\Node\Attribute;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\PhpParser\Node\BetterNodeFinder;
use Rector\PhpParser\Node\Resolver\NameResolver;
use Rector\PhpParser\Printer\BetterStandardPrinter;
final class ClassMethodMaintainer
@ -23,10 +25,33 @@ final class ClassMethodMaintainer
*/
private $betterStandardPrinter;
public function __construct(BetterNodeFinder $betterNodeFinder, BetterStandardPrinter $betterStandardPrinter)
{
/**
* @var NodeTypeResolver
*/
private $nodeTypeResolver;
/**
* @var FunctionLikeMaintainer
*/
private $functionLikeMaintainer;
/**
* @var NameResolver
*/
private $nameResolver;
public function __construct(
BetterNodeFinder $betterNodeFinder,
BetterStandardPrinter $betterStandardPrinter,
NodeTypeResolver $nodeTypeResolver,
FunctionLikeMaintainer $functionLikeMaintainer,
NameResolver $nameResolver
) {
$this->betterNodeFinder = $betterNodeFinder;
$this->betterStandardPrinter = $betterStandardPrinter;
$this->nodeTypeResolver = $nodeTypeResolver;
$this->functionLikeMaintainer = $functionLikeMaintainer;
$this->nameResolver = $nameResolver;
}
public function isParameterUsedMethod(Param $param, ClassMethod $classMethod): bool
@ -41,12 +66,12 @@ final class ClassMethodMaintainer
public function hasParentMethodOrInterfaceMethod(ClassMethod $classMethod): bool
{
$class = $classMethod->getAttribute(Attribute::CLASS_NAME);
if ($class === null) {
if (is_string($class) === false) {
return false;
}
$method = $classMethod->getAttribute(Attribute::METHOD_NAME);
if ($method === null) {
if (is_string($method) === false) {
return false;
}
@ -90,6 +115,33 @@ final class ClassMethodMaintainer
return false;
}
/**
* @return string[]
*/
public function resolveReturnType(ClassMethod $classMethodNode): array
{
if ($classMethodNode->returnType !== null) {
return $this->nodeTypeResolver->resolve($classMethodNode->returnType);
}
$staticReturnType = $this->functionLikeMaintainer->resolveStaticReturnTypeInfo($classMethodNode);
if ($staticReturnType === null) {
return [];
}
$getFqnTypeNode = $staticReturnType->getFqnTypeNode();
if ($getFqnTypeNode === null) {
return [];
}
$fqnTypeName = $this->nameResolver->resolve($getFqnTypeNode);
if ($fqnTypeName === null) {
return [];
}
return [$fqnTypeName];
}
private function isMethodInParent(string $class, string $method): bool
{
$parentClass = $class;

View File

@ -89,6 +89,25 @@ final class NameResolver
$this->nameResolversPerNode = $callableCollectorPopulator->populate($resolvers);
}
/**
* @param string[] $map
*/
public function matchNameInsensitiveInMap(Node $node, array $map): ?string
{
foreach ($map as $nameToMatch => $return) {
if ($this->isNameInsensitive($node, $nameToMatch)) {
return $return;
}
}
return null;
}
public function isNameInsensitive(Node $node, string $name): bool
{
return strtolower((string) $this->resolve($node)) === strtolower($name);
}
public function isName(Node $node, string $name): bool
{
$resolvedName = $this->resolve($node);

View File

@ -0,0 +1,125 @@
<?php declare(strict_types=1);
namespace Rector\PhpParser\Node\Value;
use PhpParser\ConstExprEvaluator;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Scalar\MagicConst\Dir;
use PhpParser\Node\Scalar\MagicConst\File;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Application\ConstantNodeCollector;
use Rector\NodeTypeResolver\Node\Attribute;
use Rector\PhpParser\Node\Resolver\NameResolver;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;
final class ValueResolver
{
/**
* @var NameResolver
*/
private $nameResolver;
/**
* @var ConstExprEvaluator
*/
private $constExprEvaluator;
/**
* @var ConstantNodeCollector
*/
private $constantNodeCollector;
public function __construct(NameResolver $nameResolver, ConstantNodeCollector $constantNodeCollector)
{
$this->nameResolver = $nameResolver;
$this->constantNodeCollector = $constantNodeCollector;
}
/**
* @return mixed|null
*/
public function resolve(Expr $node)
{
return $this->getConstExprEvaluator()->evaluateDirectly($node);
}
private function getConstExprEvaluator(): ConstExprEvaluator
{
if ($this->constExprEvaluator !== null) {
return $this->constExprEvaluator;
}
$this->constExprEvaluator = new ConstExprEvaluator(function (Expr $expr): ?string {
if ($expr instanceof Dir) {
// __DIR__
return $this->resolveDirConstant($expr);
}
if ($expr instanceof File) {
// __FILE__
return $this->resolveFileConstant($expr);
}
// resolve "SomeClass::SOME_CONST"
if ($expr instanceof ClassConstFetch) {
return $this->resolveClassConstFetch($expr);
}
return null;
});
return $this->constExprEvaluator;
}
private function resolveDirConstant(Dir $dirNode): string
{
$fileInfo = $dirNode->getAttribute(Attribute::FILE_INFO);
if (! $fileInfo instanceof SmartFileInfo) {
throw new ShouldNotHappenException();
}
return $fileInfo->getPath();
}
private function resolveFileConstant(File $fileNode): string
{
$fileInfo = $fileNode->getAttribute(Attribute::FILE_INFO);
if (! $fileInfo instanceof SmartFileInfo) {
throw new ShouldNotHappenException();
}
return $fileInfo->getPathname();
}
private function resolveClassConstFetch(ClassConstFetch $classConstFetchNode): string
{
$class = $this->nameResolver->resolve($classConstFetchNode->class);
$constant = $this->nameResolver->resolve($classConstFetchNode->name);
if ($class === null) {
throw new ShouldNotHappenException();
}
if ($constant === null) {
throw new ShouldNotHappenException();
}
if ($class === 'self') {
$class = (string) $classConstFetchNode->class->getAttribute(Attribute::CLASS_NAME);
}
if ($constant === 'class') {
return $class;
}
$classConstNode = $this->constantNodeCollector->findConstant($constant, $class);
if ($classConstNode === null) {
// fallback to the name
return $class . '::' . $constant;
}
return $this->constExprEvaluator->evaluateDirectly($classConstNode->consts[0]->value);
}
}

View File

@ -2,7 +2,6 @@
namespace Rector\Rector;
use PhpParser\ConstExprEvaluator;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt;
@ -13,6 +12,7 @@ use Rector\Application\RemovedFilesCollector;
use Rector\Contract\Rector\PhpRectorInterface;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\Attribute;
use Rector\PhpParser\Node\Value\ValueResolver;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;
@ -35,28 +35,28 @@ abstract class AbstractRector extends NodeVisitorAbstract implements PhpRectorIn
*/
private $symfonyStyle;
/**
* @var ConstExprEvaluator
*/
private $constExprEvaluator;
/**
* @var RemovedFilesCollector
*/
private $removedFilesCollector;
/**
* @var ValueResolver
*/
private $valueResolver;
/**
* @required
*/
public function setAbstractRectorDependencies(
AppliedRectorCollector $appliedRectorCollector,
SymfonyStyle $symfonyStyle,
ConstExprEvaluator $constExprEvaluator,
ValueResolver $valueResolver,
RemovedFilesCollector $removedFilesCollector
): void {
$this->appliedRectorCollector = $appliedRectorCollector;
$this->symfonyStyle = $symfonyStyle;
$this->constExprEvaluator = $constExprEvaluator;
$this->valueResolver = $valueResolver;
$this->removedFilesCollector = $removedFilesCollector;
}
@ -130,7 +130,7 @@ abstract class AbstractRector extends NodeVisitorAbstract implements PhpRectorIn
*/
protected function getValue(Expr $node)
{
return $this->constExprEvaluator->evaluateSilently($node);
return $this->valueResolver->resolve($node);
}
/**

View File

@ -2,6 +2,7 @@
namespace Rector\Rector;
use Nette\Utils\Strings;
use PhpParser\Node;
use Rector\PhpParser\Node\Resolver\NameResolver;
@ -34,9 +35,22 @@ trait NameResolverTrait
return $this->getName($firstNode) === $this->getName($secondNode);
}
public function matchName(Node $node, string $pattern): bool
{
return (bool) Strings::match($this->getName($node), $pattern);
}
public function isNameInsensitive(Node $node, string $name): bool
{
return strtolower((string) $this->getName($node)) === strtolower($name);
return $this->nameResolver->isNameInsensitive($node, $name);
}
/**
* @param string[] $map
*/
public function matchNameInsensitiveInMap(Node $node, array $map): ?string
{
return $this->nameResolver->matchNameInsensitiveInMap($node, $map);
}
/**

View File

@ -2,6 +2,7 @@
namespace Rector\Util;
use Nette\Utils\Strings;
use Symfony\Component\Console\Input\StringInput;
use Symplify\PackageBuilder\Reflection\PrivatesCaller;
@ -16,4 +17,26 @@ final class RectorStrings
return $privatesCaller->callPrivateMethod(new StringInput(''), 'tokenize', $command);
}
public static function camelCaseToDashes(string $input): string
{
return self::camelCaseToGlue($input, '-');
}
public static function camelCaseToUnderscore(string $input): string
{
return self::camelCaseToGlue($input, '_');
}
private static function camelCaseToGlue(string $input, string $glue): string
{
$matches = Strings::matchAll($input, '!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!');
$parts = [];
foreach ($matches as $match) {
$parts[] = $match[0] === strtoupper($match[0]) ? strtolower($match[0]) : lcfirst($match[0]);
}
return implode($glue, $parts);
}
}

View File

@ -16,9 +16,6 @@ services:
PhpParser\NodeFinder: ~
# value resolver
PhpParser\ConstExprEvaluator:
factory: ['@Rector\PhpParser\Node\ConstExprEvaluatorFactory', 'create']
Symfony\Component\Filesystem\Filesystem: ~
# Symfony\Console

View File

@ -1,34 +0,0 @@
<?php declare(strict_types=1);
namespace Rector\Tests\PhpParser\Node;
use PhpParser\BuilderFactory;
use PhpParser\ConstExprEvaluator;
use PhpParser\Node\Name;
use PHPUnit\Framework\TestCase;
use Rector\NodeTypeResolver\Node\Attribute;
use Rector\PhpParser\Node\ConstExprEvaluatorFactory;
final class ConstExprEvaluatorFactoryTest extends TestCase
{
/**
* @var ConstExprEvaluator
*/
private $constExprEvaluator;
protected function setUp(): void
{
$this->constExprEvaluator = (new ConstExprEvaluatorFactory())->create();
}
public function test(): void
{
$classConstFetchNode = (new BuilderFactory())->classConstFetch('SomeClass', 'SOME_CONSTANT');
$classConstFetchNode->class->setAttribute(Attribute::RESOLVED_NAME, new Name('SomeClassResolveName'));
$this->assertSame(
'SomeClassResolveName::SOME_CONSTANT',
$this->constExprEvaluator->evaluateDirectly($classConstFetchNode)
);
}
}

View File

@ -0,0 +1,33 @@
<?php declare(strict_types=1);
namespace Rector\Tests\PhpParser\Node\Value;
use PhpParser\BuilderFactory;
use PhpParser\Node\Name\FullyQualified;
use Rector\NodeTypeResolver\Node\Attribute;
use Rector\PhpParser\Node\Value\ValueResolver;
use Rector\Tests\AbstractContainerAwareTestCase;
final class ValueResolverTest extends AbstractContainerAwareTestCase
{
/**
* @var ValueResolver
*/
private $valueResolver;
protected function setUp(): void
{
$this->valueResolver = $this->container->get(ValueResolver::class);
}
public function test(): void
{
$classConstFetchNode = (new BuilderFactory())->classConstFetch('SomeClass', 'SOME_CONSTANT');
$classConstFetchNode->class->setAttribute(Attribute::RESOLVED_NAME, new FullyQualified('SomeClassResolveName'));
$this->assertSame(
'SomeClassResolveName::SOME_CONSTANT',
$this->valueResolver->resolve($classConstFetchNode)
);
}
}

View File

@ -3,13 +3,14 @@
namespace ChangeMe_ToNamespaced;
use PHPUnit_TestCase;
use Rector\Tests\Rector\Namespace_\PseudoNamespaceToNamespaceRector\Source\Keep_This;
class SomeTestCase
{
/**
* @return \ChangeMe_AnotherNamespace
*/
public function someMethod(): \Keep_ThisThough
public function someMethod(): Keep_This
{
if ($this instanceof PHPUnit_TestCase) {
return true;
@ -24,13 +25,14 @@ class SomeTestCase
namespace ChangeMe\ToNamespaced;
use PHPUnit\TestCase;
use Rector\Tests\Rector\Namespace_\PseudoNamespaceToNamespaceRector\Source\Keep_This;
class SomeTestCase
{
/**
* @return \ChangeMe_AnotherNamespace
*/
public function someMethod(): \Keep_ThisThough
public function someMethod(): Keep_This
{
if ($this instanceof \PHPUnit\TestCase) {
return true;

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace Rector\Tests\Rector\Namespace_\PseudoNamespaceToNamespaceRector\Source;
final class Keep_This
{
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace Rector\Tests\Util;
use Iterator;
use PHPUnit\Framework\TestCase;
use Rector\Util\RectorStrings;
final class RectorStringsTest extends TestCase
{
/**
* @dataProvider provideDataForCamelCaseToUnderscore()
*/
public function testCamelCaseToUnderscore(string $content, string $expected): void
{
$this->assertSame($expected, RectorStrings::camelCaseToUnderscore($content));
}
public function provideDataForCamelCaseToUnderscore(): Iterator
{
yield ['simpleTest', 'simple_test'];
yield ['easy', 'easy'];
yield ['HTML', 'html'];
yield ['simpleXML', 'simple_xml'];
yield ['PDFLoad', 'pdf_load'];
yield ['startMIDDLELast', 'start_middle_last'];
yield ['AString', 'a_string'];
yield ['Some4Numbers234', 'some4_numbers234'];
yield ['TEST123String', 'test123_string'];
}
}

View File

@ -1,5 +1,5 @@
services:
- Rector\PHPStanExtensions\Utils\ValueResolver
- Rector\PHPStanExtensions\Utils\PHPStanValueResolver
# $node->geAttribute($1) => Type|null by $1
- { class: Rector\PHPStanExtensions\Rector\Type\GetAttributeReturnTypeExtension, tags: [phpstan.broker.dynamicMethodReturnTypeExtension] }

View File

@ -22,7 +22,7 @@ use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Rector\PHPStanExtensions\Utils\ValueResolver;
use Rector\PHPStanExtensions\Utils\PHPStanValueResolver;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;
final class GetAttributeReturnTypeExtension implements DynamicMethodReturnTypeExtension
@ -52,13 +52,13 @@ final class GetAttributeReturnTypeExtension implements DynamicMethodReturnTypeEx
];
/**
* @var ValueResolver
* @var PHPStanValueResolver
*/
private $valueResolver;
private $phpStanValueResolver;
public function __construct(ValueResolver $valueResolver)
public function __construct(PHPStanValueResolver $phpStanValueResolver)
{
$this->valueResolver = $valueResolver;
$this->phpStanValueResolver = $phpStanValueResolver;
}
public function getClass(): string
@ -107,7 +107,7 @@ final class GetAttributeReturnTypeExtension implements DynamicMethodReturnTypeEx
private function resolveArgumentValue(Expr $node): ?string
{
if ($node instanceof ClassConstFetch) {
return $this->valueResolver->resolveClassConstFetch($node);
return $this->phpStanValueResolver->resolveClassConstFetch($node);
}
return null;

View File

@ -6,7 +6,7 @@ use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
final class ValueResolver
final class PHPStanValueResolver
{
public function resolveClassConstFetch(ClassConstFetch $classConstFetch): ?string
{