読者です 読者をやめる 読者になる 読者になる

SideCI Blog

継続的インテグレーションツール(CI)のSideCIが運営しています。コード品質向上や生産性向上など、ソフトウェアエンジニアに役立つCI全般について記事を投稿しています。

RuboCopを調教、規約よりも速度優先なrubocop.ymlを試してみよう

こんにちは。SideCIでも提供しているツール、RuboCopについて、「規約規約うるさい!」という方も多くいらっしゃるかと思います。規約よりも開発速度重視な開発スタイル向けのrubocop.ymlを作成してみたので、宜しければ参考にしてみてください!「バグにつながるような箇所を発見」することに焦点を当てたRuboCopの活用法・設定です。

rubocop.ymlはレポジトリのディレクトリトップに配置頂ければ、SideCiはその設定に従いチェック、コメント致します。SideCIではなくピュアにRuboCopをご利用の方にも参考になれば幸いです。

Table Of Contents

  • RuboCopを効率よく利用してバグを抑止しよう
    • 基本的にLint copsは有効にする
    • 例外的に無効にするLint cops
    • 基本的にStyle copsは無効にする
    • 例外的に有効にするStyle cops
    • その他のcops
    • 以上を踏まえた.rubocop.yml
  • まだまだ違反が多くて大変! さらに効率的に、簡単なチェックだけをしたい場合
  • まとめ

RuboCopを効率よく利用してバグを抑止しよう

「RuboCop」はRubyの静的コード解析ツールです。

RuboCopはコーディングチェックツールとして利用されることが多いと思われるのですが、「バグにつながるような箇所を発見するツール」としても十分使うことが出来ます。

しかし、特にRailsアプリ開発の途中からRuboCopを採用すると、デフォルトの設定では違反(Offence)がたくさん出てきてしまいます。

たくさん違反が出ると、重要な、バグにつながるような違反も埋もれてしまってうまく活用できません。

以下、「バグにつながるような箇所を発見」できるようなcopのみを有効にするように、.rubocop.ymlを組み立てていきましょう。

基本的にLint copsは有効にする

RuboCop内の用語で、それぞれのコードのチェックルールのことを「cop」と呼んでいます。

その中でLint copsは、バグに繋がる可能性のあるコードを検知するcopです。

RuboCopのREADMEによると、Lint copsを無効にするのは「悪いアイデア」と説明されています。

例えば、以下のRubyコードはバグを含んでいますが、プログラマといえど、一瞬ではバグに気づかないかもしれません。

def raises_sometimes(value)
  if value == 1
    raise '1 is bad value!'
  elif value == 2
    raise '2 is bad value, too!'
  end
end

Lint copsは間接的にですが、こういったコードの中の、バグの可能性のある箇所を検知することができます。

-l/--lintオプションでLint copのみをチェックできます(もちろんこのオプションなしでもチェックできますが)。

実際に実行してみます。

rubocop test.rb --lint
Inspecting 1 file
W

Offenses:

test.rb:4:5: W: Lint/UnreachableCode: Unreachable code detected.
    elif value == 2
    ^^^^^^^^^^^^^^^

1 file inspected, 1 offense detected

rubyでは、elifは無いため(elsifが正)、raiseの行以降がUnreachableCode(到達不能)として検知されています。

この例では、Rubyシンタックス的には合っているので、ruby -cなどのチェックでは検出できません。

このような違反を検出できる可能性があるので、基本的にLint copsは有効にすることにします。

例外的に無効にするLint cops

例外的に、Lint copsの中でも、すぐ影響のある不具合発見には寄与しないだろうと考えられるcopを以下に2個挙げます。

Lint/DeprecatedClassMethods

File.exists?など、deprecatedなメソッドを検出します。

deprecatedなメソッドは使わないほうがいいですが、これによる不具合はすぐには起きなそうです。

Lint/StringConversionInInterpolation

式展開の中で余分に#to_sを呼んでいるところを検知します。

これも、余計なメソッドは呼び出しは無い方がいいですが不具合はすぐには起きなそうです。

以上2個のcopについては、例外的に、本エントリの.rubocop.ymlにおいては無効にすることにします。

基本的にStyle copsは無効にする

最初に述べましたが、railsアプリ開発にRuboCopを途中から採用しようとすると、違反がたくさん出てしまってうるさく感じてしまいます。

Styleに関するcopは、バグ発見には必ずしも寄与しないことが多いので、基本的に無効にするのが良いです。

例外的に有効にするStyle cops

例外的に、Style copsの中で、不具合発見に寄与する可能性があると考えられるcopを、以下に3個挙げます。

Style/AndOr

andよりも&&orよりも||を推奨するcopです。and&&は結合の優先度が違います。

これは少し難しいので こちらのわかりやすい解説記事から抜粋して説明します。

if true and false
  puts "1: true"
else
  puts "1: false"
end
#=> 1: false

というのは驚きは少ないのですが、代入(=)と組み合わせると驚くような結果になります。

true_and_false = true and false
if true_and_false
  puts "2: true"
else
  puts "2: false"
end
#=> 2: true

このような驚きやバグを防ぐために&&||を利用するのは有用です。

Style/CaseEquality

===の利用を禁止するcopです。

===はCase式で利用するものなので、書いた人の意図と違う動作をする可能性があり、このcopは有用であると考えます。

Style/GlobalVars

グローバル変数の使用を禁止するcopです。

グローバル変数は、意図と違う動作をする可能性があると考えます。

以上3個のStyle copについては、本エントリの.rubocop.ymlにおいては有効にすることにします。

その他のcops

その他のcopsについては、以下の方針とします。

  • Metrics copsは、クリーンコードに関わるものが多いので、無効にする。
  • Performance copsは、copの数も少なく、効率性の問題を検知する可能性があるから有効にする。
  • Rails copsは、Styleに関するものもあるが、copの数も少なく、時制に関するハマりどころ(Rails/DateRails/TimeZone)を指摘してくれるので、有効にする。

以上を踏まえた.rubocop.yml

以上を踏まえた.rubocop.ymlは以下のようになりました。
Download: rubocop.yml

Style copsのほとんどを無効化しているので、かなりの量の警告を減らすことができるでしょう。 上記リンクからダウンロードして利用する場合はファイル名を.rubocop.ymlにリネームしてご利用下さい。

AllCops:
  Exclude:
    - 'vendor/**/*'
  RunRailsCops: true
  DisplayCopNames: true

Style/AccessModifierIndentation:
  Enabled: false

Style/AccessorMethodName:
  Enabled: false

Style/Alias:
  Enabled: false

Style/AlignArray:
  Enabled: false

Style/AlignHash:
  Enabled: false

Style/AlignParameters:
  Enabled: false

Style/AndOr:
  Enabled: true

Style/ArrayJoin:
  Enabled: false

Style/AsciiComments:
  Enabled: false

Style/AsciiIdentifiers:
  Enabled: false

Style/Attr:
  Enabled: false

Style/BeginBlock:
  Enabled: false

Style/BarePercentLiterals:
  Enabled: false

Style/BlockComments:
  Enabled: false

Style/BlockEndNewline:
  Enabled: false

Style/Blocks:
  Enabled: false

Style/BracesAroundHashParameters:
  Enabled: false

Style/CaseEquality:
  Enabled: true

Style/CaseIndentation:
  Enabled: false

Style/CharacterLiteral:
  Enabled: false

Style/ClassAndModuleCamelCase:
  Enabled: false

Style/ClassAndModuleChildren:
  Enabled: false

Style/ClassCheck:
  Enabled: false

Style/ClassMethods:
  Enabled: false

Style/ClassVars:
  Enabled: false

Style/ColonMethodCall:
  Enabled: false

Style/CommentAnnotation:
  Enabled: false

Style/CommentIndentation:
  Enabled: false

Style/ConstantName:
  Enabled: false

Style/DefWithParentheses:
  Enabled: false

Style/DeprecatedHashMethods:
  Enabled: false

Style/Documentation:
  Enabled: false

Style/DotPosition:
  Enabled: false

Style/DoubleNegation:
  Enabled: false

Style/EachWithObject:
  Enabled: false

Style/ElseAlignment:
  Enabled: false

Style/EmptyElse:
  Enabled: false

Style/EmptyLineBetweenDefs:
  Enabled: false

Style/EmptyLines:
  Enabled: false

Style/EmptyLinesAroundAccessModifier:
  Enabled: false

Style/EmptyLinesAroundBlockBody:
  Enabled: false

Style/EmptyLinesAroundClassBody:
  Enabled: false

Style/EmptyLinesAroundModuleBody:
  Enabled: false

Style/EmptyLinesAroundMethodBody:
  Enabled: false

Style/EmptyLiteral:
  Enabled: false

Style/EndBlock:
  Enabled: false

Style/EndOfLine:
  Enabled: false

Style/EvenOdd:
  Enabled: false

Style/FileName:
  Enabled: false

Style/FirstParameterIndentation:
  Enabled: false

Style/FlipFlop:
  Enabled: false

Style/For:
  Enabled: false

Style/FormatString:
  Enabled: false

Style/GlobalVars:
  Enabled: true

Style/GuardClause:
  Enabled: false

Style/HashSyntax:
  Enabled: false

Style/IfUnlessModifier:
  Enabled: false

Style/IfWithSemicolon:
  Enabled: false

Style/IndentationConsistency:
  Enabled: false

Style/IndentationWidth:
  Enabled: false

Style/IndentArray:
  Enabled: false

Style/IndentHash:
  Enabled: false

Style/InfiniteLoop:
  Enabled: false

Style/Lambda:
  Enabled: false

Style/LambdaCall:
  Enabled: false

Style/LeadingCommentSpace:
  Enabled: false

Style/LineEndConcatenation:
  Enabled: false

Style/MethodCallParentheses:
  Enabled: false

Style/MethodDefParentheses:
  Enabled: false

Style/MethodName:
  Enabled: false

Style/ModuleFunction:
  Enabled: false

Style/MultilineBlockChain:
  Enabled: false

Style/MultilineBlockLayout:
  Enabled: false

Style/MultilineIfThen:
  Enabled: false

Style/MultilineOperationIndentation:
  Enabled: false

Style/MultilineTernaryOperator:
  Enabled: false

Style/NegatedIf:
  Enabled: false

Style/NegatedWhile:
  Enabled: false

Style/NestedTernaryOperator:
  Enabled: false

Style/Next:
  Enabled: false

Style/NilComparison:
  Enabled: false

Style/NonNilCheck:
  Enabled: false

Style/Not:
  Enabled: false

Style/NumericLiterals:
  Enabled: false

Style/OneLineConditional:
  Enabled: false

Style/OpMethod:
  Enabled: false

Style/ParenthesesAroundCondition:
  Enabled: false

Style/PercentLiteralDelimiters:
  Enabled: false

Style/PercentQLiterals:
  Enabled: false

Style/PerlBackrefs:
  Enabled: false

Style/PredicateName:
  Enabled: false

Style/Proc:
  Enabled: false

Style/RaiseArgs:
  Enabled: false

Style/RedundantBegin:
  Enabled: false

Style/RedundantException:
  Enabled: false

Style/RedundantReturn:
  Enabled: false

Style/RedundantSelf:
  Enabled: false

Style/RegexpLiteral:
  Enabled: false

Style/RescueModifier:
  Enabled: false

Style/SelfAssignment:
  Enabled: false

Style/Semicolon:
  Enabled: false

Style/SignalException:
  Enabled: false

Style/SingleLineBlockParams:
  Enabled: false

Style/SingleLineMethods:
  Enabled: false

Style/SingleSpaceBeforeFirstArg:
  Enabled: false

Style/SpaceAfterColon:
  Enabled: false

Style/SpaceAfterComma:
  Enabled: false

Style/SpaceAfterControlKeyword:
  Enabled: false

Style/SpaceAfterMethodName:
  Enabled: false

Style/SpaceAfterNot:
  Enabled: false

Style/SpaceAfterSemicolon:
  Enabled: false

Style/SpaceBeforeBlockBraces:
  Enabled: false

Style/SpaceBeforeComma:
  Enabled: false

Style/SpaceBeforeComment:
  Enabled: false

Style/SpaceBeforeSemicolon:
  Enabled: false

Style/SpaceInsideBlockBraces:
  Enabled: false

Style/SpaceAroundBlockParameters:
  Enabled: false

Style/SpaceAroundEqualsInParameterDefault:
  Enabled: false

Style/SpaceAroundOperators:
  Enabled: false

Style/SpaceBeforeModifierKeyword:
  Enabled: false

Style/SpaceInsideBrackets:
  Enabled: false

Style/SpaceInsideHashLiteralBraces:
  Enabled: false

Style/SpaceInsideParens:
  Enabled: false

Style/SpaceInsideRangeLiteral:
  Enabled: false

Style/SpecialGlobalVars:
  Enabled: false

Style/StringLiterals:
  Enabled: false

Style/StringLiteralsInInterpolation:
  Enabled: false

Style/StructInheritance:
  Enabled: false

Style/SymbolProc:
  Enabled: false

Style/Tab:
  Enabled: false

Style/TrailingBlankLines:
  Enabled: false

Style/TrailingComma:
  Enabled: false

Style/TrailingWhitespace:
  Enabled: false

Style/TrivialAccessors:
  Enabled: false

Style/UnlessElse:
  Enabled: false

Style/UnneededCapitalW:
  Enabled: false

Style/UnneededPercentQ:
  Enabled: false

Style/UnneededPercentX:
  Enabled: false

Style/VariableInterpolation:
  Enabled: false

Style/VariableName:
  Enabled: false

Style/WhenThen:
  Enabled: false

Style/WhileUntilDo:
  Enabled: false

Style/WhileUntilModifier:
  Enabled: false

Style/WordArray:
  Enabled: false

Lint/DeprecatedClassMethods:
  Enabled: false

Lint/StringConversionInInterpolation:
  Enabled: false

Metrics/AbcSize:
  Enabled: false

Metrics/BlockNesting:
  Enabled: false

Metrics/ClassLength:
  Enabled: false

Metrics/CyclomaticComplexity:
  Enabled: false

Metrics/LineLength:
  Enabled: false

Metrics/MethodLength:
  Enabled: false

Metrics/ParameterLists:
  Enabled: false

Metrics/PerceivedComplexity:
  Enabled: false

まだまだ違反が多くて大変! さらに効率的に、簡単なチェックだけをしたい場合

上記の.rubocop.ymlのルールを元に、github上で公開されている、任意で選んだ8個のRailsプロジェクト(スターの数が300以上、直近のコミットがある)に対しrubocopをかけたところ、違反の数はデフォルトの場合と比較すると10%~50%ほどに減少しました。

しかし、規模が大きいRailsアプリだと、依然として違反の数が1000件以上出力されてしまい、まだまだチェックするのが大変な状況 です。

そこで、最初はチェックが簡単になるように、Lint copsの中でも簡単でバグ検出力の高いと思われるcopを以下に4つ厳選いたしました。

Lint/Debugger

デバッグ用のメソッド(p/pp/puts/printなど)が残っていないか検出します。

Lint/DuplicateMethods

同クラス内で重複してメソッド定義している箇所を検出します。

Lint/UnreachableCode

到達不能箇所を検出します。

Lint/Void

演算子や変数、リテラルがあるのみで何もしていない箇所を検出します。

上記4個のcopをのみを実行するには、特に.rubocop.ymlを用意せず、以下のコマンドを実行します。

$ rubocop --only Lint/Debugger,Lint/DuplicateMethods,Lint/UnreachableCode,Lint/Void

8個のRailsプロジェクトそれぞれに対してこのコマンドを実行し、違反箇所を筆者が確認したところ、以下の様な結果になりました。

検出した違反数 うち、修正するのが妥当と考えられる箇所
fastladder 0 0
gitlabhq 68 0
rubygems.org 3 0
spree 3 2
whitehall 4 4
discourse 156 1
diaspora 5 4
kandan 0 0

修正しなくても問題ない箇所も多くありましたが、 8個のRailsプロジェクトに対して4個のプロジェクトで、修正するのが妥当だと判断できる箇所を検出 することが出来ました。

開発中のRailsアプリの規模が大きく、検出された違反をチェックするのが大変な場合でも、とりあえずこの4つだけでもチェックすることが、バグ発見につながるかもしれません。

まとめ

いかがでしたでしょうか。

本エントリーでは、Lint copが有用になるケースとその重要性、.rubocop.ymlの例、中でも重要なLint copの例を見てきました。

RuboCopを実際使ってみると、たくさんのcopに対しそれぞれ有効/無効を判断していかねばなりません。

まだRuboCopを活用していない人向けに試しにrubocopを使ってみたい、という方には判断するだけでも大変な作業だと思いますので、ぜひ、今回ご紹介した.rubocop.ymlや、最後にご紹介した4個のcopのみを実行する方法などをお試しいただければと思います。