Create a custom Rubocop cop

Create a custom Rubocop cop

Recently I worked on a client project that required me to implement good code conventions across the project. One of the tasks besides implementing the Rubocop standard cops was to write a custom cop for two different Datetime methods, so in this article I will explain how I created a custom Rubocop cop that solved that problem.

What is rubocop?

RuboCop is a Ruby static code analyzer (a.k.a. linter) and code formatter. Out of the box it will enforce many of the guidelines outlined in the community Ruby Style Guide. Apart from reporting the problems discovered in your code, RuboCop can also automatically fix many of them for you.

Why a custom cop?

You might be wondering: “Why would I have a custom cop if out of the box Rubocop has most of the cops I will need?”

Well, there are times when the rubocop standards are not enough to cover organization, company or project standards that you came up with and you want to enforce them in the same way Rubocop enforces the Ruby standards.

Creating a cop for Datetime.now

In this section I will create a cop to check the usage of Datetime.now and propose the use of Datetime.current instead.

If you want to learn more about the differences between Datetime current and now check this article.

First I create a class that inherits from Rubocop::Cop::Base

module CustomCops
  class DateTimeNowUsage < RuboCop::Cop::Base
    def on_send(node)
      # do stuff with the AST node
    end
  end
end

I am puting our class inside the CustomCops module just to namespace things and avoid future name collisions with Rubocop standard cops if ever…

The def_node_matcher method

Rubocop has a macro called def_node_matcher that receives a name and a pattern to match the Ruby AST node you want to mark as an “offense”.

There’s several ways to get the AST node matcher for def_node_matcher, I could use the Node Pattern or simply pass the node source string to it.

I used the ruby-parse gem to get the node source string of my offensing code. i.e:

$ gem intsall ruby-parse
$ ruby-parse -e "Datetime.now"

(send
  (const nil :Datetime) :now)

Then I use the output as a patttern in def_node_matcher

module CustomCops
  class DateTimeNowUsage < RuboCop::Cop::Base
    def_node_matcher :on_datetime_now, <<~PATTERN
      # ruby-parse output
      (send (... :Datetime) :now)
    PATTERN

    def on_send(node)
      # do stuff with the AST node
    end
  end
end

NOTE: I am using (send (... :Datetime) :now) instead of (send (const nil :Datetime) :now). This is because the const node, when I tested it, was actually an Object instead of nil, as ruby-parse showed us. I noticed this because the pattern was not being matched by Rubocop when I tried to run the custom cop. With the ... it will match any node.

Add the offense

Now, when rubocop finds any occurence of Datetime.now I want to add it as a “Rubocop offense”.

module CustomCops
  class DateTimeNowUsage < RuboCop::Cop::Base
    MSG = "You are using `Datetime.now` please replace it with `Datetime.current`"

    def_node_matcher :on_datetime_now, <<~PATTERN
      # ruby-parse output
      (send (... :Datetime) :now)
    PATTERN

    def on_send(node)
      on_datetime_now(node) do
        add_offense(node, message: MSG)
      end
    end
  end
end

Auto correct

Ok. If you have followed all the steps you should have a custom Rubocop cop that will trigger an offense when you use Datetime.now


$ rubocop --only CustomCops/DateTimeNowUsage

Inspecting 138 files
.........C.....................................................................................................C..........................

Offenses:

app/controllers/roadmap_payments_controller.rb:3:5: C: CustomCops/DateTimeNowUsage: You are using Datetime.now please replace it with Datetime.current
    Datetime.now
    ^^^^^^^^^^^^
^^^^^^^^^^^^

But we can make it better, we could add the autocorrect feature that rubocop has built in.

module CustomCops
  class DateTimeNowUsage < RuboCop::Cop::Base
    extend RuboCop::Cop::AutoCorrector

    MSG = "You are using `Datetime.now` please replace it with `Datetime.current`"

    def_node_matcher :on_datetime_now, <<~PATTERN
      (send (... :Datetime) :now)
    PATTERN


    def on_send(node)
      on_datetime_now(node) do
        add_offense(node, message: MSG) do |corrector|
          corrector.replace(node, "Datetime.current")
        end
      end
    end
  end
end

Now, if we run our cop with the autocorrect flag, the cop will update our code with Datetime.current:

✗ rubocop -A --only CustomCops/DateTimeNowUsage

Inspecting 138 files
.........C........................................................................................................................
........

Offenses:

app/controllers/roadmap_payments_controller.rb:3:5: C: [Corrected] CustomCops/DateTimeNowUsage: You are using Datetime.now please replace it with Datetime.current
    Datetime.now
    ^^^^^^^^^^^^

138 files inspected, 1 offense detected, 1 offense corrected

Conclusion

Learning about an AST and Rubocop internals could seem intimidating, but Rubocop has great documentation. You can learn more about it here.

Thanks for reading. I hope you find this blog post helpful!

Get the book