diff --git a/composer.json b/composer.json index c88bd0e32861..3875e40457be 100644 --- a/composer.json +++ b/composer.json @@ -111,7 +111,8 @@ "Rector\\Phalcon\\": "packages/Phalcon/src", "Rector\\DoctrineGedmoToKnplabs\\": "packages/DoctrineGedmoToKnplabs/src", "Rector\\MinimalScope\\": "packages/MinimalScope/src", - "Rector\\Polyfill\\": "packages/Polyfill/src" + "Rector\\Polyfill\\": "packages/Polyfill/src", + "Rector\\CakePHPToSymfony\\": "packages/CakePHPToSymfony/src" } }, "autoload-dev": { @@ -174,7 +175,8 @@ "Rector\\DoctrineGedmoToKnplabs\\Tests\\": "packages/DoctrineGedmoToKnplabs/tests", "Rector\\MinimalScope\\Tests\\": "packages/MinimalScope/tests", "Rector\\Utils\\PHPStanStaticTypeMapperChecker\\": "utils/PHPStanStaticTypeMapperChecker/src", - "Rector\\Polyfill\\Tests\\": "packages/Polyfill/tests" + "Rector\\Polyfill\\Tests\\": "packages/Polyfill/tests", + "Rector\\CakePHPToSymfony\\Tests\\": "packages/CakePHPToSymfony/tests" }, "classmap": [ "packages/CakePHP/tests/Rector/Name/ImplicitShortClassNameUseStatementRector/Source", @@ -188,7 +190,8 @@ "tests/Issues/Issue1243/Source", "packages/Autodiscovery/tests/Rector/FileSystem/MoveInterfacesToContractNamespaceDirectoryRector/Expected", "packages/Autodiscovery/tests/Rector/FileSystem/MoveServicesBySuffixToDirectoryRector/Expected", - "packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Source" + "packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Source", + "packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerComponentToSymfonyRector/Source" ], "files": [ "packages/DeadCode/tests/Rector/MethodCall/RemoveDefaultArgumentValueRector/Source/UserDefined.php", diff --git a/config/set/cakephp-to-symfony/cakephp-24-to-symfony-50.yaml b/config/set/cakephp-to-symfony/cakephp-24-to-symfony-50.yaml new file mode 100644 index 000000000000..462974897a24 --- /dev/null +++ b/config/set/cakephp-to-symfony/cakephp-24-to-symfony-50.yaml @@ -0,0 +1,7 @@ +services: + Rector\CakePHPToSymfony\Rector\Class_\CakePHPControllerToSymfonyControllerRector: null + Rector\CakePHPToSymfony\Rector\ClassMethod\CakePHPControllerActionToSymfonyControllerActionRector: null + Rector\CakePHPToSymfony\Rector\Class_\CakePHPControllerHelperToSymfonyRector: null + Rector\CakePHPToSymfony\Rector\Class_\CakePHPControllerComponentToSymfonyRector: null + Rector\CakePHPToSymfony\Rector\ClassMethod\CakePHPControllerRedirectToSymfonyRector: null + Rector\CakePHPToSymfony\Rector\ClassMethod\CakePHPControllerRenderToSymfonyRector: null diff --git a/docs/AllRectorsOverview.md b/docs/AllRectorsOverview.md index d92366c482d7..5d02b4da380e 100644 --- a/docs/AllRectorsOverview.md +++ b/docs/AllRectorsOverview.md @@ -1,4 +1,4 @@ -# All 432 Rectors Overview +# All 438 Rectors Overview - [Projects](#projects) - [General](#general) @@ -8,6 +8,7 @@ - [Architecture](#architecture) - [Autodiscovery](#autodiscovery) - [CakePHP](#cakephp) +- [CakePHPToSymfony](#cakephptosymfony) - [Celebrity](#celebrity) - [CodeQuality](#codequality) - [CodingStyle](#codingstyle) @@ -283,6 +284,144 @@ services:
+## CakePHPToSymfony + +### `CakePHPControllerActionToSymfonyControllerActionRector` + +- class: `Rector\CakePHPToSymfony\Rector\ClassMethod\CakePHPControllerActionToSymfonyControllerActionRector` + +Migrate CakePHP 2.4 Controller action to Symfony 5 + +```diff ++use Symfony\Component\HttpFoundation\Response; ++ + class HomepageController extends \AppController + { +- public function index() ++ public function index(): Response + { + $value = 5; + } + } +``` + +
+ +### `CakePHPControllerComponentToSymfonyRector` + +- class: `Rector\CakePHPToSymfony\Rector\Class_\CakePHPControllerComponentToSymfonyRector` + +Migrate CakePHP 2.4 Controller $components property to Symfony 5 + +```diff + class MessagesController extends \AppController + { +- public $components = ['Overview']; ++ private function __construct(OverviewComponent $overviewComponent) ++ { ++ $this->overviewComponent->filter(); ++ } + + public function someAction() + { +- $this->Overview->filter(); ++ $this->overviewComponent->filter(); + } + } + + class OverviewComponent extends \Component + { + public function filter() + { + } + } +``` + +
+ +### `CakePHPControllerHelperToSymfonyRector` + +- class: `Rector\CakePHPToSymfony\Rector\Class_\CakePHPControllerHelperToSymfonyRector` + +Migrate CakePHP 2.4 Controller $helpers and $components property to Symfony 5 + +```diff + class HomepageController extends AppController + { +- public $helpers = ['Flash']; +- + public function index() + { +- $this->Flash->success(__('Your post has been saved.')); +- $this->Flash->error(__('Unable to add your post.')); ++ $this->addFlash('success', __('Your post has been saved.')); ++ $this->addFlash('error', __('Unable to add your post.')); + } + } +``` + +
+ +### `CakePHPControllerRedirectToSymfonyRector` + +- class: `Rector\CakePHPToSymfony\Rector\ClassMethod\CakePHPControllerRedirectToSymfonyRector` + +Migrate CakePHP 2.4 Controller redirect() to Symfony 5 + +```diff + class RedirectController extends \AppController + { + public function index() + { +- $this->redirect('boom'); ++ return $this->redirect('boom'); + } + } +``` + +
+ +### `CakePHPControllerRenderToSymfonyRector` + +- class: `Rector\CakePHPToSymfony\Rector\ClassMethod\CakePHPControllerRenderToSymfonyRector` + +Migrate CakePHP 2.4 Controller render() to Symfony 5 + +```diff + class RedirectController extends \AppController + { + public function index() + { +- $this->render('custom_file'); ++ return $this->render('redirect/custom_file.twig'); + } + } +``` + +
+ +### `CakePHPControllerToSymfonyControllerRector` + +- class: `Rector\CakePHPToSymfony\Rector\Class_\CakePHPControllerToSymfonyControllerRector` + +Migrate CakePHP 2.4 Controller to Symfony 5 + +```diff +-class HomepageController extends AppController ++use Symfony\Component\HttpFoundation\Response; ++use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; ++ ++class HomepageController extends AbstractController + { +- public function index() ++ public function index(): Response + { + } + } +``` + +
+ ## Celebrity ### `CommonNotEqualRector` @@ -8724,7 +8863,7 @@ services: Rector\Rector\Visibility\ChangeMethodVisibilityRector: $methodToVisibilityByClass: FrameworkClass: - someMethod: protected + someMethod: protected ``` ↓ diff --git a/packages/CakePHPToSymfony/config/config.yaml b/packages/CakePHPToSymfony/config/config.yaml new file mode 100644 index 000000000000..8000f0a5e607 --- /dev/null +++ b/packages/CakePHPToSymfony/config/config.yaml @@ -0,0 +1,9 @@ +services: + _defaults: + autowire: true + public: true + + Rector\CakePHPToSymfony\: + resource: '../src' + exclude: + - '../src/Rector/**/*Rector.php' diff --git a/packages/CakePHPToSymfony/src/Rector/AbstractCakePHPRector.php b/packages/CakePHPToSymfony/src/Rector/AbstractCakePHPRector.php new file mode 100644 index 000000000000..214341ca800e --- /dev/null +++ b/packages/CakePHPToSymfony/src/Rector/AbstractCakePHPRector.php @@ -0,0 +1,26 @@ +isObjectType($node, 'AppController')) { + return true; + } + + $class = $node->getAttribute(AttributeKey::CLASS_NODE); + if ($class !== null) { + return true; + } + + return false; + } +} diff --git a/packages/CakePHPToSymfony/src/Rector/ClassMethod/CakePHPControllerActionToSymfonyControllerActionRector.php b/packages/CakePHPToSymfony/src/Rector/ClassMethod/CakePHPControllerActionToSymfonyControllerActionRector.php new file mode 100644 index 000000000000..b50ec8425eb3 --- /dev/null +++ b/packages/CakePHPToSymfony/src/Rector/ClassMethod/CakePHPControllerActionToSymfonyControllerActionRector.php @@ -0,0 +1,80 @@ +view + * + * @see \Rector\CakePHPToSymfony\Tests\Rector\ClassMethod\CakePHPControllerActionToSymfonyControllerActionRector\CakePHPControllerActionToSymfonyControllerActionRectorTest + */ +final class CakePHPControllerActionToSymfonyControllerActionRector extends AbstractCakePHPRector +{ + public function getDefinition(): RectorDefinition + { + return new RectorDefinition('Migrate CakePHP 2.4 Controller action to Symfony 5', [ + new CodeSample( + <<<'PHP' +class HomepageController extends \AppController +{ + public function index() + { + $value = 5; + } +} +PHP +, + <<<'PHP' +use Symfony\Component\HttpFoundation\Response; + +class HomepageController extends \AppController +{ + public function index(): Response + { + $value = 5; + } +} +PHP + + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->isInCakePHPController($node)) { + return null; + } + + if (! $node->isPublic()) { + return null; + } + + $node->returnType = new FullyQualified('Symfony\Component\HttpFoundation\Response'); + + return $node; + } +} diff --git a/packages/CakePHPToSymfony/src/Rector/ClassMethod/CakePHPControllerRedirectToSymfonyRector.php b/packages/CakePHPToSymfony/src/Rector/ClassMethod/CakePHPControllerRedirectToSymfonyRector.php new file mode 100644 index 000000000000..772ca5018cc4 --- /dev/null +++ b/packages/CakePHPToSymfony/src/Rector/ClassMethod/CakePHPControllerRedirectToSymfonyRector.php @@ -0,0 +1,151 @@ +redirect('boom'); + } +} +PHP +, + <<<'PHP' +class RedirectController extends \AppController +{ + public function index() + { + return $this->redirect('boom'); + } +} +PHP + + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->isInCakePHPController($node)) { + return null; + } + + $this->traverseNodesWithCallable($node, function (Node $node) { + if ($node instanceof Return_) { + $returnedExpr = $node->expr; + if ($returnedExpr === null) { + return null; + } + + return $this->refactorRedirectMethodCall($returnedExpr); + } + + if ($node instanceof Expression) { + return $this->refactorRedirectMethodCall($node); + } + + return null; + }); + + return $node; + } + + /** + * @param Expr|Expression $expr + */ + private function refactorRedirectMethodCall($expr): ?Return_ + { + if ($expr instanceof Expression) { + $expr = $expr->expr; + } + + if (! $expr instanceof MethodCall) { + return null; + } + + if (! $this->isName($expr->var, 'this')) { + return null; + } + + if (! $this->isName($expr->name, 'redirect')) { + return null; + } + + $this->refactorRedirectArgs($expr); + + $parentNode = $expr->getAttribute(AttributeKey::PARENT_NODE); + if ($parentNode instanceof Return_) { + return null; + } + + // add "return" + return new Return_($expr); + } + + private function refactorRedirectArgs(MethodCall $methodCall): void + { + $argumentValue = $methodCall->args[0]->value; + if ($argumentValue instanceof String_) { + return; + } + + // not sure what to do + if (! $argumentValue instanceof Array_) { + return; + } + + $argumentValue = $this->getValue($argumentValue); + + if (! isset($argumentValue['controller']) || ! isset($argumentValue['action'])) { + return; + } + + $composedRouteName = $argumentValue['controller'] . '_' . $argumentValue['action']; + $composedRouteName = RectorStrings::camelCaseToUnderscore($composedRouteName); + + $methodCall->args[0]->value = new String_($composedRouteName); + $methodCall->name = new Identifier('redirectToRoute'); + } +} diff --git a/packages/CakePHPToSymfony/src/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector.php b/packages/CakePHPToSymfony/src/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector.php new file mode 100644 index 000000000000..0e6ae1b9dbe7 --- /dev/null +++ b/packages/CakePHPToSymfony/src/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector.php @@ -0,0 +1,215 @@ +templatePathResolver = $templatePathResolver; + $this->compactConverter = $compactConverter; + $this->templateMethodCallManipulator = $templateMethodCallManipulator; + } + + public function getDefinition(): RectorDefinition + { + return new RectorDefinition('Migrate CakePHP 2.4 Controller render() to Symfony 5', [ + new CodeSample( + <<<'PHP' +class RedirectController extends \AppController +{ + public function index() + { + $this->render('custom_file'); + } +} +PHP +, + <<<'PHP' +class RedirectController extends \AppController +{ + public function index() + { + return $this->render('redirect/custom_file.twig'); + } +} +PHP + + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->isInCakePHPController($node)) { + return null; + } + + if ($this->hasRenderMethodCall($node)) { + $this->templateMethodCallManipulator->refactorExistingRenderMethodCall($node); + return null; + } + + $this->completeImplicitRenderMethodCall($node); + + return $node; + } + + /** + * Creates "$this->render('...', [...])"; + */ + private function createThisRenderMethodCall(ClassMethod $classMethod): MethodCall + { + $thisVariable = new Variable('this'); + $thisRenderMethodCall = new MethodCall($thisVariable, 'render'); + + $templateName = $this->templatePathResolver->resolveForClassMethod($classMethod); + $thisRenderMethodCall->args[] = new Arg(new String_($templateName)); + + $setValues = $this->collectAndRemoveSetMethodCallArgs((array) $classMethod->stmts); + + if ($setValues !== []) { + $parametersArray = $this->createArrayFromSetValues($setValues); + $thisRenderMethodCall->args[] = new Arg($parametersArray); + } + + return $thisRenderMethodCall; + } + + /** + * @return Arg[][] + */ + private function collectAndRemoveSetMethodCallArgs(array $stmts): array + { + $setMethodCallArgs = []; + + $this->traverseNodesWithCallable($stmts, function (Node $node) use (&$setMethodCallArgs) { + if (! $node instanceof MethodCall) { + return null; + } + + if (! $this->isName($node->name, 'set')) { + return null; + } + + $setMethodCallArgs[] = $node->args; + $this->removeNode($node); + + return null; + }); + + return $setMethodCallArgs; + } + + /** + * @param Arg[][] $setValues + */ + private function createArrayFromSetValues(array $setValues): Array_ + { + $arrayItems = []; + + foreach ($setValues as $setValue) { + if (count($setValue) > 1) { + $arrayItems[] = new ArrayItem($setValue[1]->value, $setValue[0]->value); + } elseif ($this->isCompactFuncCall($setValue[0]->value)) { + /** @var FuncCall $compactFuncCall */ + $compactFuncCall = $setValue[0]->value; + + return $this->compactConverter->convertToArray($compactFuncCall); + } + } + + return new Array_($arrayItems); + } + + private function isCompactFuncCall(Node $node): bool + { + if (! $node instanceof FuncCall) { + return false; + } + + return $this->isName($node, 'compact'); + } + + private function hasRenderMethodCall(ClassMethod $classMethod): bool + { + return (bool) $this->betterNodeFinder->findFirst($classMethod, function (Node $node) { + return $this->isThisRenderMethodCall($node); + }); + } + + private function completeImplicitRenderMethodCall(ClassMethod $classMethod): void + { + $methodCall = $this->createThisRenderMethodCall($classMethod); + $return = new Return_($methodCall); + + $classMethod->stmts[] = $return; + } + + private function isThisRenderMethodCall(Node $node): bool + { + if (! $node instanceof MethodCall) { + return false; + } + + if (! $this->isName($node->var, 'this')) { + return false; + } + + return $this->isName($node->name, 'render'); + } +} diff --git a/packages/CakePHPToSymfony/src/Rector/Class_/CakePHPControllerComponentToSymfonyRector.php b/packages/CakePHPToSymfony/src/Rector/Class_/CakePHPControllerComponentToSymfonyRector.php new file mode 100644 index 000000000000..9cd93e1d5413 --- /dev/null +++ b/packages/CakePHPToSymfony/src/Rector/Class_/CakePHPControllerComponentToSymfonyRector.php @@ -0,0 +1,193 @@ +classManipulator = $classManipulator; + $this->classNaming = $classNaming; + } + + public function getDefinition(): RectorDefinition + { + return new RectorDefinition('Migrate CakePHP 2.4 Controller $components property to Symfony 5', [ + new CodeSample( + <<<'PHP' +class MessagesController extends \AppController +{ + public $components = ['Overview']; + + public function someAction() + { + $this->Overview->filter(); + } +} + +class OverviewComponent extends \Component +{ + public function filter() + { + } +} +PHP +, + <<<'PHP' +class MessagesController extends \AppController +{ + private function __construct(OverviewComponent $overviewComponent) + { + $this->overviewComponent->filter(); + } + + public function someAction() + { + $this->overviewComponent->filter(); + } +} + +class OverviewComponent extends \Component +{ + public function filter() + { + } +} +PHP + + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->isInCakePHPController($node)) { + return null; + } + + $componentsProperty = $this->classManipulator->getProperty($node, 'components'); + if ($componentsProperty === null) { + return null; + } + + $defaultValue = $componentsProperty->props[0]->default; + if ($defaultValue === null) { + return null; + } + + $componentNames = $this->getValue($defaultValue); + if (! is_array($componentNames)) { + throw new ShouldNotHappenException(); + } + + $this->removeNode($componentsProperty); + + $componentClasses = $this->matchComponentClass($componentNames); + + $oldProperyNameToNewPropertyName = []; + + foreach ($componentClasses as $componentName => $componentClass) { + $componentClassShortName = $this->classNaming->getShortName($componentClass); + $propertyShortName = lcfirst($componentClassShortName); + $this->addPropertyToClass($node, new ObjectType($componentClass), $propertyShortName); + + $oldProperyNameToNewPropertyName[$componentName] = $propertyShortName; + } + + $this->classManipulator->renamePropertyFetches($node, $oldProperyNameToNewPropertyName); + + return $node; + } + + /** + * @return string[] + */ + private function getComponentClasses(): array + { + if ($this->componentsClasses) { + return $this->componentsClasses; + } + + foreach (get_declared_classes() as $declaredClass) { + if ($declaredClass === 'Component') { + continue; + } + + if (! is_a($declaredClass, 'Component', true)) { + continue; + } + + $this->componentsClasses[] = $declaredClass; + } + + return $this->componentsClasses; + } + + /** + * @param string[] $componentNames + * @return string[] + */ + private function matchComponentClass(array $componentNames): array + { + $componentsClasses = $this->getComponentClasses(); + + $matchedComponentClasses = []; + + foreach ($componentNames as $componentName) { + foreach ($componentsClasses as $componentClass) { + $shortComponentClass = $this->classNaming->getShortName($componentClass); + if (! Strings::startsWith($shortComponentClass, $componentName)) { + continue; + } + + $matchedComponentClasses[$componentName] = $componentClass; + } + } + + return $matchedComponentClasses; + } +} diff --git a/packages/CakePHPToSymfony/src/Rector/Class_/CakePHPControllerHelperToSymfonyRector.php b/packages/CakePHPToSymfony/src/Rector/Class_/CakePHPControllerHelperToSymfonyRector.php new file mode 100644 index 000000000000..d12cab8bdd53 --- /dev/null +++ b/packages/CakePHPToSymfony/src/Rector/Class_/CakePHPControllerHelperToSymfonyRector.php @@ -0,0 +1,142 @@ +classManipulator = $classManipulator; + } + + public function getDefinition(): RectorDefinition + { + return new RectorDefinition('Migrate CakePHP 2.4 Controller $helpers and $components property to Symfony 5', [ + new CodeSample( + <<<'PHP' +class HomepageController extends AppController +{ + public $helpers = ['Flash']; + + public function index() + { + $this->Flash->success(__('Your post has been saved.')); + $this->Flash->error(__('Unable to add your post.')); + } +} +PHP +, + <<<'PHP' +class HomepageController extends AppController +{ + public function index() + { + $this->addFlash('success', __('Your post has been saved.')); + $this->addFlash('error', __('Unable to add your post.')); + } +} +PHP + + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->isInCakePHPController($node)) { + return null; + } + + $helpersProperty = $this->classManipulator->getProperty($node, 'helpers'); + if ($helpersProperty === null) { + return null; + } + + // remove $helpers completely + $this->removeNode($helpersProperty); + + // replace $this->Flash->.. → $this->addFlash(...) + foreach ($node->getMethods() as $classMethod) { + $this->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) { + if (! $this->isThisFlashMethodCall($node)) { + return null; + } + + /** @var MethodCall $node */ + $message = $node->args[0]->value; + + $kind = $this->getName($node->name); + if ($kind === null) { + throw new ShouldNotHappenException(); + } + + $thisVariable = new Variable('this'); + + $args = [new Arg(new String_($kind)), new Arg($message)]; + + return new MethodCall($thisVariable, 'addFlash', $args); + }); + } + + return $node; + } + + private function isThisFlashMethodCall(Node $node): bool + { + if (! $node instanceof MethodCall) { + return false; + } + + if (! $node->var instanceof PropertyFetch) { + return false; + } + + if (! $this->isName($node->var->var, 'this')) { + return false; + } + + if (! $this->isName($node->var->name, 'Flash')) { + return false; + } + + return true; + } +} diff --git a/packages/CakePHPToSymfony/src/Rector/Class_/CakePHPControllerToSymfonyControllerRector.php b/packages/CakePHPToSymfony/src/Rector/Class_/CakePHPControllerToSymfonyControllerRector.php new file mode 100644 index 000000000000..0b748a1cc254 --- /dev/null +++ b/packages/CakePHPToSymfony/src/Rector/Class_/CakePHPControllerToSymfonyControllerRector.php @@ -0,0 +1,73 @@ +isInCakePHPController($node)) { + return null; + } + + $node->extends = new FullyQualified('Symfony\Bundle\FrameworkBundle\Controller\AbstractController'); + + return $node; + } +} diff --git a/packages/CakePHPToSymfony/src/Rector/Template/TemplateMethodCallManipulator.php b/packages/CakePHPToSymfony/src/Rector/Template/TemplateMethodCallManipulator.php new file mode 100644 index 000000000000..357f4d048aa3 --- /dev/null +++ b/packages/CakePHPToSymfony/src/Rector/Template/TemplateMethodCallManipulator.php @@ -0,0 +1,130 @@ +valueResolver = $valueResolver; + $this->templatePathResolver = $templatePathResolver; + $this->callableNodeTraverser = $callableNodeTraverser; + $this->nameResolver = $nameResolver; + } + + public function refactorExistingRenderMethodCall(ClassMethod $classMethod): void + { + $controllerNamePart = $this->templatePathResolver->resolveClassNameTemplatePart($classMethod); + + $this->callableNodeTraverser->traverseNodesWithCallable($classMethod, function (Node $node) use ( + $controllerNamePart + ) { + $renderMethodCall = $this->matchThisRenderMethodCallBareOrInReturn($node); + if ($renderMethodCall === null) { + return null; + } + + return $this->refactorRenderTemplateName($renderMethodCall, $controllerNamePart); + }); + } + + private function refactorRenderTemplateName(Node $node, string $controllerNamePart): ?Return_ + { + /** @var MethodCall $node */ + $renderArgumentValue = $this->valueResolver->getValue($node->args[0]->value); + + /** @var string|mixed $renderArgumentValue */ + if (! is_string($renderArgumentValue)) { + return null; + } + + if (Strings::contains($renderArgumentValue, '/')) { + $templateName = $renderArgumentValue . '.twig'; + } else { + // add explicit controller + $templateName = $controllerNamePart . '/' . $renderArgumentValue . '.twig'; + } + + $node->args[0]->value = new String_($templateName); + + return new Return_($node); + } + + private function isThisRenderMethodCall(Node $node): bool + { + if (! $node instanceof MethodCall) { + return false; + } + + if (! $this->nameResolver->isName($node->var, 'this')) { + return false; + } + + return $this->nameResolver->isName($node->name, 'render'); + } + + private function matchThisRenderMethodCallBareOrInReturn(Node $node): ?MethodCall + { + if ($node instanceof Return_) { + $nodeExpr = $node->expr; + if ($nodeExpr === null) { + return null; + } + + if (! $this->isThisRenderMethodCall($nodeExpr)) { + return null; + } + + /** @var MethodCall $nodeExpr */ + return $nodeExpr; + } + + if ($node instanceof Expression) { + if (! $this->isThisRenderMethodCall($node->expr)) { + return null; + } + + return $node->expr; + } + + return null; + } +} diff --git a/packages/CakePHPToSymfony/src/Rector/TemplatePathResolver.php b/packages/CakePHPToSymfony/src/Rector/TemplatePathResolver.php new file mode 100644 index 000000000000..52ce1962fc73 --- /dev/null +++ b/packages/CakePHPToSymfony/src/Rector/TemplatePathResolver.php @@ -0,0 +1,122 @@ +callableNodeTraverser = $callableNodeTraverser; + $this->propertyFetchManipulator = $propertyFetchManipulator; + $this->valueResolver = $valueResolver; + $this->classNaming = $classNaming; + $this->nodeRemovingCommander = $nodeRemovingCommander; + } + + public function resolveForClassMethod(ClassMethod $classMethod): string + { + $viewPropertyValue = $this->resolveViewPropertyValue($classMethod); + if ($viewPropertyValue !== null) { + $viewPropertyValue = Strings::lower($viewPropertyValue); + return $viewPropertyValue . '.twig'; + } + + $classAndMethodValue = $this->resolveFromClassAndMethod($classMethod); + + return $classAndMethodValue . '.twig'; + } + + public function resolveClassNameTemplatePart(ClassMethod $classMethod): string + { + /** @var string $className */ + $className = $classMethod->getAttribute(AttributeKey::CLASS_NAME); + $shortClassName = $this->classNaming->getShortName($className); + + $shortClassName = Strings::replace($shortClassName, '#Controller$#i'); + + return Strings::lower($shortClassName); + } + + private function resolveViewPropertyValue(ClassMethod $classMethod): ?string + { + $setViewProperty = null; + + $this->callableNodeTraverser->traverseNodesWithCallable($classMethod, function (Node $node) use ( + &$setViewProperty + ) { + if (! $node instanceof Assign) { + return null; + } + + if (! $this->propertyFetchManipulator->isToThisPropertyFetchOfSpecificNameAssign($node, 'view')) { + return null; + } + + $setViewProperty = $node->expr; + + $this->nodeRemovingCommander->addNode($node); + }); + + if ($setViewProperty === null) { + return null; + } + + $setViewValue = $this->valueResolver->getValue($setViewProperty); + if (is_string($setViewValue)) { + return Strings::lower($setViewValue); + } + + return null; + } + + private function resolveFromClassAndMethod(ClassMethod $classMethod): string + { + $shortClassName = $this->resolveClassNameTemplatePart($classMethod); + $methodName = $classMethod->getAttribute(AttributeKey::METHOD_NAME); + + return $shortClassName . '/' . $methodName; + } +} diff --git a/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerActionToSymfonyControllerActionRector/CakePHPControllerActionToSymfonyControllerActionRectorTest.php b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerActionToSymfonyControllerActionRector/CakePHPControllerActionToSymfonyControllerActionRectorTest.php new file mode 100644 index 000000000000..daabcd930758 --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerActionToSymfonyControllerActionRector/CakePHPControllerActionToSymfonyControllerActionRectorTest.php @@ -0,0 +1,30 @@ +doTestFile($file); + } + + public function provideDataForTest(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + protected function getRectorClass(): string + { + return CakePHPControllerActionToSymfonyControllerActionRector::class; + } +} diff --git a/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerActionToSymfonyControllerActionRector/Fixture/fixture.php.inc b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerActionToSymfonyControllerActionRector/Fixture/fixture.php.inc new file mode 100644 index 000000000000..f0ac934fabb6 --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerActionToSymfonyControllerActionRector/Fixture/fixture.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRedirectToSymfonyRector/CakePHPControllerRedirectToSymfonyRectorTest.php b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRedirectToSymfonyRector/CakePHPControllerRedirectToSymfonyRectorTest.php new file mode 100644 index 000000000000..94daa5a8a24c --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRedirectToSymfonyRector/CakePHPControllerRedirectToSymfonyRectorTest.php @@ -0,0 +1,30 @@ +doTestFile($file); + } + + public function provideDataForTest(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + protected function getRectorClass(): string + { + return CakePHPControllerRedirectToSymfonyRector::class; + } +} diff --git a/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRedirectToSymfonyRector/Fixture/fixture.php.inc b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRedirectToSymfonyRector/Fixture/fixture.php.inc new file mode 100644 index 000000000000..c5027ea9626f --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRedirectToSymfonyRector/Fixture/fixture.php.inc @@ -0,0 +1,27 @@ +redirect('boom'); + } +} + +?> +----- +redirect('boom'); + } +} + +?> diff --git a/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRedirectToSymfonyRector/Fixture/redirect_to_controller.php.inc b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRedirectToSymfonyRector/Fixture/redirect_to_controller.php.inc new file mode 100644 index 000000000000..be8591912e68 --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRedirectToSymfonyRector/Fixture/redirect_to_controller.php.inc @@ -0,0 +1,30 @@ +redirect([ + 'controller' => 'DocumentVersions', + 'action' => 'getFile' + ]); + } +} + +?> +----- +redirectToRoute('document_versions_get_file'); + } +} + +?> diff --git a/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/CakePHPControllerRenderToSymfonyRectorTest.php b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/CakePHPControllerRenderToSymfonyRectorTest.php new file mode 100644 index 000000000000..5b0908361c7a --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/CakePHPControllerRenderToSymfonyRectorTest.php @@ -0,0 +1,30 @@ +doTestFile($file); + } + + public function provideDataForTest(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + protected function getRectorClass(): string + { + return CakePHPControllerRenderToSymfonyRector::class; + } +} diff --git a/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/Fixture/changed_view_property.php.inc b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/Fixture/changed_view_property.php.inc new file mode 100644 index 000000000000..5740ce2772a6 --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/Fixture/changed_view_property.php.inc @@ -0,0 +1,27 @@ +view = 'Controller/this_value'; + } +} + +?> +----- +render('controller/this_value.twig'); + } +} + +?> diff --git a/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/Fixture/compact_too.php.inc b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/Fixture/compact_too.php.inc new file mode 100644 index 000000000000..1c635cddd030 --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/Fixture/compact_too.php.inc @@ -0,0 +1,27 @@ +set(compact('clientId', 'states', 'invoiceTemplates')); + } +} + +?> +----- +render('compacttoo/index.twig', ['clientId' => $clientId, 'states' => $states, 'invoiceTemplates' => $invoiceTemplates]); + } +} + +?> diff --git a/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/Fixture/fixture.php.inc b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/Fixture/fixture.php.inc new file mode 100644 index 000000000000..13e924726fce --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/Fixture/fixture.php.inc @@ -0,0 +1,27 @@ +render('custom_file'); + } +} + +?> +----- +render('redirect/custom_file.twig'); + } +} + +?> diff --git a/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/Fixture/set.inc b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/Fixture/set.inc new file mode 100644 index 000000000000..f565987bedad --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/ClassMethod/CakePHPControllerRenderToSymfonyRector/Fixture/set.inc @@ -0,0 +1,27 @@ +set('name', 5); + } +} + +?> +----- +render('homepage/index.twig', ['name' => 5]); + } +} + +?> diff --git a/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerComponentToSymfonyRector/CakePHPControllerComponentToSymfonyRectorTest.php b/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerComponentToSymfonyRector/CakePHPControllerComponentToSymfonyRectorTest.php new file mode 100644 index 000000000000..bd1a02b4d3fc --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerComponentToSymfonyRector/CakePHPControllerComponentToSymfonyRectorTest.php @@ -0,0 +1,30 @@ +doTestFile($file); + } + + public function provideDataForTest(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + protected function getRectorClass(): string + { + return CakePHPControllerComponentToSymfonyRector::class; + } +} diff --git a/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerComponentToSymfonyRector/Fixture/fixture.php.inc b/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerComponentToSymfonyRector/Fixture/fixture.php.inc new file mode 100644 index 000000000000..522db5d8d346 --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerComponentToSymfonyRector/Fixture/fixture.php.inc @@ -0,0 +1,51 @@ +Overview->filter(); + } +} + +class OverviewComponent extends \Component +{ + public function filter() + { + } +} + +?> +----- +overviewComponent = $overviewComponent; + } + public function someAction() + { + $this->overviewComponent->filter(); + } +} + +class OverviewComponent extends \Component +{ + public function filter() + { + } +} + +?> diff --git a/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerComponentToSymfonyRector/Source/Component.php b/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerComponentToSymfonyRector/Source/Component.php new file mode 100644 index 000000000000..f3b370c1fa6e --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerComponentToSymfonyRector/Source/Component.php @@ -0,0 +1,8 @@ +doTestFile($file); + } + + public function provideDataForTest(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + protected function getRectorClass(): string + { + return CakePHPControllerHelperToSymfonyRector::class; + } +} diff --git a/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerHelperToSymfonyRector/Fixture/fixture.php.inc b/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerHelperToSymfonyRector/Fixture/fixture.php.inc new file mode 100644 index 000000000000..6530f12cfb92 --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerHelperToSymfonyRector/Fixture/fixture.php.inc @@ -0,0 +1,31 @@ +Flash->success(__('Your post has been saved.')); + $this->Flash->error(__('Unable to add your post.')); + } +} + +?> +----- +addFlash('success', __('Your post has been saved.')); + $this->addFlash('error', __('Unable to add your post.')); + } +} + +?> diff --git a/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerToSymfonyControllerRector/CakePHPControllerToSymfonyControllerRectorTest.php b/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerToSymfonyControllerRector/CakePHPControllerToSymfonyControllerRectorTest.php new file mode 100644 index 000000000000..76c358c19dab --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerToSymfonyControllerRector/CakePHPControllerToSymfonyControllerRectorTest.php @@ -0,0 +1,30 @@ +doTestFile($file); + } + + public function provideDataForTest(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + protected function getRectorClass(): string + { + return CakePHPControllerToSymfonyControllerRector::class; + } +} diff --git a/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerToSymfonyControllerRector/Fixture/fixture.php.inc b/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerToSymfonyControllerRector/Fixture/fixture.php.inc new file mode 100644 index 000000000000..7562300b7395 --- /dev/null +++ b/packages/CakePHPToSymfony/tests/Rector/Class_/CakePHPControllerToSymfonyControllerRector/Fixture/fixture.php.inc @@ -0,0 +1,25 @@ + +----- + diff --git a/packages/CodeQuality/config/config.yaml b/packages/CodeQuality/config/config.yaml new file mode 100644 index 000000000000..149bc0265bbb --- /dev/null +++ b/packages/CodeQuality/config/config.yaml @@ -0,0 +1,9 @@ +services: + _defaults: + autowire: true + public: true + + Rector\CodeQuality\: + resource: '../src' + exclude: + - '../src/Rector/**/*Rector.php' diff --git a/packages/CodeQuality/src/CompactConverter.php b/packages/CodeQuality/src/CompactConverter.php new file mode 100644 index 000000000000..1657e8654b3d --- /dev/null +++ b/packages/CodeQuality/src/CompactConverter.php @@ -0,0 +1,43 @@ +valueResolver = $valueResolver; + } + + public function convertToArray(FuncCall $funcCall): Array_ + { + $array = new Array_(); + + foreach ($funcCall->args as $arg) { + /** @var string|null $variableName */ + $variableName = $this->valueResolver->getValue($arg->value); + if (! is_string($variableName)) { + throw new ShouldNotHappenException(); + } + + $array->items[] = new ArrayItem(new Variable($variableName), new String_($variableName)); + } + + return $array; + } +} diff --git a/packages/CodeQuality/src/Rector/FuncCall/CompactToVariablesRector.php b/packages/CodeQuality/src/Rector/FuncCall/CompactToVariablesRector.php index 9d8b3287b1dd..3e551ae4fb2e 100644 --- a/packages/CodeQuality/src/Rector/FuncCall/CompactToVariablesRector.php +++ b/packages/CodeQuality/src/Rector/FuncCall/CompactToVariablesRector.php @@ -5,11 +5,8 @@ namespace Rector\CodeQuality\Rector\FuncCall; use PhpParser\Node; -use PhpParser\Node\Expr\Array_; -use PhpParser\Node\Expr\ArrayItem; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Expr\Variable; -use PhpParser\Node\Scalar\String_; +use Rector\CodeQuality\CompactConverter; use Rector\Rector\AbstractRector; use Rector\RectorDefinition\CodeSample; use Rector\RectorDefinition\RectorDefinition; @@ -21,6 +18,16 @@ */ final class CompactToVariablesRector extends AbstractRector { + /** + * @var CompactConverter + */ + private $compactConverter; + + public function __construct(CompactConverter $compactConverter) + { + $this->compactConverter = $compactConverter; + } + public function getDefinition(): RectorDefinition { return new RectorDefinition('Change compact() call to own array', [ @@ -71,13 +78,6 @@ public function refactor(Node $node): ?Node return null; } - $array = new Array_(); - - foreach ($node->args as $arg) { - $variableName = $this->getValue($arg->value); - $array->items[] = new ArrayItem(new Variable($variableName), new String_($variableName)); - } - - return $array; + return $this->compactConverter->convertToArray($node); } } diff --git a/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/ConstructorPropertyTypeInferer.php b/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/ConstructorPropertyTypeInferer.php index 254f1ff85a14..b5ff421f6ec8 100644 --- a/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/ConstructorPropertyTypeInferer.php +++ b/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/ConstructorPropertyTypeInferer.php @@ -21,6 +21,7 @@ use PHPStan\Type\NullType; use PHPStan\Type\Type; use Rector\NodeTypeResolver\Node\AttributeKey; +use Rector\PhpParser\Node\Manipulator\PropertyFetchManipulator; use Rector\PHPStan\Type\AliasedObjectType; use Rector\PHPStan\Type\FullyQualifiedObjectType; use Rector\TypeDeclaration\Contract\TypeInferer\PropertyTypeInfererInterface; @@ -28,6 +29,16 @@ final class ConstructorPropertyTypeInferer extends AbstractTypeInferer implements PropertyTypeInfererInterface { + /** + * @var PropertyFetchManipulator + */ + private $propertyFetchManipulator; + + public function __construct(PropertyFetchManipulator $propertyFetchManipulator) + { + $this->propertyFetchManipulator = $propertyFetchManipulator; + } + public function inferProperty(Property $property): Type { /** @var Class_|null $class */ @@ -44,7 +55,7 @@ public function inferProperty(Property $property): Type $propertyName = $this->nameResolver->getName($property); - $param = $this->resolveParamForPropertyFetch($classMethod, $propertyName); + $param = $this->propertyFetchManipulator->resolveParamForPropertyFetch($classMethod, $propertyName); if ($param === null) { return new MixedType(); } diff --git a/phpstan.neon b/phpstan.neon index 59ef8f43c635..76bdd250188d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -43,13 +43,9 @@ parameters: # 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#' - - '#Access to an undefined property PhpParser\\Node\\Stmt\\ClassLike\:\:\$extends#' # misuse of interface and class - '#Parameter \#1 (.*?) expects Symfony\\Component\\DependencyInjection\\ContainerBuilder, Symfony\\Component\\DependencyInjection\\ContainerInterface given#' - - '#Method Rector\\Symfony\\Bridge\\DefaultAnalyzedSymfonyApplicationContainer::getContainer\(\) should return Symfony\\Component\\DependencyInjection\\ContainerBuilder but returns Symfony\\Component\\DependencyInjection\\Container#' - - - '#Property Rector\\DependencyInjection\\Loader\\RectorServiceParametersShifter::\$serviceKeywords \(array\) does not accept ReflectionProperty#' - '#Strict comparison using === between string and null will always evaluate to false#' # false positive - type is set by annotation above @@ -112,7 +108,6 @@ parameters: # false positive 0.11.5 - '#Unreachable statement \- code above always terminates#' - - '#Method Rector\\NodeTypeResolver\\NodeVisitor\\(.*?)\:\:enterNode\(\) should return int\|PhpParser\\Node\|void\|null but return statement is missing#' - '#Negated boolean expression is always true#' - '#Strict comparison using \=\=\= between PhpParser\\Node and null will always evaluate to false#' @@ -133,29 +128,21 @@ parameters: - '#Cannot cast array\|bool\|string\|null to string#' - '#Method Rector\\Legacy\\Rector\\ClassMethod\\ChangeSingletonToServiceRector\:\:matchStaticPropertyFetchAndGetSingletonMethodName\(\) should return array\|null but returns array#' - - '#Parameter \#1 \$rule of method Rector\\Configuration\\Configuration\:\:setRule\(\) expects string\|null, array\|bool\|string\|null given#' - - '#Empty catch block\. If you are sure this is meant to be empty, please add a "// @ignoreException" comment in the catch block#' - - '#Method Rector\\Rector\\AbstractRector\:\:wrapToArg\(\) should return array but returns array#' - '#Parameter \#2 \$currentNode of method Rector\\CodingStyle\\Rector\\String_\\ManualJsonStringToJsonEncodeArrayRector\:\:matchNextExpressionAssignConcatToSameVariable\(\) expects PhpParser\\Node\\Expr\\Assign\|PhpParser\\Node\\Expr\\AssignOp\\Concat, PhpParser\\Node given#' - '#Method Rector\\FileSystemRector\\Rector\\AbstractFileSystemRector\:\:wrapToArg\(\) should return array but returns array#' # array is callable - - '#Parameter \#2 \$listener of method Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher\:\:getListenerPriority\(\) expects callable\(\)\: mixed, array given#' - - '#Parameter \#1 \$kernelClass of method Rector\\Symfony\\Bridge\\DependencyInjection\\SymfonyContainerFactory\:\:createFromKernelClass\(\) expects string, string\|null given#' - '#If condition is always true#' - '#Ternary operator condition is always true#' - - '#Method Rector\\Symfony\\Bridge\\DefaultAnalyzedSymfonyApplicationContainer\:\:getService\(\) should return object but returns object\|null#' - '#Call to function property_exists\(\) with string and (.*?) will always evaluate to false#' - '#Access to an undefined property PhpParser\\Node\\FunctionLike\|PhpParser\\Node\\Stmt\\ClassLike\:\:\$stmts#' - '#Property Rector\\TypeDeclaration\\TypeInferer\\(.*?)\:\:\$(.*?)TypeInferers \(array\) does not accept array#' # sense-less errors - - '#In method "Rector\\Rector\\Property\\InjectAnnotationClassRector\:\:resolveJMSDIInjectType", caught "Throwable" must be rethrown\. Either catch a more specific exception or add a "throw" clause in the "catch" block to propagate the exception\. More info\: http\://bit\.ly/failloud#' - - '#Instanceof between PHPStan\\Type\\Type and PHPStan\\Type\\VoidType will always evaluate to false#' - '#Parameter \#1 \$functionLike of method Rector\\NodeTypeResolver\\PhpDoc\\NodeAnalyzer\\DocBlockManipulator\:\:getParamTypesByName\(\) expects PhpParser\\Node\\Expr\\Closure\|PhpParser\\Node\\Stmt\\ClassMethod\|PhpParser\\Node\\Stmt\\Function_, PhpParser\\Node\\FunctionLike given#' # PHP 7.4 1_000 support @@ -171,14 +158,11 @@ parameters: - '#Parameter \#1 \$sprintfFuncCall of method Rector\\PhpParser\\NodeTransformer\:\:transformSprintfToArray\(\) expects PhpParser\\Node\\Expr\\FuncCall, PhpParser\\Node given#' - '#Method Rector\\BetterPhpDocParser\\Tests\\PhpDocParser\\OrmTagParser\\AbstractPhpDocInfoTest\:\:parseFileAndGetFirstNodeOfType\(\) should return PhpParser\\Node but returns PhpParser\\Node\|null#' - - '#Parameter \#1 \$phpDocTagValueNode of method Rector\\BetterPhpDocParser\\PhpDocInfo\\PhpDocInfo\:\:removeTagValueNodeFromNode\(\) expects PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocTagValueNode, PHPStan\\PhpDocParser\\Ast\\PhpDoc\\VarTagValueNode\|null given#' - - '#Method Rector\\BetterPhpDocParser\\PhpDocNodeFactory\\JMS\\SerializerTypePhpDocNodeFactory::resolveTypeAnnotation\(\) should return JMS\\Serializer\\Annotation\\Type\|null but returns object\|null#' # known value - '#Method Rector\\StrictCodeQuality\\Rector\\Stmt\\VarInlineAnnotationToAssertRector\:\:findVariableByName\(\) should return PhpParser\\Node\\Expr\\Variable\|null but returns PhpParser\\Node\|null#' - - '#In method "Rector\\BetterPhpDocParser\\AnnotationReader\\NodeAnnotationReader\:\:createPropertyReflectionFromPropertyNode", caught "Throwable" must be rethrown\. Either catch a more specific exception or add a "throw" clause in the "catch" block to propagate the exception\. More info\: http\://bit\.ly/failloud#' - '#Method Rector\\PhpParser\\Node\\Manipulator\\MethodCallManipulator\:\:findAssignToVariableName\(\) should return PhpParser\\Node\\Expr\\Assign\|null but returns PhpParser\\Node\|null#' - '#Method Rector\\PhpParser\\Node\\Manipulator\\MethodCallManipulator\:\:findMethodCallsIncludingChain\(\) should return array but returns array#' - '#Method Rector\\NodeTypeResolver\\PHPStan\\Type\\TypeFactory\:\:createUnionOrSingleType\(\) should return PHPStan\\Type\\MixedType\|PHPStan\\Type\\UnionType but returns PHPStan\\Type\\Type#' @@ -216,47 +200,26 @@ parameters: - '#Parameter \#3 \$nodeCallback of method PHPStan\\Analyser\\NodeScopeResolver::processNodes\(\) expects Closure\(PhpParser\\Node, PHPStan\\Analyser\\Scope\): void, Closure\(PhpParser\\Node, PHPStan\\Analyser\\MutatingScope\): void given#' # false positive - - '#Array \(array\) does not accept key#' - - '#Array \(array\) does not accept key#' - '#Comparison operation "<" between 0 and 2 is always true#' - - '#Property Rector\\Compiler\\Process\\SymfonyProcess::\$process type has no value type specified in iterable type Symfony\\Component\\Process\\Process#' - - '#Method Rector\\Compiler\\Process\\SymfonyProcess::getProcess\(\) return type has no value type specified in iterable type Symfony\\Component\\Process\\Process#' - - '#Method Rector\\Compiler\\Contract\\Process\\ProcessInterface::getProcess\(\) return type has no value type specified in iterable type Symfony\\Component\\Process\\Process#' - - # phpstan compiler bug - - '#Parameter \#1 \$docComment of method PhpParser\\Builder\\Property::setDocComment\(\) expects _HumbugBox(.*?)\\PhpParser\\Comment\\Doc\|string, PhpParser\\Comment\\Doc given#' - - - - message: '#Call to function in_array\(\) with arguments PhpParser\\Node\\Expr\\Variable, array\(\) and true will always evaluate to false#' - path: packages/Php56/src/Rector/FunctionLike/AddDefaultValueForUndefinedVariableRector.php - - # known values - - - message: '#Method Rector\\Rector\\Property\\InjectAnnotationClassRector\:\:resolveJMSDIInjectType\(\) should return PHPStan\\Type\\Type but returns PHPStan\\Type\\Type\|null#' - path: src/Rector/Property/InjectAnnotationClassRector.php - message: '#Method Rector\\Symfony\\Rector\\FrameworkBundle\\AbstractToConstructorInjectionRector\:\:getServiceTypeFromMethodCallArgument\(\) should return PHPStan\\Type\\Type but returns PHPStan\\Type\\Type\|null#' path: packages/Symfony/src/Rector/FrameworkBundle/AbstractToConstructorInjectionRector.php - '#Parameter \#1 \$error_handler of function set_error_handler expects \(callable\(int, string, string, int, array\)\: bool\)\|null, Closure\(int, string\)\: void given#' - - '#Parameter \#1 \$source of method Rector\\Scan\\ErrorScanner\:\:scanSource\(\) expects array, array\|string\|null given#' - '#Method Rector\\BetterPhpDocParser\\PhpDocNodeFactory\\Gedmo\\(.*?)\:\:createFromNodeAndTokens\(\) should return Rector\\BetterPhpDocParser\\PhpDocNode\\Gedmo\\(.*?)\|null but returns PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocTagValueNode\|null#' - - '#Access to an undefined property PhpParser\\Node\\Identifier\|PhpParser\\Node\\Name\|PhpParser\\Node\\NullableType\:\:\$type#' - - '#Call to an undefined method PhpParser\\Node\\Identifier\|PhpParser\\Node\\Name\|PhpParser\\Node\\NullableType\:\:toString\(\)#' - '#Parameter \#1 \$expected of method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) expects class\-string, string given#' - '#Unable to resolve the template type ExpectedType in call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\)#' - '#Right side of \|\| is always false#' - - '#Method Rector\\NodeContainer\\ParsedNodesByType\:\:findNewNodesByClass\(\) should return array but returns array#' - # fix Symplify 7.2 later - '#Method (.*?) returns bool type, so the name should start with is/has/was#' - - '#Property Rector\\BetterPhpDocParser\\PhpDocNode\\Gedmo\\BlameableTagValueNode\:\:\$value has no typehint specified#' - # known value - "#^Parameter \\#1 \\$variable of class Rector\\\\Php70\\\\ValueObject\\\\VariableAssignPair constructor expects PhpParser\\\\Node\\\\Expr\\\\ArrayDimFetch\\|PhpParser\\\\Node\\\\Expr\\\\PropertyFetch\\|PhpParser\\\\Node\\\\Expr\\\\StaticPropertyFetch\\|PhpParser\\\\Node\\\\Expr\\\\Variable, PhpParser\\\\Node\\\\Expr given\\.$#" - '#Cannot cast \(array\)\|string\|true to string#' + + - '#In method "Rector\\BetterPhpDocParser\\AnnotationReader\\NodeAnnotationReader\:\:createPropertyReflectionFromPropertyNode", caught "Throwable" must be rethrown\. Either catch a more specific exception or add a "throw" clause in the "catch" block to propagate the exception\. More info\: http\://bit\.ly/failloud#' + - '#Method Rector\\CakePHPToSymfony\\Rector\\Template\\TemplateMethodCallManipulator\:\:matchThisRenderMethodCallBareOrInReturn\(\) should return PhpParser\\Node\\Expr\\MethodCall\|null but returns PhpParser\\Node\\Expr#' diff --git a/src/PhpParser/Node/Manipulator/ClassManipulator.php b/src/PhpParser/Node/Manipulator/ClassManipulator.php index c2d2854f6ab0..d59b90093dda 100644 --- a/src/PhpParser/Node/Manipulator/ClassManipulator.php +++ b/src/PhpParser/Node/Manipulator/ClassManipulator.php @@ -7,6 +7,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Param; @@ -465,6 +466,34 @@ public function removeInterface(Class_ $class, string $desiredInterface): void } } + /** + * @param string[] $oldToNewPropertyNames + */ + public function renamePropertyFetches(Class_ $class, array $oldToNewPropertyNames): void + { + $this->callableNodeTraverser->traverseNodesWithCallable($class, function (Node $node) use ( + $oldToNewPropertyNames + ) { + if (! $node instanceof Node\Expr\PropertyFetch) { + return null; + } + + if (! $this->nameResolver->isName($node->var, 'this')) { + return null; + } + + foreach ($oldToNewPropertyNames as $oldPropertyName => $newPropertyName) { + if (! $this->nameResolver->isName($node->name, $oldPropertyName)) { + continue; + } + + $node->name = new Identifier($newPropertyName); + } + + return null; + }); + } + private function tryInsertBeforeFirstMethod(Class_ $classNode, Stmt $stmt): bool { foreach ($classNode->stmts as $key => $classStmt) { diff --git a/src/PhpParser/Node/Manipulator/PropertyFetchManipulator.php b/src/PhpParser/Node/Manipulator/PropertyFetchManipulator.php index 24ccd48bb8d5..659059de140c 100644 --- a/src/PhpParser/Node/Manipulator/PropertyFetchManipulator.php +++ b/src/PhpParser/Node/Manipulator/PropertyFetchManipulator.php @@ -5,7 +5,6 @@ namespace Rector\PhpParser\Node\Manipulator; use PhpParser\Node; -use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\PropertyFetch; @@ -52,11 +51,6 @@ final class PropertyFetchManipulator */ private $callableNodeTraverser; - /** - * @var AssignManipulator - */ - private $assignManipulator; - public function __construct( NodeTypeResolver $nodeTypeResolver, ReflectionProvider $reflectionProvider, @@ -69,14 +63,6 @@ public function __construct( $this->callableNodeTraverser = $callableNodeTraverser; } - /** - * @required - */ - public function autowirePropertyFetchManipulator(AssignManipulator $assignManipulator): void - { - $this->assignManipulator = $assignManipulator; - } - public function isPropertyToSelf(PropertyFetch $propertyFetch): bool { if (! $this->nameResolver->isName($propertyFetch->var, 'this')) { @@ -204,58 +190,6 @@ public function isLocalProperty(Node $node): bool return $this->nameResolver->isName($node->var, 'this'); } - public function getFirstVariableAssignedToPropertyOfName( - ClassMethod $classMethod, - string $propertyName - ): ?Variable { - $variable = null; - - $this->callableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) use ( - $propertyName, - &$variable - ): ?int { - if (! $node instanceof Assign) { - return null; - } - - if (! $this->isLocalPropertyOfNames($node->var, [$propertyName])) { - return null; - } - - if (! $node->expr instanceof Variable) { - return null; - } - - $variable = $node->expr; - - return NodeTraverser::STOP_TRAVERSAL; - }); - - return $variable; - } - - /** - * @return Expr[] - */ - public function getExprsAssignedToPropertyName(ClassMethod $classMethod, string $propertyName): array - { - $assignedExprs = []; - - $this->callableNodeTraverser->traverseNodesWithCallable($classMethod, function (Node $node) use ( - $propertyName, - &$assignedExprs - ) { - if (! $this->assignManipulator->isLocalPropertyAssignWithPropertyNames($node, [$propertyName])) { - return null; - } - - /** @var Assign $node */ - $assignedExprs[] = $node->expr; - }); - - return $assignedExprs; - } - /** * In case the property name is different to param name: * @@ -329,6 +263,27 @@ public function matchPropertyFetch(Node $node): ?Node return null; } + /** + * Matches: + * "$this->someValue = $;" + */ + public function isToThisPropertyFetchOfSpecificNameAssign(Node $node, string $propertyName): bool + { + if (! $node instanceof Assign) { + return false; + } + + if (! $node->var instanceof PropertyFetch) { + return false; + } + + if (! $this->nameResolver->isName($node->var->var, 'this')) { + return false; + } + + return $this->nameResolver->isName($node->var->name, $propertyName); + } + private function hasPublicProperty(PropertyFetch $propertyFetch, string $propertyName): bool { $nodeScope = $propertyFetch->getAttribute(AttributeKey::SCOPE); diff --git a/utils/PHPStanStaticTypeMapperChecker/src/Command/CheckStaticTypeMappersCommand.php b/utils/PHPStanStaticTypeMapperChecker/src/Command/CheckStaticTypeMappersCommand.php index e5ccfb1a9b45..822cc79c94a2 100644 --- a/utils/PHPStanStaticTypeMapperChecker/src/Command/CheckStaticTypeMappersCommand.php +++ b/utils/PHPStanStaticTypeMapperChecker/src/Command/CheckStaticTypeMappersCommand.php @@ -64,8 +64,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->symfonyStyle->error('Some classes are missing nodes'); $this->symfonyStyle->listing($missingNodeClasses); - die; - return Shell::CODE_ERROR; }