cane 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/HISTORY.md +5 -0
- data/README.md +89 -0
- data/bin/cane +11 -0
- data/cane.gemspec +33 -0
- data/lib/cane.rb +49 -0
- data/lib/cane/abc_check.rb +96 -0
- data/lib/cane/abc_max_violation.rb +13 -0
- data/lib/cane/cli.rb +21 -0
- data/lib/cane/cli/spec.rb +114 -0
- data/lib/cane/cli/translator.rb +49 -0
- data/lib/cane/doc_check.rb +51 -0
- data/lib/cane/style_check.rb +98 -0
- data/lib/cane/style_violation.rb +10 -0
- data/lib/cane/threshold_check.rb +30 -0
- data/lib/cane/threshold_violation.rb +15 -0
- data/lib/cane/version.rb +3 -0
- data/lib/cane/violation_formatter.rb +49 -0
- data/spec/abc_check_spec.rb +46 -0
- data/spec/cane_spec.rb +95 -0
- data/spec/doc_check_spec.rb +27 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/style_check_spec.rb +18 -0
- data/spec/threshold_check_spec.rb +12 -0
- data/spec/violation_formatter_spec.rb +16 -0
- metadata +122 -0
data/HISTORY.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# Cane
|
2
|
+
|
3
|
+
Fails your build if code quality thresholds are not met.
|
4
|
+
|
5
|
+
> Discipline will set you free.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
gem install cane
|
10
|
+
cane --abc-glob '{lib,spec}/**/*.rb' --abc-max 15
|
11
|
+
|
12
|
+
Your main build task should run this, probably via `bundle exec`. It will have
|
13
|
+
a non-zero exit code if any quality checks fail. Also, a report:
|
14
|
+
|
15
|
+
> cane
|
16
|
+
|
17
|
+
Methods exceeded maximum allowed ABC complexity (2):
|
18
|
+
|
19
|
+
lib/cane.rb Cane > sample 23
|
20
|
+
lib/cane.rb Cane > sample_2 17
|
21
|
+
|
22
|
+
Lines violated style requirements (2):
|
23
|
+
|
24
|
+
lib/cane.rb:20 Line length >80
|
25
|
+
lib/cane.rb:42 Trailing whitespace
|
26
|
+
|
27
|
+
Classes are not documented (1):
|
28
|
+
lib/cane:3 SomeClass
|
29
|
+
|
30
|
+
Customize behaviour with a wealth of options:
|
31
|
+
|
32
|
+
> cane --help
|
33
|
+
Usage: cane [options]
|
34
|
+
--abc-glob GLOB Glob to run ABC metrics over (default: lib/**/*.rb)
|
35
|
+
--abc-max VALUE Report any methods with complexity greater than VALUE (default: 15)
|
36
|
+
--no-abc Disable ABC checking
|
37
|
+
|
38
|
+
--style-glob GLOB Glob to run style metrics over (default: {lib,spec}/**/*.rb)
|
39
|
+
--style-measure VALUE Max line length (default: 80)
|
40
|
+
--no-style Disable style checking
|
41
|
+
|
42
|
+
--doc-glob GLOB Glob to run documentation metrics over (default: lib/**/*.rb)
|
43
|
+
--no-doc Disable documentation checking
|
44
|
+
|
45
|
+
--gte FILE,THRESHOLD If FILE contains a single number, verify it is >= to THRESHOLD.
|
46
|
+
|
47
|
+
--max-violations VALUE Max allowed violations (default: 0)
|
48
|
+
|
49
|
+
--version Show version
|
50
|
+
-h, --help Show this message
|
51
|
+
|
52
|
+
## Adding to a legacy project
|
53
|
+
|
54
|
+
Cane can be configured to still pass in the presence of a set number of
|
55
|
+
violations using the `--max-violations` option. This is ideal for retrofitting
|
56
|
+
on to an existing application that may already have many violations. By setting
|
57
|
+
the maximum to the current number, no immediate changes will be required to
|
58
|
+
your existing code base, but you will be protected from things getting worse.
|
59
|
+
|
60
|
+
## Integrating with SimpleCov
|
61
|
+
|
62
|
+
Any value in a file can be used as a threshold:
|
63
|
+
|
64
|
+
> echo "89" > coverage/covered_percent
|
65
|
+
> cane --gte 'coverage/covered_percent,90'
|
66
|
+
|
67
|
+
Quality threshold crossed
|
68
|
+
|
69
|
+
coverage/covered_percent is 89, should be >= 90
|
70
|
+
|
71
|
+
You can use a `SimpleCov` formatter to create the required file:
|
72
|
+
|
73
|
+
class SimpleCov::Formatter::QualityFormatter
|
74
|
+
def format(result)
|
75
|
+
SimpleCov::Formatter::HTMLFormatter.new.format(result)
|
76
|
+
File.open("coverage/covered_percent", "w") do |f|
|
77
|
+
f.puts result.source_files.covered_percent.to_f
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
SimpleCov.formatter = SimpleCov::Formatter::QualityFormatter
|
83
|
+
|
84
|
+
## Compatibility
|
85
|
+
|
86
|
+
Requires MRI 1.9, since it depends on the `ripper` library to calculate
|
87
|
+
complexity metrics. This only applies to the Ruby used to run Cane, not the
|
88
|
+
project it is being run against. In other words, you can run Cane against your
|
89
|
+
1.8 project.
|
data/bin/cane
ADDED
data/cane.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/cane/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Xavier Shay"]
|
6
|
+
gem.email = ["xavier@squareup.com"]
|
7
|
+
gem.description =
|
8
|
+
%q{Fails your build if code quality thresholds are not met}
|
9
|
+
gem.summary = %q{
|
10
|
+
Fails your build if code quality thresholds are not met. Provides
|
11
|
+
complexity and style checkers built-in, and allows integration with with
|
12
|
+
custom quality metrics.
|
13
|
+
}
|
14
|
+
gem.homepage = "http://github.com/square/cane"
|
15
|
+
|
16
|
+
gem.executables = []
|
17
|
+
gem.files = Dir.glob("{spec,lib}/**/*.rb") + %w(
|
18
|
+
README.md
|
19
|
+
HISTORY.md
|
20
|
+
cane.gemspec
|
21
|
+
)
|
22
|
+
gem.test_files = Dir.glob("spec/**/*.rb")
|
23
|
+
gem.name = "cane"
|
24
|
+
gem.require_paths = ["lib"]
|
25
|
+
gem.bindir = "bin"
|
26
|
+
gem.executables << "cane"
|
27
|
+
gem.version = Cane::VERSION
|
28
|
+
gem.has_rdoc = false
|
29
|
+
gem.add_dependency 'tailor'
|
30
|
+
gem.add_development_dependency 'rspec', '~> 2.0'
|
31
|
+
gem.add_development_dependency 'rake'
|
32
|
+
gem.add_development_dependency 'simplecov'
|
33
|
+
end
|
data/lib/cane.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'cane/abc_check'
|
2
|
+
require 'cane/style_check'
|
3
|
+
require 'cane/doc_check'
|
4
|
+
require 'cane/threshold_check'
|
5
|
+
require 'cane/violation_formatter'
|
6
|
+
|
7
|
+
module Cane
|
8
|
+
def run(opts)
|
9
|
+
Runner.new(opts).run
|
10
|
+
end
|
11
|
+
module_function :run
|
12
|
+
|
13
|
+
# Orchestrates the running of checks per the provided configuration, and
|
14
|
+
# hands the result to a formatter for display. This is the core of the
|
15
|
+
# application, but for the actual entry point see `Cane::CLI`.
|
16
|
+
class Runner
|
17
|
+
CHECKERS = {
|
18
|
+
abc: AbcCheck,
|
19
|
+
style: StyleCheck,
|
20
|
+
doc: DocCheck,
|
21
|
+
threshold: ThresholdCheck
|
22
|
+
}
|
23
|
+
|
24
|
+
def initialize(opts)
|
25
|
+
@opts = opts
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
outputter.print ViolationFormatter.new(violations)
|
30
|
+
|
31
|
+
violations.length <= opts.fetch(:max_violations)
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
attr_reader :opts
|
37
|
+
|
38
|
+
def violations
|
39
|
+
@violations ||= CHECKERS.
|
40
|
+
select { |key, _| opts.has_key?(key) }.
|
41
|
+
map { |key, check| check.new(opts.fetch(key)).violations }.
|
42
|
+
flatten
|
43
|
+
end
|
44
|
+
|
45
|
+
def outputter
|
46
|
+
opts.fetch(:out, $stdout)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'ripper'
|
2
|
+
|
3
|
+
require 'cane/abc_max_violation'
|
4
|
+
|
5
|
+
module Cane
|
6
|
+
|
7
|
+
# Creates violations for methods that are too complicated using a simple
|
8
|
+
# algorithm run against the parse tree of a file to count assignments,
|
9
|
+
# branches, and conditionals. Borrows heavily from metric_abc.
|
10
|
+
class AbcCheck < Struct.new(:opts)
|
11
|
+
def violations
|
12
|
+
order file_names.map { |file_name|
|
13
|
+
find_violations(file_name)
|
14
|
+
}.flatten
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def find_violations(file_name)
|
20
|
+
ast = sexps_from_file(file_name)
|
21
|
+
|
22
|
+
process_ast(ast).
|
23
|
+
select { |nesting, complexity| complexity > max_allowed_complexity }.
|
24
|
+
map { |x| AbcMaxViolation.new(file_name, x.first, x.last) }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Recursive function to process an AST. The `complexity` variable mutates,
|
28
|
+
# which is a bit confusing. `nesting` does not.
|
29
|
+
def process_ast(node, complexity = {}, nesting = [])
|
30
|
+
if method_nodes.include?(node[0])
|
31
|
+
nesting = nesting + [node[1][1]]
|
32
|
+
complexity[nesting.join(" > ")] = calculate_abc(node)
|
33
|
+
elsif container_nodes.include?(node[0])
|
34
|
+
parent = if node[1][1][1].is_a?(Symbol)
|
35
|
+
node[1][1][1]
|
36
|
+
else
|
37
|
+
node[1][-1][1]
|
38
|
+
end
|
39
|
+
nesting = nesting + [parent]
|
40
|
+
end
|
41
|
+
|
42
|
+
if node.is_a? Array
|
43
|
+
node[1..-1].each { |n| process_ast(n, complexity, nesting) if n }
|
44
|
+
end
|
45
|
+
complexity
|
46
|
+
end
|
47
|
+
|
48
|
+
def sexps_from_file(file_name)
|
49
|
+
Ripper::SexpBuilder.new(File.open(file_name, 'r:utf-8').read).parse
|
50
|
+
end
|
51
|
+
|
52
|
+
def max_allowed_complexity
|
53
|
+
opts.fetch(:max)
|
54
|
+
end
|
55
|
+
|
56
|
+
def calculate_abc(method_node)
|
57
|
+
a = count_nodes(method_node, assignment_nodes)
|
58
|
+
b = count_nodes(method_node, branch_nodes) + 1
|
59
|
+
c = count_nodes(method_node, condition_nodes)
|
60
|
+
abc = Math.sqrt(a**2 + b**2 + c**2).round
|
61
|
+
abc
|
62
|
+
end
|
63
|
+
|
64
|
+
def count_nodes(node, types)
|
65
|
+
node.flatten.select { |n| types.include?(n) }.length
|
66
|
+
end
|
67
|
+
|
68
|
+
def file_names
|
69
|
+
Dir[opts.fetch(:files)]
|
70
|
+
end
|
71
|
+
|
72
|
+
def order(result)
|
73
|
+
result.sort_by(&:complexity).reverse
|
74
|
+
end
|
75
|
+
|
76
|
+
def assignment_nodes
|
77
|
+
[:assign, :opassign]
|
78
|
+
end
|
79
|
+
|
80
|
+
def method_nodes
|
81
|
+
[:def]
|
82
|
+
end
|
83
|
+
|
84
|
+
def container_nodes
|
85
|
+
[:class, :module]
|
86
|
+
end
|
87
|
+
|
88
|
+
def branch_nodes
|
89
|
+
[:call, :fcall, :brace_block, :do_block]
|
90
|
+
end
|
91
|
+
|
92
|
+
def condition_nodes
|
93
|
+
[:==, :===, :"<>", :"<=", :">=", :"=~", :>, :<, :else, :"<=>"]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Cane
|
2
|
+
|
3
|
+
# Value object used by AbcCheck for a method that is too complicated.
|
4
|
+
class AbcMaxViolation < Struct.new(:file_name, :detail, :complexity)
|
5
|
+
def columns
|
6
|
+
[file_name, detail, complexity]
|
7
|
+
end
|
8
|
+
|
9
|
+
def description
|
10
|
+
"Methods exceeded maximum allowed ABC complexity"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/cane/cli.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'cane'
|
2
|
+
require 'cane/version'
|
3
|
+
|
4
|
+
require 'cane/cli/spec'
|
5
|
+
require 'cane/cli/translator'
|
6
|
+
|
7
|
+
module Cane
|
8
|
+
module CLI
|
9
|
+
|
10
|
+
def run(args)
|
11
|
+
opts = Spec.new.parse(args)
|
12
|
+
if opts
|
13
|
+
Cane.run(opts)
|
14
|
+
else
|
15
|
+
true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
module_function :run
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'cane/cli/translator'
|
2
|
+
|
3
|
+
module Cane
|
4
|
+
module CLI
|
5
|
+
|
6
|
+
# Provides a specification for the command line interface that drives
|
7
|
+
# documentation, parsing, and default values.
|
8
|
+
class Spec
|
9
|
+
DEFAULTS = {
|
10
|
+
abc_glob: 'lib/**/*.rb',
|
11
|
+
abc_max: '15',
|
12
|
+
style_glob: '{lib,spec}/**/*.rb',
|
13
|
+
style_measure: '80',
|
14
|
+
doc_glob: 'lib/**/*.rb',
|
15
|
+
max_violations: '0',
|
16
|
+
}
|
17
|
+
|
18
|
+
# Exception to indicate that no further processing is required and the
|
19
|
+
# program can exit. This is used to handle --help and --version flags.
|
20
|
+
class OptionsHandled < RuntimeError; end
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
add_abc_options
|
24
|
+
add_style_options
|
25
|
+
add_doc_options
|
26
|
+
add_threshold_options
|
27
|
+
add_cane_options
|
28
|
+
|
29
|
+
add_version
|
30
|
+
add_help
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse(args)
|
34
|
+
parser.parse!(args)
|
35
|
+
Translator.new(options, DEFAULTS).to_hash
|
36
|
+
rescue OptionsHandled
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_abc_options
|
41
|
+
add_option %w(--abc-glob GLOB), "Glob to run ABC metrics over"
|
42
|
+
add_option %w(--abc-max VALUE), "Ignore methods under this complexity"
|
43
|
+
add_option %w(--no-abc), "Disable ABC checking"
|
44
|
+
|
45
|
+
parser.separator ""
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_style_options
|
49
|
+
add_option %w(--style-glob GLOB), "Glob to run style metrics over"
|
50
|
+
add_option %w(--style-measure VALUE), "Max line length"
|
51
|
+
add_option %w(--no-style), "Disable style checking"
|
52
|
+
|
53
|
+
parser.separator ""
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_doc_options
|
57
|
+
add_option %w(--doc-glob GLOB), "Glob to run documentation checks over"
|
58
|
+
add_option %w(--no-doc), "Disable documentation checking"
|
59
|
+
|
60
|
+
parser.separator ""
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_threshold_options
|
64
|
+
desc = "If FILE contains a number, verify it is >= to THRESHOLD."
|
65
|
+
parser.on("--gte FILE,THRESHOLD", Array, desc) do |opts|
|
66
|
+
(options[:threshold] ||= []) << opts.unshift(:>=)
|
67
|
+
end
|
68
|
+
|
69
|
+
parser.separator ""
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_cane_options
|
73
|
+
add_option %w(--max-violations VALUE), "Max allowed violations"
|
74
|
+
|
75
|
+
parser.separator ""
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_version
|
79
|
+
parser.on_tail("--version", "Show version") do
|
80
|
+
puts Cane::VERSION
|
81
|
+
raise OptionsHandled
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_help
|
86
|
+
parser.on_tail("-h", "--help", "Show this message") do
|
87
|
+
puts parser
|
88
|
+
raise OptionsHandled
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_option(option, description)
|
93
|
+
option_key = option[0].gsub('--', '').tr('-', '_').to_sym
|
94
|
+
|
95
|
+
if DEFAULTS.has_key?(option_key)
|
96
|
+
description += " (default: %s)" % DEFAULTS[option_key]
|
97
|
+
end
|
98
|
+
|
99
|
+
parser.on(option.join(' '), description) do |v|
|
100
|
+
options[option_key] = v
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def options
|
105
|
+
@options ||= {}
|
106
|
+
end
|
107
|
+
|
108
|
+
def parser
|
109
|
+
@parser ||= OptionParser.new
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Cane
|
2
|
+
module CLI
|
3
|
+
|
4
|
+
# Translates CLI options with given defaults to a hash suitable to be
|
5
|
+
# passed to `Cane.run`.
|
6
|
+
class Translator < Struct.new(:options, :defaults)
|
7
|
+
def to_hash
|
8
|
+
result = {}
|
9
|
+
translate_abc_options(result)
|
10
|
+
translate_doc_options(result)
|
11
|
+
translate_style_options(result)
|
12
|
+
|
13
|
+
result[:threshold] = options.fetch(:threshold, [])
|
14
|
+
result[:max_violations] = option_with_default(:max_violations).to_i
|
15
|
+
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
def translate_abc_options(result)
|
20
|
+
result[:abc] = {
|
21
|
+
files: option_with_default(:abc_glob),
|
22
|
+
max: option_with_default(:abc_max).to_i
|
23
|
+
} unless check_disabled(:no_abc, [:abc_glob, :abc_max])
|
24
|
+
end
|
25
|
+
|
26
|
+
def translate_style_options(result)
|
27
|
+
result[:style] = {
|
28
|
+
files: option_with_default(:style_glob),
|
29
|
+
measure: option_with_default(:style_measure).to_i,
|
30
|
+
} unless check_disabled(:no_style, [:style_glob])
|
31
|
+
end
|
32
|
+
|
33
|
+
def translate_doc_options(result)
|
34
|
+
result[:doc] = {
|
35
|
+
files: option_with_default(:doc_glob),
|
36
|
+
} unless check_disabled(:no_doc, [:doc_glob])
|
37
|
+
end
|
38
|
+
|
39
|
+
def check_disabled(check, params)
|
40
|
+
((params + [check]) & options.keys) == [check]
|
41
|
+
end
|
42
|
+
|
43
|
+
def option_with_default(key)
|
44
|
+
options.fetch(key, defaults.fetch(key))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Cane
|
2
|
+
|
3
|
+
# Creates violations for class definitions that do not have an explantory
|
4
|
+
# comment immediately preceeding.
|
5
|
+
class DocCheck < Struct.new(:opts)
|
6
|
+
def violations
|
7
|
+
file_names.map { |file_name|
|
8
|
+
find_violations(file_name)
|
9
|
+
}.flatten
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_violations(file_name)
|
13
|
+
last_line = ""
|
14
|
+
File.open(file_name, 'r:utf-8').lines.map.with_index do |line, number|
|
15
|
+
result = if class_definition?(line) && !comment?(last_line)
|
16
|
+
UndocumentedClassViolation.new(file_name, number + 1, line)
|
17
|
+
end
|
18
|
+
last_line = line
|
19
|
+
result
|
20
|
+
end.compact
|
21
|
+
end
|
22
|
+
|
23
|
+
def file_names
|
24
|
+
Dir[opts.fetch(:files)]
|
25
|
+
end
|
26
|
+
|
27
|
+
def class_definition?(line)
|
28
|
+
line =~ /^\s*class\s/
|
29
|
+
end
|
30
|
+
|
31
|
+
def comment?(line)
|
32
|
+
line =~ /^\s*#/
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Value object used by DocCheck.
|
37
|
+
class UndocumentedClassViolation < Struct.new(:file_name, :number, :line)
|
38
|
+
def description
|
39
|
+
"Classes are not documented"
|
40
|
+
end
|
41
|
+
|
42
|
+
def columns
|
43
|
+
["%s:%i" % [file_name, number], extract_class_name(line)]
|
44
|
+
end
|
45
|
+
|
46
|
+
def extract_class_name(line)
|
47
|
+
line.match(/class (\S+)/)[1]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'tailor'
|
2
|
+
|
3
|
+
require 'cane/style_violation'
|
4
|
+
|
5
|
+
module Cane
|
6
|
+
|
7
|
+
# Creates violations for files that do not meet style conventions. This uses
|
8
|
+
# the `tailor` gem, configured to exclude some of its less reliable checks
|
9
|
+
# (mostly spacing around punctuation).
|
10
|
+
class StyleCheck < Struct.new(:opts)
|
11
|
+
def violations
|
12
|
+
Dir[opts.fetch(:files)].map do |file_name|
|
13
|
+
find_violations_in_file(file_name)
|
14
|
+
end.flatten
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def find_violations_in_file(file_name)
|
20
|
+
source = File.open(file_name, 'r:utf-8')
|
21
|
+
file_path = Pathname.new(file_name)
|
22
|
+
|
23
|
+
source.each_line.map.with_index do |source_line, line_number|
|
24
|
+
violations_for_line(file_path, source_line, line_number)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def violations_for_line(file_path, source_line, line_number)
|
29
|
+
FileLine.new(source_line, opts).problems.map do |message|
|
30
|
+
StyleViolation.new(file_path, line_number + 1, message)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# The `tailor` gem was not designed to be used as a library, so interfacing
|
36
|
+
# with it is a bit of a mess. This wrapper is attempt to confine that mess to
|
37
|
+
# a single point in the code.
|
38
|
+
class FileLine < Tailor::FileLine
|
39
|
+
attr_accessor :opts
|
40
|
+
|
41
|
+
def initialize(source_line, opts)
|
42
|
+
self.opts = opts
|
43
|
+
|
44
|
+
super(source_line, nil, nil)
|
45
|
+
end
|
46
|
+
|
47
|
+
def find_problems
|
48
|
+
# This is weird. These methods actually have side-effects! We capture
|
49
|
+
# the effects my monkey-patching #print_problem below.
|
50
|
+
spacing_problems
|
51
|
+
method_line? && camel_case_method?
|
52
|
+
class_line? && snake_case_class?
|
53
|
+
too_long?
|
54
|
+
end
|
55
|
+
|
56
|
+
def print_problem(message)
|
57
|
+
@problems << message.gsub(/\[.+\]\s+/, '')
|
58
|
+
end
|
59
|
+
|
60
|
+
def problems
|
61
|
+
@problems = []
|
62
|
+
find_problems
|
63
|
+
@problems
|
64
|
+
end
|
65
|
+
|
66
|
+
# A copy of the parent method that only uses a small subset of the spacing
|
67
|
+
# checks we actually want (the others are too buggy or controversial).
|
68
|
+
def spacing_problems
|
69
|
+
spacing_conditions.each_pair do |condition, values|
|
70
|
+
unless self.scan(values.first).empty?
|
71
|
+
print_problem values[1]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def spacing_conditions
|
77
|
+
SPACING_CONDITIONS.select {|k, _|
|
78
|
+
[:hard_tabbed, :trailing_whitespace].include?(k)
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
# Copy of parent method using a configurable line length.
|
83
|
+
def too_long?
|
84
|
+
length = self.length
|
85
|
+
if length > line_length_max
|
86
|
+
print_problem "Line is >#{line_length_max} characters (#{length})"
|
87
|
+
return true
|
88
|
+
end
|
89
|
+
|
90
|
+
false
|
91
|
+
end
|
92
|
+
|
93
|
+
def line_length_max
|
94
|
+
opts.fetch(:measure)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'cane/threshold_violation'
|
2
|
+
|
3
|
+
# Configurable check that allows the contents of a file to be compared against
|
4
|
+
# a given value.
|
5
|
+
class ThresholdCheck < Struct.new(:checks)
|
6
|
+
def violations
|
7
|
+
checks.map do |operator, file, limit|
|
8
|
+
value = value_from_file(file)
|
9
|
+
|
10
|
+
unless value.send(operator, limit.to_f)
|
11
|
+
ThresholdViolation.new(file, operator, value, limit)
|
12
|
+
end
|
13
|
+
end.compact
|
14
|
+
end
|
15
|
+
|
16
|
+
def value_from_file(file)
|
17
|
+
begin
|
18
|
+
contents = File.read(file).chomp.to_f
|
19
|
+
rescue Errno::ENOENT
|
20
|
+
UnavailableValue.new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Null object for all cases when the value to be compared against cannot be
|
25
|
+
# read.
|
26
|
+
class UnavailableValue
|
27
|
+
def >=(_); false end
|
28
|
+
def to_s; 'unavailable' end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Value object used by ThresholdCheck.
|
2
|
+
class ThresholdViolation < Struct.new(:name, :operator, :value, :limit)
|
3
|
+
def description
|
4
|
+
"Quality threshold crossed"
|
5
|
+
end
|
6
|
+
|
7
|
+
def columns
|
8
|
+
["%s is %s, should be %s %s" % [
|
9
|
+
name,
|
10
|
+
value,
|
11
|
+
operator,
|
12
|
+
limit
|
13
|
+
]]
|
14
|
+
end
|
15
|
+
end
|
data/lib/cane/version.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module Cane
|
4
|
+
|
5
|
+
# Computes a string to be displayed as output from an array of violations
|
6
|
+
# computed by the checks.
|
7
|
+
class ViolationFormatter < Struct.new(:violations)
|
8
|
+
def to_s
|
9
|
+
return '' if violations.empty?
|
10
|
+
|
11
|
+
grouped_violations.map do |description, violations|
|
12
|
+
format_group_header(description, violations) +
|
13
|
+
format_violations(violations)
|
14
|
+
end.flatten.join("\n") + "\n\n"
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def format_group_header(description, violations)
|
20
|
+
["", "%s (%i):" % [description, violations.length], ""]
|
21
|
+
end
|
22
|
+
|
23
|
+
def format_violations(violations)
|
24
|
+
column_widths = calculate_columm_widths(violations)
|
25
|
+
|
26
|
+
violations.map do |violation|
|
27
|
+
format_violation(violation, column_widths)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def format_violation(violation, column_widths)
|
32
|
+
[
|
33
|
+
' ' + violation.columns.map.with_index { |column, index|
|
34
|
+
"%-#{column_widths[index]}s" % column
|
35
|
+
}.join(' ')
|
36
|
+
]
|
37
|
+
end
|
38
|
+
|
39
|
+
def calculate_columm_widths(violations)
|
40
|
+
violations.map { |violation|
|
41
|
+
violation.columns.map { |x| x.to_s.length }
|
42
|
+
}.transpose.map(&:max)
|
43
|
+
end
|
44
|
+
|
45
|
+
def grouped_violations
|
46
|
+
violations.group_by(&:description)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'cane/abc_check'
|
4
|
+
|
5
|
+
describe Cane::AbcCheck do
|
6
|
+
it 'creates an AbcMaxViolation for each method above the threshold' do
|
7
|
+
file_name = make_file(<<-RUBY)
|
8
|
+
class Harness
|
9
|
+
def not_complex
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def complex_method(a)
|
14
|
+
b = a
|
15
|
+
return b if b > 3
|
16
|
+
end
|
17
|
+
end
|
18
|
+
RUBY
|
19
|
+
|
20
|
+
violations = described_class.new(files: file_name, max: 1).violations
|
21
|
+
violations.length.should == 1
|
22
|
+
violations[0].should be_instance_of(Cane::AbcMaxViolation)
|
23
|
+
violations[0].to_s.should include("Harness")
|
24
|
+
violations[0].to_s.should include("complex_method")
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'sorts violations by complexity' do
|
28
|
+
file_name = make_file(<<-RUBY)
|
29
|
+
class Harness
|
30
|
+
def not_complex
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def complex_method(a)
|
35
|
+
b = a
|
36
|
+
return b if b > 3
|
37
|
+
end
|
38
|
+
end
|
39
|
+
RUBY
|
40
|
+
|
41
|
+
violations = described_class.new(files: file_name, max: 0).violations
|
42
|
+
violations.length.should == 2
|
43
|
+
complexities = violations.map(&:complexity)
|
44
|
+
complexities.should == complexities.sort.reverse
|
45
|
+
end
|
46
|
+
end
|
data/spec/cane_spec.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require "stringio"
|
3
|
+
require 'cane/cli'
|
4
|
+
|
5
|
+
describe 'Cane' do
|
6
|
+
def capture_stdout &block
|
7
|
+
real_stdout, $stdout = $stdout, StringIO.new
|
8
|
+
yield
|
9
|
+
$stdout.string
|
10
|
+
ensure
|
11
|
+
$stdout = real_stdout
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(cli_args)
|
15
|
+
result = nil
|
16
|
+
output = capture_stdout do
|
17
|
+
default_cli_opts = %w(--no-style --no-abc --no-doc)
|
18
|
+
result = Cane::CLI.run(default_cli_opts + cli_args.split(' '))
|
19
|
+
end
|
20
|
+
|
21
|
+
[output, result ? 0 : 1]
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'fails if ABC metric does not meet requirements' do
|
25
|
+
file_name = make_file(<<-RUBY)
|
26
|
+
class Harness
|
27
|
+
def complex_method(a)
|
28
|
+
if a < 2
|
29
|
+
return "low"
|
30
|
+
else
|
31
|
+
return "high"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
RUBY
|
36
|
+
|
37
|
+
_, exitstatus = run("--abc-glob #{file_name} --abc-max 1")
|
38
|
+
|
39
|
+
exitstatus.should == 1
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'fails if style metrics do not meet requirements' do
|
43
|
+
file_name = make_file("whitespace ")
|
44
|
+
|
45
|
+
output, exitstatus = run("--style-glob #{file_name}")
|
46
|
+
exitstatus.should == 1
|
47
|
+
output.should include("Lines violated style requirements")
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'allows measure to be configured' do
|
51
|
+
file_name = make_file("toolong")
|
52
|
+
|
53
|
+
output, exitstatus = run("--style-glob #{file_name} --style-measure 3")
|
54
|
+
exitstatus.should == 1
|
55
|
+
output.should include("Lines violated style requirements")
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'allows upper bound of failed checks' do
|
59
|
+
file_name = make_file("whitespace ")
|
60
|
+
|
61
|
+
output, exitstatus = run("--style-glob #{file_name} --max-violations 1")
|
62
|
+
exitstatus.should == 0
|
63
|
+
output.should include("Lines violated style requirements")
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'allows checking of a value in a file' do
|
67
|
+
file_name = make_file("89")
|
68
|
+
|
69
|
+
output, exitstatus = run("--gte #{file_name},90")
|
70
|
+
exitstatus.should == 1
|
71
|
+
output.should include("Quality threshold crossed")
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'allows checking of class documentation' do
|
75
|
+
file_name = make_file("class NoDoc")
|
76
|
+
|
77
|
+
output, exitstatus = run("--doc-glob #{file_name}")
|
78
|
+
exitstatus.should == 1
|
79
|
+
output.should include("Classes are not documented")
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'displays a help message' do
|
83
|
+
output, exitstatus = run("--help")
|
84
|
+
|
85
|
+
exitstatus.should == 0
|
86
|
+
output.should include("Usage:")
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'displays version' do
|
90
|
+
output, exitstatus = run("--version")
|
91
|
+
|
92
|
+
exitstatus.should == 0
|
93
|
+
output.should include(Cane::VERSION)
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'cane/doc_check'
|
4
|
+
|
5
|
+
describe Cane::DocCheck do
|
6
|
+
it 'creates a DocViolation for each undocumented class' do
|
7
|
+
file_name = make_file <<-RUBY
|
8
|
+
# This class is documented
|
9
|
+
class Doc; end
|
10
|
+
class NoDoc; end # No doc
|
11
|
+
class AlsoNoDoc; end
|
12
|
+
[:class]
|
13
|
+
# class Ignore
|
14
|
+
RUBY
|
15
|
+
|
16
|
+
violations = described_class.new(files: file_name).violations
|
17
|
+
violations.length.should == 2
|
18
|
+
|
19
|
+
violations[0].should be_instance_of(Cane::UndocumentedClassViolation)
|
20
|
+
violations[0].file_name.should == file_name
|
21
|
+
violations[0].number.should == 3
|
22
|
+
|
23
|
+
violations[1].should be_instance_of(Cane::UndocumentedClassViolation)
|
24
|
+
violations[1].file_name.should == file_name
|
25
|
+
violations[1].number.should == 4
|
26
|
+
end
|
27
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
# Keep a reference to all tempfiles so they are not garbage collected until the
|
4
|
+
# process exits.
|
5
|
+
$tempfiles = []
|
6
|
+
|
7
|
+
def make_file(content)
|
8
|
+
tempfile = Tempfile.new('cane')
|
9
|
+
$tempfiles << tempfile
|
10
|
+
tempfile.print(content)
|
11
|
+
tempfile.flush
|
12
|
+
tempfile.path
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'simplecov'
|
16
|
+
|
17
|
+
class SimpleCov::Formatter::QualityFormatter
|
18
|
+
def format(result)
|
19
|
+
SimpleCov::Formatter::HTMLFormatter.new.format(result)
|
20
|
+
File.open("coverage/covered_percent", "w") do |f|
|
21
|
+
f.puts result.source_files.covered_percent.to_i
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
SimpleCov.formatter = SimpleCov::Formatter::QualityFormatter
|
27
|
+
SimpleCov.start
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'cane/style_check'
|
4
|
+
|
5
|
+
describe Cane::StyleCheck do
|
6
|
+
it 'creates a StyleViolation for each method above the threshold' do
|
7
|
+
ruby = [
|
8
|
+
"def test ",
|
9
|
+
"\t1",
|
10
|
+
"end"
|
11
|
+
].join("\n")
|
12
|
+
file_name = make_file(ruby)
|
13
|
+
|
14
|
+
violations = Cane::StyleCheck.new(files: file_name, measure: 80).violations
|
15
|
+
violations.length.should == 2
|
16
|
+
violations[0].should be_instance_of(StyleViolation)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'cane/threshold_check'
|
4
|
+
|
5
|
+
describe ThresholdCheck do
|
6
|
+
it 'returns a value of unavailable when file cannot be read' do
|
7
|
+
check = ThresholdCheck.new([[:>=, 'bogus_file', 20]])
|
8
|
+
violations = check.violations
|
9
|
+
violations.length.should == 1
|
10
|
+
violations[0].to_s.should include("unavailable")
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'cane/violation_formatter'
|
4
|
+
|
5
|
+
describe Cane::ViolationFormatter do
|
6
|
+
def violation(description)
|
7
|
+
stub("violation",
|
8
|
+
description: description,
|
9
|
+
columns: []
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'includes number of violations in the group header' do
|
14
|
+
described_class.new([violation("FAIL")]).to_s.should include("(1)")
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cane
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Xavier Shay
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: tailor
|
16
|
+
requirement: &2156048880 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2156048880
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
requirement: &2156048380 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2156048380
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rake
|
38
|
+
requirement: &2156047960 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2156047960
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: simplecov
|
49
|
+
requirement: &2156047500 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *2156047500
|
58
|
+
description: Fails your build if code quality thresholds are not met
|
59
|
+
email:
|
60
|
+
- xavier@squareup.com
|
61
|
+
executables:
|
62
|
+
- cane
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- spec/abc_check_spec.rb
|
67
|
+
- spec/cane_spec.rb
|
68
|
+
- spec/doc_check_spec.rb
|
69
|
+
- spec/spec_helper.rb
|
70
|
+
- spec/style_check_spec.rb
|
71
|
+
- spec/threshold_check_spec.rb
|
72
|
+
- spec/violation_formatter_spec.rb
|
73
|
+
- lib/cane/abc_check.rb
|
74
|
+
- lib/cane/abc_max_violation.rb
|
75
|
+
- lib/cane/cli/spec.rb
|
76
|
+
- lib/cane/cli/translator.rb
|
77
|
+
- lib/cane/cli.rb
|
78
|
+
- lib/cane/doc_check.rb
|
79
|
+
- lib/cane/style_check.rb
|
80
|
+
- lib/cane/style_violation.rb
|
81
|
+
- lib/cane/threshold_check.rb
|
82
|
+
- lib/cane/threshold_violation.rb
|
83
|
+
- lib/cane/version.rb
|
84
|
+
- lib/cane/violation_formatter.rb
|
85
|
+
- lib/cane.rb
|
86
|
+
- README.md
|
87
|
+
- HISTORY.md
|
88
|
+
- cane.gemspec
|
89
|
+
- bin/cane
|
90
|
+
homepage: http://github.com/square/cane
|
91
|
+
licenses: []
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ! '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubyforge_project:
|
110
|
+
rubygems_version: 1.8.10
|
111
|
+
signing_key:
|
112
|
+
specification_version: 3
|
113
|
+
summary: Fails your build if code quality thresholds are not met. Provides complexity
|
114
|
+
and style checkers built-in, and allows integration with with custom quality metrics.
|
115
|
+
test_files:
|
116
|
+
- spec/abc_check_spec.rb
|
117
|
+
- spec/cane_spec.rb
|
118
|
+
- spec/doc_check_spec.rb
|
119
|
+
- spec/spec_helper.rb
|
120
|
+
- spec/style_check_spec.rb
|
121
|
+
- spec/threshold_check_spec.rb
|
122
|
+
- spec/violation_formatter_spec.rb
|