cane 1.1.0 → 1.2.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,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