rails
Configuring RuboCop to scan the right files in a Rails project
A surprising amount of complexity and magic can hide in RuboCop’s YAML settings.
Despite using RuboCop for years, I was recently surprised to find that it was not scanning certain important files in my Rails project. When I dug further, I discovered that RuboCop’s system for configuring inclusion and exclusion rules is quite complicated and exhibits some of the infamous “Rails magic”. What started out as a simple question – why isn’t RuboCop checking my bin/setup
script? – turned into hours of troubleshooting. Here’s what I learned.
Including and excluding files
The Ruby ecosystem uses several file extensions to denote Ruby code (.rb
, .ru
, .rake
, and .thor
, among many others) and there are common Ruby files that don’t have extensions at all, like Gemfile
and Rakefile
. RuboCop deals with this by shipping with a large list of file and directory inclusion and exclusion rules that are active by default. Unlike other popular lint and formatting tools like ESLint and Prettier, Rubocop doesn’t use a .rubocopignore
file. Instead, overriding or extending the RuboCop’s default rules involves writing expressions in a YAML syntax.
RuboCop has two configuration keys that determine which files are scanned: Include
and Exclude
. If there is a conflict between them, exclusions always take precedence. For example:
AllCops:
Include:
- bin/setup
Exclude:
- bin/*
With this configuration,
bin/setup
will not be processed by RuboCop, because the bin/*
exclusion rule takes precedence over the bin/setup
inclusion rule.
RuboCop’s defaults
To establish a reasonable baseline, RuboCop has a predefined list of inclusion and exclusion rules. This is how RuboCop knows to scan Ruby files and to ignore node_modules
and .git
directories.
Include:
- '**/*.rb'
- '**/*.arb'
- '**/*.axlsx'
- '**/*.builder'
- '**/*.fcgi'
- '**/*.gemfile'
- '**/*.gemspec'
- '**/*.god'
- '**/*.jb'
- '**/*.jbuilder'
- '**/*.mspec'
- '**/*.opal'
- '**/*.pluginspec'
- '**/*.podspec'
- '**/*.rabl'
- '**/*.rake'
- '**/*.rbuild'
- '**/*.rbw'
- '**/*.rbx'
- '**/*.ru'
- '**/*.ruby'
- '**/*.spec'
- '**/*.thor'
- '**/*.watchr'
- '**/.irbrc'
- '**/.pryrc'
- '**/.simplecov'
- '**/buildfile'
- '**/Appraisals'
- '**/Berksfile'
- '**/Brewfile'
- '**/Buildfile'
- '**/Capfile'
- '**/Cheffile'
- '**/Dangerfile'
- '**/Deliverfile'
- '**/Fastfile'
- '**/*Fastfile'
- '**/Gemfile'
- '**/Guardfile'
- '**/Jarfile'
- '**/Mavenfile'
- '**/Podfile'
- '**/Puppetfile'
- '**/Rakefile'
- '**/rakefile'
- '**/Snapfile'
- '**/Steepfile'
- '**/Thorfile'
- '**/Vagabondfile'
- '**/Vagrantfile'
Exclude:
- 'node_modules/**/*'
- 'tmp/**/*'
- 'vendor/**/*'
- '.git/**/*'
The default config tells RuboCop to scan
*.rb
files as well as other common Ruby-based files like Rakefile
and Gemfile
.
RuboCop ignores hidden files, but allows opt-in
By default, RuboCop ignores files and directories that start with the .
character. However, this rule has lower precedence than an Exclude
pattern, and can be overridden by explicitly listing files using Include
:
inherit_mode:
merge:
- Include
AllCops:
Include:
- .tomo/**/*.rb
Normally RuboCop ignores the
.tomo
directory because it starts with a dot, so it has to be listed explicitly. The inherit_mode.merge.Include
directive tells RuboCop to add to its default list of inclusion patterns, rather then replacing them.
The rubocop-rails gem changes the defaults
Things get more complicated when RuboCop extension gems are added to a project. Gems can overwrite or augment the default inclusion and exclusion rules. The rubocop-rails
gem, for example, adds db/schema.rb
and other patterns to the default list of exclusions.
inherit_mode:
merge:
- Exclude
AllCops:
Exclude:
- bin/*
- db/*schema.rb
- log/**/*
- public/**/*
- storage/**/*
The
inherit_mode.merge.Exclude
directive tells RuboCop that the exclusions listed in this file should be added to, rather than replace, RuboCop’s default list of exclusions.
The .rubocop.yml config file can contain Regexp objects and use ERB
YAML is an esoteric language, and this is especially true in the Rails ecosystem, where preprocessors and directives are often layered on top. In RuboCop, the .rubocop.yml
file has these surprising properties:
- Before being parsed as YAML,
.rubocop.yml
is first processed as ERB. That means<%= %>
expressions can be used to embed arbitrary Ruby code, such as reading from the filesystem or reaching out to the network, right inside the config file. - YAML usually just allows for safe data types like strings, arrays, hashes, and integers, but when RuboCop parses YAML it also permits
Regexp
object literals using a special!ruby/regexp
directive. - RuboCop allows relative string paths for include/exclude rules, but regular expressions are always evaluated against absolute paths.
Putting these three together, complex rules can be expressed in YAML, like this one:
AllCops:
Exclude:
- !ruby/regexp /<%= Regexp.quote(File.expand_path("bin", __dir__)) %>\/(?!setup).*/
This YAML instructs RuboCop to exclude all files in the bin directory, except
bin/setup
. To make the regular expression work with absolute paths, I used ERB to expand bin/
into an absolute path and insert it into the regex.
Putting it all together
I wrote this post because what started as a simple thought – I wanted RuboCop to scan my bin/setup
script – turned out to be surprisingly difficult for a variety of reasons:
- The
rubocop-rails
gem was magically adding abin/*
exclusion rule to my project without my knowledge. - RuboCop exclusion rules take precedence, so adding an
Include: bin/setup
rule had no effect. - RuboCop’s default exclusion rules are “take them or leave them”; if I didn’t like one of the rules, I would have to rewrite the entire list from scratch.
Furthermore, my tomo deployment scripts and configuration files, even though they are written in Ruby and have a .rb
extension, were being completely ignored by RuboCop because they live in the .tomo/
directory, which starts with a dot.
Given what I now know about RuboCop, I’m using the following Include
/Exclude
configuration for my Rails apps going forward:
inherit_mode:
merge:
- Include
AllCops:
Include:
# RuboCop by default ignores files and directories starting with `.`, so they have to be listed explicitly
- .tomo/**/*.rb
Exclude:
# ignore bin/* except for bin/setup
- !ruby/regexp /<%= Regexp.quote(File.expand_path("bin", __dir__)) %>\/(?!setup).*/
- .git/**/*
- db/*schema.rb
- log/**/*
- node_modules/**/*
- public/**/*
- storage/**/*
- tmp/**/*
- vendor/**/*
My standard configuration for Rails.
Check out my nextgen interactive Rails app generator for this and several other recommendations for configuring new Rails apps.
Aside: how to test a RuboCop config
An invaluable tool during my troubleshooting process was RuboCop’s -L
option. With this, I could quickly check if files were being included or excluded as I expected:
bundle exec rubocop -L