Inside of LOVOT

GROOVE X 技術ブログ

protoをclang-formatでいい感じにする

はじめに

こんにちは〜。GROOVE X クラウドチームのmineoです。この記事はGROOVE Xアドベントカレンダーの24日目の記事です。

qiita.com

みなさん、protoファイル書いていますか? 弊社では、gRPCとProtocol BuffersをクラウドやLOVOTでのサービス間通信に大いに活用しているため、それらのAPI定義をprotoで書いています。(詳細は、以下記事を参照してください!)

tech.groove-x.com

この記事では、protoをclang-formatを導入して、統一的に整形するようにした取り組みについて、紹介します。

背景

弊社では、ほぼ全てのprotoファイルを一つのGitHubリポジトリで管理しています。そのため、さまざまなチームがprotoファイルを書いており、インデントなどのフォーマットがバラバラな状態が長く続いていました。

統一性がないと可読性が悪化し、定義を追加したり修正するときにも悩んでしまいます。それらの課題を解決するため、フォーマッターを導入することにしました。

フォーマッターの選定

protoのフォーマッターの選択肢はメジャーなものだと以下の二つがあります。

最終的にはclang-formatを選定したのですが、実は一度buf formatを導入していました。

buf format

buf formatはbufが開発しているzero configを掲げているフォーマッターです。

buf.build

こちらを導入したところ、我々のprotoファイルでは、崩れてしまう箇所がありました。(実際のコードではなく、サンプルコードです)

以下のように、カスタムのプラグインを書いてオプションを設定しているコードが

以下のようにフォーマットされてしまいます。

可読性をよくするはずが、むしろこれでは可読性が悪くなってしまいました。 また、buf formatではzero configのため、設定項目がほとんどなく、line lengthを設定することもできません。辛うじて、--exclude-path でファイルを除外することは可能ですが、除外すると当然フォーマットが効かなくなりますし、対象のファイルを抽出するのも大変です。

そのため、clang-formatの導入を検討し始めました。

clang-format

clang-formatでは、buf formatと異なり、.clang-formatファイルで細かく設定できます。

以下のように設定しました。Go言語のgofmtに近い設定にしています。

Language: Proto
DisableFormat: false
BasedOnStyle: Google
AlignConsecutiveAssignments:
  Enabled: true
  AcrossEmptyLines: false
AlignConsecutiveDeclarations:
  Enabled: true
  AcrossEmptyLines: false
ColumnLimit: 0

それぞれの設定の意味合いです。

  • AlignConsecutiveAssignments
    • =の位置を揃える
  • AlignConsecutiveDeclarations
    • プロパティ名の位置を揃える
  • AcrossEmptyLines
    • 空行が入ったら位置揃えをリセット。↑二つに適用 (gofmtリスペクト)
  • ColumnLimit
    • 一行の文字数。0にして改行をさせない

いい感じになったと思います!*1

option内でsemicolonがあると崩れてしまう問題とその解決方法

このようにclang-formatで万事上手くいったかと思ったのですが、一つ問題がありました。以下のようにオプションを設定しているコードの場合です。(こちらも実際のコードではなく、サンプルコードです)

これをフォーマットしたら以下のようになってしまいました。

既知の問題のようでした。

Protocol Buffersにclang-formatをかけるとインデント崩れを起こすパターンがある - 生涯未熟

これはoption内のsemicolonを削除すると解決します。

ただし、前述の通り弊社の全てのprotoファイルがリポジトリに含まれているため、一つ一つ修正していくのは大変です。

正規表現などでも整形できそうですが、ここでbuf formatを利用することにしました。buf formatを適用すると、semicolonが削除されるため、buf formatのあとに、clang-formatを適用すればよいと考えました。

buf formatのあとにclang-formatを適用すると以下のようになりました!成功です。

もし、今後semicolonが混入してしまったとしても、clang-formatを実行すればすぐ気付けるので、許容できるリスクかと思います。

make targetの用意とGitHub ActionsでCI

editor で clang-format の拡張機能を入れて頂くと編集しやすいですが、さまざまな環境のメンバーがいるので、コマンドを用意します。手元で実行しやすいようにフォーマットを実行する make target と、CI用にチェック用の make target を以下のように用意しました。

.PHONY: check-fmt
check-fmt:
   @find . -name "*.proto" | while read -r file; do \
      clang-format --dry-run --Werror "$$file" || exit 1; \
  done

.PHONY: fmt
fmt:
   @find . -name "*.proto" | while read -r file; do \
      clang-format -i "$$file" || exit 1; \
  done

CIは、GitHub Actionsで以下のようにしました。簡単ですね

name: CI Lint
on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master
    paths:
      - '.github/workflows/ci-lint.yml'
      - '**/*.proto'
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{ github.head_ref }}
      - name: Install clang-format
        run: |
          sudo apt-get update
          sudo apt-get install -y clang-format
      - name: Check formatting
        run: make check-fmt

まとめ

clang-formatを導入することで、理想的な設定でprotoファイルのフォーマットを統一することができました。振り返ってみると、ソフトウェアチーム全体に関わる内容にも関わらず意外とすんなりと進んだと思っており、以下のような要素がうまく働いたのではと考えています。

  • 1つのリポジトリで管理されている
  • カジュアルに提案しやすく、建設的な意見がでやすい
  • ソフトウェアチーム全体でコミュニケーションがしやすい雰囲気がある

GROOVE Xでは、クラウドチーム含め各チームで仲間を募集中です!もし、少しでも興味が湧いたら、お気軽にお声掛けください

recruit.jobcan.jp

*1:実はタグナンバーの桁数が混在すると、オプションの開始位置がずれてしまうという課題はあります...