Hardscrabble 🍫

By Max Jacobson

See also: the archives and an RSS feed

why I think RuboCop is so cool, and how to contribute to it

August 9, 2015

RuboCop as compiler

Ruby is not a compiled language.

You can write code which has obvious flaws and Ruby will run it and then it will fail at runtime. For example:

puts "hello world"
puts hello world

That produces this output:

$ ruby lol.rb
hello world
/Users/max/Desktop/lol.rb:2:in `<main>': undefined local variable or method `world' for main:Object (NameError)

Some other languages wouldn’t even run that program. For example, the same code in Go:

package main

import "fmt"

func main() {
	fmt.Println("hello world")
	fmt.Println(hello_world)
}

Running that produces this output:

$ go run lol.go
# command-line-arguments
./lol.go:7: undefined: hello_world

But Ruby can’t run anything. Look at this bullshit:

puts "hello world"

def lol
  puts "lol world"

lol

Running that produces this output:

$ ruby lol.rb
/Users/max/Desktop/lol.rb:7: syntax error, unexpected end-of-input, expecting keyword_end

Notice: it doesn’t even output “hello world”; it just straight-up fails to run. You might say it doesn’t compile.

Ruby actually has a command line flag for checking the syntactic-correctness of a program:

$ ruby -c lol.rb
/Users/max/Desktop/lol.rb:7: syntax error, unexpected end-of-input, expecting keyword_end
$ ruby -c ok_program.rb
Syntax OK

This is useful, but only to a point. It means your programs will usually run, but you’ll have more errors at runtime than you would writing in a language like Go (for example).

I barely know Go, but in the small exposure I’ve had, I’ve really enjoyed how nit-picky the compiler is. For example, this program:

package main

import "fmt"

func main() {
	msg := "Hello world"
	other_msg := "lol world"
	fmt.Println(msg)
}

Running it produces this output:

# command-line-arguments
./lol.go:7: other_msg declared and not used

What! I can’t run my program because I declared a variable but then didn’t use it?? Who cares?? Go cares! And I kind of do too, now. Why have it if you don’t need it? Go kind of forces you to write really intention-revealing code and to clean up anything which might obscure your intentions. That’s great.

Ruby doesn’t care as much about that.

msg = 'hello world'
other_msg = 'lol world'
puts msg

That’s fine:

$ ruby -c lol.rb
Syntax OK
$ ruby lol.rb
hello world

So now your code has this random unused variable. It’s not really hurting anyone or anything. In theory it has a performance impact, as it’s allocating an object you don’t really need it to, and if this code is run a ton that could matter. But more importantly, I think, it’s just clutter. When people come across this code in the future they won’t know why it’s there, but they might assume it’s there for a reason, and they’ll mentally mark this area of the code base as kind of strange and unknowable.

RuboCop is a gem which can help bring Ruby closer toward Go levels of nit-pickiness, and I’m a huge fan of it. I recommend including it in your Ruby projects and running it alongside your tests to enforce adherence to its rules (“cops” in its parlance).

Running that same ruby program through RuboCop produces this output (note: you have to gem install rubocop first):

$ rubocop lol.rb
Inspecting 1 file
W

Offenses:

lol.rb:2:1: W: Useless assignment to variable - other.
other = 'lol world'
^^^^^

1 file inspected, 1 offense detected

Awesome! Now we know our code contains some offense and might be confusing our collaborators, and we know exactly where to make the change.


RuboCop as code style nit picker

RuboCop is also very opinionated about code style. For example, it will complain if you aren’t consistent about using single or double quotes, or if you aren’t consistent about using two spaces for indentation, or if you leave spaces at the end of your lines.

I recommend configuring it to your taste and to not feel guilty about disabling cops which you don’t find valuable. As a tool, it adheres to the “strong opinions, weakly held” mantra: it’s very easy to bribe this cop into changing its opinions by adding a simple yml file to the root of your project.

The Go compiler isn’t picky about things like code style, but Go ships with a secondary, optional tool called gofmt which is extraordinarily opinionated: it straight-up rewrites your code to follow Go style conventions.

Similarly, many RuboCop cops are auto-correctable. Consider this program a “before picture”:

class Dog

  def initialize(name:name)
    
    @name=name
    end
end

milo = Dog.new(name: "Milo")
p milo


Kind of ugly looking, but it’s syntactically valid:

$ ruby -v
ruby 2.0.0p451 (2014-02-24 revision 45167) [x86_64-darwin14.3.0]
$ ruby -c lol.rb
Syntax OK
$ ruby lol.rb
#<Dog:0x007fbe64207a40 @name="Milo">

So let’s try autocorrecting it:

$ rubocop lol.rb --auto-correct
Inspecting 1 file
W

Offenses:

lol.rb:1:1: C: Missing top-level class documentation comment.
class Dog
^^^^^
lol.rb:2:1: C: [Corrected] Extra empty line detected at class body beginning.
lol.rb:2:23: W: Circular argument reference - name.
  def initialize(name:name)
                      ^^^^
lol.rb:3:1: C: [Corrected] Extra empty line detected at method body beginning.
lol.rb:3:1: C: [Corrected] Trailing whitespace detected.
lol.rb:4:1: C: [Corrected] Trailing whitespace detected.
lol.rb:4:5: W: end at 4, 4 is not aligned with def at 2, 2.
    end
    ^^^
lol.rb:4:10: C: [Corrected] Surrounding space missing for operator =.
    @name=name
         ^
lol.rb:5:10: C: [Corrected] Surrounding space missing for operator =.
    @name=name
         ^
lol.rb:8:22: C: [Corrected] Prefer single-quoted strings when you don't need string interpolation or special symbols.
milo = Dog.new(name: "Milo")
                     ^^^^^^
lol.rb:9:22: C: [Corrected] Prefer single-quoted strings when you don't need string interpolation or special symbols.
milo = Dog.new(name: "Milo")
                     ^^^^^^
lol.rb:10:1: C: [Corrected] 2 trailing blank lines detected.
lol.rb:11:1: C: [Corrected] 2 trailing blank lines detected.

1 file inspected, 13 offenses detected, 10 offenses corrected

Afterwards, the program looks like:

class Dog
  def initialize(name:name)
    @name = name
    end
end

milo = Dog.new(name: 'Milo')
p milo

It’s not perfect. I’m surprised it didn’t autocorrect the indentation on the first end. But it’s neat that it did as much as it did.

EDIT: I opened an issue about this surprise and it turns out they don’t want to autocorrect that unless you really opt in.

In the past, I’ve written about how Ruby Keyword Arguments aren’t Obvious, and mentioned “circular argument references” as a mistake I had to learn not to make. In an attempt to give back to others like me, I submitted a pull request which adds a cop to RuboCop that checks for the presence of those circular argument references and warns you about them. This is particularly useful on Ruby 2.0.0 and Ruby 2.1, which don’t emit warnings about their presence as Ruby 2.2 does.


contributing to RuboCop

Contributing to RuboCop is somewhat intimidating because, as you might imagine, its code base has very high standards. It also seems like it ought to be very complicated, because the project needs to be able to statically analyze code in order to complain about it.

Fortunately, the maintainers are very clear and quick in their feedback, and happy to merge things which seem useful. And the code base has many examples of how to write a cop. A cop is a class which inherits from RuboCop::Cop::Cop.

require 'rubocop'
require 'active_support/all'
RuboCop::Cop::Cop.subclasses.count #=> 226
# (required active support for the subclasses method, which I love)

Once you subclass Cop, your class will be on duty and RuboCop will ask it if it’s offended by the code it’s analyzing. You just need to tell it which types of code you care about. RuboCop thinks of Ruby code as a tree of nested nodes, each having a type. For example, when you assign a local variable, RuboCop sees that as a node whose type is :lvasgn. Let’s say you wanted to write a cop where local variables aren’t allowed to be named “harold”. You would write that like this:

# encoding: utf-8

module RuboCop
  module Cop
    module Lint
      class Harold < Cop
        def on_lvasgn(node)
          local_variable_name, value = *node
          return unless local_variable_name == :harold
          add_offense(node, :expression, 'Do not name local variables harold')
        end
      end
    end
  end
end

The trickiest bit is learning how RuboCop “sees” code as nodes and learning what the different parts are called. That comes from looking through the existing examples and experimenting. It’s fun, I recommend it. I added one other cop on a bored evening, about discouraging option hashes.


RuboCop as teacher

This summer, I coordinated a group of 5 developer interns at work. I helped pick them out, so I knew they were all very bright but not super experienced with Ruby. We asked them to study a bit and follow the Rails Tutorial Book before starting to help get them on the same page, but we didn’t expect them to know Ruby conventions or best practices.

From the beginning, their project had RuboCop linting their code, with zero configuration. I warned them, “you’re going to hate this”. On their last week, I asked them if it was helpful, and they all said yes, but that it was often very annoying. Some of them liked it more than others. One even made a contribution to RuboCop fixing an issue in the cop I added. They produced a very interesting code base. Superficially, it’s immaculate. They picked up a few tricks from RuboCop suggestions. More than one cited guard clauses as a thing they wouldn’t know about otherwise.

The class length maximum (100 lines) and the method length maximum (10 lines) were both unhelpful. They’re meant to guide you toward following the single responsibility principle, but if it’s not something you’ve internalized, it just sort of makes you stuck and frustrated.

I came to think of RuboCop (and, later, coffeelint and scss-lint) as an automated layer of mentorship filtering out some potential questions that didn’t need to reach the human layer, so that layer could be reserved for more interesting problems and discussions.


RuboCop as totalitarian police state?

I do think of programming as a creative outlet, but don’t worry about linters inhibiting your creativity; think of it like a poetic form with strict rules, like the sonnet. There’s something very satisfying about consistency, right?

Note: I don't have comments or analytics on this website, so it's hard to tell if people are reading or enjoying it. Please feel free to share any feedback or thoughts by shooting me an email or tagging me in a post on Mastodon @maxjacobson@mastodon.online.