Rectorの独自ルールを作成する方法(2022年版)
モチベーション
Rectorという自動リファクタリングツールについて紹介した素晴らしい記事があります。
しかし、記事が執筆されてから2年ほど経過し、独自ルールの作成方法が現在は結構変更されています。
そこで本稿では、上述の記事の独自ルールを作成するハンズオン部分について、2022年版を書いておこうかと思います。
サンプル
サンプルリポジトリはここにあります。
環境構築
// build
docker build . -t rector-tutorial
// run
docker run -it --rm -v ${pwd}:/app tutorial bash
独自ルールを作成するハンズオン
composerをセットアップする
以下のコマンドを実行します。
composer init
対話形式で色々聞かれるので適当に入力していきましょう。
Rectorを入れる
以下のコマンドを実行します。
composer require --dev rector/rector
PHPUnitを入れる
以下のコマンドを実行します。
composer require -dev phpunit/phpunit
独自ルールのレシピを生成する
Rectorのルール作成には様々な作法があるため、すべて一から手作業で作成するのは大変です。
解決策としてrector generate
コマンドが提供されており、recipe
ファイルを元にテストを含めた必用なファイル郡を自動生成できるようになっています。
bin/rector init-recipe
それっぽさのある出力がなされ、ルート直下にrector-recipe.php
が生成されます。
レシピを編集する
生成されたrector-recipe.php
を以下のように編集します。
<?php
declare(strict_types=1);
namespace RectorPrefix202206;
- use PhpParser\Node\Expr\MethodCall;
+ use PhpParser\Node\Stmt\ClassMethod;
use Rector\RectorGenerator\Provider\RectorRecipeProvider;
use Rector\RectorGenerator\ValueObject\Option;
use RectorPrefix202206\Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$rectorRecipeConfiguration = [
- Option::PACKAGE => 'Naming',
- Option::NAME => 'RenameMethodCallRector',
- Option::NODE_TYPES => [MethodCall::class],
- Option::DESCRIPTION => '"something()" will be renamed to "somethingElse()"',
+ Option::PACKAGE => 'RectorTutorial',
+ Option::NAME => 'AddTestAnnotationRector',
+ Option::NODE_TYPES => [ClassMethod::class],
+ Option::DESCRIPTION => 'Rector Tutorial',
Option::CODE_BEFORE => <<<'CODE_SAMPLE'
- class SomeClass
+class SomeTest extends \PHPUnit\Framework\TestCase
{
- public function run()
+ public function testSome() : void
{
- $this->something();
+ // do test
}
}
CODE_SAMPLE,
Option::CODE_AFTER => <<<'CODE_SAMPLE'
- class SomeClass
+ class SomeTest extends \PHPUnit\Framework\TestCase
{
- public function run()
+ /**
+ * @test
+ */
+ public function some() : void
{
- $this->somethingElse();
+ // do test
}
}
CODE_SAMPLE,
];
$services->set(RectorRecipeProvider::class)->arg('$rectorRecipeConfiguration', $rectorRecipeConfiguration);
};
差分の内容について、もう少し解説します。
15 ~ 18行目
ここは見たまんまの内容になっていて、独自ルールのパッケージ名やルール名、ルールの内容の説明を設定しています。
Option::PACKAGE
とOption::NAME
の内容を元にディレクトリと名前空間が決まるイメージです。
- Option::PACKAGE => 'Naming',
- Option::NAME => 'RenameMethodCallRector',
- Option::NODE_TYPES => [MethodCall::class],
- Option::DESCRIPTION => '"something()" will be renamed to "somethingElse()"',
+ Option::PACKAGE => 'RectorTutorial',
+ Option::NAME => 'AddTestAnnotationRector',
+ Option::NODE_TYPES => [ClassMethod::class],
+ Option::DESCRIPTION => 'Rector Tutorial',
17行目のOption::NODE_TYPES => [ClassMethod::class]
の部分でリファクタリング対象のNodeクラスの配列を指定します。
選択できるNodeクラスはPhpParser\Nodeを確認すると良いでしょう。
19行目から40行目
ヒアドキュメントの中にリファクタリング前後の内容を書きます。
Option::CODE_BEFORE => <<<'CODE_SAMPLE'
- class SomeClass
+class SomeTest extends \PHPUnit\Framework\TestCase
{
- public function run()
+ public function testSome() : void
{
- $this->something();
+ // do test
}
}
CODE_SAMPLE,
Option::CODE_AFTER => <<<'CODE_SAMPLE'
- class SomeClass
+ class SomeTest extends \PHPUnit\Framework\TestCase
{
- public function run()
+ /**
+ * @test
+ */
+ public function some() : void
{
- $this->somethingElse();
+ // do test
この内容を元にルールのテストで利用するFixtureが作成されるのでキチンと記述しておく必用があります。
recipeからコードを生成する
以下のコマンドを実行します。
bin/rector generate
それっぽさのある出力がなされた後、以下の4つのファイルが出力されます。
- rules-tests/RectorTutorial/Rector/ClassMethod/AddTestAnnotationRector/AddTestAnnotationRectorTest.php
- rules-tests/RectorTutorial/Rector/ClassMethod/AddTestAnnotationRector/Fixture/some_class.php.inc
- rules-tests/RectorTutorial/Rector/ClassMethod/AddTestAnnotationRector/config/configured_rule.php
- rules/RectorTutorial/Rector/ClassMethod/AddTestAnnotationRector.php
rules
配下にはこれから独自ルールを記述するファイル、rules-tests
配下には独自ルールのテストに必用なファイルがそれぞれ出力されます。
Fixtureを追加する
自動生成したFixtureだけではテスト項目に不足があるためパターンを網羅出来るようにFixtureを追加していきます。
Fixture
ディレクトリ配下に以下のファイルを追加します。
<?php
namespace Rector\Tests\RectorTutorial\Rector\ClassMethod\AddTestAnnotationRector\Fixture;
class NotTestPrefix extends \PHPUnit\Framework\TestCase
{
public function noPrefix() : void
{
}
}
<?php
namespace Rector\Tests\RectorTutorial\Rector\ClassMethod\AddTestAnnotationRector\Fixture;
class NotPublicMethodTest extends \PHPUnit\Framework\TestCase
{
protected function testProtected() : void
{
}
private function testPrivate() : void
{
}
}
<?php
namespace Rector\Tests\RectorTutorial\Rector\ClassMethod\AddTestAnnotationRector\Fixture;
class NotTest
{
public function testSome() : void
{
}
}
PHPUnitを設定する
phpunit.xml.dist
を作成して、テストを実行できるようにします。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit executionOrder="random"
bootstrap="vendor/autoload.php"
colors="true">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">rules</directory>
</include>
</coverage>
<testsuites>
<testsuite name="all">
<directory>rules-tests</directory>
</testsuite>
</testsuites>
<php>
<ini name="error_reporting" value="-1"/>
</php>
</phpunit>
autoloadを設定する
composer.json
を編集してautoloadの設定を加えます。
"autoload": {
"psr-4": {
- "Isanasan\\RectorTutorial\\": "src/"
+ "Isanasan\\RectorTutorial\\": "src/",
+ "Rector\\": "rules"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Rector\\Tests\\": "rures-tests"
}
},
PHPUnitを実行する
以下のコマンドを実行します。
bin/phpunit rules-tests/
すると以下のような出力がなされ、テストが失敗します。
PHPUnit 9.5.21 #StandWithUkraine
Random Seed: 1656132964
F... 4 / 4 (100%)
Time: 00:33.579, Memory: 72.50 MB
There was 1 failure:
1) Rector\Tests\RectorTutorial\Rector\ClassMethod\AddTestAnnotationRector\AddTestAnnotationRectorTest::test with data set #3 (RectorPrefix202206\Symplify\SmartFileSystem\SmartFileInfo Object (...))
rules-tests/RectorTutorial/Rector/ClassMethod/AddTestAnnotationRector/Fixture/some_class.php.inc
Failed asserting that string matches format description.
--- Expected
+++ Actual
@@ @@
class SomeTest extends \PHPUnit\Framework\TestCase
{
- /**
- * @test
- */
- public function some() : void
+ public function testSome() : void
{
// do test
}
/app/vendor/rector/rector/packages/Testing/PHPUnit/AbstractRectorTestCase.php:128
/app/vendor/rector/rector/packages/Testing/PHPUnit/AbstractRectorTestCase.php:102
/app/rules-tests/RectorTutorial/Rector/ClassMethod/AddTestAnnotationRector/AddTestAnnotationRectorTest.php:16
FAILURES!
Tests: 4, Assertions: 9, Failures: 1.
あとはこの失敗したテストが通るように、独自ルールの実装を進めていきます。
独自ルールを実装する
以下のようにAddTestAnnotationRector
を編集します。
namespace Rector\RectorTutorial\Rector\ClassMethod;
use PhpParser\Node;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\ClassLike;
+use PHPUnit\Framework\TestCase;
+use PHPStan\Type\ObjectType;
+use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
+use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use Rector\Core\Rector\AbstractRector;
+use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\PHPUnit\PHPUnitTest;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
@@ -52,7 +59,41 @@ public function getNodeTypes(): array
*/
public function refactor(Node $node): ?Node
{
+ if (!$this->shouldRefactor($node)) {
+ return null;
+ }
+
+ // メソッドの PHP Doc を取得
+ $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
+
+ $phpDocInfo->addPhpDocTagNode(
+ new PhpDocTagNode('@test', new GenericTagValueNode(''))
+ );
+
+ // 元のメソッド名から `test` prefix を削除し, 1文字目を小文字に変更した文字列
+ $testName = \lcfirst(
+ (string) \preg_replace('/\Atest/', '', (string) $this->getName($node))
+ );
+ // `$testName` を新しいメソッド名として設定しなおす
+ $node->name = new Node\Identifier($testName);
+
// change the node
return $node;
}
+
+ private function shouldRefactor(ClassMethod $node): bool
+ {
+ // クラスメソッドが実装されているクラス情報を取得
+ $class = $this->betterNodeFinder->findParentType($node, ClassLike::class);
+
+ return
+ // メソッドの可視性が public かどうか
+ $node->isPublic()
+ // メソッド名が `test` から始まっているかどうか
+ && $this->isName($node, 'test*')
+ // メソッドが実装されているクラスが `PHPUnit\Framework\TestCase` を継承しているかどうか
+ && $this->nodeTypeResolver->isObjectType(
+ $class,
+ new ObjectType(TestCase::class),
+ );
+ }
}
詳しい内容についてはあひるさんの元記事を参照ください。
作成したルールをテストする
以下のコマンドを実行します。
bin/phpunit rules-tests/
エラーが発生していなければOKです。
まとめ
rector-recipe
とrector generate
コマンドによっていくらか独自ルールの開発がしやすくなりました。
ハマりポイントとしてはディレクトリがrules
とrules-tests
になっていることです。
phpunit.xml.dist
とcomposer.json
のautoload
の設定を間違えやすいので注意してください。
Discussion