cane 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|