gocognit

Table of Contents

  1. 解決する問題
  2. 設定
  3. オプション
  4. 認知的複雑度の計算ルール
  5. サンプル
    1. 検出例
    2. 修正例
  6. 注意点
  7. 参考リンク

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
// Before: 認知的複雑度が高い関数(ネストが深く読みにくい)
func ProcessItems(items []Item, config Config) error {
for _, item := range items { // +1 (for)
if item.IsActive() { // +2 (if, ネスト1)
if config.ValidateAll { // +3 (if, ネスト2)
for _, rule := range config.Rules { // +4 (for, ネスト3)
if rule.Matches(item) { // +5 (if, ネスト4)
if err := rule.Apply(item); err != nil { // +6 (if, ネスト5)
return err
}
}
}
} else { // +1 (else)
if err := defaultValidate(item); err != nil { // +4 (if, ネスト3)
return err
}
}
}
}
return nil
}
// 認知的複雑度: 26
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
// After: 早期リターンと関数分割で複雑度を低減
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
# .golangci.yml 設定例
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
// gocognit が警告: cognitive complexity 15 of func HandleRequest is high (> 10)
func HandleRequest(r *http.Request) (*Response, error) {
if r == nil { // +1
return nil, ErrNilRequest
}

if r.Method == "GET" { // +1
if r.URL.Query().Has("id") { // +2 (ネスト1)
return handleGetByID(r)
} else if r.URL.Query().Has("q") { // +1
return handleSearch(r)
} else { // +1
return handleList(r)
}
} else if r.Method == "POST" { // +1
if r.Body == nil { // +2 (ネスト1)
return nil, ErrEmptyBody
}
if r.Header.Get("Content-Type") != "application/json" { // +2 (ネスト1)
return nil, ErrUnsupportedMedia
}
return handleCreate(r)
} else if r.Method == "DELETE" { // +1
if !isAdmin(r) { // +2 (ネスト1)
return nil, ErrForbidden
}
return handleDelete(r)
} else { // +1
return nil, ErrMethodNotAllowed
}
}
// 認知的複雑度: 15

修正例

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 で抑制を検討する

参考リンク