🐭

「golangci/golangci-lint」の重複チェックはどのようにチェックしているのか見てみた

2024/04/28に公開

こんにちは、ikechan こといけがわです。

これまでのGoを使用した開発業務でコードの品質を維持するために、「golangci-lint」を活用しており、github actionsを使ってpush時に自動的にgolangci-lintを走らせるようにしています。その際、重複コードのチェック(dupl)で何度か重複エラーが発生し、その対応に手間取ることがありました。
その際に重複チェックの基準が気になったので、今回はパッケージ内の重複チェック処理についてメモがてら少しに取り上げたいと思います。

重複チェックの流れ


golangci/golangci-lintの重複コードチェックの機能は、pkg/golinters/duplパッケージで提供されており、dupl.goの実装の中で別のgolangci/duplパッケージで提供されている処理を利用しています。

golangci/duplでは、以下のような流れで重複コードのチェックを行なっているようです。

  1. ソースコードをプログラムの構造を表すツリー構造に変換する
  2. そのツリー構造をduplが使いやすい形式に変換する
  3. 重複を検索できるデータ構造に変換する
  4. そのデータ構造を使って、類似したコードを重複として判定する

これらの手順は、dupl.goファイルのRun関数に実装されているようです。

  1. ソースコードをプログラムの構造を表すツリー構造に変換する
    まず、duplは、Go言語の標準パッケージgo/parserを使って、ソースコードをプログラムの構造を表すツリー構造に変換します。
    job/parse.goファイルのParse関数で、この変換を行っています。
ast, err := golang.Parse(file)
if err != nil {
    log.Println(err)
    continue
}
achan <- ast

ここでは、golang.Parse関数を使ってファイルを解析し、ツリー構造を生成し、生成されたツリー構造は、achanというチャネル(データを受け渡しするための通り道)に送られているようです。

  1. そのツリー構造をduplが使いやすい形式に変換する
    次に、duplは、go/parserで生成されたツリー構造を、duplが使いやすいsyntax.Nodeという形式に変換します。
    job/parse.goファイルの以下の部分で、この変換を行っているようです。
seq := syntax.Serialize(ast)
schan <- seq

syntax.Serialize関数は、ツリー構造をチェックし、syntax.Nodeのスライスを生成します。生成されたスライスはschanチャネルに送られるようです。

  1. 変換された構造体を元に重複を検索できるデータ構造に変換する
    syntax.Nodeの並びは、重複を検索できるデータ構造に変換されるようです。
    job/buildtree.goファイルのBuildTree関数で、この変換を行っているようです。
t = suffixtree.New()
data := make([]*syntax.Node, 0, 100)
done = make(chan bool)
go func() {
    for seq := range schan {
        data = append(data, seq...)
        for _, node := range seq {
            t.Update(node)
        }
    }
    done <- true
}()

ここでは、suffixtree.New()で新しいデータ構造を作成し、schanチャネルから受け取ったsyntax.Nodeの並びを使って、データ構造を構築し、t.Update(node)で、各ノードをデータ構造に追加しているようです。
4. そのデータ構造を使って、類似したコードを重複として判定する
最後に、duplは、構築されたデータ構造を使って、ソースコード内の重複部分を検出します。重複部分は、関数やブロックなど、まとまったコードの塊として検出され流ようです。
suffixtree/dupl.goファイルのFindDuplOverメソッドwalkTrans関数で、データ構造をチェックし、重複を判定しています。

func (t *STree) FindDuplOver(threshold int) <-chan Match {
    auxTran := newTran(0, 0, t.root)
    ch := make(chan Match)
    go func() {
        walkTrans(auxTran, 0, threshold, ch)
        close(ch)
    }()
    return ch
}

func walkTrans(parent *tran, length, threshold int, ch chan<- Match) *contextList {
    // ...
    if length >= threshold && len(cl.lists) > 1 {
        m := Match{cl.getAll(), Pos(length)}
        ch <- m
    }
    // ...
}

FindDuplOverメソッドは、指定された長さ以上の重複を検索します。walkTrans関数は、データ構造を再帰的にチェックし、重複を見つけるとMatchという形式でchチャネルに送ります。
重複の判定では、以下のような基準が使われています。

  • 一定のトークン数以上の連続するトークンの並びが一致する場合、重複とみなす
  • 変数名、関数名、リテラルなどの違いは無視する
  • 構造体のフィールド定義が同じであれば、JSONタグなどの違いは無視する

ここで、トークンとは、プログラムを構成する最小単位のことを指します。例えば、for、if、=、+、変数名、関数名などがトークンに当たります。
つまり、duplは、コードの構造が一致していれば、変数名や関数名、リテラルなどの違いは無視して、重複とみなします。

トークンについては以下の記事参照
https://gimo.jp/glossary/details/token.html

では、実際のサンプルコードを見てみましょう。

func f1() int {
	x := 0
	for i := 0; i < 10; i++ {
		x += i
	}
	return x
}

func f2() int {
	y := 0
	for j := 0; j < 10; j++ {
		y += j
	}
	return y
}

このコードでは、f1関数とf2関数に重複があります。両方の関数とも、0から9までのループを回して、変数に値を加算しています。
duplは、このような構造的にとてもよく似ている重複を検出します。

重複のしきい値の設定


duplは、デフォルトでは15個以上連続するトークンが一致していれば、重複とみなします。
また。このしきい値は設定ファイルで変更できます。

linters-settings:
  dupl:
    threshold: 100

上記の設定では、しきい値が100に設定されています。つまり、100個以上連続するトークンが一致する場合に、重複とみなされます。

まとめ
今回は、「golangci/golangci-lint」の重複チェックがどのような基準でチェックしているのか、ソースコードを見ながら少し見てみました。

duplは、コードの品質を維持し、重複を減らすことで、より保守性の高いコードを書くことができるので、Goを使用するプロジェクトでは是非活用してみてください。

参考リンク


お知らせ


最後に、toraco株式会社ではエンジニアを積極採用中です。
フロントエンドエンジニア、バックエンドエンジニア、クラウドインフラエンジニアなど職種問わず、様々な技術領域にチャレンジできます。また、PM(プロジェクトマネージャー) や EM(エンジニアリングマネージャー)のキャリアパスも用意しています。
興味のある方は Wantedly の募集をぜひ読んでください。
https://www.wantedly.com/companies/company_5649245
また協力会社として、エンジニア未経験の方や将来フリーランスを見据えている方向けのSES企業が立ちあがりました。
未経験だけど、エンジニアやフリーランスに興味がある!という方は是非下記から確認してみてください!
https://www.wantedly.com/companies/linefeed2024

toraco株式会社のテックブログ

Discussion