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/*
.rubocop.yml
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/**/*'
rubocop-1.48.1/config/default.yml
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
.rubocop.yml
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/**/*
rubocop-rails-2.18.0/config/default.yml
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).*/
.rubocop.yml
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:

  1. The rubocop-rails gem was magically adding a bin/* exclusion rule to my project without my knowledge.
  2. RuboCop exclusion rules take precedence, so adding an Include: bin/setup rule had no effect.
  3. 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/**/*
.rubocop.yml
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
The -L option lists all files that RuboCop will inspect.

Share this? Copy link

Feedback? Email me!

Hi! 👋 I’m Matt Brictson, a software engineer in San Francisco. This site is my excuse to practice UI design, fuss over CSS, and share my interest in open source. I blog about Rails, design patterns, and other development topics.

Recent articles

RSS
View all posts →

Open source projects

mattbrictson/nextgen

Generate your next Rails app interactively!

11
Updated 1 day ago

mattbrictson/tomo

A friendly CLI for deploying Rails apps ✨

360
Updated 1 day ago

More on GitHub →