Ruby Hoedown how conferences get made, and attended
pinned under “Ruby & Rails”

Ruby static analysis tools: RuboCop, Reek, Brakeman and Sorbet

Four questions, four tools, and no single one of them answers more than one question well.

Ruby static code analysis is the family of tools that read source without running it and report something about it. Because Ruby is dynamically typed and its classes stay open, these tools can say less with certainty than their equivalents in a compiled language, and what they can say divides cleanly into four kinds of question. Choosing among ruby static analysis tools is mostly a matter of working out which of those four is actually being asked.

The four are style, design, security and types, and they are answered by four different programs with four very different costs of adoption. No single tool covers more than one of them well, which is why almost every project that does ruby static analysis at all ends up running two or three rather than one.

Four questions, four tools, and how much configuration each needs RuboCop answers whether code follows a style, and needs the most configuration. Reek answers whether a design smells, and needs a moderate amount. Brakeman answers whether a Rails application has a known vulnerability class, and needs almost none. Sorbet answers whether types line up, and needs the most work of all because signatures have to be written. Bar lengths show the configuration and annotation effort each one asks for before it is useful. Setup effort before each tool is useful Brakeman runs usefully with no configuration at all Reek a short exclusion list, then it settles RuboCop a real config file, argued over Sorbet Relative effort, not hours. The order is stable: the tools that say the most about a codebase are the ones that need the most told to them first.
What each tool asks for before it earns its place. The cheapest one to adopt is the one that answers the narrowest question, which is the usual trade.

What ruby static analysis checks

Four distinct questions, and no single tool answers more than one of them well.

Style. Whether the source follows a stated convention: naming, line length, string quoting, method length. Mechanical, uncontroversial once agreed, and endlessly arguable before that.

Design. Whether a class or method exhibits a known smell: doing too many things, reaching too far into another object, carrying too many parameters. Advisory rather than binary.

Security. Whether the code matches a known vulnerable pattern, which in practice means Rails-specific ones: unsafe parameter handling, string-built SQL, unescaped output, unsafe redirects.

Types. Whether the values flowing between methods line up. This is the question ruby static code analysis can least easily answer on its own, because the answer has to be supplied as annotations before anything can check it.

The four tools, side by side

ToolWhat it answersHow it behaves in a build
RuboCopStyle, plus some correctness copsFails on any offence; autocorrects most
ReekDesign smellsAdvisory; noisy on legacy code until scoped
BrakemanRails security patternsFails on new warnings; needs a checked-in ignore file
SorbetType agreementFails on type errors; needs signatures first

The column that decides adoption is the third one. A tool that fails a build on its own opinion of style has to have that opinion agreed first, and a tool that reports design smells across a large existing codebase produces a list nobody reads unless it is scoped to changed files.

The false-positive question separates them further. Brakeman reports a small number of findings and some are wrong, which is manageable because each one is worth reading. Reek on an old codebase reports a great many findings and most are true and not worth acting on, which is worse, because it teaches everybody to skip the output.

RuboCop, and what it is actually for

RuboCop is a linter with an opinion, and its value is not the opinion. It is that the opinion is written down in a file, applied identically to everybody, and can be corrected automatically. What it removes is the argument in code review, not the badness from the code.

A linter does not make code good. It makes it stop being discussed.

Two adoption patterns work and one does not. Enabling everything on a large existing codebase produces thousands of offences and is abandoned within a week. Generating a todo file that accepts the current state and enforcing only new code works, and so does adopting a preconfigured wrapper that removes the configuration debate entirely at the cost of the ability to have it.

The one genuinely important setting is which cops fail the build as opposed to merely reporting. A build that fails on formatting teaches people to run the autocorrect; a build that fails on a subjective metric like method length teaches people to add an exclusion.

Types, and where they sit now

Ruby has shipped a type signature language, RBS, since 3.0, with signatures kept in separate files rather than inline. Two checkers read them, and Sorbet takes a different approach again with its own inline signature syntax and its own file format.

The practical position is unchanged from when they arrived: types in Ruby are opt-in, they pay off in proportion to how much of a codebase is annotated, and the annotation is the expensive part. For a small library the cost is low and the benefit is mostly documentation. For a large application it is a project rather than a tool adoption, which is why the bar for Sorbet on the chart is the longest one.

Putting them in a build

Run them on changed files rather than on everything, at least at first. Every ruby static analysis tool is pleasant on a new codebase and overwhelming on an old one, and the difference between one that gets used and one that gets disabled is almost always whether its first run produced a readable number of findings.

Order matters slightly: the fast, uncontroversial ones first, so a build fails in ten seconds on a formatting problem rather than four minutes later after a type check. Where that build lives and what else belongs in it is on testing, and the dependency management underneath all of it is on gems and Bundler.

For the framework most of these are pointed at, Rails covers the conventions, the release timeline covers which versions the tools still support, and the section index holds the rest.

Questions

What is ruby static analysis?

Tools that read source without running it and report something about it. In Ruby they divide into four questions: whether the code follows a style, whether the design smells, whether it matches a known vulnerable pattern, and whether types agree.

Which ruby static analysis tools should a project start with?

Brakeman if it is a Rails application, because it runs usefully with no configuration and reports a small readable number of findings. RuboCop next, adopted with a todo file so only new code is enforced. Reek and Sorbet are later decisions.

What does RuboCop actually give a team?

Not better code. A written-down opinion applied identically to everybody and corrected automatically, which removes a recurring argument from code review. The value is the consistency rather than the particular choices.

How should a linter be introduced to an old codebase?

Generate a todo file that accepts the current state and enforce only new code. Enabling everything at once produces thousands of offences and is abandoned within a week, which is the single commonest way adoption fails.

Is it worth adding types to a Ruby project?

They are opt-in and they pay off in proportion to how much is annotated, which is the expensive part. For a small library the cost is low and the benefit is mostly documentation; for a large application it is a project rather than a tool adoption.