コードレビューに便利!PHPの重複コードを見つけてくれるPHPCPD | A Day In The Boy's Life

A Day In The Boy's Life

とあるエンジニアのとある1日のつぶやき。

いけないとわかっていても時間の関係や取りあえずという誘惑からコードをコピーしてしまい、リファクタリングの際にはどこをどうしたのかわからなくなってしまって、そのまま放置してしまうというのはよくあったりもします。

また、コードレビューの際にも「これと似たような処理をどこかで見かけたな」と思うこともよくあったりして、そういったPHPの冗長なコードを効率的に発見してくれるツールがphpcpdです。



phpcpdをインストールする


phpcpdはPEARで提供されているのでpearコマンドでさっくりインストール、といきたいのですが自分の環境だと結構めんどくさかったです。

ちなみに、PEAR自信のバージョンが1.9.4以上を求められますので、環境が合わない場合は


# pear upgrade PEAR

とかしてPEARのバージョンをあげておきましょう。


最初に、PEARのチャネルを登録して、phpcpdをインストールします。


# pear channel-discover pear.phpunit.de

# pear install phpunit/phpcpd
Unknown remote channel: components.ez.no
Unknown remote channel: pear.netpirates.net
Unknown remote channel: pear.symfony.com
phpunit/phpcpd requires package "channel://components.ez.no/ConsoleTools" (version >= 1.6)
phpunit/FinderFacade requires package "channel://pear.netpirates.net/fDOMDocument" (version >= 1.3.1)
phpunit/FinderFacade requires package "channel://pear.symfony.com/Finder" (version >= 2.1.0)
downloading PHP_Timer-1.0.4.tgz ...
Starting to download PHP_Timer-1.0.4.tgz (3,694 bytes)
....done: 3,694 bytes
install ok: channel://pear.phpunit.de/PHP_Timer-1.0.4


しかし、上記のようにPHP_Timerはインストールされていますが、phpcpdはインストールされません。

幾つか必要なパッケージが要るみたいなのでそれを先にインストールします。

といっても、新たなチャネルを登録してphpcpdのインストールをリトライをしてみます。


# pear channel-discover components.ez.no
# pear channel-discover pear.netpirates.net
# pear channel-discover pear.symfony.com

# pear install phpunit/phpcpd
downloading phpcpd-1.4.0.tgz ...
Starting to download phpcpd-1.4.0.tgz (10,439 bytes)
.....done: 10,439 bytes
downloading FinderFacade-1.0.5.tgz ...
Starting to download FinderFacade-1.0.5.tgz (4,498 bytes)
...done: 4,498 bytes
downloading fDOMDocument-1.3.2.tgz ...
Starting to download fDOMDocument-1.3.2.tgz (13,907 bytes)
...done: 13,907 bytes
install ok: channel://pear.netpirates.net/fDOMDocument-1.3.2
install ok: channel://pear.phpunit.de/FinderFacade-1.0.5
install ok: channel://pear.phpunit.de/phpcpd-1.4.0


これでようやくphpcpdがインストールされました。

インストールするとphpcpdコマンドが利用可能になっています。


# which phpcpd
/usr/bin/phpcpd

おまけ的な内容ですが、phpcpdを実行した際に下記のようなエラーが出た場合は、必要なパッケージがインストールされていないか、単純にPEARディレクトリにパスが通ってないためだと思われます。


$ phpcpd
PHP Warning:  require(SebastianBergmann/PHPCPD/autoload.php): failed to open stream: No such file or directory in /usr/bin/phpcpd on line 52
PHP Fatal error:  require(): Failed opening required 'SebastianBergmann/PHPCPD/autoload.php' (include_path='.:/var/www/lib:/usr/local/lib/php') in /usr/bin/phpcpd on line 52

後者の場合は、php.iniのinclude_pathにPEAR用のディレクトリを追加しておきましょう。

PEARディレクトリは、pear config-showで確認できます。



phpcpdの使い方


準備ができたところで、早速phpcpdの使い方を見ていきます。

phpcpdは基本的に対象ファイルを引数に指定すれば重複コードのチェックを行ってくれますが、幾つかのオプションを指定するとチェックする内容を柔軟に変更できます。


$ phpcpd --min-lines 5 --min-tokens 5 foo.php
phpcpd 1.4.0 by Sebastian Bergmann.

Found 1 exact clones with 11 duplicated lines in 1 files:

  - /home/work/workspace/foo.php:7-18
    /home/work/workspace/foo.php:28-39

25.58% duplicated lines out of 43 total lines of code.

Time: 0 seconds, Memory: 2.00Mb


上記は、foo.phpを解析するように指定し、その結果7行目から18行目と28行目から39行目に重複しているコードがあるという判定が出ています。

実際にチェックしてみたソースコードはこちら。


<?php

function getId() {
    $conn = pg_connect("host=localhost port=5432 dbname=fuga");

    $result = pg_query($conn, "SELECT id FROM dogss");
    if (!$result) {
        echo "Error!.\n";
        exit;
     }

    $arr = pg_fetch_all($result);

    $cnt = count($arr);
    for ($i = 0; $i < $cnt; $i++) {
        $key = key($arr[$i]);
        $data[$key][$i] = $arr[$i][$key];
    }

    pg_close($conn);
    return $data;
}

function getName() {
    $conn = pg_connect("host=localhost port=5432 dbname=fuga");

    $result = pg_query($conn, "SELECT name FROM dogs");
    if (!$result) {
        echo "Error!.\n";
        exit;
     }

    $arr = pg_fetch_all($result);

    $cnt = count($arr);
    for ($i = 0; $i < $cnt; $i++) {
        $key = key($arr[$i]);
        $data[$key][$i] = $arr[$i][$key];
    }

    pg_close($conn);
    return $arr;
}


DBから値を取得するメソッドが2つありますが、実質違うのはメソッド名とSQLだけです。


であれば、結構判定が漏れているのではないかと思うかもしれませんが、あまり細かいレベルで重複コードをチェックしていると、例えばIF文の処理一つでも冗長と判定されかねません。

phpcpdでは、その辺の調整を「--min-lines」オプション(指定の行以下の重複は無視する)と「--min-tokens」オプション(指定のPHPトークンの数以下は無視する)というオプションでできます。

先ほどの例だと、5行以下の重複とPHPトークンの数が5以下の該当コードは無視しています。

ですので、最初のpg_connectや最後のpg_closeやreturnの行は判定されていません。


ちなみに、PHPトークンはPHPコードの字句解析の単位でtoken_get_all メソッドで調べることができたりします。

先ほどのfoo.phpのコードのPHPトークンを調べてみると、


<?php

$file = file_get_contents("./foo.php");
$tokens = token_get_all($file);

var_dump(count($tokens));

表示される数は「282」です。

これは、コード全体でのPHPトークンですので、phpcpdの解析ではそれが「--min-lines」オプションを超える行数で、「--min-tokens」を超えるトークン数で一致していたら重複コードと判定されているようです。

これらのオプションは、デフォルトで「--min-lines」が5、「--min-tokens」が70となっているので、少し判定が厳しいかもしれません(これはソースコードの量にもよるので状況によって調整が必要でしょうが)。


あと、phpcpdは特定のPHPファイルを指定しなければならないというわけではなく、ディレクトリ単位や特定の2つのPHPファイルを解析対称にするということもできたりします。


# カレントディレクトリ以下を全て解析
$ phpcpd ./*

# foo.phpとbar.phpの2つを解析する
$ phpcpd foo.php bar.php


デフォルトでは、拡張子が「.php」のもののみが解析対象になるので、それを変更したい場合は「--names」オプションで拡張子を指定できます。

その他にも解析対象外の指定や重複コード部分を表示してくれるというようなオプションもあるので、ヘルプもあわせて参照してください。


$ phpcpd
phpcpd 1.4.0 by Sebastian Bergmann.

Usage: phpcpd [switches] <directory|file> ...

  --log-pmd <file>         Write report in PMD-CPD XML format to file.

  --min-lines <N>          Minimum number of identical lines (default: 5).
  --min-tokens <N>         Minimum number of identical tokens (default: 70).

  --exclude <dir>          Exclude <dir> from code analysis.
  --names <names>          A comma-separated list of file names to check.
                           (default: *.php)

  --help                   Prints this usage information.
  --version                Prints the version and exits.

  --progress               Show progress bar.
  --quiet                  Only print the final summary.
  --verbose                Print duplicated code.


自分自身でコードのチェックをしてみたり、コードレビューで冗長な書き方をしていないかをチェックしたいというときに作業が効率的に行えるのではないでしょうか。