はじめに:
弊社のとある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
orString
- 先述の例:
- ケース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の作成に比べて容易にルール設定を行える点が特徴です。具体的なユースケースとして、コーディングスタイルの統一や特定ワードの使用禁止、パッケージング規則の簡易版実装など多岐にわたる活用が可能であることを述べました。