dupl

Table of Contents

  1. 解決する問題
  2. 設定
  3. オプション
  4. サンプル
    1. 検出例
    2. 修正例
  5. 注意点
  6. 参考リンク

dupl はコード内の重複(コードクローン)を検出するツールです。

解決する問題

コピー&ペーストで類似のコードが複数箇所に存在すると、バグ修正や機能変更の際にすべての箇所を更新する必要があり、修正漏れが発生しやすくなります。dupl は AST(抽象構文木)を解析して構造的に類似したコードを検出するため、変数名や定数値が異なっていても発見できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Before: 構造が同じコードが複数箇所に存在
func CreateUser(db *sql.DB, u User) error {
query := "INSERT INTO users (name, email) VALUES ($1, $2)"
_, err := db.Exec(query, u.Name, u.Email)
if err != nil {
return fmt.Errorf("create user: %w", err)
}
return nil
}

func CreateProduct(db *sql.DB, p Product) error {
query := "INSERT INTO products (name, price) VALUES ($1, $2)"
_, err := db.Exec(query, p.Name, p.Price)
if err != nil {
return fmt.Errorf("create product: %w", 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
// After: 共通処理を抽出
func execInsert(db *sql.DB, query string, args ...any) error {
_, err := db.Exec(query, args...)
if err != nil {
return fmt.Errorf("exec insert: %w", err)
}
return nil
}

func CreateUser(db *sql.DB, u User) error {
return execInsert(db,
"INSERT INTO users (name, email) VALUES ($1, $2)",
u.Name, u.Email,
)
}

func CreateProduct(db *sql.DB, p Product) error {
return execInsert(db,
"INSERT INTO products (name, price) VALUES ($1, $2)",
p.Name, p.Price,
)
}

設定

公式ドキュメント

1
2
3
4
5
6
7
8
9
# .golangci.yml 設定例
version: "2"

linters:
enable:
- dupl
settings:
dupl:
threshold: 150

オプション

オプション デフォルト 説明
threshold int 150 重複と判定する最小トークン数。小さいほど多くの重複を検出する

サンプル

検出例

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
// dupl が警告: duplicate of ... (threshold 100)

// ファイル A: handler/user.go
func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
users, err := h.service.ListUsers(r.Context())
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(users); err != nil {
http.Error(w, "encode error", http.StatusInternalServerError)
}
}

// ファイル B: handler/product.go (構造が同じ)
func (h *ProductHandler) List(w http.ResponseWriter, r *http.Request) {
products, err := h.service.ListProducts(r.Context())
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(products); err != nil {
http.Error(w, "encode error", http.StatusInternalServerError)
}
}

修正例

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
// 共通の JSON レスポンスヘルパーを抽出
func respondJSON(w http.ResponseWriter, data any) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(data); err != nil {
http.Error(w, "encode error", http.StatusInternalServerError)
}
}

func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
users, err := h.service.ListUsers(r.Context())
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
respondJSON(w, users)
}

func (h *ProductHandler) List(w http.ResponseWriter, r *http.Request) {
products, err := h.service.ListProducts(r.Context())
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
respondJSON(w, products)
}

注意点

  • dupl は AST の構造を比較するため、変数名や定数値が異なっていても構造が同じなら重複として検出する
  • threshold を小さくしすぎると誤検知が増える。150〜200 が一般的な目安
  • テストコードではテーブルドリブンテストの各ケースが重複として検出されることがある。issues.exclude-rules でテストファイルを除外するか、threshold を調整する
  • すべての重複をリファクタリングすべきとは限らない。過度な抽象化はかえって可読性を下げることがある

参考リンク