cane 1.4.0 → 2.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 +12 -0
- data/README.md +9 -25
- data/lib/cane.rb +7 -18
- data/lib/cane/abc_check.rb +85 -22
- data/lib/cane/cli.rb +3 -5
- data/lib/cane/cli/spec.rb +59 -53
- data/lib/cane/doc_check.rb +30 -16
- data/lib/cane/encoding_aware_iterator.rb +35 -0
- data/lib/cane/file.rb +22 -0
- data/lib/cane/rake_task.rb +12 -41
- data/lib/cane/style_check.rb +36 -8
- data/lib/cane/threshold_check.rb +47 -22
- data/lib/cane/version.rb +1 -1
- data/lib/cane/violation_formatter.rb +33 -25
- data/spec/abc_check_spec.rb +53 -21
- data/spec/cane_spec.rb +21 -12
- data/spec/doc_check_spec.rb +30 -9
- data/spec/encoding_aware_iterator_spec.rb +24 -0
- data/spec/rake_task_spec.rb +13 -0
- data/spec/style_check_spec.rb +10 -4
- data/spec/threshold_check_spec.rb +4 -3
- data/spec/violation_formatter_spec.rb +4 -5
- metadata +8 -7
- data/lib/cane/abc_max_violation.rb +0 -15
- data/lib/cane/cli/translator.rb +0 -71
- data/lib/cane/style_violation.rb +0 -10
- data/lib/cane/syntax_violation.rb +0 -20
- data/lib/cane/threshold_violation.rb +0 -15
data/HISTORY.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# Cane History
|
2
2
|
|
3
|
+
## 2.0.0 - 19 August 2012 (35cae086)
|
4
|
+
|
5
|
+
* ABC check labels `MyClass = Struct.new {}` and `Class.new` correctly (#20)
|
6
|
+
* Magic comments (`# encoding: utf-8`) are not recognized as appropriate class documentation (#21)
|
7
|
+
* Invalid UTF-8 is handled correctly (#22)
|
8
|
+
* Gracefully handle unknown options
|
9
|
+
* ABC check output uses a standard format (Foo::Bar#method rather than Foo > Bar > method)
|
10
|
+
* **BREAKING** Add `--abc-exclude`, `--style-exclude` CLI flags, remove YAML support
|
11
|
+
* **BREAKING-INTERNAL** Use hashes rather than explicit violation classes
|
12
|
+
* **BREAKING-INTERNAL** Remove translator class, pass CLI args direct to checks
|
13
|
+
* **INTERNAL** Wiring in a new check only requires changing one file (#15)
|
14
|
+
|
3
15
|
## 1.4.0 - 2 July 2012 (1afc999d)
|
4
16
|
|
5
17
|
* Allow files and methods to be whitelisted (#16)
|
data/README.md
CHANGED
@@ -16,8 +16,8 @@ a non-zero exit code if any quality checks fail. Also, a report:
|
|
16
16
|
|
17
17
|
Methods exceeded maximum allowed ABC complexity (2):
|
18
18
|
|
19
|
-
lib/cane.rb Cane
|
20
|
-
lib/cane.rb Cane
|
19
|
+
lib/cane.rb Cane#sample 23
|
20
|
+
lib/cane.rb Cane#sample_2 17
|
21
21
|
|
22
22
|
Lines violated style requirements (2):
|
23
23
|
|
@@ -36,21 +36,22 @@ Customize behaviour with a wealth of options:
|
|
36
36
|
|
37
37
|
--abc-glob GLOB Glob to run ABC metrics over (default: {app,lib}/**/*.rb)
|
38
38
|
--abc-max VALUE Ignore methods under this complexity (default: 15)
|
39
|
+
--abc-exclude METHOD Exclude method from analysis (eg. Foo::Bar#method)
|
39
40
|
--no-abc Disable ABC checking
|
40
41
|
|
41
|
-
--style-glob GLOB Glob to run style
|
42
|
+
--style-glob GLOB Glob to run style checks over (default: {app,lib,spec}/**/*.rb)
|
42
43
|
--style-measure VALUE Max line length (default: 80)
|
44
|
+
--style-exclude FILE Exclude file from style checking
|
43
45
|
--no-style Disable style checking
|
44
46
|
|
45
|
-
--doc-glob GLOB Glob to run
|
47
|
+
--doc-glob GLOB Glob to run doc checks over (default: {app,lib}/**/*.rb)
|
46
48
|
--no-doc Disable documentation checking
|
47
49
|
|
48
|
-
--gte FILE,THRESHOLD If FILE contains a number, verify it is >= to THRESHOLD
|
50
|
+
--gte FILE,THRESHOLD If FILE contains a number, verify it is >= to THRESHOLD
|
49
51
|
|
50
52
|
--max-violations VALUE Max allowed violations (default: 0)
|
51
|
-
--exclusions-file FILE YAML file containing a list of exclusions
|
52
53
|
|
53
|
-
|
54
|
+
-v, --version Show version
|
54
55
|
-h, --help Show this message
|
55
56
|
|
56
57
|
Set default options into a `.cane` file:
|
@@ -74,6 +75,7 @@ It works just like this:
|
|
74
75
|
cane.abc_max = 10
|
75
76
|
cane.add_threshold 'coverage/covered_percent', :>=, 99
|
76
77
|
cane.no_style = true
|
78
|
+
cane.abc_exclude = %w(Foo::Bar.some_method)
|
77
79
|
end
|
78
80
|
|
79
81
|
task :default => :quality
|
@@ -118,24 +120,6 @@ You can use a `SimpleCov` formatter to create the required file:
|
|
118
120
|
|
119
121
|
SimpleCov.formatter = SimpleCov::Formatter::QualityFormatter
|
120
122
|
|
121
|
-
## Defining Exclusions
|
122
|
-
|
123
|
-
Occasionally, you may want to permanently ignore specific cane violations.
|
124
|
-
Create a YAML file like so:
|
125
|
-
|
126
|
-
abc:
|
127
|
-
- Some::Fully::Qualified::Class.some_class_method
|
128
|
-
- Some::Fully::Qualified::Class#some_instance_method
|
129
|
-
style:
|
130
|
-
relative/path/to/some/file.rb
|
131
|
-
relative/path/to/some/other/file.rb
|
132
|
-
|
133
|
-
Tell cane about this file using the `--exclusions-file` option:
|
134
|
-
|
135
|
-
> cane --exclusions-file path/to/exclusions.yml
|
136
|
-
|
137
|
-
Currently, only the abc and style checks support exclusions.
|
138
|
-
|
139
123
|
## Compatibility
|
140
124
|
|
141
125
|
Requires MRI 1.9, since it depends on the `ripper` library to calculate
|
data/lib/cane.rb
CHANGED
@@ -1,12 +1,8 @@
|
|
1
|
-
require 'cane/abc_check'
|
2
|
-
require 'cane/style_check'
|
3
|
-
require 'cane/doc_check'
|
4
|
-
require 'cane/threshold_check'
|
5
1
|
require 'cane/violation_formatter'
|
6
2
|
|
7
3
|
module Cane
|
8
|
-
def run(
|
9
|
-
Runner.new(
|
4
|
+
def run(*args)
|
5
|
+
Runner.new(*args).run
|
10
6
|
end
|
11
7
|
module_function :run
|
12
8
|
|
@@ -14,15 +10,9 @@ module Cane
|
|
14
10
|
# hands the result to a formatter for display. This is the core of the
|
15
11
|
# application, but for the actual entry point see `Cane::CLI`.
|
16
12
|
class Runner
|
17
|
-
|
18
|
-
abc: AbcCheck,
|
19
|
-
style: StyleCheck,
|
20
|
-
doc: DocCheck,
|
21
|
-
threshold: ThresholdCheck
|
22
|
-
}
|
23
|
-
|
24
|
-
def initialize(opts)
|
13
|
+
def initialize(opts, checks)
|
25
14
|
@opts = opts
|
15
|
+
@checks = checks
|
26
16
|
end
|
27
17
|
|
28
18
|
def run
|
@@ -33,12 +23,11 @@ module Cane
|
|
33
23
|
|
34
24
|
protected
|
35
25
|
|
36
|
-
attr_reader :opts
|
26
|
+
attr_reader :opts, :checks
|
37
27
|
|
38
28
|
def violations
|
39
|
-
@violations ||=
|
40
|
-
|
41
|
-
map { |key, check| check.new(opts.fetch(key)).violations }.
|
29
|
+
@violations ||= checks.
|
30
|
+
map {|check| check.new(opts).violations }.
|
42
31
|
flatten
|
43
32
|
end
|
44
33
|
|
data/lib/cane/abc_check.rb
CHANGED
@@ -1,17 +1,41 @@
|
|
1
1
|
require 'ripper'
|
2
|
-
|
3
|
-
require 'cane/abc_max_violation'
|
4
|
-
require 'cane/syntax_violation'
|
5
2
|
require 'set'
|
6
3
|
|
4
|
+
require 'cane/file'
|
5
|
+
|
7
6
|
module Cane
|
8
7
|
|
9
8
|
# Creates violations for methods that are too complicated using a simple
|
10
9
|
# algorithm run against the parse tree of a file to count assignments,
|
11
10
|
# branches, and conditionals. Borrows heavily from metric_abc.
|
12
11
|
class AbcCheck < Struct.new(:opts)
|
12
|
+
|
13
|
+
def self.key; :abc; end
|
14
|
+
def self.name; "ABC check"; end
|
15
|
+
def self.options
|
16
|
+
{
|
17
|
+
abc_glob: ['Glob to run ABC metrics over',
|
18
|
+
default: '{app,lib}/**/*.rb',
|
19
|
+
variable: 'GLOB',
|
20
|
+
clobber: :no_abc],
|
21
|
+
abc_max: ['Ignore methods under this complexity',
|
22
|
+
default: 15,
|
23
|
+
cast: :to_i,
|
24
|
+
clobber: :no_abc],
|
25
|
+
abc_exclude: ['Exclude method from analysis (eg. Foo::Bar#method)',
|
26
|
+
variable: 'METHOD',
|
27
|
+
type: Array,
|
28
|
+
default: [],
|
29
|
+
clobber: :no_abc],
|
30
|
+
no_abc: ['Disable ABC checking',
|
31
|
+
cast: ->(x) { !x }]
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
13
35
|
def violations
|
14
|
-
|
36
|
+
return [] if opts[:no_abc] == false
|
37
|
+
|
38
|
+
order file_names.map {|file_name|
|
15
39
|
find_violations(file_name)
|
16
40
|
}.flatten
|
17
41
|
end
|
@@ -19,7 +43,7 @@ module Cane
|
|
19
43
|
protected
|
20
44
|
|
21
45
|
def find_violations(file_name)
|
22
|
-
ast = Ripper::SexpBuilder.new(File.
|
46
|
+
ast = Ripper::SexpBuilder.new(Cane::File.contents(file_name)).parse
|
23
47
|
case ast
|
24
48
|
when nil
|
25
49
|
InvalidAst.new(file_name)
|
@@ -31,36 +55,51 @@ module Cane
|
|
31
55
|
# Null object for when the file cannot be parsed.
|
32
56
|
class InvalidAst < Struct.new(:file_name)
|
33
57
|
def violations
|
34
|
-
[
|
58
|
+
[{file: file_name, description: "Files contained invalid syntax"}]
|
35
59
|
end
|
36
60
|
end
|
37
61
|
|
38
62
|
# Wrapper object around sexps returned from ripper.
|
39
63
|
class RubyAst < Struct.new(:file_name, :max_allowed_complexity,
|
40
64
|
:sexps, :exclusions)
|
65
|
+
|
66
|
+
def initialize(*args)
|
67
|
+
super
|
68
|
+
self.anon_method_add = true
|
69
|
+
end
|
70
|
+
|
41
71
|
def violations
|
42
72
|
process_ast(sexps).
|
43
|
-
select {
|
44
|
-
map {
|
73
|
+
select {|nesting, complexity| complexity > max_allowed_complexity }.
|
74
|
+
map {|x| {
|
75
|
+
file: file_name,
|
76
|
+
label: x.first,
|
77
|
+
value: x.last,
|
78
|
+
description: "Methods exceeded maximum allowed ABC complexity"
|
79
|
+
}}
|
45
80
|
end
|
46
81
|
|
47
82
|
protected
|
48
83
|
|
84
|
+
# Stateful flag used to determine whether we are currently parsing an
|
85
|
+
# anonymous class. See #container_label.
|
86
|
+
attr_accessor :anon_method_add
|
87
|
+
|
49
88
|
# Recursive function to process an AST. The `complexity` variable mutates,
|
50
89
|
# which is a bit confusing. `nesting` does not.
|
51
90
|
def process_ast(node, complexity = {}, nesting = [])
|
52
91
|
if method_nodes.include?(node[0])
|
53
92
|
nesting = nesting + [label_for(node)]
|
54
|
-
|
55
|
-
|
93
|
+
desc = method_description(node, *nesting)
|
94
|
+
unless excluded?(desc)
|
95
|
+
complexity[desc] = calculate_abc(node)
|
56
96
|
end
|
57
|
-
elsif
|
58
|
-
parent = node[1][-1][1]
|
97
|
+
elsif parent = container_label(node)
|
59
98
|
nesting = nesting + [parent]
|
60
99
|
end
|
61
100
|
|
62
101
|
if node.is_a? Array
|
63
|
-
node[1..-1].each {
|
102
|
+
node[1..-1].each {|n| process_ast(n, complexity, nesting) if n }
|
64
103
|
end
|
65
104
|
complexity
|
66
105
|
end
|
@@ -73,6 +112,27 @@ module Cane
|
|
73
112
|
abc
|
74
113
|
end
|
75
114
|
|
115
|
+
def container_label(node)
|
116
|
+
if container_nodes.include?(node[0])
|
117
|
+
# def foo, def self.foo
|
118
|
+
node[1][-1][1]
|
119
|
+
elsif node[0] == :method_add_block
|
120
|
+
if anon_method_add
|
121
|
+
# Class.new do ...
|
122
|
+
"(anon)"
|
123
|
+
else
|
124
|
+
# MyClass = Class.new do ...
|
125
|
+
# parent already added when processing a parent node
|
126
|
+
anon_method_add = true
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
elsif node[0] == :assign && node[2][0] == :method_add_block
|
130
|
+
# MyClass = Class.new do ...
|
131
|
+
self.anon_method_add = false
|
132
|
+
node[1][-1][1]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
76
136
|
def label_for(node)
|
77
137
|
# A default case is deliberately omitted since I know of no way this
|
78
138
|
# could fail and want it to fail fast.
|
@@ -82,7 +142,7 @@ module Cane
|
|
82
142
|
end
|
83
143
|
|
84
144
|
def count_nodes(node, types)
|
85
|
-
node.flatten.select {
|
145
|
+
node.flatten.select {|n| types.include?(n) }.length
|
86
146
|
end
|
87
147
|
|
88
148
|
def assignment_nodes
|
@@ -107,27 +167,30 @@ module Cane
|
|
107
167
|
|
108
168
|
METH_CHARS = { def: '#', defs: '.' }
|
109
169
|
|
110
|
-
def excluded?(
|
111
|
-
|
112
|
-
|
113
|
-
|
170
|
+
def excluded?(method_description)
|
171
|
+
exclusions.include?(method_description)
|
172
|
+
end
|
173
|
+
|
174
|
+
def method_description(node, *modules, meth_name)
|
175
|
+
separator = METH_CHARS.fetch(node.first)
|
176
|
+
description = [modules.join('::'), meth_name].join(separator)
|
114
177
|
end
|
115
178
|
end
|
116
179
|
|
117
180
|
def file_names
|
118
|
-
Dir[opts.fetch(:
|
181
|
+
Dir[opts.fetch(:abc_glob)]
|
119
182
|
end
|
120
183
|
|
121
184
|
def order(result)
|
122
|
-
result.sort_by
|
185
|
+
result.sort_by {|x| x[:value].to_i }.reverse
|
123
186
|
end
|
124
187
|
|
125
188
|
def max_allowed_complexity
|
126
|
-
opts.fetch(:
|
189
|
+
opts.fetch(:abc_max)
|
127
190
|
end
|
128
191
|
|
129
192
|
def exclusions
|
130
|
-
opts.fetch(:
|
193
|
+
opts.fetch(:abc_exclude, []).flatten.to_set
|
131
194
|
end
|
132
195
|
end
|
133
196
|
end
|
data/lib/cane/cli.rb
CHANGED
@@ -2,17 +2,15 @@ require 'cane'
|
|
2
2
|
require 'cane/version'
|
3
3
|
|
4
4
|
require 'cane/cli/spec'
|
5
|
-
require 'cane/cli/translator'
|
6
5
|
|
7
6
|
module Cane
|
8
7
|
module CLI
|
9
|
-
|
10
8
|
def run(args)
|
11
9
|
opts = Spec.new.parse(args)
|
12
|
-
if opts
|
13
|
-
Cane.run(opts)
|
10
|
+
if opts.is_a?(Hash)
|
11
|
+
Cane.run(opts, Spec::CHECKS)
|
14
12
|
else
|
15
|
-
|
13
|
+
opts
|
16
14
|
end
|
17
15
|
end
|
18
16
|
module_function :run
|
data/lib/cane/cli/spec.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
require 'optparse'
|
2
|
-
|
2
|
+
|
3
|
+
require 'cane/abc_check'
|
4
|
+
require 'cane/style_check'
|
5
|
+
require 'cane/doc_check'
|
6
|
+
require 'cane/threshold_check'
|
3
7
|
|
4
8
|
module Cane
|
5
9
|
module CLI
|
@@ -7,14 +11,19 @@ module Cane
|
|
7
11
|
# Provides a specification for the command line interface that drives
|
8
12
|
# documentation, parsing, and default values.
|
9
13
|
class Spec
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
CHECKS = [AbcCheck, StyleCheck, DocCheck, ThresholdCheck]
|
15
|
+
|
16
|
+
def self.defaults(check)
|
17
|
+
x = check.options.each_with_object({}) {|(k, v), h|
|
18
|
+
h[k] = (v[1] || {})[:default]
|
19
|
+
}
|
20
|
+
x
|
21
|
+
end
|
22
|
+
|
23
|
+
OPTIONS = {
|
24
|
+
max_violations: 0,
|
25
|
+
exclusions_file: nil,
|
26
|
+
}.merge(CHECKS.inject({}) {|a, check| a.merge(defaults(check)) })
|
18
27
|
|
19
28
|
# Exception to indicate that no further processing is required and the
|
20
29
|
# program can exit. This is used to handle --help and --version flags.
|
@@ -23,27 +32,31 @@ module Cane
|
|
23
32
|
def initialize
|
24
33
|
add_banner
|
25
34
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
35
|
+
CHECKS.each do |check|
|
36
|
+
add_check_options(check)
|
37
|
+
end
|
38
|
+
|
30
39
|
add_cane_options
|
31
40
|
|
32
41
|
add_version
|
33
42
|
add_help
|
34
43
|
end
|
35
44
|
|
36
|
-
def parse(args)
|
45
|
+
def parse(args, ret = true)
|
37
46
|
parser.parse!(get_default_options + args)
|
38
47
|
|
39
|
-
|
48
|
+
OPTIONS.merge(options)
|
49
|
+
rescue OptionParser::InvalidOption
|
50
|
+
args = %w(--help)
|
51
|
+
ret = false
|
52
|
+
retry
|
40
53
|
rescue OptionsHandled
|
41
|
-
|
54
|
+
ret
|
42
55
|
end
|
43
56
|
|
44
57
|
def get_default_options
|
45
|
-
if File.exists?('./.cane')
|
46
|
-
File.read('./.cane').gsub("\n", ' ').split(' ')
|
58
|
+
if ::File.exists?('./.cane')
|
59
|
+
::File.read('./.cane').gsub("\n", ' ').split(' ')
|
47
60
|
else
|
48
61
|
[]
|
49
62
|
end
|
@@ -58,48 +71,38 @@ You can also put these options in a .cane file.
|
|
58
71
|
BANNER
|
59
72
|
end
|
60
73
|
|
61
|
-
def
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
add_option %w(--no-doc), "Disable documentation checking"
|
80
|
-
|
81
|
-
parser.separator ""
|
82
|
-
end
|
83
|
-
|
84
|
-
def add_threshold_options
|
85
|
-
desc = "If FILE contains a number, verify it is >= to THRESHOLD."
|
86
|
-
parser.on("--gte FILE,THRESHOLD", Array, desc) do |opts|
|
87
|
-
(options[:threshold] ||= []) << opts.unshift(:>=)
|
74
|
+
def add_check_options(check)
|
75
|
+
check.options.each do |key, data|
|
76
|
+
cli_key = key.to_s.tr('_', '-')
|
77
|
+
opts = data[1] || {}
|
78
|
+
variable = opts[:variable] || "VALUE"
|
79
|
+
defaults = opts[:default] || []
|
80
|
+
|
81
|
+
if opts[:type] == Array
|
82
|
+
parser.on("--#{cli_key} #{variable}", Array, data[0]) do |opts|
|
83
|
+
(options[key.to_sym] ||= []) << opts
|
84
|
+
end
|
85
|
+
else
|
86
|
+
if [*defaults].length > 0
|
87
|
+
add_option ["--#{cli_key}", variable], *data
|
88
|
+
else
|
89
|
+
add_option ["--#{cli_key}"], *data
|
90
|
+
end
|
91
|
+
end
|
88
92
|
end
|
89
93
|
|
90
94
|
parser.separator ""
|
91
95
|
end
|
92
96
|
|
93
97
|
def add_cane_options
|
94
|
-
add_option %w(--max-violations VALUE),
|
95
|
-
|
96
|
-
"YAML file containing a list of exclusions"
|
98
|
+
add_option %w(--max-violations VALUE),
|
99
|
+
"Max allowed violations", default: 0, cast: :to_i
|
97
100
|
|
98
101
|
parser.separator ""
|
99
102
|
end
|
100
103
|
|
101
104
|
def add_version
|
102
|
-
parser.on_tail("--version", "Show version") do
|
105
|
+
parser.on_tail("-v", "--version", "Show version") do
|
103
106
|
puts Cane::VERSION
|
104
107
|
raise OptionsHandled
|
105
108
|
end
|
@@ -112,15 +115,18 @@ BANNER
|
|
112
115
|
end
|
113
116
|
end
|
114
117
|
|
115
|
-
def add_option(option, description)
|
118
|
+
def add_option(option, description, opts={})
|
116
119
|
option_key = option[0].gsub('--', '').tr('-', '_').to_sym
|
120
|
+
default = opts[:default]
|
121
|
+
cast = opts[:cast] || ->(x) { x }
|
117
122
|
|
118
|
-
if
|
119
|
-
description += " (default: %s)" %
|
123
|
+
if default
|
124
|
+
description += " (default: %s)" % default
|
120
125
|
end
|
121
126
|
|
122
127
|
parser.on(option.join(' '), description) do |v|
|
123
|
-
options[option_key] = v
|
128
|
+
options[option_key] = cast.to_proc.call(v)
|
129
|
+
options.delete(opts[:clobber])
|
124
130
|
end
|
125
131
|
end
|
126
132
|
|