Linting Ruby Code

Miguel Miguel Palhas on

Linting is the process of statically analyzing code in search of potential problems.

What constitutes a problem, in this case, can vary across programming languages, or even across projects within the same language. I would put these problems under a few different categories:

Let’s take a look at a few examples of each.

Stylistic Problems

There’s no objectively right way of styling code, as it’s all about the reader’s preference. The key is consistency though. Common points of debate include:

  1. double-quotes vs single-quotes
  2. tabs vs space
  3. maximum line length
  4. indentation of multiline calls, as shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# always single-line
foo(:a, :b, :c)

# aligned with first argument
foo(:a,
    :b,
    :c
)

# aligned with function name
foo(
  :a,
  :b
)

These are entirely subjective, but it’s usually beneficial to agree on a standard for each particular project, to keep the entire codebase consistent.

Programmatic Problems

I include here such problems as: - Extremely long method bodies, which leads to readability & maintainability issues - Cyclomatic Complexity, a metric commonly used to measure code complexity - Assignments within conditions. Most likely, if you type if x = true, you actually meant if x == true. and even if you did mean an assignment, it’s still a less-intuitive approach

Security Problems

Some functions or practices have potential security problems that developers may not be aware of.

For instance, in Ruby, Kernel#open is a flexible function that allows opening files or external URLs. But it also allows arbitrary filesystem access, with weird calls such as open("| ls"). Therefore, it is sensible to warn developers about it so that they can either use a safer approach (File#open, IO.popen, URI.parse#open), or explicitly decide to keep the behavior at their own risk.

Performance Problem

There are many details about Ruby’s inner workings that make some options more performant than others, depending on the context.

A linter warning us about them helps us learn along the way while optimizing some details of our program.

For example, Ruby 2.5 introduced String#delete_suffix which deletes a substring from the end of a string. These two lines are equivalent, but the latter one is more performant since it doesn’t rely on a generic string regex match:

1
2
3
4
5
6
7
str = 'string_with_suffix'

# bad
str.gsub(/suffix\z/, '')

# good
str.delete_suffix('suffix')

Auto Fixing

An important aspect of linters is their ability to automatically fix some or all of the found issues. Styling aspects such as line length are easily automated, so it makes sense to remove that burden from the developer. Other issues might be subjective or require human intervention, such as refactoring a large method. In these cases, no automation is possible.

Convention or Configuration

There is often heavy debate within a community or project on which rules make sense.

The traditional solution is to allow each team to solve the debate within its members by allowing them to configure linting rules to their own taste. However, in recent years, there has been a push across several languages to standardize to a single convention. While this isn’t enforced everywhere, the general idea is to entirely remove the mental overhead developers have when styling code. Instead of discussing which line length works best, everyone just uses the community-agreed rules.

In Ruby, this more or less translates to the two existing linters: RuboCop, which allows full-configuration and StandardRB, which takes the opposite approach and defines a common standard.

RuboCop

Employes the usual approach of providing a documented set of rules, each of which looks for a particular problem. Developers are able to disable or tweak certain rules within their own projects:

1
2
3
4
5
6
7
# configure max allowed line length
Layout/LineLength:
  Max: 80

# disable cyclomatic complexity
Metrics/CyclomaticComplexity:
  Enabled: false

It already includes sane defaults, so configuration is only needed for the specific rules you want to change.

Running bundle exec rubocop will make RuboCop analyze the entire codebase and list all the issues it found:

1
2
3
4
5
6
7
8
# test.rb
def badName
  if something
    return "inner result"
  end

  "outer result"
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ bundle exec rubocop
Inspecting 1 file
C

Offenses:

test.rb:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment.
def badName
^
test.rb:1:5: C: Naming/MethodName: Use snake_case for method names.
def badName
    ^^^^^^^
test.rb:2:3: C: [Correctable] Style/IfUnlessModifier: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||.
  if something
  ^^
test.rb:3:12: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
    return "inner result"
           ^^^^^^^^^^^^^^
test.rb:6:3: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
  "outer result"
  ^^^^^^^^^^^^^^

1 file inspected, 5 offenses detected, 4 offenses auto-correctable

You can then run bundle exec rubocop --auto-correct, and a large majority of your issues will be fixed according to your configurations.

Setting up bundle exec rubocop as part of your CI pipeline will ensure no code gets through without first fulfilling the linting rules.

StandardRB

A more recent project, which actually uses RuboCop under the hood. The main goal of StandardRB is not to build an entirely separate linter, but to reach a standard that everyone can just use instead of arguing about.

The lightning talk where it was first announced is pretty clear about the motivations: If people spend less time arguing about syntactic details and just follow the decisions of a community-wide agreement, we can all spend more time doing what really matters: building great products and libraries.

Since RuboCop is used underneath, the output you get is actually in the same format. The only difference is that you’re not allowed to customize any of the rules.

StandardRB recently reached 1.0.0, which means most of the discussion about which rules to use has already happened on their issues page. If you care or disagree about a particular rule, chances are you’ll see a related discussion about it in there.

Ultimately though, you can be confident it was discussed in some depth. It’s impossible for an entire community to agree 100% on all points. The philosophy of this approach is that people are flexible and can disagree and commit to a decision.

Final Thoughts

After spending more time than I’m proud of nitpicking linting rules in past projects, I definitely see the value in StandardRB’s approach and I recommend using it whenever possible.

Keeping all the benefits of consistency while removing the overhead of discussions, along with the automated fixes for most rules helps us deliver better software more efficiently, and focus on what really matters.

Other languages are adopting similar low-configurability code formatters. mix formatter in Elixir, and rustfmt in Rust both allow for some configurability, but the community is surprisingly onboard with keeping within the standard.

With that said, RuboCop is still a perfectly valid option if you, ironically, disagree with this sentiment.

P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Guest author Miguel is a professional over-engineer at Portuguese-based Subvisual. He works mostly with Ruby, Elixir, DevOps, and Rust. He likes building fancy keyboards and playing excessive amounts of online chess.

5 favorite Ruby articles

10 latest Ruby articles

Go back
Ruby magic icon

Subscribe to

Ruby Magic

Magicians never share their secrets. But we do. Sign up for our Ruby Magic email series and receive deep insights about garbage collection, memory allocation, concurrency and much more.

We'd like to set cookies, read why.