I recently set up RuboCop on an existing Rails project. I’ll share how I approached it and what could have gone better. Specifically, I’ll help you fit RuboCop to your project’s needs, and not the other way around.
What is RuboCop?
RuboCop is a Ruby linter. It has a bunch of rules about how Ruby should be formatted. It calls these formatting units cops, and has many built in. You can define your own cops if you want to get crazy. Most of the built-in cops have configuration, and at least the ability to disable the cop.
Why use a Ruby linter?
Having a style guide of some sort saves the software team time. It’s nice to have a canonical guide on what the source code should look like. Having a guide saves the team from wasting time formatting and lets them get on to more productive things. It also makes reading the code easier because everything is in a consistent format.
But having a style guide is not enough. Developers end up either unwittingly violating the style guide, or feeling like they are making passive-aggressive comments in code review. With a linter and continuous integration, you can ensure that the project automatically points out style violations. It stops wasting time and effort and lets you focus on the things that matter. It takes your documentation of what the software should look like and turns it into an executable specification.
Avoiding poor decisions
The built-in cops are based closely on the Ruby style guide. However, those guidelines probably don’t line up with the current code your project has. Sometimes the cops are overbearing. Sometimes they don’t make much sense. Sometimes they are just too strict.
The first thing to do–which I did a poor job of this time–is to ask the team that you are working with which things in the guide they disagree with. I spent a little too much time thinking on my own and changing things that eventually needed to be reverted.
Another poor decision was when I disagreed with the linter, but instead of listening to my experience and judgment, acquiesced to the tool’s demands. I think that linters should serve the project’s goals, not the other way around. If you find yourself rewriting or restyling swaths of code, consider if you could make the cops less picky or disable them entirely. Hopefully this post will help with understanding the tool’s settings well enough to change them.
The first run
I’d recommend installing RuboCop and then just running it and seeing what happens. You will likely get a lot of errors. Some basic checks to start putting in your RuboCop configuration (.rubocop.yml
):
- does it at least finish the run without crashing? :)
- do I have the right Ruby files being linted?
- do I have the right files excluded?
Sizing up the suggestions
Now that you have a list of suggestions from RuboCop, it’s time to whittle them down. But finding exactly what you need to do for each cop can be tough. What I would recommend at this point is to enable two RuboCop settings, either on the command-line or in your configuration.
Printing cop names
The first setting I recommend prints the full name of cops when there is a failure. This helps you learn more about them and to know the right name for disabling and configuring the cop.
On the command-line:
$ rubocop --display-cop-names
This turns the output from something like:
lib/foo.rb:42:10: C: Prefer single-quoted strings when you don't need string interpolation or special symbols.
to:
lib/foo.rb:42:10: C: Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
I generally prefer the long form of flags for scripts since I’ll have to type it once and it’s self-documenting. On the command line I’ll usually use the shorter version of the flag. Another good approach is changing the .rubocop.yml
file to have this be the default configuration, so you just need to invoke rubocop
and it uses the settings there.
In the second example above, you can see that the cop category is Style
, and the name is StringLiterals
. So if you want to disable this check, you can add the following to your rubocop.yml
:
Style/StringLiterals:
Enabled: false
Understanding cops
The second setting I recommend is --display-style-guide
. This setting is useful for seeing what RuboCop wants and if you want to actually follow that rule or not. It links to an explanation of the rule, typically in the Ruby style guide.
For example, you might get:
lib/foo.rb:39:39: C: Use def with parentheses when there are parameters. (https://github.com/bbatsov/ruby-style-guide#method-parens)
Sometimes the style guide is less than helpful: it just says this pattern is bad, with no explanation of why. So you need to use discretion. But linking to the docs is better than needing to guess what format RuboCop wants your code to be in.
Getting everything working
So you’ve got 47 errors. Where to begin?
You might want to go through and fix any common style violations in bulk, or disable cops that are overly picky. This might cut the errors down to something more manageable.
If your project is big, you might work on a subset of the project. For example, if you have many Ruby files in lib
, and many others under app
, do each of these subdirectories separately. Then when you lint the whole project, it will be clean.
Overwhelmed with errors and want to work on one change at a time? Try the --fast-fail
flag to stop the RuboCop run after the first issue. Then fix the issue and continue running, and it should give a different error, hopefully later in the lint process. Another approach is to remember how many violations were spotted on the last run and the number should go down by one after each fix.
Linting on Rails
If you are on a Rails project, there is a flag for Rails-specific style checks. You can enable this with --rails
. I recommend getting all of the Ruby checks working first, and then you can nail down the Rails-specific things. I learned about some interesting deprecations or recommendations, which were useful since I hadn’t worked on a Rails project in a little while.
Handling complexity
RuboCop has some interesting defaults for code complexity. While we can all agree that less complex code is good, it is labor-intensive to retrofit an existing codebase to follow code complexity guidelines. You are probably going to get errors like:
lib/foo.rb:39:3: C: Method has too many lines. [47/20]
lib/foo.rb:39:3: C: Perceived complexity for `bar` is too high. [8/7]
At first I tried cleaning some of these up, but there are a few issues with this:
- I’m new on the project
- the project may not have solid test coverage
- the code works now, and if I modify it, it might not actually make it much better and might introduce bugs
- who is to say what the ideal complexity should be?
- it’s just going to take a lot of time that could be better used at this point on the project lifecycle
However, lint errors cause our continuous integration to fail, so we need to address them somehow. Rather than disable the complexity cops, I think a balanced approach is to agree on a reasonable upper limit for a method’s length in our codebase and then fix any offenders. Since measures like function or module length tend to follow a power law, there should be only a few very complex areas.
For the rest, we set the limits high enough that they don’t fail, and if the module or function then goes over the limit, RuboCop will warn us and we will have increased feedback that our design is unsustainable. Generally code review should filter out egregious examples, but the fact that we added twenty lines to an already 200+ line file is often lost unless we use a tool that is more objective. Basically if the linter fails on complexity when the limits are high, then we know it is a useful failure to report and “stop the line” on.
An interesting approach would be to make the limits high and make a plan to scale down over time. Say our goal is a maximum of 100 lines in a particular file, but right now we have many that are over 200 lines long. We set the limit high at 250 to start, and then every week decrement it by ten until it gets to 100 lines. We could do this with a calender reminder, a bot that rewrites the configuration file, or actually encode it in the .rubocop.yml
file or an environment variable, depending on how RuboCop reads in the configuration file.
(Again, just because your project passes linting doesn’t mean that it is well-written. It is just a tool to try to help code quality and save the team time.)
Don’t let your hard work go to waste
So you’ve gotten down to zero style suggestions. Congratulations!
But just because the cops are passing now doesn’t mean they will stay that way. Unless you put it on your continuous integration, the project will quickly gain style violations. Partially because there may still be some wrinkles in our RuboCop configuration, partially because we are humans and sometimes do things in different or suboptimal ways.
When the codebase is under CI, team members get quick feedback when they have angered RuboCop. Putting RuboCop on CI is pretty easy once you have fixed the issues.
Some handy aliases
At the beginning I commonly mistyped rubocop
, so I made these aliases for ZSH to prevent me from having to retype it:
alias rubycop="rubocop"
alias rubocopy="rubocop"
Found this post helpful?
If you found this post helpful and want more like it, check out my guide to Starting on an Existing Rails Project, where I cover how to quickly come up to speed on a Rails project and make an impact.