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
andnow
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 theconst node
, when I tested it, was actually anObject
instead ofnil
, 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!