09 · Code Quality & RuboCop¶
RuboCop is a static analyzer and style enforcer for Ruby — it reads your code without running it and flags style violations, common bug patterns, and complexity smells. Most Ruby teams run it in CI (as shown in the Testing at Scale module) so style debt is caught automatically rather than relying on every reviewer to notice a missing space.
A messy file, analyzed¶
$ rubocop messy.rb
Inspecting 1 file
C
Offenses:
messy.rb:1:1: C: Style/Documentation: Missing top-level documentation comment for class Foo.
class Foo
^^^^^^^^^
messy.rb:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment.
class Foo
^
messy.rb:2:11: C: [Correctable] Layout/SpaceInsideParens: Space inside parentheses detected.
def bar( x,y )
^
messy.rb:2:12: C: Naming/MethodParameterName: Method parameter must be at least 3 characters long.
def bar( x,y )
^
messy.rb:3:5: C: [Correctable] Style/IfUnlessModifier: Favor modifier if usage when having a single-line body.
if x == nil
^^
messy.rb:3:10: C: [Correctable] Style/NilComparison: Prefer the use of the nil? predicate.
if x == nil
^^
messy.rb:6:6: C: [Correctable] Layout/SpaceAroundOperators: Surrounding space missing for operator =.
z=x+y
^
messy.rb:7:5: C: [Correctable] Style/RedundantReturn: Redundant return detected.
return z
^^^^^^
1 file inspected, 13 offenses detected, 10 offenses autocorrectable
Every line names a cop — a single, focused rule (Style/NilComparison,
Layout/SpaceAroundOperators) — and the severity (C for Convention
here; RuboCop also has Warning, Error, and Fatal levels for more
serious issues).
Auto-correction¶
Many offenses are mechanical enough to fix automatically:
Result:
-A (autocorrect, including "unsafe" corrections that could
theoretically change behavior in edge cases — -a is the more
conservative "safe corrections only" flag) rewrote: spacing, x == nil
→ x.nil?, the guard clause into modifier-if form, z = x + y → the
value returned directly (since return at the end of a method is
redundant — the last expression's value is returned automatically), and
added the frozen_string_literal magic comment. It did not rename
x/y to longer names or add class documentation — those require a
human decision RuboCop won't make unilaterally.
.rubocop.yml — configuring which rules apply¶
Not every project agrees with every default rule. A .rubocop.yml
customizes behavior:
# .rubocop.yml
AllCops:
NewCops: enable
TargetRubyVersion: 3.3
Style/Documentation:
Enabled: false # don't require a comment above every class
Layout/LineLength:
Max: 100
Metrics/MethodLength:
Max: 15
Exclude:
- 'spec/**/*'
Enabled: false turns a cop off project-wide when the team has
deliberately decided against it (many teams disable
Style/Documentation — requiring a comment on every class is often more
noise than signal). Exclude scopes a rule away from specific paths —
long RSpec describe blocks routinely exceed a method-length limit
meant for application code, and it's normal to exempt specs from it.
Complexity metrics — beyond style¶
RuboCop also flags structural complexity, not just formatting:
Metrics/MethodLength: too many lines in one method — often a sign it's doing more than one job and should be split.Metrics/AbcSize: Assignment/Branch/Condition complexity — a method with many branches and assignments even if each line is short.Metrics/CyclomaticComplexity: counts independent paths through a method (if/case/&&/||each add one) — a high count means many test cases are needed to cover every path, and the method is a good candidate for extraction into smaller pieces.
These are heuristics, not absolute truths — a long method that's a
straightforward, linear sequence of steps might be perfectly readable
despite tripping MethodLength. Use the flags as a prompt to look
closer, not an automatic verdict.
RuboCop-specific traps¶
- Running
-A(unsafe autocorrect) without reviewing the diff. Unsafe corrections are labeled that way because they can change behavior in edge cases (e.g. some string/frozen-literal corrections interact with mutation) — always diff and review before committing an autocorrect run, especially with-Arather than the safer-a. Reading the diff betweenmessy.rband the corrected version above is exactly this review step. - A team ignoring RuboCop's suggestions project-wide via a giant
.rubocop_todo.yml(generated byrubocop --auto-gen-configto grandfather in existing violations) and then never revisiting it — the todo file is meant as a temporary adoption aid, not a permanent exemption list. - Disabling a cop inline with
# rubocop:disableto silence a false positive without a comment explaining why — six months later, nobody remembers whether the disable is still justified or was a lazy fix for something that should have been refactored instead. - Chasing 100% cop compliance over actual code quality. A method
that's genuinely clearer as one longer block than three fragmented
three-line methods shouldn't be split purely to satisfy
Metrics/MethodLength— use judgment, not blind compliance. - Not pinning the RuboCop version in CI. RuboCop regularly adds new
cops between releases; an unpinned
gem "rubocop"in aGemfilecan suddenly fail CI on an unrelated PR when a new default cop starts flagging existing code that was previously fine.
How It Actually Works¶
RuboCop doesn't pattern-match your source text — it uses Ruby's own
Ripper (or the parser gem) to build a full Abstract Syntax Tree from
your file, the same kind of structural representation MRI's own compiler
builds internally before generating YARV bytecode. Each cop is a small
visitor that walks specific node types in that tree (a send node for
method calls, a def node for method definitions) and checks structural
properties — which is why RuboCop can reliably flag "method too long" by
counting AST lines between a def and end node, but can be fooled by
things that look identical in the AST but differ only in your intent.
Auto-correction works by recording the exact source-range offsets of each
flagged node and generating textual replacements at those offsets, then
rewriting the file — it's mechanical text surgery guided by the AST's
position data, not a re-interpretation of your logic, which is exactly why
-A autofixes are safe for style violations (whitespace, naming) but
riskier for anything that could change runtime behavior.
Cheat sheet¶
| Task | Command |
|---|---|
| Check a file | rubocop path/to/file.rb |
| Check the whole project | rubocop |
| Safe auto-fix | rubocop -a |
| Full auto-fix (review the diff!) | rubocop -A |
| Generate a todo file for existing violations | rubocop --auto-gen-config |
| Disable a cop for one line | # rubocop:disable Cop/Name ... # rubocop:enable Cop/Name |
| Disable a cop project-wide | Cop/Name: { Enabled: false } in .rubocop.yml |
| List all available cops | rubocop --show-cops |
Exercise¶
- Write a deliberately messy 15-line class (inconsistent spacing,
== nil, a redundantreturn, an overly long single method) and runrubocopagainst it — read every offense before running any auto-fix. - Run
rubocop -a(safe only), review what changed, then runrubocopagain and note which offenses remain — these are the ones requiring a human decision (naming, documentation, splitting a method). - Write a
.rubocop.ymldisablingStyle/Documentationand settingMetrics/MethodLengthtoMax: 10,Exclude: ['spec/**/*']— runrubocopagain and confirm the configuration takes effect.