🤖

静的解析ライブラリのSpoonを使って、コード規約を遵守させる

2024/05/20に公開

概要

Javaの静的解析ライブラリであるSpoonを使い、コード規約に違反するコードを検知するテストを書きました。パイプラインでのテスト実行時にこれらのテストを実行することで、規約に違反したコードがコードベースにマージされることを防ぐことができます。
今回は下記のようなコード規約が存在すると仮定し、規約違反するコードを検知するためのテストを実装していきます。

  • メソッドの長さが50行を超える場合は分割すること
  • フィールド数が20を超える場合はクラスを分割すること
  • 値オブジェクトのクラス名はチームで運用しているユビキタス言語の一覧にあるものを使うこと(ない場合はユビキタス言語のリストをメンテナンスすること。)

本文中で紹介するソースコードの全量はこちらに配置しています。

実装・解説

spoonによるコード解析では、まずSponnAPIクラスを使ってCtModelなるものをビルドします。
このとき、解析したいフォルダのパスを引数として渡すことで特定のフォルダ内のみテストの対象とすることができます。

SpoonAPI spoon = new Launcher();
spoon.addInputResource("src/main/java/org/example"); //解析したいフォルダ・ファイルパス
CtModel ctModel = spoon.buildModel();

長すぎるメソッドを検知する

長すぎるメソッドはしばしば可読性を下げる要因となりえます。
一概にそうとは言いませんが、長すぎる場合は適切にメソッドが分割されておらず、一つのメソッドが複数の責務を負ってしまっていることが多いかと思います。

実装としてはシンプルで、メソッドの内容を文字列とし、改行コードの数により、行数を算出しています。
今回は100行を超えるメソッドの場合はエラーとするような実装としました。

    @Test
    void testMethodLength() {
        SpoonAPI spoon = new Launcher();
        spoon.addInputResource("src/main/java/org/example");
        CtModel model = spoon.buildModel();

        Map<String, Map<String, Integer>> results = new HashMap<>();

        for (CtType<?> type : model.getAllTypes()) {
            Map<String, Integer> overLengthMethods = new HashMap<>();
            for (CtMethod method : type.getMethods()) {
                //メソッドの行数を算出
                int length = method.getOriginalSourceFragment().getSourceCode().split("\r\n|\r|\n").length;
                if (length > 50) {
                    overLengthMethods.put(method.getSimpleName(), length);
                }
            }
            if (!overLengthMethods.isEmpty()) {
                results.put(type.getQualifiedName(), overLengthMethods);
            }
        }
        // エラーとなったメソッド情報を出力
        for (Map.Entry<String, Map<String, Integer>> result : results.entrySet()) {
            System.out.println(result.getKey() + " has over length methods");
            result.getValue().forEach((key, value) -> System.out.println("\t " + key + " length is " + value));
        }
        assertTrue(results.isEmpty());
    }

フィールド数が多すぎるクラスを検知する

メソッドと同様、大きすぎるクラスも複数の責務を背負い、単一責任の原則に違反している可能性が高いです。
フィールド数をクラスの大きさの指標ととらえ、フィールド数が20を超える場合は、失敗となるようなテストを書いていきます。

    @Test
    void testFiledCount() {
        SpoonAPI spoon = new Launcher();
        spoon.addInputResource("src/main/java/org/example");
        CtModel model = spoon.buildModel();

        Map<String, Integer> classToFieldCnt = model.getAllTypes().stream()
                .filter(t -> t.getFields().size() > 20)
                .collect(Collectors.toMap(CtTypeInformation::getQualifiedName, t -> t.getFields().size()));
        classToFieldCnt.forEach((key, value) -> System.out.println(key + " has " + value + " fields"));
        assertTrue(classToFieldCnt.isEmpty());
    }

用語集にない値オブジェクトのクラス名を検知する

ユビキタス言語とは、DDDの文脈で紹介されることの多い概念でチーム全体で用いる共通言語のことです。
口頭での会話、ドキュメントやソースコードで統一された言葉を使うことにより、関係者間の認識の齟齬を減らすことができます。

ユビキタス言語の一覧ファイル(以下、用語集と表現)を作ろうとしたものの、開発が進むにつれてメンテナンスされず、結局うまく使いきれない経験をされた方もいるのではないでしょうか。

そんな事態を防ぐため、値オブジェクトを実装する際にクラス名が用語集になかった場合は失敗するテストを実装していきます。
これにより開発者は、用語集のメンテナンスを強制されるため、用語集運用の形骸化を一部防ぐことができます。

一覧の読み込み

今回は用語集をExcelで管理することを前提とします。

Excelファイルの読み込みには、Apache POIを使います。

build.gradle
    testImplementation('org.apache.poi:poi:4.1.2')
    testImplementation('org.apache.poi:poi-ooxml:4.1.2')

まずは、Excelから用語集を読み込んでいきます。

UbiquitousTest.java
    private List<String> getUbiquitousList() throws Exception {
        //ファイル・シートの読み込み
        Workbook workbook = WorkbookFactory.create(new FileInputStream(filePath));
        Sheet sheet = workbook.getSheetAt(0);

        List<String> ubiquitousList = new ArrayList<>();
        for (Row row : sheet) {
            ubiquitousList.add(row.getCell(0).getStringCellValue());
        }
        return ubiquitousList;
    }

クラス名の取得・テストの実装

Excelから用語集を読み込めたら、あとはクラス名がそこに含まれているかを確認するだけです。
尚、今回は大文字・小文字の区別まではしないこととします。また、値オブジェクトはdomainパッケージ下に定義するルールが遵守されていることを前提とします。

UbiquitousTest.java
    @Test
    void ubiquitousTest() throws Exception {
        List<String> ubiquitousList = getUbiquitousList();
        SpoonAPI spoon = new Launcher();
        spoon.addInputResource("src/main/java/org/example/domain");
        CtModel model = spoon.buildModel();

        List<CtType<?>> notUbiquitousList = ctModel
                .getAllTypes()
                .stream()
                .filter(type -> {
                    for (String ubiquitous : ubiquitousList/* 前項で作成した用語集に含まれるユビキタス言語のリスト*/) {
                        //大文字小文字までは区別しない
                        if (ubiquitous.equalsIgnoreCase(type.getSimpleName())) return false;
                    }
                    return true;
                })
                .toList();

        // エラーとなったメソッド情報を出力
        notUbiquitousList.forEach(t -> System.out.println(t.getQualifiedName() + " is not included ubiquitousList"));
        assertTrue(notUbiquitousList.isEmpty());
    }

感想

プロジェクトやチームによって規約は様々と思いますが、そこそこ柔軟にテストがかけそうだなという印象でした。
Javaの静的解析ツールのSpotBugsも触って、比較してみたい。

参考

https://spoon.gforge.inria.fr/
https://magazine.techacademy.jp/magazine/22244

GitHubで編集を提案

Discussion