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 +6 -0
- data/README.md +1 -0
- data/lib/cane/abc_check.rb +75 -54
- data/lib/cane/abc_max_violation.rb +2 -0
- data/lib/cane/rake_task.rb +7 -0
- data/lib/cane/syntax_violation.rb +20 -0
- data/lib/cane/version.rb +1 -1
- data/spec/abc_check_spec.rb +37 -2
- metadata +32 -11
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
data/lib/cane/abc_check.rb
CHANGED
@@ -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 =
|
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
|
-
|
23
|
-
|
24
|
-
|
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
|
-
#
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
43
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
84
|
+
def assignment_nodes
|
85
|
+
[:assign, :opassign]
|
86
|
+
end
|
67
87
|
|
68
|
-
|
69
|
-
|
70
|
-
|
88
|
+
def method_nodes
|
89
|
+
[:def, :defs]
|
90
|
+
end
|
71
91
|
|
72
|
-
|
73
|
-
|
74
|
-
|
92
|
+
def container_nodes
|
93
|
+
[:class, :module]
|
94
|
+
end
|
75
95
|
|
76
|
-
|
77
|
-
|
78
|
-
|
96
|
+
def branch_nodes
|
97
|
+
[:call, :fcall, :brace_block, :do_block]
|
98
|
+
end
|
79
99
|
|
80
|
-
|
81
|
-
|
100
|
+
def condition_nodes
|
101
|
+
[:==, :===, :"<>", :"<=", :">=", :"=~", :>, :<, :else, :"<=>"]
|
102
|
+
end
|
82
103
|
end
|
83
104
|
|
84
|
-
def
|
85
|
-
[:
|
105
|
+
def file_names
|
106
|
+
Dir[opts.fetch(:files)]
|
86
107
|
end
|
87
108
|
|
88
|
-
def
|
89
|
-
|
109
|
+
def order(result)
|
110
|
+
result.sort_by(&:sort_index).reverse
|
90
111
|
end
|
91
112
|
|
92
|
-
def
|
93
|
-
|
113
|
+
def max_allowed_complexity
|
114
|
+
opts.fetch(:max)
|
94
115
|
end
|
95
116
|
end
|
96
117
|
end
|
data/lib/cane/rake_task.rb
CHANGED
@@ -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
|
data/lib/cane/version.rb
CHANGED
data/spec/abc_check_spec.rb
CHANGED
@@ -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].
|
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.
|
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-
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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.
|
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
|