Home (日本語版)

Masaaki Goshima edited this page Feb 22, 2013 · 14 revisions

Compiler::Tools::CopyPasteDetectorは、Perl5コードのコピペを検出するモジュールです。

Resources

Dependencies

  • B::Deparse
  • MIME::Base64
  • Digest::MD5
  • HTML::Template
  • File::Copy::Recursive
  • File::Basename
  • File::Path
  • JSON::XS
  • Module::CoreList
  • List::MoreUtils
  • Compiler::Lexer

Compiler::Lexerは次のリンク先から取得することができます : p5-Compiler-Lexer

使用方法

example以下にあるサンプルコードを用いて説明します。

example/simple/simple-detector.pl

本モジュールを使って最小コードで書いたコピペ検出器です。 オプションには次のようなものがあります。

  • -i, --ignore-variable-name : 変数名のゆれをなくす
  • -j, --jobs : 並列実行によって検出する
  • -t, --min-token-num : 検出対象にする最小のトークン数を決める
  • -l, --min-line-num : 検出対象にする最小の行数を決める
  • -e, --encoding : 対象コードのエンコードを指定する
  • --order-by : 結果算出時に使用するメトリクスの種類を変更する

詳しくはhelpオプションで確認して下さい。

example/with_db/detect-with-mysql.pl

一度検出した結果をデータベースに保存し、二度目以降からの検出を高速化するためのサンプルです。

詳しくは、helpで確認して下さい。

使用前の準備

$ example/simple/simple-detector.pl example/projects/lib

として実行すると、"Can't locate ~.pm"と出て実行が終了する場合があります。 これはコピペ検出の際に、対象コードのコンパイルを行っているために起こるものです。 そのため、コピペ検出対象のコードが用いているモジュールは事前にインストールしておく必要があります。 また、インストールしたモジュールにパスが通っていない場合は、

perl -I/path/to/perl5/lib example/simple/simple-detector.pl example/projects/lib

としてロードパスを指定した上で実行してください。

example/projects/lib以下にはサンプルとしてCatalyst/Mojoliciousがインストールされて いますが、それぞれのフレームワークが入っていない場合は、事前にインストールしてから サンプルコードを実行してください。

使用例

Catalyst/Mojoliciousのインストールが終わっていれば、次のコードで実行することができます

$ example/simple/simple-detector.pl -j 4 example/projects/lib

実行には少し時間がかかります。お茶菓子でも食べつつ数分程度待ちましょう。 次に出力結果について説明します。

出力結果

実行が終わると、以下のような結果が得られます

        score    : 130
        location : example/projects/lib/Mojo/IOLoop.pm (100 ~ 117), example/projects/lib/Mojo/IOLoop.pm (189 ~ 206)
        src      : $stream->on('close', sub {
                       my $c = $$self{'connections'}{$id};
                       $$c{'close'}($self, $id) if $$c{'close'};
                   }
                   );
                   $stream->on('error', sub {
                       my $c = $$self{'connections'}{$id};
                       $$c{'error'}($self, $id, pop()) if $$c{'error'};
                   }
                   );
                   $stream->on('read', sub {
                       my $c = $$self{'connections'}{$id};
                       $$c{'read'}($self, $id, pop()) if $$c{'read'};
                   }
                   );

        score    : 66
        location : example/projects/lib/Catalyst/DispatchType/Chained.pm (108 ~ 113), example/projects/lib/Catalyst/DispatchType/Chained.pm (369 ~ 374)
        src      : if (my $pp = $curr->attributes->{'PathPart'}) {
                       unshift @parts, $$pp[0] if defined $$pp[0] and length $$pp[0];
                   }
                   $parent = $curr->attributes->{'Chained'}[0];
                   $curr = $self->_actions->{$parent};

        score    : 46
        location : example/projects/lib/Mojo/DOM.pm (216 ~ 221), example/projects/lib/Mojo/DOM.pm (340 ~ 345)
        src      : my $parent = $$tree[3];
                   my $i = $$parent[0] eq 'root' ? 1 : 4;
                   foreach my $e (@$parent[$i .. $#$parent]) {
                       last if $e == $tree;
                       ++$i;
                   }

        score    : 45
        location : example/projects/lib/Catalyst/DispatchType/Path.pm (49 ~ 52), example/projects/lib/Catalyst/DispatchType/Regex.pm (49 ~ 52)
        src      : my($self, $c) = @_;
                   my $avail_width = Catalyst::Utils::term_width() - 9;
                   my $col1_width = $avail_width * 0.5 < 35 ? 35 : int $avail_width * 0.5;
                   my $col2_width = $avail_width - $col1_width;

        score    : 44
        location : example/projects/lib/Mojo/Cookie/Request.pm (32 ~ 36), example/projects/lib/Mojo/Cookie/Response.pm (64 ~ 70)
        src      : my $self = shift @ARGV;
                   return '' unless my $cookie = $self->name;
                   $cookie .= '=';
                   my $value = $self->value;
                   $cookie .= $value =~ /[,;"]/ ? quote($value) : $value if defined $value;

        score    : 39
        location : example/projects/lib/Mojo/Transaction/HTTP.pm (51 ~ 57), example/projects/lib/Mojo/Transaction/HTTP.pm (155 ~ 161)
        src      : unless ($headers->connection) {
                       if ($self->keep_alive) {
                           $headers->connection('keep-alive');
                       }
                       else {
                           $headers->connection('close');
                       }
                   }
                   $$self{'state'} = 'write_start_line';

        score    : 35
        location : example/projects/lib/Catalyst/Dispatcher.pm (178 ~ 181), example/projects/lib/Catalyst/Dispatcher.pm (241 ~ 244)
        src      : my $self = shift @ARGV;
                   my $opname = shift @ARGV;
                   my($c, $command) = @_;
                   my($action, $args, $captures) = $self->_command2action(@_);

        score    : 35
        location : example/projects/lib/Mojo/DOM.pm (16 ~ 21), example/projects/lib/Mojolicious/Routes.pm (24 ~ 29), example/projects/lib/Mojolicious.pm (40 ~ 45)
        src      : my $self = shift @ARGV;
                   my($package, $method) = our $AUTOLOAD =~ /^([\w\:]+)\:\:(\w+)$/;
                   croak("Undefined subroutine &${package}::$method called") unless blessed $self and $self->isa('main');

        score    : 32
        location : example/projects/lib/Mojo/UserAgent.pm (224 ~ 228), example/projects/lib/Mojo/UserAgent.pm (266 ~ 270)
        src      : my($loop, $err, $stream) = @_;
                   return $self->_error($id, $err) if $err;
                   $self->_events($stream, $id);

        score    : 31
        location : example/projects/lib/Catalyst/Script/Create.pm (15 ~ 21), example/projects/lib/Catalyst/Script/Server.pm (9 ~ 15)
        src      : has('debug', ('traits', ['Getopt'], 'cmd_aliases', 'd', 'isa', 'Bool', 'is', 'ro', 'documentation', 'Force debug mode'));

        score    : 31
        location : example/projects/lib/Mojolicious/Controller.pm (585 ~ 590), example/projects/lib/Mojolicious/Controller.pm (598 ~ 603)
        src      : my($self, $chunk, $cb) = @_;
                   if (ref $chunk and ref $chunk eq 'CODE') {
                       $cb = $chunk;
                       $chunk = undef;
                   }

        score    : 31
        location : example/projects/lib/Mojo/Content.pm (360 ~ 365), example/projects/lib/Mojo/Content.pm (384 ~ 389)
        src      : my $self = shift @ARGV;
                   my $headers = $self->headers;
                   $headers->parse($$self{'pre_buffer'});
                   $$self{'pre_buffer'} = '';

本モジュールは、コピペコードをいくつかの基準に従って評価することができます。 上記の出力結果は、コピペしているコード断片に関して、トークン数が多い順にソートして出力したものです。 トークン数が多いコード断片ほど悪いコピペである可能性が高く、 結果の上位にあるものから対応していくことが望ましいことを表しています。

各パラメータは次のようになっています

  • score: トークン数
  • location: コピペコード断片があるファイル・行番号のリスト
  • src: コピペコード (Perlがコンパイルした後の状態で表示)

コピペコードが存在しない場合は、何も表示されません。

ここまでで簡単な検出結果を読み解くところまできました。 しかし、トークン数以外でも、幅広い視点でコピペ結果を見てみたいと思った方もいるかもしれません。

安心してください。あくまでコンソールでの表示結果は「簡易表示版」にすぎません。 本モジュールには、強力な検出結果表示機能が存在します。

サンプルを実行したディレクトリ上で、copy_paste_detector_outputというディレクトリ はできているでしょうか。その中にはコピペ検出結果をhtmlで表示するためのファイル群が入っています。 これらは、検出時に自動的に生成されます。

次項からは、このhtmlでの表示結果について解説します。

HTML形式による結果表示

copy_paste_detector_output以下のファイルを全て静的ファイル配信するようにして、 index.htmlにアクセスすると以下のページが表示されます.

続いて、各機能について解説していきます.

clone set metrics

最初にアクセスした際に表示されているのはこの項目です。 clone set metricsは、コピペコード断片(コードクローンと呼ぶ)に関するメトリクス値です。 こちらの項目では、検出されたコード断片を、length, population, radius, nifの各視点から 評価し、ソートして表示します。各評価視点は次のような意味を持ちます。

  • length : コードクローンを構成しているトークンの数(この値が大きい程巨大なコードクローンになる)
  • population : コピペされている箇所の数(この値が大きい程広範囲にコピペされている可能性が高い)
  • radius : コードクローンを持っているソースファイルが、ディレクトリ上でどれだけ広がっているか(0なら同じソースファイル内)を表す(この値が大きい場合、コピペが広範囲に広がっているので、より注意が必要)
  • nif : 対象コードクローンを1つ以上含むソースファイルの個数(populationは単純に数を表示し、nifはソースの種類がuniqueになるように絞って表示)

上記4つの評価基準によって、様々な視点でコード断片を定量的に捉えることができます。 更に、実際にソースコード上でどの部分がコピペコードになっているかを見るには、「location」項目にあるdetailをクリックしてください。 すると、以下のような画面になり、エディタでいちいち確認しなくても、該当部分をすぐに確認できるようになります。

ファイル名の部分をクリックすれば、そのファイルのみのコードを見ることもできます。

次は、評価視点をコード断片自体から、それを含むファイルに移して見てみます。

file metrics

この項目では、コードクローンを含んでいるファイルを、coverage, self similarity, another files similarity, neighborの各視点から評価します。 各評価視点は次のような意味を持ちます。

  • coverage : 対象ファイルが、何らかのコードクローンによって占められている割合
  • self similarity : 対象ファイルが、そのファイル内のコードクローンによって占められている割合
  • another files similarity : 対象ファイルが、そのファイル以外とのコードクローンによって占められている割合(この割合が100%に近いものは、ファイル全体をコピーすることによって作られた可能性が高い)
  • neighbor : 対象ファイルとの間にコードクローンを持っているファイルの数

これらの視点から、早急に対応しなければいけないファイルが何なのかを知ることができます。

ここまで紹介してきたclone set metrics, file metricsは、CCFinderのメトリクス分析を参考にしています(CCFinderのGUIフロントエンド(GemX)) これらについて詳しく知りたい方は、上記サイトを参考になさると良いかもしれません。

そして、本モジュールオリジナルの評価項目として、file metricsの考え方をディレクトリに対して適応できるように拡張したものがあります。 それが次項で説明するdirectory metricsです。

directory metrics

この項目は、前述したファイルに対するメトリクス分析を、ディレクトリに対して適用するようにしたものです。 評価視点には次のものがあります。

  • coverage : 対象ディレクトリ内にあるコードクローンの割合
  • self similartiy : 対象ディレクトリが、そのディレクトリ内のコードクローンによって占められている割合
  • another directories similarity : 対象ディレクトリが、他のディレクトリとのコードクローンによって占められている割合
  • neighbor : 対象ディレクトリとの間にコードクローンを持っているディレクトリの数

これらの評価視点は、Namespaceをディレクトリで分ける風習があるPerlに対してより効果を発揮します。 Perlで書かれたシステムが大規模になってくると、サービス名でディレクトリを分けたりすることが多くなるかと思います。 そして、そのように分けられたディレクトリに対し、それぞれメンテナンス担当者が割り当てられることも多いはずです。 各メンテナンス担当者は、基本的には自分が担当するディレクトリ配下のファイル群のメンテナンスしか行わないため、 他のディレクトリにコピペコードがあることがわかってもあまり意味がありません。

そこで、このdirectory metricsを利用し、自分が担当しているディレクトリ配下のコード品質を定量的に評価することで、 他のディレクトリ(サービス)と比べて自分の担当コードの品質が良いのか悪いのかを客観的に理解することができるようになります。

本モジュールには、このdirectory metricsを使って、指定したディレクトリ群のメトリクス値をコミット毎に測定・収集・観測し、 リファクタリングを促すような仕組み作りができるようになるサンプルが含まれています。

これについては、後ほど応用例の項目で説明します。

Scattergram

こちらの項目では、以下のような図が表示されます。

この図は、コードクローンが指定ディレクトリ配下でどのように分布しているのかを直感的に把握するための図です。 見方が少々難しいですが、一つずつ説明していきます。 まず、図中の小さなマス目(グレーで囲われた部分)から説明します。 これは、コードクローンが検出された各pmファイルを表しています。 つまり、一番上の行に着目すると、次のような状態になります。

図中のA.pmは左上隅のマス目と一致し、その右隣のマスがB.pmに該当します。 そのまま右に見ていくとG.pmのマス目がありますが、前述したA.pm, B.pmとはマスの大きさが異なることがお分かりになるでしょうか。 これは、一つマス目の大きさが、pmファイルが含んでいるコードクローンの種類によって決まるためです。 つまり、A.pm, B.pmはコードクローンが1種類しか含まれていないのに対して、 G.pmは数種類含んでいるため、その分だけマス目が大きくなっています。 つまりマス目が大きい程、注意しなければいけないファイルだということがわかります。

次に、最初の図を縦方向に見ていきます。といっても新しいことを覚える必要はなく、 横方向と同じpmファイルが左上隅から下方向に並んでいるだけです。

例えるなら、総当たり戦の早見表を思い浮かべると良いと思います。

次に、図中に見られる赤いマスについて説明します。 これは、そのマスで交わっている二つのファイルに、共通するコードクローンが存在することを表しています。 上の例だと左上隅が赤くなっていますが、これは"A.pmとA.pmに同じコードクローンが含まれている"ということを表していると考えてください(当たり前ですね)。 ですので、図中の斜めのラインが全て赤くなっているのは、そのマスには同じファイルが当てはまるからだと分かります。

また、枠に色がついている箇所が何カ所か見られると思いますが、その中にマウスカーソルを持っていくと、該当するディレクトリやファイル名が表示されます。

file tree

こちらの項目については特に解説する必要もないと思います。 コードクローンが検出されたファイルが、ディレクトリ上の親子関係を保った状態で表示されています。 ファイル名をクリックすれば、ソースコードを見ることができます。

以上でHTML形式による表示機能の説明は終わりです。 次項では、example/extension以下に含まれるサンプルコードを用いて、本モジュールの応用例について解説します。

応用例

この項目では、example/extension以下にあるサンプルを用いて、より効果的な運用方法について紹介します。 このサンプルでは、あらかじめ指定したディレクトリに対して、コミットされたタイミングでdirectory metricsの値を取得し、 グラフ化します。これによって、コード品質の向上・劣化の過程が直感的に分かるようになり、 リファクタリングの機会を判断することが格段にしやすくなります。

例として、example/projects/lib以下にあるCatalyst, Mojoliciousを例にとって説明します。 以下の3つのディレクトリを監視対象にします。

  • example/projects/lib/Catalyst
  • example/projects/lib/Mojolicious
  • example/projects/lib/Mojo

設定を終えたら、観測し始めたいrevisionまでrepositoryを巻き戻して、scripts/routine-exec.plを実行します。 すると、対象repositoryの最新のrevisionまで順にrevisionをupdateしつつ、自動でコピペ結果を収集します。 その後、start.psgiをplackupで起動します。

plackup start.psgi

すると、以下のようなページが表示されます。

Homeには、directory metricsの値が悪い順にソートした結果を載せています。 続いて、左側の監視対象ディレクトリ(以下ではNamespaceと呼ぶことにします)のリストのどれかをクリックしてみます。 すると、以下のように、各コミットによってメトリクスの値がどう変化してきたかを見ることができます。

また、ページ上段にあるCoverage, Another Directories Similarityなどの項目をクリックします。 すると、以下のようなNamepsaceごとのメトリクス値比較を見ることができます。

このように、監視対象のNamespaceを設定するだけで、簡単に変化を可視化して観測することができるようになります。 自分のサービスの状態を一歩引いて把握したいときなどに使うと便利です。

そしてもちろん、詳細なコピペ検出結果を見るためのリンクもあります。 タイトル脇の"goto detail report"をクリックすれば、最新のrevisionに対するコピペ検出結果を見ることができます。

Summaryに戻りたいときは"goto summary report"をクリックしてください。

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.