👋

gomockのFinishを検知するリンターを作成した話

2023/12/09に公開

こんにちは。新卒のDaikiです。
今回は昔に作成したリンターを紹介していきたいと思います。

golangci-lintとは

golangci-lintは、Go言語用のlinterのrunnerで、複数のリンターを並行して実行し、キャッシングを利用してパフォーマンスを向上させることなどが可能でリンターの高速化や効率性向上などが期待できます。さらに、golangci-lintは便利なYAML設定とIDEとの統合をサポートしているのでGo開発者にとっては非常に便利なツールとして愛用されています。

gomockのFinishメソッドの非推奨化

今回linterの作成において主題となっているのはこのgomockのFinish関数がgo1.14以降非推奨となったことです。その背景としてはgo1.14のt.Cleanup機能をgoがサポートしたからです。これにより、Finishを呼ぶ必要がなくなり(自動で呼ばれる)開発者もより楽にgomockを使用した開発ができるようになりました。
しかし、非推奨になりgomockでのFinishを呼ぶ必要がなくなったことから、Finishが呼ばれている箇所を検知するlinterが必要となりました。

linterの実装

今回リンターを実装する際にskeletonという静的解析ツールの雛形を生成するコマンドラインツールを使って簡単に静的解析ツールを開発をできるようにしました。
analysis.Anlyzer構造体のRunのfield以外はほぼ定形になっているので今回はRunのfieldに割り当てられているrun関数の中身について説明したいと思います。

以下がコードです:

package finishgomock

import (
	"go/ast"
	"strings"

	"golang.org/x/tools/go/analysis"
	"golang.org/x/tools/go/analysis/passes/inspect"
	"golang.org/x/tools/go/ast/inspector"
)

const doc = "finishgomock is a linter that detects an unnecessary call to Finish on gomock.Controller"

var Analyzer = &analysis.Analyzer{
	Name: "finishgomock",
	Doc:  doc,
	Run:  run,
	Requires: []*analysis.Analyzer{
		inspect.Analyzer,
	},
}

func run(pass *analysis.Pass) (interface{}, error) {
	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

	nodeFilter := []ast.Node{
		(*ast.CallExpr)(nil),
	}

	inspect.Preorder(nodeFilter, func(n ast.Node) {
		callExpr, ok := n.(*ast.CallExpr)
		if !ok || callExpr.Fun == nil {
			return
		}

		// Type assertion for selector expression
		selectorExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
		if !ok {
			return
		}

		// Check if the callExpr is to a gomock.Controller method
		if isGomockControllerMethod(pass, callExpr) {
			// Check for the specific Finish method call
			if selectorExpr.Sel.Name == "Finish" {
				// Report
				pass.Reportf(selectorExpr.Sel.NamePos, "detected an unnecessary call to Finish on gomock.Controller")
			}
		}
	})
	return nil, nil
}

// Helper function to check if a call expression is a method on gomock.Controller
func isGomockControllerMethod(pass *analysis.Pass, call *ast.CallExpr) bool {
	if callExpr, ok := call.Fun.(*ast.SelectorExpr); ok {
		receiverType := pass.TypesInfo.TypeOf(callExpr.X)
		if receiverType != nil {
			return strings.HasSuffix(receiverType.String(), "github.com/golang/mock/gomock.Controller")
		}
	}
	return false
}

初めに、ノードのタイプであるCallExpr(関数呼び出し)を見つけたいのでnodeFilterでそのCallExpr型のAST Nodeを限定します。

次に抽象構文木をPreorder順で巡回していきその中で、各CallExpr NodeのFunction ExpressionのNodeの型がSelectorExprならそのSelectorExpr NodeのExpressionのレシーバーのタイプを取得し、strings.HasSuffixを使用して接尾辞が github.com/golang/mock/gomock.Controllerで終わっているものであればtrueのbool値を返しています。

最後に、selectorExprのSelのNameに格納されてある名前がFinishと同じなら検出し、analysis.PassのReportfメソッドを使用して、メッセージを生成します。

まとめ

かなり昔に実装したものを今回紹介する形となりましたが、リンターがskeletonなどのツールによってより簡易化され作りやすくなっていることを知っていただければ幸いです。

Discussion