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 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