Linting is the automated checking of source code for programmatic and stylistic errors. This checking is performed by a static code analysis tool called a linter. A code formatter, however, is a tool concerned with formatting source code so that it strictly adheres to a pre-configured set of rules. A linter will typically report violations, but it's usually up to the programmer to fix the problem, while a code formatter tends to apply its rules directly to the source code, thus correcting formatting mistakes automatically.
The task of creating a more consistent code style in a project usually necessitates the introduction of a separate linting and formatting tools, but in some cases, a single tool will be capable of addressing both concerns. A good example of the latter is RuboCop, which is the tool we'll consider extensively in this article. You'll learn how to set it up in your Ruby project and adjust its configuration options so that its output matches your expectations. Aside from integrating it into your local development process, you'll also learn how to make it a part of your continuous integration workflow.
Installing RuboCop
Installing RuboCop is straightforward through RubyGems:
$ gem install rubocop
Check the version that was installed:
$ rubocop --version
1.18.3
If you'd rather use Bundler, place the snippet below in your Gemfile
and then run bundle install
. The require: false
part tells Bundler.require
not to require that specific gem in your code since it will only be used from the command line.
gem 'rubocop', require: false
Check the version that was installed:
$ bundle exec rubocop --version
1.18.3
Running RuboCop
You can run RuboCop using its default settings on your project by typing rubocop
(or bundle exec rubocop
if installed with Bundler). If you don't pass any arguments to the command, it will check all the Ruby source files in the current directory, as well as all sub directories. Alternatively, you may pass a list of files and directories that should be analyzed.
$ bundle exec rubocop
$ bundle exec rubocop src/lib
Without any configuration, RuboCop enforces many of the guidelines outlined in the community-driven Ruby Style Guide. After running the command, you may get several errors (offenses). Each reported offense is decorated with all the information necessary to resolve it, such as a description of the offense, and the file and line number where it occurred.
At the bottom of the report, you'll see a line describing the number of inspected files, the total number of offenses, and how many of the offenses can be fixed automatically. If you append the -a
or --auto-correct
argument, RuboCop will try to automatically correct the problems found in your source files (those prefixed with [Correctable]
).
$ bundle exec rubocop -a
Notice how each corrected offense is now prefixed with [Corrected]
. A summary of the number of corrected offenses is also presented at the bottom of the report. In the above example, there's another correctable offense that wasn't auto-fixed, even after appending the -a
flag. This is because some automatic corrections might change the semantics of the code slightly, so RuboCop considers it to be unsafe. If you want to autocorrect these offenses as well, use the -A
or --auto-correct-all
flag.
$ bundle exec rubocop -A
A good rule of thumb to follow is to run your test suite after using the autocorrect functionality to ensure that the behavior of your code hasn't changed unexpectedly.
Configuring RuboCop
RuboCop can be configured through a .rubocop.yml
file placed at the root of your project. If you want to use the same checks for all projects, you can place a global config file in your home directory (~/.rubocop.yml
) or XDG config directory (~/.config/rubocop/config.yml
). This global config file will be used if a locally scoped project configuration file is not found in the current directory or successive parent directories.
The default configuration for RuboCop is placed in its configuration home directory (~/.config/rubocop/default.yml
), and all other config files inherit from it. This means that when setting up your project configuration, you only need to make changes that are different from the default. This could mean enabling or disable certain checks or altering their behavior if they accept any parameters.
RuboCop refers to each individual check as cops, and each one is responsible for detecting a specific offense. The available cops are also grouped into the following departments:
- Style cops are mostly based on the aforementioned Ruby Style Guide, and they check the consistency of your code.
- Layout cops catch issues related to formatting, such as the use of white space.
- Lint cops detect possible errors in your code, similar to
ruby -w
, but with a host of additional checks. - Metric cops deals with issues related to source code measurements such as class length and method length.
- Naming cops are concerned with naming conventions.
- Security cops help with catching potential security issues.
- Bundler cops check for bad practices in Bundler files (such as
Gemfile
). - Gemspec cops check for bad practices in
.gemspec
files.
It's also possible to extend RuboCop through additional linters and formatters. You can build your own extensions or take advantage of existing ones if they are relevant to your project. For example, a Rails extension is available for the purpose of enforcing Rails best practices and coding conventions.
When you create your configuration file for the first time, you'll get a slew of messages warning you about the presence of new cops that were added but not configured. This is because RuboCop adds new cops on each release, and these are set to a special pending status until they are explicitly enabled or disabled in the user configuration. You can enable or disable each of the listed cops in the message individually or use the snippet below to enable all new cops (recommended). Afterwards, the messages will be suppressed.
# .rubocop.yml
AllCops:
NewCops: enable
If you don't want to fiddle with configuration files and the wealth of options provided by RuboCop, consider taking a look at the Standard project. It's largely a pre-configured version of RuboCop that aims to enforce a consistent style in your Ruby project without allowing the customization of any of its rules. The lightning talk where it was first announced gives more details about its origin and motivations.
You can install it by adding the following line to your Gemfile
and then run bundle install
.
# Gemfile
gem "standard", group: [:development, :test]
Afterwards, you can execute Standard from the command line as follows:
$ bundle exec standardrb
Adding RuboCop to an Existing Project
Most Rubyists do not have the luxury of working on greenfield projects. Much of our development time is spent in legacy codebases that may produce an overwhelming amount of linting offenses that cannot be tackled immediately. Fortunately, RuboCop has a useful feature that generates an allowlist of existing offenses, which can be addressed slowly over time. The benefit is that it allows you to introduce linting to your existing project without being bombarded with a mountain of unmanageable linting errors while flagging any new violations going forward.
$ bundle exec rubocop
523 files inspected, 1018 offenses detected
Creating the allowlist config file can be done through the command below:
$ bundle exec rubocop --auto-gen-config
Added inheritance from `.rubocop_todo.yml` in `.rubocop.yml`.
Created .rubocop_todo.yml.
The --auto-gen-config
option collects all offenses and their counts and generates a .rubocop_todo.yml
file in the current directory where all the current offenses are ignored. Finally, it causes .rubocop.yml
to inherit from the .rubocop_todo.yml
file so that running RuboCop on the codebase once again will not yield any offenses.
$ bundle exec rubocop
523 files inspected, no offenses detected
While generating the allowlist file, RuboCop will turn off a cop altogether if the number of violations exceeds a certain threshold (15 by default). This is typically not what you want because it prevents new code from being checked against that cop due to the number of existing violations. Fortunately, it's possible to increase the threshold so that cops are not disabled even if the number of violations is high.
$ bundle exec rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 10000
The --auto-gen-only-exclude
option ensures that each cop in the allowlist has an Exclude
block that lists all the files where a violation occurred, instead of Max
, which sets the maximum number of excluded files for a cop. Setting the --exclude-limit
also changes the maximum number of files that can be added to the Exclude
block for each cop. Specifying an arbitrary number larger than the total number of files being examined ensures that no cops will be disabled outright, and any new code added to existing or new files will be checked accordingly.
Fixing Existing Violations
After generating the .rubocop_todo.yml
file, it's important not to forget the existing violations, but to slowly address them one after the other. You can do this by removing a file from the Exclude
block of a cop, then fix the reported violations, run your test suite to avoid introducing bugs, and commit. Once you've removed all the files from a cop, you can delete the cop from the file manually, or regenerate the allowlist file once again. Don't forget to utilize the --auto-correct
option where possible to make the process much faster.
Adopting a Style Guide
RuboCop is very configurable, which makes it viable for any type of project. However, it may take a long time to configure the rules to your requirements, especially if you disagree with many of the default rules. In such circumstances, adopting an existing style guide may be beneficial. Several companies have already released their Ruby style guides for public consumption, such as Shopify and Airbnb. Utilizing your preferred style guide in RuboCop can be achieved by adding the relevant gem to your Gemfile
:
# Gemfile
gem "rubocop-shopify", require: false
Then, require it in your project configuration:
# .rubocop.yml
inherit_gem:
rubocop-shopify: rubocop.yml
Suppressing Linting Errors
Although RuboCop is great tool, it can yield false positives from time to time or suggest fixing the code in a way that is detrimental to the intent of the programmer. When such situations arise, you can ignore the violation with a comment in the source code. You can mention the individual cops or departments to be disabled, as shown below:
# rubocop:disable Layout/LineLength, Style
[..]
# rubocop:enable Layout/LineLength, Style
Or you can disable all cops for a section of the code in one fell swoop:
# rubocop:disable all
[..]
# rubocop:enable all
If you use an end of line comment, the specified cops will be disabled on that line alone.
for x in (0..10) # rubocop:disable Style/For
Editor Integration
It's handy to view the warnings and errors produced by RuboCop as you type code in the editor instead of having to run the checks through the command line every time. Thankfully, RuboCop integration is available in most of the popular code editors and IDEs, mostly through third-party plugins. In Visual Studio Code, all you need to do is install this Ruby extension and place the following in your user settings.json
file:
{
"ruby.lint": {
"rubocop": true
}
}
If you use Vim or Neovim, you can display RuboCop's diagnostics through coc.nvim. You need to install the Solargraph language server (gem install solargraph
), followed by the coc-solargraph extension (:CocInstall coc-solargraph
). Afterwards, configure your coc-settings.json
file as shown below:
{
"coc.preferences.formatOnSaveFiletypes": ["ruby"],
"solargraph.autoformat": true,
"solargraph.diagnostics": true,
"solargraph.formatting": true
}
Setting Up a Pre-commit Hook
A great way to ensure that all Ruby code in a project is linted and formatted properly before being checked into source control is by setting up a Git pre-commit hook that runs RuboCop on each staged file. This article will show you how to set it up with Overcommit, a tool for managing and configuring Git pre-commit hooks, but you can also integrate RuboCop with other tools if you already have an existing pre-commit workflow.
First, install Overcommit through RubyGems and then install it in your project:
$ gem install overcommit
$ overcommit --install # at the root of your project
The second command above will create a repo-specific settings file (.overcommit.yml
) in the current directory and back up any existing hooks. This file extends the default configuration, so you only need to specify your configuration with respect to the defaults. For example, you may enable the RuboCop pre-commit hook through the following snippet:
# .overcommit.yml
PreCommit:
RuboCop:
enabled: true
on_warn: fail
problem_on_unmodified_line: ignore
command: ['bundle', 'exec', 'rubocop']
The on_warn: fail
setting causes Overcommit to treat warnings as failures, while problem_on_unmodified_line: ignore
causes warnings and errors on lines that were not staged to be ignored. You can browse all the available hook options and their range of acceptable values on the project's GitHub page. You may need to run overcommit --sign
after changing your configuration file for the changes to take effect.
Occasionally, if you want to commit a file that does not pass all the checks (such as a work-in-progress), you can skip individual checks on a case-by-case basis:
$ SKIP=RuboCop git commit -m "WIP: Unfinished work"
Adding RuboCop to Your CI Workflow
Running RuboCop checks on each pull request is another way to prevent badly formatted code from being merged into your project. Although you can set it up with any CI tool, this article will only discuss how to run RuboCop through GitHub Actions.
The first step is to create a .github/workflows
directory at the root of your project and a rubocop.yml
file within the new directory. Open up the file in your editor and update it as follows:
# .github/workflows/rubocop.yml
name: Lint code with RuboCop
on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v2
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: true
- name: Run RuboCop
run: bundle exec rubocop
The workflow file above describes a single job that will be executed when code is pushed to GitHub or when a pull request is made against any branch. A job
is a sequence of steps that run sequentially. This particular job will run once on the latest Ubuntu, MacOS, and Windows versions provided by GitHub Actions (as defined by runs-on
and strategy.matrix
). The first step checks out the code in the repository, while the next one sets up the Ruby tool chain and dependencies, and the last one executes RuboCop.
Once you're done editing the file, save it, commit, and push to GitHub. Afterwards, you’ll get an inline display of any reported issues on subsequent check-ins and pull requests.
Alternative Auto-formatters
Although RuboCop provides comprehensive auto-formatting capabilities, it's also important to be aware of alternative tools in case it doesn't fulfill your needs adequately.
Prettier
Prettier started out as an opinionated code formatter for JavaScript, but it now supports many other languages, including Ruby. Installing its Ruby plugin is straight forward: add the prettier
gem to your Gemfile
and then run bundle
.
# Gemfile
gem 'prettier'
At this point, you can format your Ruby code with Prettier through the following command:
$ bundle exec rbprettier --write '**/*.rb'
Some of Prettier's rules conflict with RuboCop's, so it's necessary to disable the formatting checks of the latter so that it does not interfere with Prettier. Luckily, it's easy to turn off the RuboCop checks that conflict or are unnecessary with Prettier. All you need to do is inherit Prettier's RuboCop configuration at the top of your project's .rubocop.yml
file:
# .rubocop.yml
inherit_gem:
prettier: rubocop.yml
When you run RuboCop (with bundle exec rubocop
) from now on, it won't report any layout related offenses, paving the way for Prettier to correct them according to its own rules. You can also configure Prettier's output through its configuration file, which can be shared between JavaScript and Ruby code in the same project.
RubyFmt
RubyFmt is a brand-new code formatter that's written in Rust and currently under active development. Like Prettier, it is intended to be a formatter and not a code analysis tool. It hasn't seen a stable release just yet, so you should probably hold off on adopting it right now, but it's definitely one to keep an eye on.
Conclusion
Linting and auto-formatting code brings so many benefits to a code base, especially in the context of a team of developers. Even if you don't like being told how to format your code, you need to keep in mind that linting isn't just for you. It is also for the other people you collaborate with so that everyone can stick to the same conventions, thus eliminating the drawbacks of dealing with multiple coding styles in the same project.
It's also important not to treat the linter's output as gospel, so endeavor to configure it in a way that provides the most benefits to you without distracting from your main objectives. With RuboCop's extensive configuration settings, this should not be a problem. However, if you find that configuring RuboCop is taking too much time of your time, you can use a predefined style guide, as discussed earlier, or adopt Standard for a no-config alternative that everyone can just use instead of worrying about the little details.
Thanks for reading, and happy coding!