I’ve always been fascinated with linters, code formatters and other static analysis tools. For Ruby, Rubocop is king, it will enforce many of the guidelines outlined in the community’s Ruby Style Guide, but it’s flexible enough for you to decide how to write your code. Setting up Rubocop when first starting a project allows you to quickly fix all the initial offenses (150+ for a vanilla Rails installation, see below) and analyze your code constantly (on save, on commit, before accepting a pull requests) to keep your project offense free.
Looking for a way to increase your engineering team's velocity?
— Brandon ✌︎('ω')✌︎ (@BrandonMathis) February 25, 2020
Install a linter and stop arguing in PRs about code formatting/style.
A friend tweeted the above recently and he’s right; I’ve spent countless hours debating: single vs. double quotes and spaces vs. tabs. But it really doesn’t matter, let’s write the code however we like and have a tool like Rubocop enforce the guidelines we’ve agreed beforehand.
I’ve been out of consulting for almost 5 years now, so I don’t get to start new Rails apps every couple of months. I created one today, to follow along a graphql tutorial (future blog post, maybe?) and stumble upon a feature from Rubocop I didn’t know: auto generate configuration. Let’s explore it together!
Tutorial
Setup and Defaults
Make sure you have installed your desired versions of both Ruby and Rails installed. At the time of this writing I wanted the latest 2.6 ruby and the latest Rails, which correspond to the versions shown below:
$ ruby -v
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin19]
$ rails -v
Rails 6.0.2.1
Create a new Rails app and get inside the newly created directory:
rails new rubocop-app-demo
cd rubocop-app-demo
Open the Gemfile
and add rubocop and rubocop-rails inside a :development
group and add require: false
to both lines according to the gems’ documentation (1)(2):
group :development do
# ...
gem 'rubocop', require: false
gem 'rubocop-rails', require: false
end
Running rubocop without an explicit configuration (default) results in 172 offenses in 40 files:
$ rubocop --format offenses
40/40 files |========================== 100 ===========================>| Time: 00:00:00
55 Style/StringLiterals
40 Layout/LineLength
39 Style/FrozenStringLiteralComment
6 Layout/EmptyLineAfterGuardClause
6 Style/IfUnlessModifier
4 Style/Documentation
3 Style/ExpandPathArguments
2 Layout/ArgumentAlignment
2 Layout/SpaceInsideArrayLiteralBrackets
2 Style/ClassAndModuleChildren
2 Style/StderrPuts
2 Style/SymbolArray
1 Bundler/OrderedGems
1 Layout/MultilineOperationIndentation
1 Metrics/AbcSize
1 Metrics/CyclomaticComplexity
1 Metrics/MethodLength
1 Metrics/PerceivedComplexity
1 Style/PerlBackrefs
1 Style/RedundantBegin
1 Style/SpecialGlobalVars
--
172 Total
Not a great start, let’s get this sorted out.
The Process
Rubocop includes a tool to auto generate a configuration . Simply pass the --auto-gen-config
flag to generate a configuration acting as a TO DO list:
$ rubocop --auto-gen-config
$ rubocop .
The following cops were added to RuboCop, but are not configured. Please set Enabled to either `true` or `false` in your `.rubocop.yml` file:
- Style/HashEachMethods
- Style/HashTransformKeys
- Style/HashTransformValues
Inspecting 40 files
......................................
40 files inspected, no offenses detected
This created two files .rubocop.yml and .rubocop_todo.yml. The first is your project’s final configuration file, for now, it simply inherits a temporary configuration from .rubocop_todo.yml (TO DO from now on). This allows the project to have no offenses. But the point of the TO DO is for us to remove the configuration records one by one as the offenses are removed from the code base or the configuration is moved outside of the TO DO.
First let’s fix the warnings by enabling the three cops that aren’t configured yet. Add the following to .rubocop.yml: 4fff12c
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
$ rubocop .
Inspecting 40 files
........................................
40 files inspected, no offenses detected
Now let’s focus on the first entry of the TO DO:
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: TreatCommentsAsGroupSeparators, Include.
# Include: **/*.gemfile, **/Gemfile, **/gems.rb
Bundler/OrderedGems:
Exclude:
- 'Gemfile'
This cop seems like a good idea, as the project grows ordered gems will make the Gemfile easier to read. We also learned that the cop supports auto correct, so simply removing this block from the TO DO and running rubocop autocorrect will remove the offense: 443b282
$ rubocop --auto-correct
Inspecting 40 files
C.......................................
Offenses:
Gemfile:39:3: C: [Corrected] Bundler/OrderedGems: Gems should be sorted in an alphabetical order within their section of the Gemfile. Gem listen should appear before web-console.
gem 'listen', '>= 3.0.5', '< 3.2'
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
40 files inspected, 1 offense detected, 1 offense corrected
$ rubocop .
Inspecting 40 files
........................................
40 files inspected, no offenses detected
Now onto the next block:
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: with_first_argument, with_fixed_indentation
Layout/ArgumentAlignment:
Exclude:
- 'bin/webpack'
- 'bin/webpack-dev-server'
We do want the Layout/ArgumentAlignment
cop, but files in the bin/ folder are usually automatically generated, and to avoid a constant back and forth, it’s probably better to avoid bin/ when running all the cops (AllCops
). Add the following to your project’s Rubocop configuration (.rubocop.yml) and remove the block from the TO DO. While we are here, let’s also ignore node_modules/, I remember having some issues with ruby files inside node-sass. 9e0f61d
AllCops:
Exclude:
- bin/*
- node_modules/**/*
Similarly, remove all other cops’ configurations that deal only with files inside the bin/ folder (8 cops and some files inside a ninth).
$ rubocop .
Inspecting 32 files
................................
32 files inspected, no offenses detected
Cop by cop, decide what you want to do with each:s
- Remove the excludes (so offenses are corrected or pop up)
- Move the excludes to .rubocop.yml if you don’t want to deal with a cop/file combination.
- Completely disabling a cop, by setting
Enabled: false
- Ignore the custom configuration in the TO DO and fallback to the default
- Change the recommended configuration.
I ended up removing the excludes for Layout/SpaceInsideArrayLiteralBrackets
, Style/SpecialGlobalVars
, Style/StringLiterals
and Style/SymbolArray
.
Moving the excludes for Style/ClassAndModuleChildren
, because the two files were created by Rails and I don’t plan to touch them much: test/channels/application_cable/connection_test.rb and test/test_helper.rb.
Disabling Style/Documentation
and Style/FrozenStringLiteralComment
.
Ignoring the configuration for Metrics/AbcSize
, Metrics/CyclomaticComplexity
, Metrics/MethodLength
and Metrics/PerceivedComplexity
.
Changing Layout/LineLength
’s Max
to 120. To match the rulers in my editor.
After going clearing the TO DO, remove the file and the line inheriting from it in your project’s configuration. Run rubocop’s autocorrect again: 32f03b8
6814cc7
$ rubocop --auto-correct
Inspecting 32 files
C.............C..........C.C.CC.
Offenses:
Gemfile:33:28: C: [Corrected] Style/SymbolArray: Use %i or %I for an array of symbols.
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
^^^^^^^^^^^^^^^^^^^^^^^^^^
...
test/channels/application_cable/connection_test.rb:1:9: C: [Corrected] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
require "test_helper"
^^^^^^^^^^^^^
32 files inspected, 17 offenses detected, 17 offenses corrected
The Result
All 17 offenses were corrected, let’s do it one more time to have a clean result:
$ rubocop .
Inspecting 32 files
................................
32 files inspected, no offenses detected
For this project, based on my preferences, the final .rubocop.yml file looks like this:
AllCops:
Exclude:
- bin/**
- node_modules/**/*
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
Style/Documentation:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: false
Style/ClassAndModuleChildren:
Exclude:
- test/channels/application_cable/connection_test.rb
- test/test_helper.rb
Layout/LineLength:
Max: 120
Photo by Lenny Kuhne | “gray vehicle being fixed inside factory using robot machines”