Skip to content

Commit

Permalink
[FEATURE] Add ExtbaseActionsWithRedirectMustReturnResponseInterfaceRe…
Browse files Browse the repository at this point in the history
…ctor

Resolves: #2863, #4393
  • Loading branch information
simonschaufi committed Oct 21, 2024
1 parent 1021763 commit 02db721
Show file tree
Hide file tree
Showing 7 changed files with 552 additions and 0 deletions.
2 changes: 2 additions & 0 deletions config/v12/typo3-120.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Ssch\TYPO3Rector\CodeQuality\General\RenameClassMapAliasRector;
use Ssch\TYPO3Rector\TYPO312\v0\AddMethodToWidgetInterfaceClassesRector;
use Ssch\TYPO3Rector\TYPO312\v0\ChangeExtbaseValidatorsRector;
use Ssch\TYPO3Rector\TYPO312\v0\ExtbaseActionsWithRedirectMustReturnResponseInterfaceRector;
use Ssch\TYPO3Rector\TYPO312\v0\ImplementSiteLanguageAwareInterfaceRector;
use Ssch\TYPO3Rector\TYPO312\v0\MigrateBackendModuleRegistrationRector;
use Ssch\TYPO3Rector\TYPO312\v0\MigrateContentObjectRendererGetTypoLinkUrlRector;
Expand Down Expand Up @@ -171,4 +172,5 @@
$rectorConfig->rule(MigrateFetchToFetchAssociativeRector::class);
$rectorConfig->rule(MigrateBackendModuleRegistrationRector::class);
$rectorConfig->rule(MigrateContentObjectRendererGetTypoLinkUrlRector::class);
$rectorConfig->rule(ExtbaseActionsWithRedirectMustReturnResponseInterfaceRector::class);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
<?php

declare(strict_types=1);

namespace Ssch\TYPO3Rector\TYPO312\v0;

use PhpParser\Node;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\Exit_;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Stmt\Throw_;
use PhpParser\NodeTraverser;
use PHPStan\Type\ObjectType;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @changelog https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/12.0/Breaking-96107-DeprecatedFunctionalityRemoved.html
* @see \Ssch\TYPO3Rector\Tests\Rector\v12\v0\ExtbaseActionsWithRedirectMustReturnResponseInterfaceRector\ExtbaseActionsWithRedirectMustReturnResponseInterfaceRectorTest
*/
final class ExtbaseActionsWithRedirectMustReturnResponseInterfaceRector extends AbstractRector
{
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [ClassMethod::class];
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Extbase controller actions with redirects must return ResponseInterface', [
new ConfiguredCodeSample(
<<<'CODE_SAMPLE'
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class MyController extends ActionController
{
public function someAction()
{
$this->redirect('foo', 'bar');
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class MyController extends ActionController
{
public function someAction(): ResponseInterface
{
return $this->redirect('foo', 'bar');
}
}
CODE_SAMPLE
,
[
'redirect_methods' => ['myRedirectMethod'],
]
),
]);
}

/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
if ($this->shouldSkip($node)) {
return null;
}

$this->traverseNodesWithCallable($node, function (Node $node) {
if ($node instanceof Class_ || $node instanceof Function_ || $node instanceof Closure) {
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
}

if (! $node instanceof Return_) {
return null;
}

$responseObjectType = new ObjectType('Psr\\Http\\Message\\ResponseInterface');

if ($node->expr !== null && $this->isObjectType($node->expr, $responseObjectType)) {
return null;
}

$returnCallExpression = $node->expr;

if ($returnCallExpression !== null && $this->isObjectType(
$returnCallExpression,
new ObjectType('Psr\Http\Message\ResponseInterface')
)) {
return null;
}

if ($returnCallExpression instanceof FuncCall && $this->isName(
$returnCallExpression->name,
'json_encode'
)) {
return new Return_($this->nodeFactory->createMethodCall(
'this',
'jsonResponse',
[$returnCallExpression]
));
}

// avoid duplication
$args = $node->expr instanceof MethodCall && $this->isName($node->expr->name, 'htmlResponse') ? [] : [
$node->expr,
];

return new Return_($this->createHtmlResponseMethodCall($args));
});

$node->returnType = new FullyQualified('Psr\Http\Message\ResponseInterface');

$statements = $node->stmts;
$lastStatement = null;

if (is_array($statements)) {
$lastStatement = array_pop($statements);
}

if (! $lastStatement instanceof Return_) {
$node->stmts[] = new Return_($this->createHtmlResponseMethodCall([]));
}

return $node;
}

private function shouldSkip(ClassMethod $classMethod): bool
{
if ($classMethod->returnType !== null
&& $this->isObjectType($classMethod->returnType, new ObjectType('Psr\\Http\\Message\\ResponseInterface'))
) {
return true;
}

if (! $this->nodeTypeResolver->isMethodStaticCallOrClassMethodObjectType(
$classMethod,
new ObjectType('TYPO3\CMS\Extbase\Mvc\Controller\ActionController')
)) {
return true;
}

if (! $classMethod->isPublic()) {
return true;
}

if ($classMethod->isAbstract()) {
return true;
}

$methodName = $this->getName($classMethod->name);

if ($methodName === null) {
return true;
}

if (! \str_ends_with($methodName, 'Action')) {
return true;
}

if (\str_starts_with($methodName, 'initialize')) {
return true;
}

if ($classMethod->stmts === null) {
return false;
}

$statements = $classMethod->stmts;
$lastStatement = array_pop($statements);

if ($lastStatement === null) {
return false;
}

if ($this->lastStatementIsExitCall($lastStatement)) {
return true;
}

if ($this->lastStatementIsForwardCall($lastStatement)) {
return true;
}

return $this->hasExceptionCall($lastStatement);
}

private function lastStatementIsExitCall(Node $lastStatement): bool
{
return $lastStatement instanceof Expression && $lastStatement->expr instanceof Exit_;
}

private function hasExceptionCall(Node $lastStatement): bool
{
if (! $lastStatement instanceof Throw_) {
return false;
}

$propagateResponseException = new ObjectType('TYPO3\CMS\Core\Http\PropagateResponseException');

return $this->getType($lastStatement->expr)
->isSuperTypeOf($propagateResponseException)
->yes();
}

private function lastStatementIsForwardCall(Node $lastStatement): bool
{
if (! $lastStatement instanceof Expression) {
return false;
}

if (! ($lastStatement->expr instanceof MethodCall)) {
return false;
}

return $this->isName($lastStatement->expr->name, 'forward');
}

/**
* @param mixed[] $args
*/
private function createHtmlResponseMethodCall(array $args): MethodCall
{
return $this->nodeFactory->createMethodCall('this', 'htmlResponse', $args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Ssch\TYPO3Rector\Tests\Rector\v12\v0\ExtbaseActionsWithRedirectMustReturnResponseInterfaceRector;

use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class ExtbaseActionsWithRedirectMustReturnResponseInterfaceRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

/**
* @return \Iterator<array<string>>
*/
public static function provideData(): \Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
Loading

0 comments on commit 02db721

Please sign in to comment.