コピペコード検出で定番のPMDはdockerから使うと楽

PMDをつかうとコピペコードの検出ができる

ある程度プロジェクトが進捗して運用フェーズに入る頃、コードの要所要所にコピペの跡が垣間見えた、なんて経験があるエンジニア諸氏は結構いるのではないでしょうか。

こういった重複コード(いわゆるコピペコード)を検出するのに便利なツールとして、比較的枯れたものの一つにPMDというものがあります。

このPMDはもともとJavaのソースコードについて、以下のような、リファクタリングに役立つ解析を行ってくれるものです。Javaには詳しくありませんが、どうやらJavaでは定番のツールかと思われます。

  • 予測可能なバグの検出(空のtry/catchなど)
  • 未使用のコード(Dead code)の検出
  • 最適ではないコードの検出(無駄なString/StringBufferの使用など)
  • 複雑すぎるコードの検出(不要なif, whichに置き換え可能なforなど)
  • 重複したコードの検出

そしてPMDは今ではプラグインを通じ、Javaの他に以下の言語に対応しています。

  • C++
  • C#
  • Fortran
  • Go
  • JavaScript
  • JSP
  • Matlab
  • Objective-C
  • PHP
  • PL/SQL
  • Python
  • Ruby
  • Velocity
  • XMLおよびXSL
  • Scala

残念ながらElixirには未対応のようです。またPerlにはpmd-perlというプラグインがありましたので、もしかすると対応しているのかもしれません。

Docker Imageを使うと楽

さて、このPMDですが、Docker Hubにいくつかビルド済みイメージがあります。私のお気に入りは rawdee/pmd:6.19.0-alpine です。

では早速、コピペコードの検出を試してみましょう。

今回解析の対象としたのは frankrap/bybit-api です。これは暗号通貨取引所「ByBit」のGo向けAPIライブラリです。

これを対象として選んだ理由は、手元に既にあり、ざっとコードを読んでそこそこ重複するコードがありそうだと思ったのと、一般公開済みのコードだからです。

あなたの環境がdockerを利用可能な状態であれば、プロジェクトルート直下で以下のコマンドを実行するだけで、コピペコードの検出が可能となります。

1
2
3
4
5
$ docker run --rm -v $(pwd):/src rawdee/pmd:6.19.0-alpine \
cpd --language go \
--files ./**/*.go \
--minimum-tokens 100 \
--exclude ./**/*_test.go

実際に bybit-api に対して実行した結果が以下の通りとなります。

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
$ docker run --rm -v $(pwd):/src rawdee/pmd:6.19.0-alpine cpd --language go --files ./**/*.go --minimum-tokens 100 --exclude ./**/*_test.go
Found a 25 line (108 tokens) duplication in the following files:
Starting at line 197 of /src/./rest/api.go
Starting at line 251 of /src/./rest/api.go

log.Printf("PublicRequest: %v", fullURL)
}
var binBody = bytes.NewReader(make([]byte, 0))

// get a http request
var request *http.Request
request, err = http.NewRequest(method, fullURL, binBody)
if err != nil {
return
}

var response *http.Response
response, err = b.client.Do(request)
if err != nil {
return
}
defer response.Body.Close()

resp, err = ioutil.ReadAll(response.Body)
if err != nil {
return
}

if b.debugMode {
log.Printf("PublicRequest: %v", string(resp))

上記のように、コピペ箇所の検出ができていることがわかるかと思います。ここでは ./rest/api.go の197行目から25行ぶんのコードについて、251行目にも同一のコードが登場していることを表しています。

それにしても、この長いdockerコマンドを毎回入力するのは非常に面倒なことです。もしご自身のプロジェクトでコピペ検出をちょいちょいやっていきたいと思っても、こんなに長いコマンドを入力するだけでやる気が萎えてしまうというものです。

ですので、Makefileに以下のようなタスクを追加しましょう(go言語のプロジェクトの場合です)。

1
2
cpd:
docker run --rm -v $(pwd):/src rawdee/pmd:6.19.0-alpine cpd --language go --files ./**/*.go --minimum-tokens 100 --exclude ./**/*_test.go

このようにすることで make cpd とするだけでプロジェクト配下のコードについてコピペ検出が可能となります。

なお、上記のタスクはコピペコードが存在するとmakeに失敗しますので、このことを利用して各種CIに組み込むなどの継続的な措置を取ることも可能です。