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.
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.
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 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.
I include here such problems as:
- Extremely long method bodies, which leads to readability & maintainability
- Cyclomatic Complexity,
a metric commonly used to measure code complexity
- Assignments within conditions. Most likely, if you type
if x = true, you
if x == true. and even if you did mean an assignment, it’s
still a less-intuitive approach
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
Therefore, it is sensible to warn developers about it so that they can either use
a safer approach (
URI.parse#open), or explicitly
decide to keep the behavior at their own risk.
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
1 2 3 4 5 6 7
str = 'string_with_suffix' # bad str.gsub(/suffix\z/, '') # good str.delete_suffix('suffix')
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.
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.
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.
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.
bundle exec rubocop as part of your CI pipeline will ensure no code
gets through without first fulfilling the linting rules.
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.
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.
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.