関数呼び出しの実引数にリテラルをコメント無しで使った場合に警告するLinterを書いてみた #go

以下のリンク先にuberのgoのコーディングスタイルのガイドがあるのですが そこにあるルールに基づいたエラーを吐くLinterを書いてみました。

github.com

よく見ると元のルールとは微妙に違うような気がします。 "foo"という文字列を渡してるけど、Goodではそこにコメントは書かれてないですね。 今回実装したLinterでは、そこもエラーにしています。

今回の記事では、どういったものを実装したかを軽く説明し Experimentalな機能である、SuggestedFixというAPIを紹介します。

実装したのはどんなLinterか

関数呼び出しの実引数としてリテラルを与えた場合にエラーとして検知して警告し、コメントを挿入するFixをするものです。

例として以下のようなコードでエラーを検知してコメントを挿入します。

func x(b bool) {}
func f(names []string) {}

func main() {
    // trueを引数として渡しているのでエラーとする
    x(true)
    // sliceのリテラルを直接渡した場合もエラーとして検知する
    f([]string{"name"})
}

挿入されるコメントは以下のようなものです。

func x(b bool) {}
func f(names []string) {}

func main() {
    // trueを引数として渡しているのでエラーとする
-  x(true)
+   x(true /* b */)
    // sliceのリテラルを直接渡した場合もエラーとして検知する
-  f([]string{"name"})
+   f([]string{"name"} /* names */)
}

自作LinterでFixを実装する方法 (この機能はexperimentalです)

goにはLinterを自作するためのライブラリがあります。 下の記事に詳しく書かれており、非常に参考になりました。 基本的な実装などは以下を参考にするとよいです。

budougumi0617.github.io

最初は、skeletonというLinterのスケルトンを吐いてくれるツールを使って リポジトリを生成しました。

この記事ではFixを実装する方法を書いていきます。 Fixを実装するAPIは現状experimentalの機能なので注意してください。 今後APIが変わる可能性があります。

golang.org/x/tools/go/analysis の analysis.Passには ReportReportfという関数があります。

Fixを実装する時に使うのは、Report関数です。 この関数に渡すのは、analysis.Diagnosticというstructなんですが SuggestedFixesというフィールドがあります。

Diagnostic

このフィールドに必要なfixを渡してやることで 自動でFixしてくれるようになります。

type Diagnostic struct {
    Pos      token.Pos
    End      token.Pos // optional
    Category string    // optional
    Message  string

    // SuggestedFixes contains suggested fixes for a diagnostic which can be used to perform
    // edits to a file that address the diagnostic.
    // TODO(matloob): Should multiple SuggestedFixes be allowed for a diagnostic?
    // Diagnostics should not contain SuggestedFixes that overlap.
    // Experimental: This API is experimental and may change in the future.
    SuggestedFixes []SuggestedFix // optional
}

SuggestedFix

実際には、SuggestedFixにはTextEditsというフィールドがまたネストしていて ここにデータを与えてやる必要があります。

type SuggestedFix struct {
    // A description for this suggested fix to be shown to a user deciding
    // whether to accept it.
    Message   string
    TextEdits []TextEdit
}

TextEdit

今度はTextEditを見てみます。 TextEditはこういうstructになっています。

type TextEdit struct {
    // For a pure insertion, End can either be set to Pos or token.NoPos.
    Pos     token.Pos
    End     token.Pos
    NewText []byte
}

実際に実装したコード

実際に実装したコードは、大体以下のコード部分です。 litcomments/litcomments.go at 090842748cf52a6f3e12c7560f3ec0e20dddbf54 · wreulicke/litcomments · GitHub 抽出して記事に貼り付けます。

d := analysis.Diagnostic{
    Pos:     e.Pos(),
    Message: "Nil literal without comments is found.",
}
// 関数の引数名があったらFixをサジェストする
if name := params.At(i).Name(); name != "" {
    d.SuggestedFixes = []analysis.SuggestedFix{
        {
            Message: "Add comments",
            TextEdits: []analysis.TextEdit{
                    {
                        Pos:     e.End(), // 変更は引数の後ろに挿入したいので、PosとEndを同じ値にしている。
                        End:     e.End(),
                        NewText: []byte(fmt.Sprintf(" /* %s */", name)),
                    },
            },
        },
    }
}
pass.Report(d)

実装としては、こんな感じになりました。 関数の引数名を省略できる仕様があるので、上みたいなコードになりましたが そこまで難しくはないと思います。

まとめ

Goにおいて関数呼び出しの実引数にリテラルを書くと警告するLinterを書いてみました。 今回初めてLinterを書いてみたんですが、Linterを自作する方法はわかった、というところですね。

書いてて便利だなと思ったのは、GoのAnalyzerのAPIから 型がちゃんと取れるのは便利だなぁと思いました。 JavaでAnnotationProcessorで、Lombokの実装の中身みたいなコードを書いたときは型取れないので・・・。

experimentalが外れるのはいつなのでしょうか。 使ってみた感想としては、このSuggestedFixのAPI便利だけど もうちょっと洗練されると良いですね、って感じですね。

以下のリポジトリに色々LinterやUtilやらがホストされていて 参考になるので、覗いてみるといいと思います。 github.com

もうちょっと複雑なLinter書いてみたいですね。