ゼロからリンターを作る
概要
リンターの有用性は過小評価されるべきではありません。リンターは軽量なPRレビューの形として機能します。
実際、Google SREガイドでは、「nitpick」コメントはリンターに任せるべきだと提案しています。
リンターは明白なバグを防ぎ、コード品質の守護者として機能します。
レッスンの基礎
リンターは通常、言語固有のものです。例えば、Go言語用のReviveなどがあります。
リンターは、コンパイラが公開する抽象構文木(AST)に依存することが多いです。
実際、GoコンパイラがGoコードをコンパイルするために使用するのと同じASTです(そうです、Goコンパイラ自体もGoで書かれています)。
ソースコード内の各関数は構文ノードに分解され、特定の型にキャストして分析できます。
このレッスンでは、以下の機能を持つシンプルなリンターを構築します:
- 20文字を超える関数名を防止する。
- 60個以上のステートメントを持つ関数にフラグを立てる。
- すべてのfloat変数に
f32またはf64のプレフィックスがあることを保証する。
実装
セットアップ
demoというフォルダを作成します。demoの中にfoo.goというファイルを作成し、以下の内容を記述します:
go code snippet start
package demo
import (
"fmt"
"time"
)
var (
waitTime = 10
)
func messageProcessor(message string, currentTime float32) error {
fmt.Println("CurrentTime", currentTime)
fmt.Println("message", message)
return nil
}
func ProcessAllMessages(inputs []string) {
for _, value := range inputs {
err := messageProcessor(value, float32(time.Now().Unix()))
if err != nil {
fmt.Println("Err %w", err)
}
}
}go code snippet end
このデモは少し抽象的ですが、私たちの目的には十分です。
go code snippet start
fSet := token.NewFileSet()
file, err := parser.ParseFile(fSet, "demo/foo.go", nil, parser.ParseComments)
if err != nil {
panic(err)
}go code snippet end
これにより、ASTがファイルを読み取り、結果を解析できるようになります。 ファイルに無効なGoコードが含まれている場合、ここでパニックします — もちろん、実際の例では、適切なエラー処理とログを含める必要があります。
次に、ASTツリーを走査します。 ASTを扱う際、「ツリーを走査する(walking the tree)」という用語をよく耳にしますが、これは単にツリー内の各ノードを訪問することを意味します。
go code snippet start
ast.Inspect(file, func(n ast.Node) bool {
return true
})go code snippet end
これは、ツリーを走査するための最も基本的な構造です。
しかし、まずすべての関数定義を見てみましょう:
go code snippet start
ast.Inspect(file, func(node ast.Node) bool {
switch node.(type) {
case *ast.FuncDecl:
funcCall, ok := node.(*ast.FuncDecl)
if ok {
fmt.Println(funcCall.Name)
}
}
return true
})
}go code snippet end
出力
go code snippet start
messageProcessor
ProcessAllMessagesgo code snippet end
この例が、ASTの力を理解するのに役立てば幸いです。例えば、これから依存関係グラフを構築することができます。 これで、関数定義のロジックを追加することが簡単になります。
go code snippet start
func checkFunctionNameLength(funcDecl *ast.FuncDecl, maxLength int) bool {
return len(funcDecl.Name.Name) > maxLength
}go code snippet end
関数内のステートメント数を確認するにはどうすればよいでしょうか? 幸いなことに、関数はそのステートメントを直接公開しているので、単純にカウントできます。
go code snippet start
func checkFunctionLength(funcDecl *ast.FuncDecl, maxLength int) bool {
return len(funcDecl.Body.List) > maxLength
}go code snippet end
次のチェックは少し恣意的ですが、異なるタイプのノードをどのように扱うかを示しています。
go code snippet start
func ensureSuffixForFloatVariables(node ast.Node) bool {
assignStmt, ok := node.(*ast.AssignStmt)
if !ok {
return true
}
callExpr, ok := assignStmt.Rhs[0].(*ast.CallExpr)
if !ok {
return true
}
ident, ok := callExpr.Fun.(*ast.Ident)
if !ok {
return true
}
switch ident.Name {
case "float32", "float64":
fmt.Printf("Found assignment with float type conversion at position %d\n", callExpr.Pos())
// You could add logic here to check variable name suffixes on the LHS
if lhsIdent, ok := assignStmt.Lhs[0].(*ast.Ident); ok {
fmt.Printf(" Variable name: %s\n", lhsIdent.Name)
}
}
return false
}go code snippet end
これは少し粗雑ですが、型を扱うことがどれほど複雑になり得るかを示しています。 さらに、これは理想的には識別子名だけでなく、実際の型を検査すべきです。
次のステップ
これらの例は非常に基本的で、多くのエッジケース(例えば、関数パラメータ内で宣言された変数など)を処理していません。 しかし、ASTの動作とシンプルなリンターの構築方法を理解するための良い出発点となります。
興味があれば、Reviveリンターに提出したプルリクエストをこちらで確認できます。