gocognit は関数の認知的複雑度(Cognitive Complexity)を計測し、閾値を超えた関数を報告するツールです。
解決する問題
関数が長く複雑になると、読みにくく保守しづらいコードになります。従来の循環的複雑度(Cyclomatic Complexity)は分岐の数を数えるだけですが、認知的複雑度はネストの深さも考慮し、人間が「読みにくい」と感じるコードをより正確に検出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| func ProcessItems(items []Item, config Config) error { for _, item := range items { if item.IsActive() { if config.ValidateAll { for _, rule := range config.Rules { if rule.Matches(item) { if err := rule.Apply(item); err != nil { return err } } } } else { if err := defaultValidate(item); err != nil { return err } } } } return nil }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| func ProcessItems(items []Item, config Config) error { for _, item := range items { if !item.IsActive() { continue } if err := processItem(item, config); err != nil { return fmt.Errorf("process item %s: %w", item.ID, err) } } return nil }
func processItem(item Item, config Config) error { if !config.ValidateAll { return defaultValidate(item) } return applyRules(item, config.Rules) }
func applyRules(item Item, rules []Rule) error { for _, rule := range rules { if !rule.Matches(item) { continue } if err := rule.Apply(item); err != nil { return fmt.Errorf("apply rule %s: %w", rule.Name, err) } } return nil }
|
設定
公式ドキュメント
1 2 3 4 5 6 7 8 9
| version: "2"
linters: enable: - gocognit settings: gocognit: min-complexity: 20
|
オプション
| オプション |
型 |
デフォルト |
説明 |
min-complexity |
int |
30 |
この値以上の認知的複雑度を持つ関数を報告する |
認知的複雑度の計算ルール
| 構造 |
加算 |
説明 |
if / else if / else |
+1 |
条件分岐 |
switch / select |
+1 |
分岐構造 |
for |
+1 |
ループ |
goto / ラベル付き break / continue |
+1 |
ラベル付きジャンプ |
&& / || のシーケンス |
+1 |
論理演算子の連鎖 |
| ネストされた構造 |
+ネスト深さ |
ネストが深いほど追加のペナルティ |
サンプル
検出例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| func HandleRequest(r *http.Request) (*Response, error) { if r == nil { return nil, ErrNilRequest }
if r.Method == "GET" { if r.URL.Query().Has("id") { return handleGetByID(r) } else if r.URL.Query().Has("q") { return handleSearch(r) } else { return handleList(r) } } else if r.Method == "POST" { if r.Body == nil { return nil, ErrEmptyBody } if r.Header.Get("Content-Type") != "application/json" { return nil, ErrUnsupportedMedia } return handleCreate(r) } else if r.Method == "DELETE" { if !isAdmin(r) { return nil, ErrForbidden } return handleDelete(r) } else { return nil, ErrMethodNotAllowed } }
|
修正例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| func HandleRequest(r *http.Request) (*Response, error) { if r == nil { return nil, ErrNilRequest }
switch r.Method { case "GET": return handleGet(r) case "POST": return handlePost(r) case "DELETE": return handleDelete(r) default: return nil, ErrMethodNotAllowed } }
func handleGet(r *http.Request) (*Response, error) { q := r.URL.Query() switch { case q.Has("id"): return handleGetByID(r) case q.Has("q"): return handleSearch(r) default: return handleList(r) } }
func handlePost(r *http.Request) (*Response, error) { if r.Body == nil { return nil, ErrEmptyBody } if r.Header.Get("Content-Type") != "application/json" { return nil, ErrUnsupportedMedia } return handleCreate(r) }
|
注意点
- 推奨閾値はプロジェクトにより異なるが、一般的に 15〜30 が目安。厳しくするなら 15、緩めるなら 30
- 認知的複雑度はネストの深さにペナルティがあるため、早期リターン(guard clause)で大幅に削減できる
if-else if チェーンは switch に置き換えると複雑度が下がる(switch は構造として直感的とみなされる)
- テストコードのテーブルドリブンテストなど、構造上複雑度が高くなる場合は
//nolint:gocognit で抑制を検討する
参考リンク