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.
The Ruby ecosystem uses several file extensions to denote Ruby code (
.thor, among many others) and there are common Ruby files that don’t have extensions at all, like
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:
Exclude. If there is a conflict between them, exclusions always take precedence. For example:
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
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
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.
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.ymlis 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
Regexpobject literals using a special
- 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:
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:
rubocop-railsgem was magically adding a
bin/*exclusion rule to my project without my knowledge.
- RuboCop exclusion rules take precedence, so adding an
Include: bin/setuprule 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
Exclude configuration for my Rails apps going forward:
Check out my nextgen interactive Rails app generator for this and several other recommendations for configuring new Rails apps.
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: