From 23e322fb28bf936ccaa960ae143007ee37cc8335 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 4 Feb 2026 17:05:45 +0100 Subject: [PATCH 1/3] Add CakePHP 6.0 rector rules for void return type changes Add rector rules for upgrading to CakePHP 6.0: - RemoveAssignmentFromVoidMethodRector: Removes variable assignments from method calls that now return void instead of bool - Method renames: validChoice() -> validateChoice() for ConsoleInput classes Affected methods: - ServerRequest::allowMethod() - Configure::load() - ResponseEmitter::emit() - Session::close() - Table::deleteOrFail() - FixtureInterface::insert() - FixtureInterface::truncate() - ConsoleInputArgument::validChoice() -> validateChoice() - ConsoleInputOption::validChoice() -> validateChoice() Refs cakephp/cakephp#19220 Refs cakephp/cakephp#19243 --- config/rector/cakephp60.php | 9 ++ config/rector/sets/cakephp60.php | 44 +++++++++ .../RemoveAssignmentFromVoidMethodRector.php | 95 +++++++++++++++++++ src/Rector/Set/CakePHPSetList.php | 5 + src/Rector/ValueObject/VoidMethod.php | 30 ++++++ tests/TestCase/Command/RectorCommandTest.php | 7 ++ .../src/VoidMethods.php | 72 ++++++++++++++ .../src/VoidMethods.php | 72 ++++++++++++++ 8 files changed, 334 insertions(+) create mode 100644 config/rector/cakephp60.php create mode 100644 config/rector/sets/cakephp60.php create mode 100644 src/Rector/Rector/MethodCall/RemoveAssignmentFromVoidMethodRector.php create mode 100644 src/Rector/ValueObject/VoidMethod.php create mode 100644 tests/test_apps/original/RectorCommand-testApply60/src/VoidMethods.php create mode 100644 tests/test_apps/upgraded/RectorCommand-testApply60/src/VoidMethods.php diff --git a/config/rector/cakephp60.php b/config/rector/cakephp60.php new file mode 100644 index 00000000..f1f840ec --- /dev/null +++ b/config/rector/cakephp60.php @@ -0,0 +1,9 @@ +sets([CakePHPSetList::CAKEPHP_60]); +}; diff --git a/config/rector/sets/cakephp60.php b/config/rector/sets/cakephp60.php new file mode 100644 index 00000000..7f02a853 --- /dev/null +++ b/config/rector/sets/cakephp60.php @@ -0,0 +1,44 @@ +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'), + ]); +}; diff --git a/src/Rector/Rector/MethodCall/RemoveAssignmentFromVoidMethodRector.php b/src/Rector/Rector/MethodCall/RemoveAssignmentFromVoidMethodRector.php new file mode 100644 index 00000000..72308413 --- /dev/null +++ b/src/Rector/Rector/MethodCall/RemoveAssignmentFromVoidMethodRector.php @@ -0,0 +1,95 @@ + + */ + 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> + */ + public function getNodeTypes(): array + { + return [Expression::class]; + } + + /** + * @param Expression $node + */ + public function refactor(Node $node): ?Node + { + if (!$node->expr instanceof Assign) { + return null; + } + + $assign = $node->expr; + if (!$assign->expr instanceof MethodCall) { + return null; + } + + $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); + } + + return null; + } + + /** + * @param array $configuration + */ + public function configure(array $configuration): void + { + $this->voidMethods = $configuration; + } +} diff --git a/src/Rector/Set/CakePHPSetList.php b/src/Rector/Set/CakePHPSetList.php index 072b7dc9..182a09b2 100644 --- a/src/Rector/Set/CakePHPSetList.php +++ b/src/Rector/Set/CakePHPSetList.php @@ -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 */ diff --git a/src/Rector/ValueObject/VoidMethod.php b/src/Rector/ValueObject/VoidMethod.php new file mode 100644 index 00000000..8ab8a712 --- /dev/null +++ b/src/Rector/ValueObject/VoidMethod.php @@ -0,0 +1,30 @@ +class); + } + + public function getClass(): string + { + return $this->class; + } + + public function getMethod(): string + { + return $this->method; + } +} diff --git a/tests/TestCase/Command/RectorCommandTest.php b/tests/TestCase/Command/RectorCommandTest.php index 97bbb9d4..6d09920a 100644 --- a/tests/TestCase/Command/RectorCommandTest.php +++ b/tests/TestCase/Command/RectorCommandTest.php @@ -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__); diff --git a/tests/test_apps/original/RectorCommand-testApply60/src/VoidMethods.php b/tests/test_apps/original/RectorCommand-testApply60/src/VoidMethods.php new file mode 100644 index 00000000..08e7d000 --- /dev/null +++ b/tests/test_apps/original/RectorCommand-testApply60/src/VoidMethods.php @@ -0,0 +1,72 @@ +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'); + } +} diff --git a/tests/test_apps/upgraded/RectorCommand-testApply60/src/VoidMethods.php b/tests/test_apps/upgraded/RectorCommand-testApply60/src/VoidMethods.php new file mode 100644 index 00000000..214dc931 --- /dev/null +++ b/tests/test_apps/upgraded/RectorCommand-testApply60/src/VoidMethods.php @@ -0,0 +1,72 @@ +request->allowMethod('post'); + + // Method call without assignment should stay the same + $this->request->allowMethod(['get', 'post']); + } + + public function testConfigureLoad(): void + { + // Assignment should be removed + Configure::load('app', 'default'); + } + + public function testResponseEmitter(): void + { + $emitter = new ResponseEmitter(); + // Assignment should be removed + $emitter->emit($response); + } + + public function testSessionClose(): void + { + // Assignment should be removed + $this->session->close(); + } + + public function testTableDeleteOrFail(): void + { + // Assignment should be removed + $this->table->deleteOrFail($entity); + } + + public function testFixtureInsertTruncate(): void + { + // Assignments should be removed + $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'); + } +} From 67f660f84e31d42c28ab2e9e4dada77f16e99dee Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 4 Feb 2026 18:58:23 +0100 Subject: [PATCH 2/3] Fix phpcs errors - use fully qualified class names in docblocks --- .../MethodCall/RemoveAssignmentFromVoidMethodRector.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Rector/Rector/MethodCall/RemoveAssignmentFromVoidMethodRector.php b/src/Rector/Rector/MethodCall/RemoveAssignmentFromVoidMethodRector.php index 72308413..1e592202 100644 --- a/src/Rector/Rector/MethodCall/RemoveAssignmentFromVoidMethodRector.php +++ b/src/Rector/Rector/MethodCall/RemoveAssignmentFromVoidMethodRector.php @@ -21,7 +21,7 @@ final class RemoveAssignmentFromVoidMethodRector extends AbstractRector implements ConfigurableRectorInterface { /** - * @var array + * @var array<\Cake\Upgrade\Rector\ValueObject\VoidMethod> */ private array $voidMethods = []; @@ -46,7 +46,7 @@ public function getRuleDefinition(): RuleDefinition } /** - * @return array> + * @return array> */ public function getNodeTypes(): array { @@ -54,7 +54,7 @@ public function getNodeTypes(): array } /** - * @param Expression $node + * @param \PhpParser\Node\Stmt\Expression $node */ public function refactor(Node $node): ?Node { From 8d0bc89cbb9b16e88f285e561bff0a2a48c2be51 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 4 Feb 2026 19:02:06 +0100 Subject: [PATCH 3/3] Fix rector rule to handle static method calls and update test expectations --- composer.json | 7 +++ .../RemoveAssignmentFromVoidMethodRector.php | 43 +++++++++++++------ .../src/VoidMethods.php | 6 --- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 0bf58d68..6f75d578 100644 --- a/composer.json +++ b/composer.json @@ -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" } } diff --git a/src/Rector/Rector/MethodCall/RemoveAssignmentFromVoidMethodRector.php b/src/Rector/Rector/MethodCall/RemoveAssignmentFromVoidMethodRector.php index 1e592202..f0870337 100644 --- a/src/Rector/Rector/MethodCall/RemoveAssignmentFromVoidMethodRector.php +++ b/src/Rector/Rector/MethodCall/RemoveAssignmentFromVoidMethodRector.php @@ -7,6 +7,7 @@ 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; @@ -63,23 +64,41 @@ public function refactor(Node $node): ?Node } $assign = $node->expr; - if (!$assign->expr instanceof MethodCall) { - return null; - } - $methodCall = $assign->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; - } + foreach ($this->voidMethods as $voidMethod) { + if (!$this->isObjectType($methodCall->var, $voidMethod->getObjectType())) { + continue; + } - if (!$this->isName($methodCall->name, $voidMethod->getMethod())) { - continue; + if (!$this->isName($methodCall->name, $voidMethod->getMethod())) { + continue; + } + + // Replace the assignment with just the method call + return new Expression($methodCall); } + } - // 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; diff --git a/tests/test_apps/upgraded/RectorCommand-testApply60/src/VoidMethods.php b/tests/test_apps/upgraded/RectorCommand-testApply60/src/VoidMethods.php index 214dc931..19c6c21f 100644 --- a/tests/test_apps/upgraded/RectorCommand-testApply60/src/VoidMethods.php +++ b/tests/test_apps/upgraded/RectorCommand-testApply60/src/VoidMethods.php @@ -21,7 +21,6 @@ class VoidMethods public function testAllowMethod(): void { - // Assignment should be removed $this->request->allowMethod('post'); // Method call without assignment should stay the same @@ -30,32 +29,27 @@ public function testAllowMethod(): void public function testConfigureLoad(): void { - // Assignment should be removed Configure::load('app', 'default'); } public function testResponseEmitter(): void { $emitter = new ResponseEmitter(); - // Assignment should be removed $emitter->emit($response); } public function testSessionClose(): void { - // Assignment should be removed $this->session->close(); } public function testTableDeleteOrFail(): void { - // Assignment should be removed $this->table->deleteOrFail($entity); } public function testFixtureInsertTruncate(): void { - // Assignments should be removed $this->fixture->insert($connection); $this->fixture->truncate($connection); }