My External Storage

Jan 22, 2021 - 2 minute read - Comments - go

[Go] ファイルが自動生成コードか判別したい

Goのコードを静的解析するとき、自動生成コードをスキップするためのメモ。

TL;DR

  • 自動生成のGoコードか判別したい
  • Goの自動生成コードは定型文を入れるのがマナー
  • 上記定型文を探す正規表現は次のようになる
    • regexp.MustCompile("(?m)^// Code generated .* DO NOT EDIT\\.$")
var c = regexp.MustCompile("(?m)^// Code generated .* DO NOT EDIT\\.$")

func process(src []byte) bool {
  return c.Match(src)
}

自動生成コードは無視したい

Goで静的解析ツールやコード修正ツールを作っていると、ディレクトリ再帰中にいくつかの存在を無視したくなる。

  • testdataディレクトリに入っているもの
  • ***_test.goファイル
  • 自動生成されたファイル

先に挙げた2つはファイル名やパスを取得すれば対応できる。 最後の「自動生成されたファイル」に関して言うと一筋縄ではいかない。 どのようにファイルを見分けるかと言うと、ファイル内の文字列をパースする。

Goの自動生成されたコードの共通ルール

お行儀の良い大抵のコード自動生成ツールは「自動生成されたコードです」ということを示すコメントが入れられている。

https://golang.org/cmd/go/#hdr-Generate_Go_files_by_processing_source

To convey to humans and machine tools that code is generated, generated source should have a line that matches the following regular expression (in Go syntax):

// Code generated by MockGen. DO NOT EDIT.

なので、このコメント行を検出できれば自動生成されたコードなのかわかる。

正規表現を利用する

当然といえば当然で、正規表現を使えばよい。

var c = regexp.MustCompile("(?m)^// Code generated .* DO NOT EDIT\\.$")

func generated(src []byte) bool {
  return c.Match(src)
}

正規表現の内容について

パっと見よくわからない(?m)は複数行対応のフラグ。

https://github.com/google/re2/wiki/Syntax

  • (?flags): set flags within current group; non-capturing
  • m: multi-line mode: ^ and $ match begin/end line in addition to begin/end text (default false)

これがないと、複数行文字列の中から該当表現を検出できない。

動作検証したテストコードは次のとおり。

https://play.golang.org/p/GMXVIXrqnLU

package main

import (
  "regexp"
  "testing"
)

func TestProcess(t *testing.T) {
  t.Parallel()
  tests := [...]struct {
    name, src string
    want      bool
  }{
    {
      name: "case1",
      src: `// Code generated by MockGen. DO NOT EDIT.
package main

import (
  "context"
  "fmt"
  "net/http"
)

func SampleFunc(ctx context.Context) {
}
`,
      want: true,
    },
    {
      name: "case2",
      src: `
package main

import (
  "context"
  "fmt"
  "net/http"
)

// Code generated by MockGen. DO NOT EDIT.

func SampleFunc(ctx context.Context) {
}
`,
      want: true,
    },
  }
  for _, tt := range tests {
    tt := tt
    t.Run(tt.name, func(t *testing.T) {
      t.Parallel()

      if got := generated([]byte(tt.src)); tt.want != got {
        t.Fatalf("want %v, got %v, source: \n%s", tt.want, got, tt.src)
      }
    })
  }
}

var c = regexp.MustCompile("(?m)^// Code generated .* DO NOT EDIT\\.$")

func generated(src []byte) bool {
  return c.Match(src)
}

ファイルの途中にかかれていた場合も検出できている。

終わりに

厳密にやるならばASTを使ってgo/ast#File.Commentsをチェックしたほうが良さそうだが、このレベルで十分だろう。 (?m)は知らない正規表現の記法だったのでそちらも学びになった。

参考リンク

関連記事