プロジェクト独自のコーディングルールを簡単に正規表現で定義できる `rubocop-grep` の活用

はじめに:

弊社のとあるEDI(電子商取引)関連のプロダクトでは、Ruby on Railsを利用してGraphQL APIを提供しています。

その開発活動の中で最近、コードの品質と整合性を維持するためのツールとして rubocop-grep を利用し始めました。

この記事ではその具体的な活用事例についてお話しします。

目次

  • rubocop-grepとは
  • 最初のユースケースと、基本的な使い方の説明
  • 複数のルールをディレクトリごとに設定するための工夫
  • ほかにどのようなユースケースがありそうか
  • まとめ

rubocop-grepとは

rubocop-grep は、rubocop の拡張ツールです。

プロジェクト独自のコーディングルールを、正規表現を用いて簡単に定義することができます。

この手の問題は、今までもカスタムCopを書くことで解決することはできましたが、カスタムCopはASTの知識やRubocopの知識が必要になることから、敷居の高いものでした。

この rubocop-grep は、rubyの正規表現の書き方を理解していれば活用することができるので、もっとかんたんにカスタムCopのようなルールを設定することができます。

使い方やコンセプトを知るうえで、作者の方の記事 正規表現でかんたんにCopを書けるRuboCop拡張 rubocop-grep をリリースしました が大変参考になります。

いくつか利用するうえでの不都合や不具合を2,3個修正し、 フォークしたものを https://github.com/route06/rubocop-grep にアップしています。ぜひそちらも参考にしてください。

最初のユースケースと、基本的な使い方の説明

ここからユースケースを紹介していきます。

社内のプロジェクトでは、graphql-rubyを使っていてfieldやargumentを定義する際、第2引数にStringを設定したいとき、 String と書くか GraphQL::Types::String と書くかで揺れていました。

# 例1
field :code, String, null: false
# 例2
field :code, GraphQL::Types::String, null: false

一貫性が合ったほうが読みやすいだろうということで、 このプロジェクトでは、 「GraphQL::Types::String (上記だと例2) に統一」するということになりました。

さて、ルールを決めてそれをドキュメント化・チェックリスト化しても、レビュワーがレビューで指摘したりレビュイーがPRを作る前に気をつけるのは面倒です。

そこで rubocop-grep でルールを作ってしまいます。

今回は、このようにしました。

# .rubocop.yml
# ...
require:
  - # ... (他の外部Cop)
  - rubocop-grep # 追加

# ...
Grep/Grep:
  Enabled: true
  Rules:
    - Pattern: '\bfield\s+\S+,\s*String[\s\S]+$'
      Message: fieldの型はStringは使わず、GraphQL::Types::Stringを使ってください

これでチェックできるようになりました :+1:

違反するコードを書くと以下のようになります。

Offenses:

app/graphql/types/sample_type.rb:6:1: C: Grep/Grep: fieldの型はStringは使わず、GraphQL::Types::Stringを使ってください
field :code, String, null: false ...
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

8 files inspected, 1 offense detected

複数のルールをディレクトリごとに設定するための工夫

さて、ここまでの書き方だと、

  • いろいろなルールを「ディレクトリごと」に設定することができない
  • ルールごとに、「チェックする必要のないディレクトリ」 もrubocopが走査してしまう

という問題があります。

これを解決するため、以下のようにしました。

# .rubocop.yml
# ...
Grep/GraphqlTypes:
  Enabled: true
  Rules:
    - Pattern: '\bfield\s\S+,\sString[\s\S]+$'
      Message: fieldの型はStringは使わず、GraphQL::Types::Stringを使ってください
  Include:
    - "app/graphql/types/**/*"

ポイントは以下のとおりです。

  • Grep/GraphqlTypes: というように 独自のCop を作っている
  • そうすることにより、 Include でこのルールを適用するディレクトリを限定できる

「独自のCopを作っている」のが工夫ポイントです。

作る、と言っても、継承するだけです。

rubocop-grep gem は、 Grep/Grep というCop(クラス)を提供しています。

これを継承し、以下のようなコードを置いておきます。

# lib/rubocop/cop/grep.rb
base_class = RuboCop::Cop::Grep::Grep
# NOTE: Copを追加したい場合はここにクラスを定義してください
# ...
class RuboCop::Cop::Grep::GraphqlTypes < base_class; end
# ...

この lib/rubocop/cop/grep.rb を読み込むため、.rubocop.yml に以下のrequire1行を追加しています。

# .rubocop.yml
# ...
require:
  # ...
  - rubocop-grep
  - './lib/rubocop/cop/grep' # rubocop-grepを活用したカスタムCop。grepのCopを追加したい場合はlib/rubocop/cop/grep.rbに追加してください。

上記のようにすることで、 Cop Grep/GraphqlTypes を利用することができます。

Grep/GraphqlTypes"app/graphql/types/**/*" 以下のディレクトリだけを走査します。

別のディレクトリに別のルールを追加したくなった際は 、同様にして、lib/rubocop/cop/grep.rb に1行追加してクラスを作り、そのルールを .rubocop.yml側に設定すればよいのです。

ほかにどのようなユースケースがありそうか

さて、どのようなケースで活躍しそうかを見ていくことで、便利さを伝えられればと思います。

  • ケース1: コーディングスタイル系・表記揺れチェック
    • 先述の例: GraphQL::Types::String or String
  • ケース2: 固有ワードのNG化・ワーディング系・表記揺れチェック
  • ケース3: パッケージング系・packwerkのゆるい代替

ケース1については先述の例で説明しました。以下ではケース2-3を見ていきます。

ケース2: 固有ワードのNG化・ワーディング系・表記揺れチェック

どちらも綴りが正しいような言葉の表記揺れを直したり。

  • cancelled or canceled (英米英語)
Grep/GrepWords:
  Enabled: true
  Rules:
    - Pattern: 'canceled'
      Message: `canceled` は使わず、`cancelled` を使ってください
  • 差戻 or 差戻し or 差し戻し (日本語の送り仮名)
Grep/GrepWords:
  Enabled: true
  Rules:
    - Pattern: '差戻し|差し戻'
      Message: `差戻し` や `差し戻し` は使わず、`差戻` を使ってください

ケース3: パッケージング系・packwerkのゆるい代替

「特定のディレクトリ配下では特定のクラスの呼び出しを禁止したい」というニーズがあると思います。

最近だと packwerk を使うこともあると思います。packwerk よりは非力ですが、かんたんに設定することができます。

以下では、モデルクラスの実装から graphql-rubyのモジュールである GraphQL に依存しないようにしています。

Grep/Models:
  Enabled: true
  Rules:
    - Pattern: '\bGraphQL\b'
      Message: app/modelsではGraphQLには依存しないでください
  Include:
    - "app/models/**/*"

まとめ

rubocop-grep の導入によって、Ruby on Railsを使用したEDI関連プロダクトのコード品質と整合性維持を効率化した事例を紹介しました。

このツールは正規表現を使ってプロジェクト固有のコーディングルールを簡単に定義可能にし、カスタムCopの作成に比べて容易にルール設定を行える点が特徴です。具体的なユースケースとして、コーディングスタイルの統一や特定ワードの使用禁止、パッケージング規則の簡易版実装など多岐にわたる活用が可能であることを述べました。