March 20, 2013
Code quality is a major component of code maintainability. Sloppy or overly complex code is sure to doom a project. If another developer can’t tell the purpose of the code at first glance he/she will be less incline to invest time to correct that code’s technical debt.
Code complexity, style, and documentation are all factors that developers can control with discipline. Luckily for those of us without discipline (myself included) Square has open sourced Cane, their code quality gem. Cane out of the box can run complexity, style, and documentation checks. Cane also supports the addition of custom checks, this allows you to specifically target the coding habits you want to discourage.
Custom Cane metrics start off easy enough, there are only three requirements:
options
method that defines the command line options your
custom check is expecting. If the check doesn’t require and options this
method can return an empty Hash
options
will be supplied as a Hash
to the constructor.violations
Those requirements can easily be achieved with the following:
class MyCheck < Struct.new(:opts)
def self.options
{ }
end
def violations
[ ]
end
end
Of course this check doesn’t actually check anything, so lets make something useful.
Something I know I am guilty of is using the puts
and print
methods to output
debugging information to the console while troubleshooting a test or bug. It can annoy
others and, honestly, it is very easy to leave behind. So lets implement a PutsCheck
metric
so these rogue puts
and print
are caught. We are going to start with a simple
class declaration, I’ll go ahead and add some of the boiler plate Cane attributes
class PutsCheck < Struct.new(:opts)
DESCRIPTION =
"Lines output to console using `puts` or `print`"
PUTS_REGEX = /^\s*p(uts|rint)?[\s\(]+(.+?)\s*[\)\s]*$/
def self.key; :puts; end
def self.name; "puts output checking"; end
def self.options
{
puts_glob: ['Glob to run puts checks over',
default: '{app,lib}/**/*.rb',
variable: 'GLOB',
clobber: :no_puts],
puts_exclude: ['Exclude file or glob from puts checking',
variable: 'GLOB',
type: Array,
default: [],
clobber: :no_puts],
no_puts: ['Disable puts checking', cast: ->(x) { !x }]
}
end
end
To walk through what is being declared here is two constants, DESCRIPTION
and PUTS_REGEX
,
along with three class level methods, key
, name
, and options
. The constants will be
explained when we implement the violation checking. As for the class methods, key
and name
are implemented to match Square’s own implementation of the metrics shipped with Cane. I have
yet to find where they are being used.
The options
method requires more explanation, this method defines the command line switches used
by the metric. The Hash
keys become the command line argument names, so :puts_glob
becomes --puts-glob
in the command line. The values in the Hash
define the defaults for the arguments as well as their
documentation. The first element in the Array
is the help text for the argument. The key :default
defines
the arguments default value. The key :variable
is used to determine how the value should be interpreted and
:type
defines the Ruby type the value should be coerced to. Using :clobber
and then the name of another
argument tells the system that if this other argument is set the ignore this one. Finally :cast
allows you
to define a lambda to do a custom coercion with.
With the options definition out of the way it is a good time to implement some helper functions to make using the arguments easier.
def file_names
Dir[opts.fetch(:puts_glob)].reject { |file| excluded?(file) }
end
def exclusions
@exclusions ||= opts.fetch(:puts_exclude, []).flatten.map do |i|
Dir[i]
end.flatten.to_set
end
def excluded?(file)
exclusions.include?(file)
end
def worker
Cane.task_runner(opts)
end
The methods exlusions
and excluded?
help to filter out files specified by the --puts-exclude
argument,
while file_names
returns the files that the user wishes to run the puts
check on. The worker
method is
just a convenient way to use Cane’s SimpleTaskRunner
when running in a single process or to use Parallel
when using multiple. With these methods in place we can implement the violation checking for our metric.
def violations
return [] if opts[:no_puts]
worker.map(file_names) do |file_name|
find_violations(file_name)
end.flatten
end
def find_violations(file_name)
Cane::File.iterator(file_name).map.with_index do |line, number|
puts_match = line.match(PUTS_REGEX)
result = if !!puts_match
{
file: file_name,
line: number + 1,
label: "Line outputs '#{puts_match[2]}'",
description: DESCRIPTION
}
end
end.compact
end
The violations
method returns an Array
of violations as long as the user does not use the command line
switch --no-puts
, in which case the code returns an empty Array
. The meat of the metric lies in the
find_violations
method, using Cane::File.iterator()
to ensure the all file encoding is accounted for.
We then iterate through the lines of the file to see if they include a call to the methods p
, puts
, or print
using our previously defined constant PUTS_REGEX
. If an offending method call is found then it is added to the
Array
to be returned. Each violations is represented by a Hash
object that has the keys :file
,
which is the file name containing the violation, and :line
, where in the file the violation was found. As well as
:label
, which is specific information about the violation, and :description
, which is a general description that
the console output groups the violations by.
At this point you can use the custom metric by running it like so, cane -r puts_check.rb --check PutsCheck
, or so
the check doesn’t have to be added every time you can add a .cane file to the project with like so:
> cat .cane
-r puts_check.rb
--check PutsCheck
The PutsCheck
metric is now ready to be used as part of a git precommit or as part of a build server process.
Written by Jason Worley who lives and works in Indianapolis building useful things.