Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,12 @@
},
"support": {
"source": "https://github.com/cakephp/upgrade"
},
"require-dev": {
"cakephp/cakephp": "5.x-dev",
"cakephp/cakephp-codesniffer": "^5.0",
"cakephp/migrations": "^4.5.0",
"mikey179/vfsstream": "^1.6.8",
"phpunit/phpunit": "^10.5.38"
}
}
9 changes: 9 additions & 0 deletions config/rector/cakephp60.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);

use Cake\Upgrade\Rector\Set\CakePHPSetList;
use Rector\Config\RectorConfig;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([CakePHPSetList::CAKEPHP_60]);
};
44 changes: 44 additions & 0 deletions config/rector/sets/cakephp60.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);

use Cake\Upgrade\Rector\Rector\MethodCall\RemoveAssignmentFromVoidMethodRector;
use Cake\Upgrade\Rector\ValueObject\VoidMethod;
use Rector\Config\RectorConfig;
use Rector\Renaming\Rector\MethodCall\RenameMethodRector;
use Rector\Renaming\ValueObject\MethodCallRename;

# @see https://book.cakephp.org/5/en/appendices/6-0-migration-guide.html
# @see https://github.com/cakephp/cakephp/pull/19220
# @see https://github.com/cakephp/cakephp/pull/19243
return static function (RectorConfig $rectorConfig): void {
// Rename validChoice() to validateChoice() for console input classes
$rectorConfig->ruleWithConfiguration(RenameMethodRector::class, [
new MethodCallRename('Cake\Console\ConsoleInputArgument', 'validChoice', 'validateChoice'),
new MethodCallRename('Cake\Console\ConsoleInputOption', 'validChoice', 'validateChoice'),
]);

// Remove assignments from methods that now return void instead of bool
// These methods either return void or throw exceptions, never return false
$rectorConfig->ruleWithConfiguration(RemoveAssignmentFromVoidMethodRector::class, [
// ServerRequest::allowMethod() - returns void or throws MethodNotAllowedException
new VoidMethod('Cake\Http\ServerRequest', 'allowMethod'),
// Configure::load() - returns void or throws CakeException
new VoidMethod('Cake\Core\Configure', 'load'),
// ResponseEmitter::emit() - returns void
new VoidMethod('Cake\Http\ResponseEmitter', 'emit'),
// Session::close() - returns void or throws RuntimeException
new VoidMethod('Cake\Http\Session', 'close'),
// Table::deleteOrFail() - returns void or throws PersistenceFailedException
new VoidMethod('Cake\ORM\Table', 'deleteOrFail'),
// FixtureInterface::insert() - returns void
new VoidMethod('Cake\Datasource\FixtureInterface', 'insert'),
new VoidMethod('Cake\TestSuite\Fixture\TestFixture', 'insert'),
// FixtureInterface::truncate() - returns void
new VoidMethod('Cake\Datasource\FixtureInterface', 'truncate'),
new VoidMethod('Cake\TestSuite\Fixture\TestFixture', 'truncate'),
// ConsoleInputArgument::validateChoice() - returns void or throws ConsoleException
new VoidMethod('Cake\Console\ConsoleInputArgument', 'validateChoice'),
// ConsoleInputOption::validateChoice() - returns void or throws ConsoleException
new VoidMethod('Cake\Console\ConsoleInputOption', 'validateChoice'),
]);
};
114 changes: 114 additions & 0 deletions src/Rector/Rector/MethodCall/RemoveAssignmentFromVoidMethodRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);

namespace Cake\Upgrade\Rector\Rector\MethodCall;

use Cake\Upgrade\Rector\ValueObject\VoidMethod;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Stmt\Expression;
use Rector\Contract\Rector\ConfigurableRectorInterface;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* Removes variable assignments from method calls that now return void.
*
* @see \Cake\Upgrade\Rector\Tests\Rector\MethodCall\RemoveAssignmentFromVoidMethodRector\RemoveAssignmentFromVoidMethodRectorTest
*/
final class RemoveAssignmentFromVoidMethodRector extends AbstractRector implements ConfigurableRectorInterface
{
/**
* @var array<\Cake\Upgrade\Rector\ValueObject\VoidMethod>
*/
private array $voidMethods = [];

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Remove variable assignment from method calls that now return void',
[
new ConfiguredCodeSample(
<<<'CODE_SAMPLE'
$result = $this->request->allowMethod('post');
CODE_SAMPLE,
<<<'CODE_SAMPLE'
$this->request->allowMethod('post');
CODE_SAMPLE,
[
new VoidMethod('Cake\Http\ServerRequest', 'allowMethod'),
],
),
],
);
}

/**
* @return array<class-string<\PhpParser\Node>>
*/
public function getNodeTypes(): array
{
return [Expression::class];
}

/**
* @param \PhpParser\Node\Stmt\Expression $node
*/
public function refactor(Node $node): ?Node
{
if (!$node->expr instanceof Assign) {
return null;
}

$assign = $node->expr;

// Handle instance method calls
if ($assign->expr instanceof MethodCall) {
$methodCall = $assign->expr;

foreach ($this->voidMethods as $voidMethod) {
if (!$this->isObjectType($methodCall->var, $voidMethod->getObjectType())) {
continue;
}

if (!$this->isName($methodCall->name, $voidMethod->getMethod())) {
continue;
}

// Replace the assignment with just the method call
return new Expression($methodCall);
}
}

// Handle static method calls
if ($assign->expr instanceof StaticCall) {
$staticCall = $assign->expr;

foreach ($this->voidMethods as $voidMethod) {
if (!$this->isName($staticCall->class, $voidMethod->getClass())) {
continue;
}

if (!$this->isName($staticCall->name, $voidMethod->getMethod())) {
continue;
}

// Replace the assignment with just the static call
return new Expression($staticCall);
}
}

return null;
}

/**
* @param array<mixed> $configuration
*/
public function configure(array $configuration): void
{
$this->voidMethods = $configuration;
}
}
5 changes: 5 additions & 0 deletions src/Rector/Set/CakePHPSetList.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ final class CakePHPSetList
*/
public const CAKEPHP_53 = __DIR__ . '/../../../config/rector/sets/cakephp53.php';

/**
* @var string
*/
public const CAKEPHP_60 = __DIR__ . '/../../../config/rector/sets/cakephp60.php';

/**
* @var string
*/
Expand Down
30 changes: 30 additions & 0 deletions src/Rector/ValueObject/VoidMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);

namespace Cake\Upgrade\Rector\ValueObject;

use PHPStan\Type\ObjectType;

final class VoidMethod
{
public function __construct(
private readonly string $class,
private readonly string $method,
) {
}

public function getObjectType(): ObjectType
{
return new ObjectType($this->class);
}

public function getClass(): string
{
return $this->class;
}

public function getMethod(): string
{
return $this->method;
}
}
7 changes: 7 additions & 0 deletions tests/TestCase/Command/RectorCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ public function testApply53()
$this->assertTestAppUpgraded();
}

public function testApply60()
{
$this->setupTestApp(__FUNCTION__);
$this->exec('upgrade rector --rules cakephp60 ' . TEST_APP);
$this->assertTestAppUpgraded();
}

public function testApplyMigrations45()
{
$this->setupTestApp(__FUNCTION__);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);

namespace MyPlugin;

use Cake\Console\ConsoleInputArgument;
use Cake\Console\ConsoleInputOption;
use Cake\Core\Configure;
use Cake\Http\ResponseEmitter;
use Cake\Http\ServerRequest;
use Cake\Http\Session;
use Cake\ORM\Table;
use Cake\TestSuite\Fixture\TestFixture;

class VoidMethods
{
private ServerRequest $request;
private Session $session;
private Table $table;
private TestFixture $fixture;

public function testAllowMethod(): void
{
// Assignment should be removed
$result = $this->request->allowMethod('post');

// Method call without assignment should stay the same
$this->request->allowMethod(['get', 'post']);
}

public function testConfigureLoad(): void
{
// Assignment should be removed
$loaded = Configure::load('app', 'default');
}

public function testResponseEmitter(): void
{
$emitter = new ResponseEmitter();
// Assignment should be removed
$result = $emitter->emit($response);
}

public function testSessionClose(): void
{
// Assignment should be removed
$closed = $this->session->close();
}

public function testTableDeleteOrFail(): void
{
// Assignment should be removed
$deleted = $this->table->deleteOrFail($entity);
}

public function testFixtureInsertTruncate(): void
{
// Assignments should be removed
$inserted = $this->fixture->insert($connection);
$truncated = $this->fixture->truncate($connection);
}

public function testValidChoiceRename(): void
{
$arg = new ConsoleInputArgument('test');
$option = new ConsoleInputOption('test');

// Method should be renamed to validateChoice
$arg->validChoice('value');
$option->validChoice('value');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);

namespace MyPlugin;

use Cake\Console\ConsoleInputArgument;
use Cake\Console\ConsoleInputOption;
use Cake\Core\Configure;
use Cake\Http\ResponseEmitter;
use Cake\Http\ServerRequest;
use Cake\Http\Session;
use Cake\ORM\Table;
use Cake\TestSuite\Fixture\TestFixture;

class VoidMethods
{
private ServerRequest $request;
private Session $session;
private Table $table;
private TestFixture $fixture;

public function testAllowMethod(): void
{
$this->request->allowMethod('post');

// Method call without assignment should stay the same
$this->request->allowMethod(['get', 'post']);
}

public function testConfigureLoad(): void
{
Configure::load('app', 'default');
}

public function testResponseEmitter(): void
{
$emitter = new ResponseEmitter();
$emitter->emit($response);
}

public function testSessionClose(): void
{
$this->session->close();
}

public function testTableDeleteOrFail(): void
{
$this->table->deleteOrFail($entity);
}

public function testFixtureInsertTruncate(): void
{
$this->fixture->insert($connection);
$this->fixture->truncate($connection);
}

public function testValidChoiceRename(): void
{
$arg = new ConsoleInputArgument('test');
$option = new ConsoleInputOption('test');

// Method should be renamed to validateChoice
$arg->validateChoice('value');
$option->validateChoice('value');
}
}