Skip to main content Alex Collie's blog

ゼロからリンターを作る

概要

リンターの有用性は過小評価されるべきではありません。リンターは軽量なPRレビューの形として機能します。
実際、Google SREガイドでは、「nitpick」コメントはリンターに任せるべきだと提案しています。
リンターは明白なバグを防ぎ、コード品質の守護者として機能します。

レッスンの基礎

リンターは通常、言語固有のものです。例えば、Go言語用のReviveなどがあります。
リンターは、コンパイラが公開する抽象構文木(AST)に依存することが多いです。
実際、GoコンパイラがGoコードをコンパイルするために使用するのと同じASTです(そうです、Goコンパイラ自体もGoで書かれています)。

ソースコード内の各関数は構文ノードに分解され、特定の型にキャストして分析できます。

このレッスンでは、以下の機能を持つシンプルなリンターを構築します:

  1. 20文字を超える関数名を防止する。
  2. 60個以上のステートメントを持つ関数にフラグを立てる。
  3. すべてのfloat変数にf32またはf64のプレフィックスがあることを保証する。

実装

セットアップ

  1. demoというフォルダを作成します。
  2. 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
ProcessAllMessages

go 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リンターに提出したプルリクエストをこちらで確認できます。