cane 1.1.0 → 1.2.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,11 @@
1
1
  # Cane History
2
2
 
3
+ ## 1.2.0 - 31 March 2012 (adce51b9)
4
+
5
+ * Gracefully handle files with invalid syntax (#1)
6
+ * Included class methods in ABC check (#8)
7
+ * Can disable style and doc checks from rake task (#9)
8
+
3
9
  ## 1.1.0 - 24 March 2012 (ba8a74fc)
4
10
 
5
11
  * `app` added to default globs
data/README.md CHANGED
@@ -72,6 +72,7 @@ It works just like this:
72
72
  Cane::RakeTask.new(:quality) do |cane|
73
73
  cane.abc_max = 10
74
74
  cane.add_threshold 'coverage/covered_percent', :>=, 99
75
+ cane.no_style = true
75
76
  end
76
77
 
77
78
  task :default => :quality
@@ -1,6 +1,7 @@
1
1
  require 'ripper'
2
2
 
3
3
  require 'cane/abc_max_violation'
4
+ require 'cane/syntax_violation'
4
5
 
5
6
  module Cane
6
7
 
@@ -17,80 +18,100 @@ module Cane
17
18
  protected
18
19
 
19
20
  def find_violations(file_name)
20
- ast = sexps_from_file(file_name)
21
+ ast = Ripper::SexpBuilder.new(File.open(file_name, 'r:utf-8').read).parse
22
+ case ast
23
+ when nil
24
+ InvalidAst.new(file_name)
25
+ else
26
+ RubyAst.new(file_name, max_allowed_complexity, ast)
27
+ end.violations
28
+ end
21
29
 
22
- process_ast(ast).
23
- select { |nesting, complexity| complexity > max_allowed_complexity }.
24
- map { |x| AbcMaxViolation.new(file_name, x.first, x.last) }
30
+ # Null object for when the file cannot be parsed.
31
+ class InvalidAst < Struct.new(:file_name)
32
+ def violations
33
+ [SyntaxViolation.new(file_name)]
34
+ end
25
35
  end
26
36
 
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]
37
+ # Wrapper object around sexps returned from ripper.
38
+ class RubyAst < Struct.new(:file_name, :max_allowed_complexity, :sexps)
39
+ def violations
40
+ process_ast(sexps).
41
+ select { |nesting, complexity| complexity > max_allowed_complexity }.
42
+ map { |x| AbcMaxViolation.new(file_name, x.first, x.last) }
40
43
  end
41
44
 
42
- if node.is_a? Array
43
- node[1..-1].each { |n| process_ast(n, complexity, nesting) if n }
45
+ protected
46
+
47
+ # Recursive function to process an AST. The `complexity` variable mutates,
48
+ # which is a bit confusing. `nesting` does not.
49
+ def process_ast(node, complexity = {}, nesting = [])
50
+ if method_nodes.include?(node[0])
51
+ nesting = nesting + [label_for(node)]
52
+ complexity[nesting.join(" > ")] = calculate_abc(node)
53
+ elsif container_nodes.include?(node[0])
54
+ parent = node[1][-1][1]
55
+ nesting = nesting + [parent]
56
+ end
57
+
58
+ if node.is_a? Array
59
+ node[1..-1].each { |n| process_ast(n, complexity, nesting) if n }
60
+ end
61
+ complexity
44
62
  end
45
- complexity
46
- end
47
63
 
48
- def sexps_from_file(file_name)
49
- Ripper::SexpBuilder.new(File.open(file_name, 'r:utf-8').read).parse
50
- end
64
+ def calculate_abc(method_node)
65
+ a = count_nodes(method_node, assignment_nodes)
66
+ b = count_nodes(method_node, branch_nodes) + 1
67
+ c = count_nodes(method_node, condition_nodes)
68
+ abc = Math.sqrt(a**2 + b**2 + c**2).round
69
+ abc
70
+ end
51
71
 
52
- def max_allowed_complexity
53
- opts.fetch(:max)
54
- end
72
+ def label_for(node)
73
+ # A default case is deliberately omitted since I know of no way this
74
+ # could fail and want it to fail fast.
75
+ node.detect {|x|
76
+ [:@ident, :@op, :@kw, :@const, :@backtick].include?(x[0])
77
+ }[1]
78
+ end
55
79
 
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
80
+ def count_nodes(node, types)
81
+ node.flatten.select { |n| types.include?(n) }.length
82
+ end
63
83
 
64
- def count_nodes(node, types)
65
- node.flatten.select { |n| types.include?(n) }.length
66
- end
84
+ def assignment_nodes
85
+ [:assign, :opassign]
86
+ end
67
87
 
68
- def file_names
69
- Dir[opts.fetch(:files)]
70
- end
88
+ def method_nodes
89
+ [:def, :defs]
90
+ end
71
91
 
72
- def order(result)
73
- result.sort_by(&:complexity).reverse
74
- end
92
+ def container_nodes
93
+ [:class, :module]
94
+ end
75
95
 
76
- def assignment_nodes
77
- [:assign, :opassign]
78
- end
96
+ def branch_nodes
97
+ [:call, :fcall, :brace_block, :do_block]
98
+ end
79
99
 
80
- def method_nodes
81
- [:def]
100
+ def condition_nodes
101
+ [:==, :===, :"<>", :"<=", :">=", :"=~", :>, :<, :else, :"<=>"]
102
+ end
82
103
  end
83
104
 
84
- def container_nodes
85
- [:class, :module]
105
+ def file_names
106
+ Dir[opts.fetch(:files)]
86
107
  end
87
108
 
88
- def branch_nodes
89
- [:call, :fcall, :brace_block, :do_block]
109
+ def order(result)
110
+ result.sort_by(&:sort_index).reverse
90
111
  end
91
112
 
92
- def condition_nodes
93
- [:==, :===, :"<>", :"<=", :">=", :"=~", :>, :<, :else, :"<=>"]
113
+ def max_allowed_complexity
114
+ opts.fetch(:max)
94
115
  end
95
116
  end
96
117
  end
@@ -9,5 +9,7 @@ module Cane
9
9
  def description
10
10
  "Methods exceeded maximum allowed ABC complexity"
11
11
  end
12
+
13
+ alias_method :sort_index, :complexity
12
14
  end
13
15
  end
@@ -10,6 +10,7 @@ module Cane
10
10
  # Cane::RakeTask.new(:quality) do |cane|
11
11
  # cane.abc_max = 10
12
12
  # cane.doc_glob = 'lib/**/*.rb'
13
+ # cane.no_style = true
13
14
  # cane.add_threshold 'coverage/covered_percent', :>=, 99
14
15
  # end
15
16
  class RakeTask < ::Rake::TaskLib
@@ -21,10 +22,14 @@ module Cane
21
22
  attr_accessor :abc_max
22
23
  # Glob to run style checks over (default: "{lib,spec}/**/*.rb")
23
24
  attr_accessor :style_glob
25
+ # TRUE to disable style checks
26
+ attr_accessor :no_style
24
27
  # Max line length (default: 80)
25
28
  attr_accessor :style_measure
26
29
  # Glob to run doc checks over (default: "lib/**/*.rb")
27
30
  attr_accessor :doc_glob
31
+ # TRUE to disable doc checks
32
+ attr_accessor :no_doc
28
33
  # Max violations to tolerate (default: 0)
29
34
  attr_accessor :max_violations
30
35
 
@@ -54,8 +59,10 @@ module Cane
54
59
  :abc_glob,
55
60
  :abc_max,
56
61
  :doc_glob,
62
+ :no_doc,
57
63
  :max_violations,
58
64
  :style_glob,
65
+ :no_style,
59
66
  :style_measure
60
67
  ].inject(threshold: @threshold) do |opts, setting|
61
68
  value = self.send(setting)
@@ -0,0 +1,20 @@
1
+ module Cane
2
+
3
+ # Value object used by AbcCheck for a file that cannot be parsed. This is
4
+ # handled by AbcCheck rather than a separate class since it is a low value
5
+ # violation (syntax errors should have been picked up by specs) but we still
6
+ # have to deal with the edge case.
7
+ class SyntaxViolation < Struct.new(:file_name)
8
+ def columns
9
+ [file_name]
10
+ end
11
+
12
+ def description
13
+ "Files contained invalid syntax"
14
+ end
15
+
16
+ def sort_index
17
+ 0
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module Cane
2
- VERSION = '1.1.0'
2
+ VERSION = '1.2.0'
3
3
  end
@@ -20,8 +20,7 @@ describe Cane::AbcCheck do
20
20
  violations = described_class.new(files: file_name, max: 1).violations
21
21
  violations.length.should == 1
22
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")
23
+ violations[0].columns.should == [file_name, "Harness > complex_method", 2]
25
24
  end
26
25
 
27
26
  it 'sorts violations by complexity' do
@@ -43,4 +42,40 @@ describe Cane::AbcCheck do
43
42
  complexities = violations.map(&:complexity)
44
43
  complexities.should == complexities.sort.reverse
45
44
  end
45
+
46
+ it 'creates a SyntaxViolation when code cannot be parsed' do
47
+ file_name = make_file(<<-RUBY)
48
+ class Harness
49
+ RUBY
50
+
51
+ violations = described_class.new(files: file_name).violations
52
+ violations.length.should == 1
53
+ violations[0].should be_instance_of(Cane::SyntaxViolation)
54
+ violations[0].columns.should == [file_name]
55
+ violations[0].description.should be_instance_of(String)
56
+ end
57
+
58
+ def self.it_should_extract_method_name(method_name, label=method_name)
59
+ it "creates an AbcMaxViolation for #{method_name}" do
60
+ file_name = make_file(<<-RUBY)
61
+ class Harness
62
+ def #{method_name}(a)
63
+ b = a
64
+ return b if b > 3
65
+ end
66
+ end
67
+ RUBY
68
+
69
+ violations = described_class.new(files: file_name, max: 1).violations
70
+ violations[0].detail.should == "Harness > #{label}"
71
+ end
72
+ end
73
+
74
+ # These method names all create different ASTs. Which is weird.
75
+ it_should_extract_method_name 'a'
76
+ it_should_extract_method_name 'self.a', 'a'
77
+ it_should_extract_method_name 'next'
78
+ it_should_extract_method_name 'GET'
79
+ it_should_extract_method_name '`'
80
+ it_should_extract_method_name '>='
46
81
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cane
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-25 00:00:00.000000000 Z
12
+ date: 2012-03-31 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: tailor
16
- requirement: &2160572120 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,15 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2160572120
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
25
30
  - !ruby/object:Gem::Dependency
26
31
  name: rspec
27
- requirement: &2160571620 !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
28
33
  none: false
29
34
  requirements:
30
35
  - - ~>
@@ -32,10 +37,15 @@ dependencies:
32
37
  version: '2.0'
33
38
  type: :development
34
39
  prerelease: false
35
- version_requirements: *2160571620
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '2.0'
36
46
  - !ruby/object:Gem::Dependency
37
47
  name: rake
38
- requirement: &2160571200 !ruby/object:Gem::Requirement
48
+ requirement: !ruby/object:Gem::Requirement
39
49
  none: false
40
50
  requirements:
41
51
  - - ! '>='
@@ -43,10 +53,15 @@ dependencies:
43
53
  version: '0'
44
54
  type: :development
45
55
  prerelease: false
46
- version_requirements: *2160571200
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
47
62
  - !ruby/object:Gem::Dependency
48
63
  name: simplecov
49
- requirement: &2160570720 !ruby/object:Gem::Requirement
64
+ requirement: !ruby/object:Gem::Requirement
50
65
  none: false
51
66
  requirements:
52
67
  - - ! '>='
@@ -54,7 +69,12 @@ dependencies:
54
69
  version: '0'
55
70
  type: :development
56
71
  prerelease: false
57
- version_requirements: *2160570720
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
58
78
  description: Fails your build if code quality thresholds are not met
59
79
  email:
60
80
  - xavier@squareup.com
@@ -79,6 +99,7 @@ files:
79
99
  - lib/cane/rake_task.rb
80
100
  - lib/cane/style_check.rb
81
101
  - lib/cane/style_violation.rb
102
+ - lib/cane/syntax_violation.rb
82
103
  - lib/cane/threshold_check.rb
83
104
  - lib/cane/threshold_violation.rb
84
105
  - lib/cane/version.rb
@@ -109,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
130
  version: '0'
110
131
  requirements: []
111
132
  rubyforge_project:
112
- rubygems_version: 1.8.6
133
+ rubygems_version: 1.8.18
113
134
  signing_key:
114
135
  specification_version: 3
115
136
  summary: Fails your build if code quality thresholds are not met. Provides complexity