(Photo by:Sebastiaan ter Burg)
ソフトウェア開発に銀の弾丸などない
プログラミングのコードレビューを実施すれば、ソフトウェアの品質が大きく向上します。わかっていながら実際の開発現場では、他の優先度高い作業、少ないリソース、足りない時間(短い納期)によって満足なコードレビューが実施されていない場合があります。
品質が高く、バグがほとんどないソフトウェアを開発するために、「システムやツールによる自動化可能な処理は自動化してしまう」を追求したいと、だれでも一度は考えると思います。
銀の弾丸:ソフトウェア工学の分野においては、フレデリック・ブルックスが1986年に発表した論文で”No Silver Bullet(銀の弾丸など無い)”というフレーズを用い、全ての問題に通用する万能な解決策などは存在しないと論じたこと(Wikipedia)
最近は、継続的インテグレーション(CI/Continuous Integration)や継続的デリバリー(CD/Continuous Delivery)という開発からサービス化・本番化(Deploy)までを自動化する考え方やツールが普及してきました。この記事では、
- ソースコードの品質を上げるオープンソースの使えるツールを紹介
- CIツールに組み込んで利用できる、コード解析ツールを紹介
複数人で協調したプログラムの開発を行う現場でソフトウェアの品質を保つためにはコードレビューが有効です。
コードレビューはプロジェクトに関わる自分以外のメンバーの時間を使いますので、人の手で1行1行丁寧にインスペクションをする前に、重複したコードや不必要なコード、意図が分かり辛いクラス名・メソッド名などは事前に修正してからコードレビューを行いたいものです。
コードレビュー前に不具合になりそうなコードを機械的に検出し、教えてもらえる今回はRubyを対象にしたコードの静的解析ツールを紹介します。
重複したコードを見つける、Flay
コードを短く保つためには、コピペで量産された重複コードを適切にメソッドに抽出する、事前処理を行って重複コードを発生しない構造にする等の対応が求められます。コーディングしている中でコードの重複に気づいても、後でやれば良いと後回しにしてそのまま忘れられてしまう事が良くあります。完全に重複していれば、まだ扱い易いですが、全く同じではなく微妙にパラメーターや変数が違ったりすると、どのように修正すれば良いのかに迷ってそのまま放置されがちです。
このように微妙に似通っている重複コードを見つけるにはFlayを使います。
- github: https://github.com/seattlerb/flay
- home: http://ruby.sadi.st/Flay.html
- rdoc: http://docs.seattlerb.org/flay/
gemでflayをインストールします。
コマンド
~$ sudo gem install flay --no-ri --no-rdoc
出力結果
[sudo] password for user: Fetching: sexp_processor-4.5.1.gem (100%) Successfully installed sexp_processor-4.5.1 Fetching: ruby_parser-3.6.6.gem (100%) Successfully installed ruby_parser-3.6.6 Fetching: flay-2.6.1.gem (100%) Successfully installed flay-2.6.1 3 gems installed
flayコマンド内でdiffを出すためにruby2rubyを利用しているので、ruby2rubyもインストールします。
コマンド
~$ sudo gem install ruby2ruby --no-ri --no-rdoc
出力結果
[sudo] password for user: Fetching: ruby2ruby-2.1.4.gem (100%) Successfully installed ruby2ruby-2.1.4 1 gem installed
これでflayのインストールが出来ました。次に手元の開発環境にrailsがインストールされていたので、ActiveRecordのコードを使ってコード重複の検出を試します。
コマンド
~$ flay /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/**/*.rb
出力結果
Total score (lower is better) = 370 1) Similar code found in :class (mass = 66) /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/associations.rb:70 /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/associations.rb:76 2) Similar code found in :defn (mass = 50) /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/persistence.rb:29 /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/persistence.rb:46 3) Similar code found in :call (mass = 48) /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/enum.rb:172 /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/enum.rb:180 /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/enum.rb:188 4) Similar code found in :rescue (mass = 48) /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/migration.rb:947 /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/migration.rb:966 5) Similar code found in :class (mass = 42) /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/associations.rb:31 /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/associations.rb:49 6) Similar code found in :defn (mass = 40) /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/migration.rb:825 /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/migration.rb:832 7) Similar code found in :defn (mass = 38) /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/associations.rb:1384 /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/associations.rb:1513 8) Similar code found in :defn (mass = 38) /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/transactions.rb:237 /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/transactions.rb:248
数字は0に近く小さいほど良く、0は重複したコードがない状態です。この中のrescue節に似ているコードが見つかった4番目の「/usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/migration.rb
」のみdiffも含めて実行します。
コマンド
~$ flay -v --diff /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/migration.rb </code >
出力結果
Processing /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/migration.rb Total score (lower is better) = 88 1) Similar code found in :rescue (mass = 48) A: /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/migration.rb:947 B: /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/migration.rb:966 begin execute_migration_in_transaction(migration, @direction) rescue => e A: canceled_msg = use_transaction?(migration) ? (", this migration was canceled") : ("") B: canceled_msg = use_transaction?(migration) ? ("this and ") : ("") A: raise(StandardError, "An error has occurred#{canceled_msg}:nn#{e}", e.backtrace) B: raise(StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:nn#{e}", e.backtrace) end 2) Similar code found in :defn (mass = 40) A: /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/migration.rb:825 B: /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/migration.rb:832 A: def up(migrations_paths, target_version = nil) B: def down(migrations_paths, target_version = nil, &block) migrations = migrations(migrations_paths) migrations.select! { |m| yield(m) } if block_given? A: new(:up, migrations, target_version).migrate B: new(:down, migrations, target_version).migrate end
1番目がrescue内に似ているコードが検出された詳細です。このようにA/Bと並んだ形でdiffが表示されるため一目で見て何が微妙に異なっているのか分かり易いです。今回試したこのコードは例外処理のため、どうしても似た内容になってしまうのは仕方がありません。
flayはコマンドの仕組みもシンプルなため、CIツールに組み込んでflayの出力結果をレポートに含めるのも難しくないと思います。
コーディングスタイルへの準拠、Rubocop
RubocopはRubyで広く使われている静的コード解析ツールとして有名で、日本語の説明も多数見つかるため詳しい説明は不要ですね。Ruby style guideと照らし合わせた解析結果を出力してくれます。CIツールとの連携方法も多数紹介されていますし、扱いに困る事はないと思います。
github: https://github.com/bbatsov/rubocop
コードの不吉な匂いを検出、reek
使われているようで使われてない、でもちょっぴり使われている。そんな印象のツールがreekです。コードからしてくる不吉な匂いを検出して警告を表示します。メンテナンスに困るメソッドのネーミング、使われていない変数等の悪しき習慣(不吉な匂い)をコードから排除するのに役に立ちます。
github: https://github.com/troessner/reek
gemでreekをインストールします。
コマンド
~$ sudo gem install reek --no-ri --no-rdoc
出力結果
[sudo] password for user: Fetching: diff-lcs-1.2.5.gem (100%) Successfully installed diff-lcs-1.2.5 Fetching: abstract_type-0.0.7.gem (100%) Successfully installed abstract_type-0.0.7 Fetching: equalizer-0.0.11.gem (100%) Successfully installed equalizer-0.0.11 Fetching: ice_nine-0.11.1.gem (100%) Successfully installed ice_nine-0.11.1 Fetching: thread_safe-0.3.5.gem (100%) Successfully installed thread_safe-0.3.5 Fetching: memoizable-0.4.2.gem (100%) Successfully installed memoizable-0.4.2 Fetching: adamantium-0.2.0.gem (100%) Successfully installed adamantium-0.2.0 Fetching: concord-0.1.5.gem (100%) Successfully installed concord-0.1.5 Fetching: procto-0.0.2.gem (100%) Successfully installed procto-0.0.2 Fetching: ast-2.0.0.gem (100%) Successfully installed ast-2.0.0 Fetching: parser-2.2.2.2.gem (100%) Successfully installed parser-2.2.2.2 Fetching: unparser-0.2.3.gem (100%) Successfully installed unparser-0.2.3 Fetching: rainbow-2.0.0.gem (100%) Successfully installed rainbow-2.0.0 Fetching: reek-2.2.1.gem (100%) Successfully installed reek-2.2.1 14 gems installed
依存しているライブラリも同時にインストールされました。次にgithub上の説明が分かり易かったので、コマンドラインから1行で挙動を試して見ます。
コマンド
~$ echo "class C; def m; end; end" | reek
出力結果
STDIN -- 3 warnings: [1]:C has no descriptive comment (IrresponsibleModule) [1]:C has the name 'C' (UncommunicativeModuleName) [1]:C#m has the name 'm' (UncommunicativeMethodName)
「C」とか「m」では何の意味か分からない事に対して警告が表示されました。括弧で囲まれている内容にはそれぞれ説明がついており、例えば「UncommunicativeModuleName」であれば以下URLを見ると何故警告になるのかが分かります。
警告内容の説明URL: https://github.com/troessner/reek/blob/develop/docs/Uncommunicative-Method-Name.md
1文字であるのと、後は末尾が数字で終わっていると警告になるようです。「C」を「Common」に変更して、「m」を「more1」と末尾に数字を付けて実行してみます。
コマンド
~$ echo "class Common; def more1; end; end" | reek
出力結果
STDIN -- 2 warnings: [1]:Common has no descriptive comment (IrresponsibleModule) [1]:Common#more1 has the name 'more1' (UncommunicativeMethodName)
期待通りの結果になりました。ここで「more1」を「more」にすると2行目の警告もなりなります。
次に実際のコードとして、ActiveRecordを使って挙動を試して見ます。flayのように全てを対象にすると警告が出すぎてしまうので1つのファイルに限定して試します。
コマンド
~$ reek /usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/type/binary.rb
出力結果
/usr/local/lib/ruby/gems/1.8/gems/activerecord-4.2.0/lib/active_record/type/binary.rb -- 5 warnings: [3]:ActiveRecord::Type::Binary has no descriptive comment (IrresponsibleModule) [12]:ActiveRecord::Type::Binary#type_cast refers to value more than self (FeatureEnvy) [21]:ActiveRecord::Type::Binary#type_cast_for_database performs a nil-check (NilCheck) [30]:ActiveRecord::Type::Binary::Data has no descriptive comment (IrresponsibleModule) [45]:ActiveRecord::Type::Binary::Data#== is controlled by argument other (ControlParameter)
良い感じに警告が出力されました。ちょっと過敏に出るなと思った方は.reekでオプションの切り替えが出来ます。reekはRubyのコーディングに使われる各エディタのプラグインやバンドルとその利用方法が揃っており、またレポート出力形式をHTML/XML/JSON/YAMLと指定できるためとても使い易いツールになっています。
(Photo by:Kuba Bo?anowski)
要約・まとめ
今回紹介したコードの静的解析ツールは、最近登場してきたツールではなく前々から使われてきているツールです。これらツールはコードマトリクス測定ツールに分類され、ここ数年のDevOpsやCIツールの隆盛に伴い再注目を集めています。
次URLにRubyのコードマトリクスツールが人気順でまとめられていますので、他にも自分たちのプロジェクトに合った良いツールがあるかも知れません。URL内容を見ると1番人気はテストコードのカバレッジを出してくれるSimpleCov。次に広く使われているRubocop、Railsに特化したRails best practicesが並びます。
Rubyのコードマトリクスツール一覧
URL:
https://www.ruby-toolbox.com/categories/code_metrics
マトリクスツールには普通は気が付かないような細かい潜在的なバグを取り除く効果があるため、コードレビューに合わせて利用すると効果的です。CIツールやプロジェクト管理ツールと組み合わせるとさらに効果的です。コードレビューを円滑に進めるためコード静的解析ツール(マトリクスツール)を活用して行きましょう。
本ブログは、Git / Subversionのクラウド型ホスティングサービス「tracpath(トラックパス)」を提供している株式会社オープングルーヴが運営しています。
エンタープライズ向け Git / Subversion 導入や DevOps による開発の効率化を検討している法人様必見!
「tracpath(トラックパス)」は、企業内の情報システム部門や、ソフトウェア開発・アプリケーション開発チームに対して、開発の効率化を支援し、品質向上を実現します。
さらに、システム運用の効率化・自動化支援サービスも提供しています。
”つくる情熱を支えるサービス”を提供し、まるで専属のインフラエンジニアのように、あなたのチームを支えていきます。
No Comments