🐶

アジャイルを支える技術としての静的解析

2023/07/17に公開

オブジェクト設計スタイルガイド』という本を読んでいます。この中で、サービス[1]の実装において、依存関係はコンストラクタ引数として明示しましょうという考え方が紹介されています[2]。そして、staticメソッドをクラスのそこかしこで自由に呼び出すのではなく、コンストラクタで注入した上で使いましょうという事例があげられています。

この考え方を採用するか否かはそれぞれのプロジェクトがおかれたコンテキストを踏まえて判断することになると思います。仮に採用すると判断したとしましょう。そしてそれが新規開発ではなく既存のプロダクトに対するものだったとしましょう。アジャイルな開発において漸進的に設計を改善する、リファクタリングを行うというのは自然な営みです。方針についてはチームメンバーの合意がとれたとして、具体的にどのように実装を進めればよいでしょうか。巨大なコードベースの中から該当箇所を探し出すのは容易ではありません。そんな時に静的解析が役立ちます。

今回はRectorを脇におき、PHPStanのカスタムルールを作成して該当箇所を特定したいと思います。 (PHPStanのカスタムルール作成を練習したい)

<?php declare (strict_types=1);

namespace Rules;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Rules\Rule;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\RuleErrorBuilder;

/**
 * @implements Rule<StaticCall>
 */
final class CallStaticMethodInClassRule implements Rule
{
    // staticメソッドがクラスの中で呼び出されていたら検出したいので
    // staticメソッドの呼び出し(StaticCallノード)を検査対象にする。
    public function getNodeType(): string
    {
        return StaticCall::class;
    }

    public function processNode(Node $node, Scope $scope): array
    {
        /** @var StaticCall $node */
        if ($this->shouldSkip($node)) {
            return [];
        }
        // $nodeがクラスの中にあるかどうかを$scopeに問い合わせる。
        if ($scope->isInClass()) {
            return [
                RuleErrorBuilder::message(
                    'Use constructor injection for dependent classes.'
                )->build(),
            ];
        }
        return [];
    }

    // self, static, parentは検出しないルールにしたい。
    private function shouldSkip(StaticCall $staticCall): bool
    {
        // StaticCallの定義を確認する。
        // SomeClass::someMethod()のSomeClassの部分は$classプロパティで
        // 型はNode\Name|Exprとわかる。
        // self, static, parentを変数等の式から展開しないだろうという前提のもと
        // Exprの場合はスキップしないと判定するとした。
        if ($staticCall->class instanceof Expr) {
            return false;
        }
        // Nameノードの定義を確認する。
        // self, static, parentかどうかを判定するisSpecialClassName()に気づく。
        if ($staticCall->class->isSpecialClassName()) {
            return true;
        }
        return false;
    }
}
$ vendor/bin/phpstan -V    
PHPStan - PHP Static Analysis Tool 1.10.25

なおこのルール自体がルールに違反している(RuleErrorBuilder::message()をコンストラクタに注入することなしに使用している)と判定されますが、郷に入りては郷に従えということで公式ガイドのスタイルに合わせて実装しました。

サービスのクラスならばこのルールを適用するといった条件をコンテキストに合わせてつけることで、より実用的にできそうです。

脚注
  1. "サービス"の定義は本書を参照のこと ↩︎

  2. この考え方の適用範囲は本書を参照のこと ↩︎

Discussion