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 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 > sample 23
20
- lib/cane.rb Cane > sample_2 17
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 metrics over (default: {app,lib,spec}/**/*.rb)
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 documentation checks over (default: {app,lib}/**/*.rb)
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
- --version Show version
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
@@ -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(opts)
9
- Runner.new(opts).run
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
- CHECKERS = {
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 ||= CHECKERS.
40
- select { |key, _| opts.has_key?(key) }.
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
 
@@ -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
- order file_names.map { |file_name|
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.open(file_name, 'r:utf-8').read).parse
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
- [SyntaxViolation.new(file_name)]
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 { |nesting, complexity| complexity > max_allowed_complexity }.
44
- map { |x| AbcMaxViolation.new(file_name, x.first, x.last) }
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
- unless excluded?(node, *nesting)
55
- complexity[nesting.join(" > ")] = calculate_abc(node)
93
+ desc = method_description(node, *nesting)
94
+ unless excluded?(desc)
95
+ complexity[desc] = calculate_abc(node)
56
96
  end
57
- elsif container_nodes.include?(node[0])
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 { |n| process_ast(n, complexity, nesting) if n }
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 { |n| types.include?(n) }.length
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?(node, *modules, meth_name)
111
- meth_char = METH_CHARS.fetch(node.first)
112
- description = [modules.join('::'), meth_name].join(meth_char)
113
- exclusions.include?(description)
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(:files)]
181
+ Dir[opts.fetch(:abc_glob)]
119
182
  end
120
183
 
121
184
  def order(result)
122
- result.sort_by(&:sort_index).reverse
185
+ result.sort_by {|x| x[:value].to_i }.reverse
123
186
  end
124
187
 
125
188
  def max_allowed_complexity
126
- opts.fetch(:max)
189
+ opts.fetch(:abc_max)
127
190
  end
128
191
 
129
192
  def exclusions
130
- opts.fetch(:exclusions, []).to_set
193
+ opts.fetch(:abc_exclude, []).flatten.to_set
131
194
  end
132
195
  end
133
196
  end
@@ -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
- true
13
+ opts
16
14
  end
17
15
  end
18
16
  module_function :run
@@ -1,5 +1,9 @@
1
1
  require 'optparse'
2
- require 'cane/cli/translator'
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
- DEFAULTS = {
11
- abc_glob: '{app,lib}/**/*.rb',
12
- abc_max: '15',
13
- style_glob: '{app,lib,spec}/**/*.rb',
14
- style_measure: '80',
15
- doc_glob: '{app,lib}/**/*.rb',
16
- max_violations: '0',
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
- add_abc_options
27
- add_style_options
28
- add_doc_options
29
- add_threshold_options
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
- Translator.new(options, DEFAULTS).to_hash
48
+ OPTIONS.merge(options)
49
+ rescue OptionParser::InvalidOption
50
+ args = %w(--help)
51
+ ret = false
52
+ retry
40
53
  rescue OptionsHandled
41
- nil
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 add_abc_options
62
- add_option %w(--abc-glob GLOB), "Glob to run ABC metrics over"
63
- add_option %w(--abc-max VALUE), "Ignore methods under this complexity"
64
- add_option %w(--no-abc), "Disable ABC checking"
65
-
66
- parser.separator ""
67
- end
68
-
69
- def add_style_options
70
- add_option %w(--style-glob GLOB), "Glob to run style metrics over"
71
- add_option %w(--style-measure VALUE), "Max line length"
72
- add_option %w(--no-style), "Disable style checking"
73
-
74
- parser.separator ""
75
- end
76
-
77
- def add_doc_options
78
- add_option %w(--doc-glob GLOB), "Glob to run documentation checks over"
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), "Max allowed violations"
95
- add_option %w(--exclusions-file FILE),
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 DEFAULTS.has_key?(option_key)
119
- description += " (default: %s)" % DEFAULTS[option_key]
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