Fixing an issue is nice — however conserving it from coming again is even higher. As we resolve points in our code base, we regularly think about preserve that classification of problem out of the code base solely. Typically we attain for RuboCop to assist us police sure patterns. This additionally helps to doc the originating problem and educates teammates on why these patterns are undesirable.
RuboCop is greater than only a linter. It’s extremely extensible and means that you can write custom cops to implement particular habits. These cops can be utilized to create higher code practices, forestall dangerous patterns from sneaking right into a legacy code base, and supply coaching for different engineers. However it may be difficult to know how to create a new cop and if it’s going to work long-term.
We are able to write unit checks to make sure the success of our customized cops, simply as we might with any utility code.
Let’s discover this with an instance to point out how testing might be performed.
Testing customized cops
With the Aha! engineering group, each mannequin has an account_id
attribute current and for security reasons, we by no means need this to be set by way of mass-assignment. To keep away from this, we need to forestall sure attributes from being added to attr_accessible.
# dangerous
class Foo
attr_accessible :title, :account_id
finish
Foo.create(account_id: 1, title: "foo")
# good
class Foo
attr_accessible :title
finish
foo = Foo.new(title: "foo")
foo.account_id = 1
foo.save
We’ve a customized cop that analyzes the arguments to that methodology and can error if any protected attribute is current. The customized cop we’ve finally ends up trying one thing like this:
class RuboCop::Cop::ProtectedAttrAccessibleFields < RuboCop::Cop::Cop
# We are able to outline an inventory of attributes we need to defend
PROTECTED_ATTRIBUTES = [
:account_id,
].freeze
# We are able to outline an error message that's displayed when an offense is detected.
# This may be useful to speak data again to different engineers
ERROR_MESSAGE = <<~ERROR.freeze
Solely allow attributes which might be protected to be fully person managed. Sometimes any *_id subject might be problematic.
As an alternative carry out direct project of the sphere after doing a scoped lookup. That is the most secure approach to deal with person enter.
Some fields equivalent to #{PROTECTED_ATTRIBUTES.examine} ought to by no means be used as a part of attr_accessible.
ERROR
# We need to look at methodology calls. Significantly these which might be calling the attr_accessible methodology
# and now have arguments we care about
def on_send(node)
if receiver_attr_accessible?(node) && protected_arguments?(node)
# If we do detect an attr_accessible name with arguments we care about, we will file an offense
add_offense(node, message: ERROR_MESSAGE)
finish
finish
non-public
def receiver_attr_accessible?(node)
node.method_name == :attr_accessible
finish
def protected_arguments?(node)
node.arguments.any? do |argument|
if argument.sym_type? || argument.str_type?
PROTECTED_ATTRIBUTES.embody?(argument.worth.to_sym)
finish
finish
finish
finish
This practice cop does the trick. Including a check for it ensures that it will not break sooner or later after we replace RuboCop or prolong the performance. With a purpose to write a check, we have to perceive how the customized cops are arrange and run.
Instantiate a customized cop
RuboCop::Cop::Cop
inherits from RuboCop::Cop::Base
and that permits the instantiation with out any arguments. So it seems this is not something particular — creating a brand new occasion of our cop is actually so simple as: RuboCop::Cop::ProtectedAttrAccessibleFields.new
If the cop requires some sort of configuration, it may be handed to the occasion by way of a RuboCop::Config
object. The RuboCop::Config
takes two arguments. RuboCop can provide configuration by way of YML recordsdata. You need to use the primary argument of RuboCop::Config
to go this configuration with varied values from the check. The second argument is the trail of the loaded YML file, which will be ignored within the checks.
config = RuboCop::Config.new({ RuboCop::Cop::ProtectedAttrAccessibleFields.badge.to_s => {} }, "https://style-tricks.com/")
cop = RuboCop::Cop::ProtectedAttrAccessibleFields.new(config)
Course of, execute, look at
Because it seems, there’s a method obtainable, RuboCop::Cop::Base#parse
, that accepts a string as enter and can return one thing the cop can course of.
This permits us to have one thing like:
supply = <<~CODE
attr_accessible :account_id
CODE
processed_source = cop.parse(supply)
There’s a class from inside RuboCop, RuboCop::Cop::Commissioner
, that’s answerable for taking a list of cops and utilizing these to investigate the processed supply code. With a purpose to run our cop, we will run this methodology.
commissioner = RuboCop::Cop::Commissioner.new([cop])
investigation_report = commissioner.examine(processed_source)
The RuboCop::Cop::Commissioner#examine
methodology will return an occasion of RuboCop::Cop::Commissioner::InvestigationReport which is an easy struct class that has an inventory of offenses which were recorded.
Put all of it collectively
We find yourself with a check file that appears one thing like this:
describe RuboCop::Cop::ProtectedAttrAccessibleFields do
let(:config) { RuboCop::Config.new({ described_class.badge.to_s => {} }, "https://style-tricks.com/") }
let(:cop) { described_class.new(config) }
let(:commissioner) { RuboCop::Cop::Commissioner.new([cop]) }
it "data an offense if we use permit account_id as a string" do
supply = <<~CODE
attr_accessible :foo, 'account_id'
CODE
investigation_report = commissioner.examine(cop.parse(supply))
anticipate(investigation_report.offenses).to_not be_blank
anticipate(investigation_report.offenses.first.message).to eql described_class::ERROR_MESSAGE
finish
it "data an offense if we use permit account_id as image" do
supply = <<~CODE
attr_accessible :foo, :account_id
CODE
investigation_report = commissioner.examine(cop.parse(supply))
anticipate(investigation_report.offenses).to_not be_blank
anticipate(investigation_report.offenses.first.message).to eql described_class::ERROR_MESSAGE
finish
it "does not file an offense if no protected attribute is used" do
supply = <<~CODE
attr_accessible :foo
CODE
investigation_report = commissioner.examine(cop.parse(supply))
anticipate(investigation_report.offenses).to be_blank
finish
finish
Now that we all know write checks, we will use them as a place to begin for constructing new cops, extending current cops, and making certain that issues proceed to operate as our utility grows and evolves. These little investments into project-specific cops can find yourself being a big funding sooner or later well being of the tasks.
Join a free trial of Aha! Develop
Aha! Develop is a totally extendable agile growth instrument. Prioritize the backlog, estimate work, and plan sprints. If you’re excited about an built-in product development strategy, use Aha! Roadmaps and Aha! Develop together. Join a free 30-day trial or join a live demo to see why greater than 5,000 firms belief our software program to construct lovable merchandise and be joyful doing it.