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