チームでWebサービスを書いていると特定の条件の関数には特定の型の引数を渡すことがルールになっていくことがある。

例えば、マルチテナントなアプリケーションを作っている場合、UsecaseはTenantIDを必ず引数として持ったり、DBの定義には必ず*sql.Txを持つ等がある。

人の手でやると漏れるのでLinterも探してみたが、軽く探したぐらいでは要件を満たせそうなLinterがなかったため自分で作ってみた。

https://github.com/nametake/mustargs

以下のように引数のルールをYAMLで定義して、 go vet -vettool=`which mustargs` -mustargs.config=$(pwd)/config.yaml . で実行できる。

ルール自体は独立していて、複数の引数の型と対象となる関数のパターンを定義できる。

---
rules:
  - args:
      - type: Context
        pkg: context
        index: 0
      - type: TenantID
        index: 1
    recv_patterns:
      - ^Usecase$
  - args:
      - type: Context
        pkg: context
        index: 0
      - type: Tx
        pkg: database/sql
        index: 1
        is_ptr: true
    recv_patterns:
      - ^DB$
  - args:
      - type: int
        index: -1
      - type: int
        index: -2
    recv_patterns:
      - ^DB$
    func_patterns:
      - ^GetMultiple.*

上記の例だと、以下のルールを満たしていない関数を検出できる。

  • Usecase構造体のレシーバを持つ関数は第1引数と第2引数にcontext.ContextとTenantIDが必要
  • DB構造体のレシーバを持つ関数は第1引数と第2引数にcontext.Contextと*sql.Txが必要
  • DB構造体のレシーバを持ちGetMultipleという名前の関数は関数の最後と最後から2番目にint型が必要
package example

import (
	"context"
	"database/sql"
)

type TenantID string

type Usecase struct{}

func (u *Usecase) GetUser(ctx context.Context, tenantID TenantID, userID string) {
}

func (u *Usecase) GetPost(ctx context.Context, userID string) { // ERROR
}

type DB struct{}

func (db *DB) GetUser(ctx context.Context, tx *sql.Tx, tenantID TenantID, userID string) {
}

func (db *DB) GetPost(ctx context.Context, tenantID TenantID, postID string) { // ERROR
}

func (db *DB) GetMultipleUsers(ctx context.Context, tx *sql.Tx, tenantID TenantID, limit, offset int) {
}

func (db *DB) GetMultiplePosts(ctx context.Context, tx *sql.Tx, tenantID TenantID) { // ERROR
}

まだプロジェクトにガッツリ入れて運用できている訳では無いが、あくまでLinterなのでちょっとずつ入れて試していこうと思う。