CodeIgniter DevKit で Rector を使いコードを修正する

この記事は CodeIgniter Advent Calendar 2022 - Qiita の20日目です。まだ、空きがありますので、興味のある方は気軽に参加してください。

CodeIgniter DevKit で php-cs-fixer を使いコーディングスタイルを修正する」 の続きです。

Rectorとは?

DevKit には Rector の設定のテンプレートも含まれています。

Rector はコードを自動的にリファクタリングするためのツールです。

Rectorのインストール

DevKit をインストールしても Rector はインストールされないため、ここではプロジェクトにインストールします。

$ composer require rector/rector --dev

バージョン 0.15.1 がインストールされました。

Rector のアップデートに伴いコードが破壊的に変更される可能性がありますので、 composer.json で Rector のバージョンを固定します。

        "rector/rector": "0.15.1"

問題がないことを確認したバージョンの Rector を使ってください。

Rectorの設定

それでは、Rector の設定をします。

vendor/codeigniter4/devkit/src/Template/rector.php をプロジェクトルートにコピーします。

そして、コピーした rector.php をプロジェクトに合わせて調整します。 また、composer cs-fix を実行してコーディングスタイルを整えましょう。

<?php

declare(strict_types=1);

use Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector;
use Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector;
use Rector\CodeQuality\Rector\Expression\InlineIfToExplicitIfRector;
use Rector\CodeQuality\Rector\For_\ForToForeachRector;
use Rector\CodeQuality\Rector\Foreach_\UnusedForeachValueToArrayKeysRector;
use Rector\CodeQuality\Rector\FuncCall\AddPregQuoteDelimiterRector;
use Rector\CodeQuality\Rector\FuncCall\ChangeArrayPushToArrayAssignRector;
use Rector\CodeQuality\Rector\FuncCall\SimplifyRegexPatternRector;
use Rector\CodeQuality\Rector\FuncCall\SimplifyStrposLowerRector;
use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector;
use Rector\CodeQuality\Rector\If_\CombineIfRector;
use Rector\CodeQuality\Rector\If_\ShortenElseIfRector;
use Rector\CodeQuality\Rector\If_\SimplifyIfElseToTernaryRector;
use Rector\CodeQuality\Rector\If_\SimplifyIfReturnBoolRector;
use Rector\CodeQuality\Rector\Ternary\UnnecessaryTernaryExpressionRector;
use Rector\CodingStyle\Rector\ClassMethod\FuncGetArgsToVariadicParamRector;
use Rector\CodingStyle\Rector\ClassMethod\MakeInheritedMethodVisibilitySameAsParentRector;
use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector;
use Rector\Config\RectorConfig;
use Rector\Core\ValueObject\PhpVersion;
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector;
use Rector\DeadCode\Rector\MethodCall\RemoveEmptyMethodCallRector;
use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector;
use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector;
use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector;
use Rector\EarlyReturn\Rector\Return_\PreparedValueToEarlyReturnRector;
use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
use Rector\Php56\Rector\FunctionLike\AddDefaultValueForUndefinedVariableRector;
use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector;
use Rector\Php73\Rector\FuncCall\StringifyStrNeedlesRector;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector;
use Rector\PSR4\Rector\FileWithoutNamespace\NormalizeNamespaceByPSR4ComposerAutoloadRector;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->sets([
        SetList::DEAD_CODE,
        LevelSetList::UP_TO_PHP_74,
        PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD,
        PHPUnitSetList::PHPUNIT_100,
    ]);

    $rectorConfig->parallel();

    // The paths to refactor (can also be supplied with CLI arguments)
    $rectorConfig->paths([
        __DIR__ . '/app/',
        __DIR__ . '/tests/',
    ]);

    // Include Composer's autoload - required for global execution, remove if running locally
    $rectorConfig->autoloadPaths([
        __DIR__ . '/vendor/autoload.php',
    ]);

    // Do you need to include constants, class aliases, or a custom autoloader?
    $rectorConfig->bootstrapFiles([
        realpath(getcwd()) . '/vendor/codeigniter4/framework/system/Test/bootstrap.php',
    ]);

    if (is_file(__DIR__ . '/phpstan.neon.dist')) {
        $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon.dist');
    }

    // Set the target version for refactoring
    $rectorConfig->phpVersion(PhpVersion::PHP_74);

    // Auto-import fully qualified class names
    $rectorConfig->importNames();

    // Are there files or rules you need to skip?
    $rectorConfig->skip([
        __DIR__ . '/app/Views',

        JsonThrowOnErrorRector::class,
        StringifyStrNeedlesRector::class,

        // Note: requires php 8
        RemoveUnusedPromotedPropertyRector::class,

        // Ignore tests that might make calls without a result
        RemoveEmptyMethodCallRector::class                    => [
            __DIR__ . '/tests',
        ],

        // Ignore files that should not be namespaced to their folder
        NormalizeNamespaceByPSR4ComposerAutoloadRector::class => [
            __DIR__ . '/app/Helpers',
        ],

        // May load view files directly when detecting classes
        StringClassNameToClassConstantRector::class,

        // May be uninitialized on purpose
        AddDefaultValueForUndefinedVariableRector::class,
    ]);

    // auto import fully qualified class names
    $rectorConfig->importNames();

    $rectorConfig->rule(SimplifyUselessVariableRector::class);
    $rectorConfig->rule(RemoveAlwaysElseRector::class);
    $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class);
    $rectorConfig->rule(ForToForeachRector::class);
    $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class);
    $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class);
    $rectorConfig->rule(SimplifyStrposLowerRector::class);
    $rectorConfig->rule(CombineIfRector::class);
    $rectorConfig->rule(SimplifyIfReturnBoolRector::class);
    $rectorConfig->rule(InlineIfToExplicitIfRector::class);
    $rectorConfig->rule(PreparedValueToEarlyReturnRector::class);
    $rectorConfig->rule(ShortenElseIfRector::class);
    $rectorConfig->rule(SimplifyIfElseToTernaryRector::class);
    $rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class);
    $rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class);
    $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class);
    $rectorConfig->rule(AddPregQuoteDelimiterRector::class);
    $rectorConfig->rule(SimplifyRegexPatternRector::class);
    $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class);
    $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class);
    $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class);
    $rectorConfig->rule(NormalizeNamespaceByPSR4ComposerAutoloadRector::class);
    $rectorConfig->rule(StringClassNameToClassConstantRector::class);
    $rectorConfig->rule(PrivatizeFinalClassPropertyRector::class);
    $rectorConfig->rule(CompleteDynamicPropertiesRector::class);
};

ここでは、上記のように修正しました。

なお、パスの設定が正しいかは必ず確認してください。DevKit のテンプレートのパスはCodeIgniter4のアプリのパスに設定されているため、ライブラリの場合は少し修正が必要です。

なお、Rector のルールついては、 https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md に一覧があります。

コードの修正

それでは Rector を実行して、コードを修正してみましょう。

$ vendor/bin/rector
 100/100 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
29 files with changes
=====================

1) app/Config/Filters.php:3

    ---------- begin diff ----------
@@ @@

 namespace Config;

+use App\Filters\Auth;
+use App\Filters\Noauth;
+use App\Filters\UsersCheck;
 use CodeIgniter\Config\BaseConfig;
 use CodeIgniter\Filters\CSRF;
 use CodeIgniter\Filters\DebugToolbar;
@@ @@
         'honeypot'      => Honeypot::class,
         'invalidchars'  => InvalidChars::class,
         'secureheaders' => SecureHeaders::class,
-        'auth'          => \App\Filters\Auth::class,
-        'noauth'        => \App\Filters\Noauth::class,
-        'userscheck'    => \App\Filters\UsersCheck::class,
+        'auth'          => Auth::class,
+        'noauth'        => Noauth::class,
+        'userscheck'    => UsersCheck::class,
     ];

     /**
    ----------- end diff -----------

2) app/Config/Database.php:85

    ---------- begin diff ----------
@@ @@
         // Ensure that we always set the database group to 'tests' if
         // we are currently running an automated test suite, so that
         // we don't overwrite live data on accident.
-        if (ENVIRONMENT === 'testing') {
-            $this->defaultGroup = 'tests';
-        }
+        $this->defaultGroup = 'tests';
     }
 }
    ----------- end diff -----------

Applied rules:
 * RemoveAlwaysTrueIfConditionRector


3) tests/_support/MigrationTestMigrations/Database/Migrations/2018-01-24-102302_Another_migration.php:3

    ---------- begin diff ----------
@@ @@

 namespace Tests\Support\MigrationTestMigrations\Database\Migrations;

-class Migration_another_migration extends \CodeIgniter\Database\Migration
+use CodeIgniter\Database\Migration;
+class Migration_another_migration extends Migration
 {
     public function up(): void
     {
    ----------- end diff -----------

...

28) app/Filters/UsersCheck.php:16

    ---------- begin diff ----------
@@ @@
         // we have to redirect the request to the second segment
         $uri = service('uri');
         if ($uri->getSegment(1) === 'users') {
-            if ($uri->getSegment(2) === '') {
-                $segment = '/';
-            } else {
-                $segment = '/' . $uri->getSegment(2);
-            }
+            $segment = $uri->getSegment(2) === '' ? '/' : '/' . $uri->getSegment(2);

             return redirect()->to($segment);
         }
    ----------- end diff -----------

Applied rules:
 * SimplifyIfElseToTernaryRector


29) app/Database/Migrations/20121031100537_add_users.php:3

    ---------- begin diff ----------
@@ @@

 namespace App\Database\Migrations;

-class AddUsers extends \CodeIgniter\Database\Migration
+use CodeIgniter\Database\Migration;
+class AddUsers extends Migration
 {
     public function up(): void
     {
    ----------- end diff -----------


 [OK] 29 files have been changed by Rector

上記のように修正された差分が表示されました。

Rector設定の修正

しかし、以下のような意図しない破壊的な変更が含まれる可能性があります。

2) app/Config/Database.php:85

    ---------- begin diff ----------
@@ @@
         // Ensure that we always set the database group to 'tests' if
         // we are currently running an automated test suite, so that
         // we don't overwrite live data on accident.
-        if (ENVIRONMENT === 'testing') {
-            $this->defaultGroup = 'tests';
-        }
+        $this->defaultGroup = 'tests';
     }
 }
    ----------- end diff -----------

Applied rules:
 * RemoveAlwaysTrueIfConditionRector

そこで、差分を確認してルールを修正します。

ここでは、特定のルールをスキップするように設定を変更します。

RemoveAlwaysTrueIfConditionRector ルールは app/Config/Database.php に対しては除外します。

--- a/rector.php
+++ b/rector.php
@@ -23,6 +23,7 @@ use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector;
 use Rector\Config\RectorConfig;
 use Rector\Core\ValueObject\PhpVersion;
 use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector;
+use Rector\DeadCode\Rector\If_\RemoveAlwaysTrueIfConditionRector;
 use Rector\DeadCode\Rector\MethodCall\RemoveEmptyMethodCallRector;
 use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector;
 use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector;
@@ -99,6 +100,10 @@ return static function (RectorConfig $rectorConfig): void {

         // May be uninitialized on purpose
         AddDefaultValueForUndefinedVariableRector::class,
+
+        RemoveAlwaysTrueIfConditionRector::class              => [
+            __DIR__ . '/app/Config/Database.php',
+        ],
     ]);

     // auto import fully qualified class names

また、composer cs-fix を実行してコーディングスタイルを整えましょう。

設定調整が完了し、問題がなければ、Gitでcommitしてください。

試しに実行した結果を GitHub にアップしておきました。

まとめ

  • CodeIgniter DevKit はCodeIgniterのライブラリとプロジェクトのための開発用のツールキットです。
  • DevKit には Rector の設定が含まれており、コードを修正・統一できます。

CodeIgniter DevKit で PHPStan を使い静的解析する」 へ続く。

この記事は CodeIgniter Advent Calendar 2022 - Qiita の20日目です。まだ、空きがありますので、興味のある方は気軽に参加してください。

参考

Date: 2022/12/20

Tags: codeigniter, codeigniter4, rector