Setting Up Rubocop for Legacy Ruby on Rails Projects

Overwhelmed after setting up Rubocop in your legacy Rails apps? Here are some tips to clean up your old codebase for long-term maintainability.

Prefer to watch a video instead? Check out the video version of this article on YouTube at the end of this article.

Every developer has a preferred style of coding. Some people prefer spaces for indentation, while others swear by tabs. Some like to include trailing commas at the end of a multi-line array, while others don’t want to add anything. Regardless of your coding style, these preferences can create inconsistent codebases that mask subtle bugs and make your applications challenging to maintain over time. This problem is especially true with dynamic programming languages like Ruby, where you don’t have a compiler or preprocessor by default to inform you of these issues. That’s where static code analysis tools come into play.

Why Are Static Code Analysis Tools Important?

These tools can quickly scan your codebase to detect performance issues and spot errors in dynamically typed languages like Ruby, JavaScript, and Python, where your code can hide nasty bugs that only surface when executed. Having these checks will help you identify problems earlier, where it’s easier and cheaper to fix.

Static code analysis also helps ensure your team follows consistent styling throughout the codebase. Consistency is essential for developers working on teams. Without defined standards for your projects, everyone will write code in their own style, which doesn’t seem to be a problem initially but eventually makes your codebase a challenge to maintain.

Static code analysis has a secret benefit besides catching potential bugs and keeping your code consistent. I’ve noticed that these tools foster a culture of quality within development teams, teaching them best practices and how to avoid common issues in their code. Having frequent touchpoints to check on our code helps us learn how to write cleaner and more efficient code.

The Top Static Code Analysis Tool for Ruby Projects

If you’re working on a Ruby application, the static code analysis tool of choice is Rubocop. Rubocop is a static code analysis tool that helps teams enforce coding standards and spot those subtle errors that might otherwise go undetected. It’s a flexible tool with an extensive set of rules or “cops” that you can customize for your team’s needs. Rubocop also has a robust extensions ecosystem to go beyond its core functionality. For example, you can find extensions with cops targeting performance, Rails applications, and RSpec tests. No matter what kind of Ruby application you’re working on, Rubocop will have what you need to keep your code organized.

Setting up Rubocop when you start a new project will help you keep your codebase organized as it grows. In fact, the upcoming Ruby on Rails release for version 8 will include Rubocop by default. Unfortunately, not all developers are blessed to work on greenfield projects that have static code analysis set up. Many developers must work on long-lived legacy applications that have gone through many hands and will likely contain different coding styles scattered throughout.

If you’re working on an older Ruby project, getting Rubocop set up for your workflow can be overwhelming. You’ll likely receive hundreds, if not thousands, of warnings the first time you set it up. How can you deal with that? In this article, I’ll go through setting up Rubocop on an existing Ruby on Rails project to give you a few tips on how to get your existing projects using static code analysis to improve their long-term maintainability and make it easier for developers to work on their tasks.

Installing Rubocop on an Existing Ruby on Rails Application

For this article, we’ll use a Ruby on Rails application I built called Airport Gap. I created this application to help developers and testers improve their API testing skills. The first commit for this codebase happened over five years ago, and since then I’ve continued to keep its dependencies up to date. However, I never set up static code analysis for this project. Let’s change that by installing Rubocop in this application and setting it up to ensure that I keep the codebase clean and consistent from here on out.

The first step to installing Rubocop in a Rails application is to include it in the Gemfile. Let’s open the project’s Gemfile in our code editor and add it there. When setting up the Rubocop gem, it’s a good idea to set it up with a version lock for the latest stable release since future releases of Rubocop can change some functionality. You can find the latest stable version number in Rubocop’s GitHub repository or in the Installation page in the documentation. Also, we don’t need to load the gem since Rubocop is meant to run as a command-line tool, so we’ll set that up by adding require: false here.

gem 'rubocop', '~> 1.62', require: false

Since this is a Ruby on Rails application, we’ll also include the Rails extension for Rubocop, making sure to set a version lock and set require to false as done when setting up the primary gem.

gem 'rubocop-rails', '~> 2.24', require: false

One thing to note is that Rubocop extensions are maintained separately from the core Rubocop gem. They don’t always follow the same versioning pattern, but they will still work. If you include other extensions in the future, keep this in mind and read the extension’s documentation to ensure compatibility.

With the gems set up, let’s install them in our project using bundle install. After installing the gems in the Rails project, we can begin using Rubocop immediately with its default rules or cops. We can run our first static code analysis simply by running the rubocop command at the application’s root directory. Rubocop will quickly scan the codebase in less than a second and print out all the offenses it has found.

When running Rubocop for the first time in the Airport Gap application, it found 431 offenses in 71 files for our project. Airport Gap is a small Rails application compared to many legacy apps, so this number is actually small. For larger projects, don’t be surprised if you see thousands of offenses on your first run. However, don’t worry about having so many offenses in your application. As mentioned, these are the results using Rubocop’s default rules. In most projects, you’ll want to tweak these to fit your existing application and how you and your team prefer to style your code. To do this, we’ll need to create a configuration file that Rubocop reads from to determine which rules to use.

Configuring Rubocop for Your Legacy Application

When executing the Rubocop command, Rubocop will search for a .rubocop.yml file in the root of the project, and if it can’t find one, it’ll continue looking up your directory structure. It will use its default rules if it can’t find any files, which happened when we ran the rubocop command for the first time on the Airport Gap application. To ensure everyone who works on your project uses the same rules, I highly recommend having the configuration file inside your project directory and under version control.

You can set up this configuration file by running the rubocop --init command. The additional flag creates an empty .rubocop.yml file in your project root. The file contains a few comments on using it for your configuration settings. If you re-run Rubocop with this configuration file, it’ll yield the same results as before. We can begin setting up rules in this file to start chipping away at the warnings Rubocop found, but addressing each issue takes a significant amount of time and effort.

Instead, Rubocop has a few ways to kick-start this configuration by creating a TODO list of existing offenses for you. This way, you can see which rules you want to correct and enforce and which ones you can disable moving forward, and it helps give you a head-start on reducing the number of issues your legacy project has.

Generating TODO Configuration for Rubocop

Setting up this initial TODO list can be done with a few additional command-line flags:

  • The first command-line flag is --auto-gen-config. This flag creates a .rubocop.yml configuration file as before, but it also runs Rubocop with the default rules and creates a second configuration file called .rubocop_todo.yml, containing the configuration to turn off rules for all existing offenses. Having this separate file will allow you to see where Rubocop is raising these offenses while having them disabled while you address them.
  • A companion flag to set is --auto-gen-only-exclude. Some Rubocop rules contain metrics configured to set a minimum or maximum number for a specific offense, like the maximum length of characters in a line of code. The --auto-gen-config will set these values to their maximum in the TODO configuration, but it’s better to turn off these rules at the start instead of having a max to make it easier to address these issues. That’s what the --auto-gen-only-exclude flag does.
  • Finally, another helpful flag to include in this initial pass is the --no-exclude-limit flag. This flag will ensure that the TODO configuration turns off rules only for the files with offenses instead of entirely disabling the rule. That means Rubocop will find offenses for new files, which can help you decide whether you want to address a specific rule.

Let’s set up this TODO configuration file using these flags with the following command:

rubocop --auto-gen-config \
  --auto-gen-only-exclude \
  --no-exclude-limit

When executing this command, Rubocop runs its analysis and creates two new files. The first is the main .rubocop.yml configuration file. Now, instead of not having any configuration, it’ll have a single setting to inherit from another file:

inherit_from: .rubocop_todo.yml

This setting is beneficial because we can begin configuring Rubocop for current and future application modifications while having a separate file with things to address for the existing code. The inherited .rubocop_todo.yml file will contain many settings that Rubocop automatically generated for us. Rubocop scanned the application’s codebase and disabled all offenses it found so they won’t bother us while we set our configuration. Running the rubocop command again with these files will no longer find any offenses.

Addressing Our First Rubocop Offense

With this initial configuration in place, we can now begin to address the issues that popped up in our TODO configuration. This article won’t cover all the offenses, but I’ll give a few tips to reduce some of them and set up the rules we want to keep in place.

When scanning the .rubocop_todo.yml file, the first cop that shows up is Bundler/OrderedGems:

Bundler/OrderedGems:
  Exclude:
    - "Gemfile"

Rubocop’s rules or “cops” use the same naming pattern: a group or library is the first part of the rule—in this case, Bundler—and the second part is the rule’s name under that group (OrderedGems). The first time you use Rubocop, you probably won’t know what these rules do, so it’s a good idea to open the Rubocop documentation along with the documentation for any extensions you have. Rubocop’s documentation is well organized, and I highly recommend having these websites close by any time you work on Ruby projects that have Rubocop set up.

The documentation for Bundler/OrderedGems tells us that this rule checks if the gems in a Gemfile are in alphabetical order within each group, and that it’s enabled by default. We can see a couple of examples of what Rubocop considers “bad” and “good” according to the default setting. It also shows some examples of how it works with different configuration settings.

Sorting the gems in alphabetical order might be necessary for your project since it’s useful for quickly finding gems in larger Gemfiles. I don’t care whether the gems are alphabetically in my Gemfile for the Airport Gap application, so I don’t want Rubocop complaining about this rule. To do this, I’ll go to my main Rubocop configuration file at .rubocop.yml, add the Bundler/OrderedGems rule under the’ inherit_from’ setting, and set the Enabled setting to false. This setting tells Rubocop not to enable this rule by default.

inherit_from: .rubocop_todo.yml

Bundler/OrderedGems:
  Enabled: false

We’ll also remove the rule from .rubocop_todo.yml since it’s handled in our project’s main Rubocop configuration. After saving these changes, re-running the rubocop command shows we set it up correctly with no offenses. That addresses our first rule in this project.

Manually Fixing Rubocop Offenses

Let’s take another rule from our TODO list to handle. The next one that shows up is Layout/CommentIndentation:

Layout/CommentIndentation:
  Exclude:
    - "config/environments/development.rb"

The documentation for the Layout/CommentIndentation rule checks if all of our comments are indented properly without any weird spacing, and it’s also a default setting. Our initial scan shows this offense is happening in one file (config/environments/development.rb), so let’s open it up to check if we can spot the issue.

Inside the offending file, we can find the issue with a comment that’s lined up incorrectly:

# Highlight code that enqueued background job in logs.
  config.active_job.verbose_enqueue_logs = true

I want the comments aligned across my codebase, so I’ll enable this rule. Since this offense occurs only in one place, I can quickly correct it:

  # Highlight code that enqueued background job in logs.
  config.active_job.verbose_enqueue_logs = true

After removing the rule from .rubocop_todo.yml and re-running the rubocop command, everything still looks good.

Auto-Correcting Rubocop Offenses

In the previous example, we only had one offense to fix in a single file. But what if you have dozens or hundreds of offenses for a single rule to address? In some cases, you can have Rubocop automatically fix them for you.

Towards the end of the .rubocop_todo.yml file, we’ll see the Style/StringLiterals rule with 207 offenses spanning multiple files. The documentation for this rule mentions that it enforces the use of single or double quotes for strings in our application. By default, it enforces single quotes if there’s no interpolation in a string. I often use double quotes for strings, even without interpolation, so the number of offenses is high. However, I’m also very inconsistent with using single and double quotes for strings. I’d like to have some consistency around this. The consensus in the Ruby community is to use single quotes that match Rubocop’s default setting. I want to use single quotes moving forward, but I don’t want to go through each of those 207 places to correct them.

The comments above the Style/StringLiterals rule say this rule supports auto-correction with the --autocorrect flag. If I remove the rule from the .rubocop_todo.yml file and run the rubocop --autocorrect command, any offenses that Rubocop finds will be automatically updated, so we don’t have to go through the tedious work ourselves. After running the command with the auto-correction flag, the output says it corrected 207 offenses in our codebase. This single command can eliminate large chunks of offenses in our legacy projects.

Why Not Let Rubocop Auto-Correct Everything?

After discovering the --autocorrect flag, I’m guessing you’re thinking “Why not use the flag to let Rubocop automatically fix everything it can?” As tempting as it is to do this, I recommend against performing any type of autocorrection at the beginning of this process, especially when you have many issues to address on older codebases. Rubocop can safely do many autocorrections, like the previous example of converting strings from double to single quotes when possible. However, not all automatic corrections are safe and can lead to invalid behavior.

As an example of a potentially unsafe autocorrection, let’s check out a rule that mentions this. In .rubocop_todo.yml, there’s one rule called Style/SafeNavigation. The comments above this rule say that Rubocop can do an unsafe autocorrection here. The documentation mentions this rule looks for non-nil checks on a variable and converts it to use safe navigation (the &. operator in Ruby). The documentation will also contain a Safety section explaining why it’s an unsafe autocorrection. One of the offenses is in a controller file containing a conditional check for the Airport Gap login action:

user = User.find_by(email: params[:email])

if user && user.authenticate(params[:password])
  # ...

The conditional check will verify that the value of the user variable is a User object and that the authenticate method for that object is true. Rubocop found the offense in the first clause, where we check the user variable without explicitly checking if it’s nil.

If I remove this file reference from the .rubocop_todo.yml configuration and run rubocop --autocorrect-all to perform the autocorrection, it will change this line to use the safe navigation operator instead:

user = User.find_by(email: params[:email])

if user&.authenticate(params[:password])
  # ...

This automatic correction wouldn’t break the conditional check because the user variable will either be a user object or nil, and the safe navigation operator handles both cases. But this correction would break this code if the user variable were false instead of nil. Because Rubocop won’t have the context to know the variable’s value, it will consider this rule unsafe for automatic correction. Verify each offense before deciding to use autocorrections for these types of rules.

Another reason you don’t want to use autocorrections at the start is that some places simply can’t be corrected automatically by Rubocop and require manual intervention. In our .rubocop_todo.yml file, we’ll find one rule that doesn’t support autocorrection, the Lint/ShadowingOuterLocalVariable rule. Its documentation says it checks for the use of local variable names from an outer scope in blocks, and it cannot be automatically corrected.

In our example, there’s one offense for the Lint/ShadowingOuterLocalVariable rule in the project’s db/seeds.rb file. Opening this file and scanning the code, we can see the offending block:

user = User.find_or_create_by(email: 'airport_gap@dev-tester.com') do |user|
  user.password = 'airportgap123'
end

Rubocop found the problem with creating a user variable in the outer scope for this section while also using the same variable name inside the block. This code works, but reusing variable names can lead to confusion and potential troubles down the road, so it’s best to fix it now. The way to handle this offense is to rename either the outer or the block variable. In my case, I’ll fix the block variable:

user = User.find_or_create_by(email: 'airport_gap@dev-tester.com') do |u|
  u.password = 'airportgap123'
end

This code works the same as before and eliminates the possibility of confusion about using the same variable name in the block. With this change, we can remove the rule from .rubocop_todo.yml and re-run the rubocop command to verify we corrected the issue.

Summary

Static code analysis is one of those things that might not seem necessary, but it does provide a ton of benefits, especially in larger development teams and older codebases. Setting code consistency and quality standards is a quick and easy way to spot issues early, helping with long-term maintainability. It doesn’t seem like much, but an organized codebase goes a long way in making your application more reliable and easier to work with.

We still have a few issues to address for the Airport Gap project. Still, the methods this article covered are an excellent start for addressing any problems found when running static code analysis for the first time in a legacy Ruby on Rails codebase. Once you have these basics running, you and your team can begin enforcing these standards by running them early and often. Rubocop takes just a few seconds to analyze your code and integrates well with modern code editors and continuous integration processes. Having these checks in place will save a ton of headaches down the road. These steps take some time, especially for larger apps, but they’re worth the effort with a cleaner and more maintainable codebase.

Do You Need a Hand With Your Legacy Ruby on Rails Applications?

With over 15 years of hands-on professional experience working with Ruby on Rails projects, I can help you and your team navigate the challenges of working on legacy applications. Whether it’s getting your codebase up to current standards or fixing issues caused by years of patchwork, I can take your Rails apps from legacy to modern.

Schedule a time with me today to discuss how we can work together to get the most out of your Rails applications, no matter how long they’ve been around.

Watch the Video Version of This Article

If you enjoyed this article or video, please consider subscribing to my YouTube channel for more tips on helping Rails developers ship their code with more confidence, from development to deployment.

More articles you might enjoy

Article cover for Automating Rubocop Into Your Rails Development Workflow
Rails
Automating Rubocop Into Your Rails Development Workflow

Do you have Rubocop on your Ruby on Rails application? Here are some ways to run it early and often to maintain your code for the long haul.

Article cover for How Testers Help Developers Elevate Their Productivity
Software Development
How Testers Help Developers Elevate Their Productivity

Testers are often seen as an obstacle to progress, but they can be the most powerful tool to developer agility in the software development lifecycle.

Article cover for How to Achieve Speed and Quality in Software Development
Software Development
How to Achieve Speed and Quality in Software Development

Discover how to find the perfect balance between shipping quickly and delivering high-quality software without cutting corners.

About the author

Hi, my name is Dennis! As a freelancer and consultant, I work with tech organizations worldwide to help them build effective, high-quality software. It's my mission to help these companies get their idea off the ground quickly and in the right way for the long haul.

For over 20 years, I've worked with startups and other tech companies across the globe to help them successfully build effective, high-quality software. My experience comes from working with early-stage companies in New York City, San Francisco, Tokyo, and remotely with dozens of organizations around the world.

My main areas of focus are full-stack web development, test automation, and DevOps. I love sharing my thoughts and expertise around test automation on my blog, Dev Tester, and have written a book on the same topic.

Dennis Martinez - Photo
Learn more about my work Schedule a call with me today