CodeIQ MAGAZINECodeIQ MAGAZINE

CodeIQ MAGAZINECodeIQ MAGAZINE

静的解析ツールRuboCopを利用して紳士・淑女Rubyistになろう #rubocop #Ruby

2014.06.12 Category:コード解説 ,リーダーボード Tag: ,

  • このエントリーをはてなブックマークに追加
akuto

「Ruby警官から警告を受けろ」問題の結果発表です。

最大警告数を叩きだしたRuboCop獄中王の中の王はどなたでしょうか?
by tbpgr

はじめに

出題者のtbpgrです。当記事では以下の内容を取り扱います。

  1. RuboCopに関して
  2. RuboCopのメリット
  3. RuboCopのTips
  4. 「Ruby警官から警告を受けろ Lv1/Lv2」問題のランキング発表
  5. 「Ruby警官から警告を受けろ Lv1/Lv2」問題の出題者解答例
  6. 「Ruby警官から警告を受けろ Lv1/Lv2」問題の挑戦者解答例

※Rubyの利用経験者を前提として話を進めます。

1.RuboCopに関して

RuboCopとは

RuboCopはRubyの静的コード解析ツールです。
Ruby Style Guideのガイドラインに沿って コーディングスタイルをチェック します。

Ruby Style Guide

Rubyプログラマが保守しやすい、ベストプラクティスとも言えるコードを書くための規約です。
Rubyコミュニティの意見や、Rubyの有名書籍など広く受け入れられている意見を取り入れた内容になっています。

詳しくは以下を参照ください。
Ruby Style Guide 原文
Ruby Style Guide 和訳

RuboCopのインストール

Rubyのgemとして提供されています。

gemやbundlerでインストールできます。

gem install rubocop

RuboCopの実行

ターミナルでrubocopコマンドを実行してください。
オプション指定無しで実行すると、カレントディレクトリ配下の全てのrbファイルを対象とします。

$ rubocop

特定のディレクトリや、ファイルを指定しての実行も可能です。

$ rubocop lib/some_directory
$ rubocop lib/sample.rb

各種オプションについては

$ rubocop -h

で参照してください。

RuboCopの規約設定

.rubocop.yml ファイルに規約内容を記載することで RuboCop がデフォルトで採用している規約内容を変更することができます。
開発チームのコーディングルールに合わせて規約を変更することができます。

2.RuboCopのメリット

全般

以降にあげる全ての項目に言えるのですが、 自動的にチェックできる ということが 大きなメリット です。
RuboCopで確認するようなルール・規約・好ましくない記法などを目でチェックする場合に比べて、 コストをかけず正確 に問題を抽出してくれます。

ソースコードの見栄えを整える

一定のスタイルで記述されたソースコードはバラバラのスタイルで 書かれたものよりも 早く理解する ことができます。
プロジェクトに関わる人数が多いほどこの恩恵を得ることができます。

ソースコード中のゴミ変数などを検出してくれる

未使用の変数などは、こういったツールを導入しないとあちこちに残りがちです。
自分が作成したプログラムではない場合に、そういった未使用の変数が本当に使用されていないのかどうか
確認するのは手間のかかるものです。

巨大なクラス、メソッドを検出してくれる

基本的にプログラムは 小さいほうが簡単で、大きいほうが難しい です。
大抵の場合、過剰に大きな処理は複数の責務が混在したものになっています。
リファクタリングすれば単一責務の小さなクラス、メソッドに分割できるケースが多いです。

RuboCopはデフォルトで10行より長いメソッドを書くと警告が出ます。
他の言語(特に静的言語)出身の方から見ると厳しすぎるように
見えるかもしれませんが「The Ruby Style Guide」にも
10行を超えるメソッドはやめる、また理想は5行以内
と記述されています。

著名な開発者を多く擁することでも有名な、ThoughtWorks社でも Sandi Metzルール というRubyのコーディングルールが公開されており、1メソッドは5行以内と記されています。

Rubyが言語として持っている記述力で適度にリファクタリングを行っていれば10行以内に収まることが多いです。

普段長いコードを書く方からすると 5行 というのは衝撃を受けるかもしれません。
if/else のコードを書くだけで 5行 になってしまいます。

def hoge(msg)
  if msg.nil?     # 1
    'msg is nil'  # 2
  else            # 3
    msg           # 4
  end             # 5
end

Rubyの学習効果

チームにRubyの作法に詳しくないエンジニアがいた場合、
Rubyの作法を覚える良い機会になります。

3.RuboCopのTips

警告をJSONファイルに出力する

rubocopコマンドの出力フォーマットを変更して JSON 形式で出力できます。

rubocop -fj > rubocop_warnings.json

警告をTODOとしてYAMLファイルに出力する

既存プロジェクトに対してRuboCopの利用を開始する際などに
現在発生している警告をYAMLファイルに出力してTODO化することができます。

rubocop --auto-gen-config

上記コマンドにより、現状発生している警告が rubocop-todo.yml ファイルに出力されます。
例えば未使用のメソッド引数が4つあった場合は以下の様な内容になります。

# This configuration was generated by `rubocop --auto-gen-config`
# on 2014-05-25 22:39:40 +0900 using RuboCop version 0.21.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.

# Offense count: 4
UnusedMethodArgument:
  Enabled: false

# Offense count: 1
# Cop supports --auto-correct.
IndentationConsistency:
  Enabled: false

このファイルを .rubocop.yml ファイルで inherit することで警告対象外になります。
.rubocop.yml は以下のように記述します。

inherit_from: rubocop-todo.yml

上記の設定後に、rubocop を実行すると警告が出なくなります。
そして、UnusedMethodArgument が出ないようにプログラムを修正したら
警告無効の設定を削除して再度 rubocop で確認します。

該当する警告が表示されなくなったら
TODOをひとつ消化したことになります。

次に IndentationConsistency の警告が出ないように…というように
TODOを消化していきます。

ちなみに IndentationConsistency の警告に以下のコメントがあります。

# Cop supports --auto-correct.

これは、次に説明する警告の自動修正が可能である、ということです。

一部警告の自動修正を実行する

auto-correct オプションを利用することで、RuboCopが検出する警告のうち自動修正可能なものを修正してくれます。
スペーシングやシングルクォートとダブルクォートの使い分けなどはこのコマンドで自動修正してくれます。

rubocop -a
# もしくは
rubocop --auto-correct

例:修正前のコード

  • 変数展開を利用していないがダブルクォートで囲まれている文字列がある
  • 12345の数値リテラルが記述されている行のインデントがずれている
  • 12345は4桁以上の数値リテラルだが「_」で区切られていない
# Sample
class Sample
  def hoge
    "hoge"
               12345
  end
end

例:rubocop -aで修正後のコード

  • 文字列hogeを囲んでいたダブルクォートがシングルクォートに修正された
  • 12345の数値リテラルが記載されている行のインデントが修正された
  • 12345の数値リテラルが 12_345 に修正された
# Sample
class Sample
  def hoge
    'hoge'
    12_345
  end
end

4.「Ruby警官から警告を受けろ Lv1/Lv2」問題のランキング発表

獄中王バッジ取得者を対象に、警告種類のランキングを発表いたします。

Lv1

Rank 警告数 名前
1 67 ciel 様
2 48 tomwot 様
3 34 fortissimo1997 様
4 33 zono244 様
5 32 k.hamada 様
6 28 NeoCat 様
7 27 みけCAT 様
8 26 StupidDog 様
9 24 ryosy383 様
9 24 permil 様

Lv2

Rank 警告数 名前
1 62 ciel 様
2 40 tomwot 様
3 31 fortissimo1997 様
4 28 zono244 様
5 27 k.hamada 様
6 26 みけCAT 様
7 23 NeoCat 様
8 19 permil 様
8 19 StupidDog 様
9 17 ryosy383 様

Lv1, Lv2 ともに ciel様 の圧勝でした!!
また、2位の tomwot様 も他の解答者と比べるとかなり飛び抜けていました。

5.「Ruby警官から警告を受けろ Lv1/Lv2」問題の出題者解答例

Lv1

解答例(警告24種)

class Answer 
def aA( a)


c=12345
["a","b"]
[].map{|v|v}
 "h"#TODO:
;
{:a=>1}
%w{}
a(1,
2)

end
end

発生する警告

1, %w-literals should be delimited by ( and )
2, Align the parameters of a method call if they span more than one line.
3, Annotation keywords should be all upper case, followed by a colon and a space, then a note describing the problem.
4, Do not use semicolons to terminate expressions.
5, Extra blank line detected.
6, Extra empty line detected at body beginning.
7, Extra empty line detected at body end.
8, Inconsistent indentation detected.
9, Missing space after #.
10, Missing top-level class documentation comment.
11, Prefer single-quoted strings when you don't need string interpolation or special symbols.
12, Separate every 3 digits in the integer portion of a number with underscores(_).
13, Space between { and | missing.
14, Space inside parentheses detected.
15, Space inside { missing.
16, Space inside } missing.
17, Space missing after comma.
18, Space missing inside }.
19, Space missing to the left of {.
20, Trailing whitespace detected.
21, Use %w or %W for array of words.
22, Use 2 (not 0) spaces for indentation.
23, Use snake_case for methods.
24, Use the new Ruby 1.9 hash syntax.

Lv2

解答例(警告15種)

class Answer 
def aA()
1234
["a","b"]
[].detect{|v|v}
 "h"#TODO:
{:a=>1}
;
%w{}
a(1,
2)
1
.a
end
end

発生する警告

1, %w-literals should be delimited by ( and )
2, Align the parameters of a method call if they span more than one line.
3, Annotation keywords should be all upper case, followed by a colon and a space, then a note describing the problem.
4, Do not use semicolons to terminate expressions.
5, Inconsistent indentation detected.
6, Method has too many lines. [11/10]
7, Missing top-level class documentation comment.
8, Omit the parentheses in defs when the method doesn't accept any arguments.
9, Prefer find over detect.
10, Prefer single-quoted strings when you don't need string interpolation or special symbols.
11, Space between { and | missing.
12, Trailing whitespace detected.
13, Use %w or %W for array of words.
14, Use snake_case for methods.
15, Use the new Ruby 1.9 hash syntax.

6.「Ruby警官から警告を受けろ Lv1/Lv2」問題の挑戦者解答例

Lv1, Lv2ともに1位の ciel様 、2位の tomwot様 が他の大きく引き離す警告数だったため両名の解答をご紹介致します。

Lv1

  • ciel様[特殊解答] (警告数 67)

出題側の想定漏れにより、「Replace interpolated variable」の警告を多数作成出来る事を利用した解答です。
(他にも未使用変数の警告など、簡単に同種の警告を増やせるケースが複数ありましたが事前に警告抑止の設定をしていました。)

解答

Answer="";->{#あ 
A{{A:?A%$",}}if(!!あ="#$:#$0#$!#$@#$;#$,#$/#$.#$_#$>#$<#$$#$?#$~#$=#$*#$A#$1&quot;.())
    }

発生する警告

1, Assignment in condition - you probably meant to use ==.
2, Avoid comma after the last item of a hash.
3, Avoid the use of Perl-style backrefs.
4, Avoid the use of double negation (!!).
5, Avoid using {...} for multi-line blocks.
6, Do not introduce global variables.
7, Do not use parentheses for method calls with no arguments.
8, Do not use semicolons to terminate expressions.
9, Do not use the character literal - use string literal instead.
10, Favor format over String#%.
11, Favor unless over if for negative conditions.
12, Line is too long. [80/79]
13, Missing space after #.
14, Prefer $ARGV from the English library, or ARGV over $*.
15, Prefer $CHILD_STATUS from the English library over $?.
16, Prefer $DEFAULT_INPUT from the English library over $.
18, Prefer $ERROR_INFO from the English library over $!.
19, Prefer $ERROR_POSITION from the English library over $@.
20, Prefer $FIELD_SEPARATOR or $FS from the English library over $;.
21, Prefer $IGNORECASE from the English library over $=.
22, Prefer $INPUT_LINE_NUMBER or $NR from the English library over $..
23, Prefer $INPUT_RECORD_SEPARATOR or $RS from the English library over $/.
24, Prefer $LAST_MATCH_INFO from the English library over $~.
25, Prefer $LAST_READ_LINE from the English library over $_.
26, Prefer $LOADED_FEATURES over $".
27, Prefer $OUTPUT_FIELD_SEPARATOR or $OFS from the English library over $,.
28, Prefer $PROCESS_ID or $PID from the English library over $$.
29, Prefer $PROGRAM_NAME over $0.
30, Prefer single-quoted strings when you don't need string interpolation or special symbols.
31, Prefer the use of lambda.call(...) over lambda.(...).
32, Put a space before the modifier keyword.
33, Replace interpolated variable $! with expression #{$!}.
34, Replace interpolated variable $$ with expression #{$$}.
35, Replace interpolated variable $* with expression #{$*}.
36, Replace interpolated variable $, with expression #{$,}.
37, Replace interpolated variable $. with expression #{$.}.
38, Replace interpolated variable $/ with expression #{$/}.
39, Replace interpolated variable $0 with expression #{$0}.
40, Replace interpolated variable $1 with expression #{$1}.
41, Replace interpolated variable $; with expression #{$;}.
42, Replace interpolated variable $< with expression #{$ with expression #{$>}.
45, Replace interpolated variable $? with expression #{$?}.
46, Replace interpolated variable $@ with expression #{$@}.
47, Replace interpolated variable $A with expression #{$A}.
48, Replace interpolated variable $_ with expression #{$_}.
49, Replace interpolated variable $~ with expression #{$~}.
50, Space inside { missing.
51, Space inside } missing.
52, Space missing after colon.
53, Space missing after comma.
54, Space missing after semicolon.
55, Space missing inside {.
56, Space missing inside }.
57, Space missing to the left of {.
58, Tab detected.
59, Trailing whitespace detected.
60, Use 2 (not -1) spaces for indentation.
61, Use SCREAMING_SNAKE_CASE for constants.
62, Use only ascii symbols in comments.
63, Use only ascii symbols in identifiers.
64, Use snake_case for variables.
65, Use space after control keywords.
66, Use the lambda method for multi-line lambdas.
67, end at 3, 1 is not aligned with - > {#あ  at 1, 10 or Answer="";- > {#あ  at 1, 0

ciel様コメント

・今回文字数判定がバイト単位ではないので、日本語の識別子・コメントを使って警告数を稼いでいます。
・VariableInterpolationで増殖バグが見つかりましたが、1個につき3文字使うので、警告数が40個を越えると実用的でなくなります。今回は使わない縛りとしました。
・Answerはclassである必要はないので、$1を代入してごまかします。「Use SCREAMING_SNAKE_CASE for constants」の警告も戴けて一石二鳥。
・Lv1に限り、2行目のsplatをブロック呼び出しに変更することで2警告稼げます。
・#$:のようなinterpolationを使うことで「3文字につき2つ」警告を稼げてしまうことがわかりました。
・Lv1については、%W!!を$""と改行とタブに置換してもう1個稼げました。
・Lv2は{a:X,}をタブと[X,]に置換してもう1個稼げました。
  • ciel様[正統派解答] (警告数 56)
    「Replace interpolated variable」の警告を利用しない解答も補足コメント欄でいただきました。
    特殊解答を利用しなかった場合でも ciel様 の警告数がトップでした。

解答

Answer=$1;->{def A _#あ 
END{A{[{:A=>%W!!*"",A:$A,}, ]}if(!!あ=_===_)
    not"#$_#{}#{?A}"%$$or$!.()}end}
  • tomwot様(警告数 48)

解答

class Answer 

def a b=c#1
 if
(not@@d=""%e);f.
g( ){||}while(!! h({:i=>[$j, ]*?k,},))end;end

    end;

Lv2

  • ciel様[特殊解答] (警告数 62)

Lv1同様、出題側の想定漏れにより「Replace interpolated variable」の警告を多数作成出来る事を利用した解答です。

解答

    Answer="";->{#あ 
[?A%%W!!, ]if(!!あ="#$:#$0#$!#$@#$;#$,#$/#$.#$_#$>#$<#$$#$?#$~#$=#$*#$A#$1#$&quot;&quot;.())}

発生する警告

1, %W-literals should be delimited by ( and )
2, Assignment in condition - you probably meant to use ==.
3, Avoid comma after the last item of an array.
4, Avoid the use of Perl-style backrefs.
5, Avoid the use of double negation (!!).
6, Avoid using {...} for multi-line blocks.
7, Do not introduce global variables.
8, Do not use %W unless interpolation is needed.  If not, use %w.
9, Do not use parentheses for method calls with no arguments.
10, Do not use semicolons to terminate expressions.
11, Do not use the character literal - use string literal instead.
12, Favor format over String#%.
13, Favor unless over if for negative conditions.
14, Line is too long. [82/79]
15, Prefer $ARGV from the English library, or ARGV over $*.
16, Prefer $CHILD_STATUS from the English library over $?.
17, Prefer $DEFAULT_INPUT from the English library over $.
19, Prefer $ERROR_INFO from the English library over $!.
20, Prefer $ERROR_POSITION from the English library over $@.
21, Prefer $FIELD_SEPARATOR or $FS from the English library over $;.
22, Prefer $IGNORECASE from the English library over $=.
23, Prefer $INPUT_LINE_NUMBER or $NR from the English library over $..
24, Prefer $INPUT_RECORD_SEPARATOR or $RS from the English library over $/.
25, Prefer $LAST_MATCH_INFO from the English library over $~.
26, Prefer $LAST_READ_LINE from the English library over $_.
27, Prefer $LOADED_FEATURES over $".
28, Prefer $OUTPUT_FIELD_SEPARATOR or $OFS from the English library over $,.
29, Prefer $PROCESS_ID or $PID from the English library over $$.
30, Prefer $PROGRAM_NAME over $0.
31, Prefer single-quoted strings when you don't need string interpolation or special symbols.
32, Prefer the use of lambda.call(...) over lambda.(...).
33, Put a space before the modifier keyword.
34, Replace interpolated variable $! with expression #{$!}.
35, Replace interpolated variable $" with expression #{$"}.
36, Replace interpolated variable $$ with expression #{$$}.
37, Replace interpolated variable $* with expression #{$*}.
38, Replace interpolated variable $, with expression #{$,}.
39, Replace interpolated variable $. with expression #{$.}.
40, Replace interpolated variable $/ with expression #{$/}.
41, Replace interpolated variable $0 with expression #{$0}.
42, Replace interpolated variable $1 with expression #{$1}.
43, Replace interpolated variable $; with expression #{$;}.
44, Replace interpolated variable $< with expression #{$ with expression #{$>}.
47, Replace interpolated variable $? with expression #{$?}.
48, Replace interpolated variable $@ with expression #{$@}.
49, Replace interpolated variable $A with expression #{$A}.
50, Replace interpolated variable $_ with expression #{$_}.
51, Replace interpolated variable $~ with expression #{$~}.
52, Space inside square brackets detected.
53, Space missing after semicolon.
54, Tab detected.
55, Trailing whitespace detected.
56, Use SCREAMING_SNAKE_CASE for constants.
57, Use only ascii symbols in comments.
58, Use only ascii symbols in identifiers.
59, Use snake_case for variables.
60, Use space after control keywords.
61, Use the lambda method for multi-line lambdas.
62, end at 2, 81 is not aligned with ->{#あ  at 1, 11 or Answer="";->{# あ  at 1, 1
  • ciel様[正統派解答] (警告数 48)

「Replace interpolated variable」の警告を利用しない解答も補足コメント欄でいただきました。
特殊解答を利用しなかった場合でも ciel様 の警告数がトップでした。

解答

Answer=$1;->{def A _#あ 
END{A{[{:A=>%W!!*"",A:$A,}, ]}if(!!あ=_===_)
    not"#$_#{}#{?A}"%$$or$!.()}end}
  • tomwot様 (警告数 40)

解答

class Answer 
def a b=c
if
(not@@d=""%e);f.
g(){||}while(!! h({ :i=>["#$j", ]*?k, },))end;end
    end;

出題の狙い

今回の問題は、ゲーム感覚でRubyの標準的な規約やイディオムやを覚えることができるのでは、ということろが狙いでした。
ただ単に規約を暗記するよりも試行錯誤をしながらソースコードとRuby Style Guideのドキュメントを何度も往復することによって、
強く記憶に残って良いのではないか、と考えたのです。
結果としては、挑戦環境構築の面倒さもあってか、あまり挑戦数が伸びず。

ただ、 別の成果 があったので結果オーライといったところです。
別の成果については、後述の Special Thanks1-3 を御覧ください。

まとめ

Rubyは柔軟に記述できる反面、その自由度ゆえに同じ内容を記述する方法が数多くあります。
RuboCopを利用することでチームのコーディングスタイルをRubyの標準的な記法に統一することができます。
Rubyの標準的な記法に詳しくない初心者や独学のRubyistが標準的な記法を覚える助力にもなります。

統一されたスタイルのソースコードは、その作法になれた開発者にとって読みやすいコードとなり、コードリーディングに費やす時間を減らすことができます。
利用しているスタイルが広く使われているものであればあるほど、多くの人にとって読みやすいコードとなります。

Special Thanks1

出題のために和訳版の Ruby Style Guide を確認していたところ、
いくつか手を入れる箇所があったのでプルリクエストを送らせていただきました。
マージありがとうございます

Special Thanks2

この問題の挑戦支援ツールとして Vagrant を利用した環境構築ツールを提供させていただきましたが、VagrantのWindows向け警告メッセージに好ましくない内容があることを みけCAT様 が発見。
ciel様 がプルリクエストを送って、Vagrantの開発者である Mitchell Hashimoto氏 によってマージされました。

https://github.com/mitchellh/vagrant/pull/3880

みけCAT様ciel様、 OSSへの貢献ありがとうございます!

Special Thanks3

今回の問題の鬼門となった(?) Vagrant + virtualbox を利用した挑戦支援環境の構築ですが、他の挑戦者のためにブログのエントリを作成してくださった方が複数名いらっしゃいました。
Tails様電脳妖精様 誠にありがとうございます。

内容が気になる方は、出題者のブログの下記記事内の「関連リンク」の項目にリンク先が列挙されています。

『Ruby警官から警告を受けろ』問題 Lv1 出題記事

採用すべきエンジニア

周囲のために 想像力を働かせ積極的に行動し実行に移す
上記の方々のような姿勢を持つエンジニアは
きっと企業にとって 心強い戦力 となる人材だと思います。

  • アプリケーションが使いにくいなら 改良してプルリクエストを送る
  • 自分の経験を ブログの記事で共有する

というような行動ができるエンジニアは 仕事ができないわけがない と思います。

参考資料

RuboCop RubyGems

RuboCop GitHub

Ruby Style Guide GitHub 原文

Ruby Style Guide GitHub 和訳

Sandi Metzルール

Vagrant GitHub

CodeIQ運営事務局より

tbpgrさんは他にもいろいろな問題を出題されています。
ぜひその他の問題にもご挑戦ください!!
バッジが付与される問題!
デスマコロシアム問題
tbpgrさん問題

  • このエントリーをはてなブックマークに追加

■この記事を書いた人

avatar

tbpgr

CodeIQでRubyや様々なカテゴリの問題を出題中。 通常問題 バッジ問題 デスマコロシアム問題 Twitter:@tbpgr GitHub:https://github.com/tbpgr ブログ:http://d.hatena.ne.jp/tbpg/

■関連記事

新着記事

週間ランキング

CodeIQとは

CodeIQ(コードアイキュー)とは、自分の実力を知りたいITエンジニア向けの、実務スキル評価サービスです。

CodeIQご利用にあたって
関連サイト
codeiq

リクルートグループサイトへ