sparqcode_cane 1.3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY.md +24 -0
- data/LICENSE +14 -0
- data/README.md +136 -0
- data/bin/cane +7 -0
- data/cane.gemspec +32 -0
- data/lib/cane.rb +49 -0
- data/lib/cane/abc_check.rb +138 -0
- data/lib/cane/abc_max_violation.rb +15 -0
- data/lib/cane/cli.rb +21 -0
- data/lib/cane/cli/spec.rb +135 -0
- data/lib/cane/cli/translator.rb +51 -0
- data/lib/cane/doc_check.rb +51 -0
- data/lib/cane/rake_task.rb +82 -0
- data/lib/cane/style_check.rb +45 -0
- data/lib/cane/style_violation.rb +10 -0
- data/lib/cane/syntax_violation.rb +20 -0
- data/lib/cane/threshold_check.rb +30 -0
- data/lib/cane/threshold_violation.rb +15 -0
- data/lib/cane/version.rb +3 -0
- data/lib/cane/violation_formatter.rb +49 -0
- data/spec/abc_check_spec.rb +81 -0
- data/spec/cane_spec.rb +130 -0
- data/spec/doc_check_spec.rb +30 -0
- data/spec/spec_helper.rb +37 -0
- data/spec/style_check_spec.rb +18 -0
- data/spec/threshold_check_spec.rb +12 -0
- data/spec/violation_formatter_spec.rb +16 -0
- metadata +130 -0
data/HISTORY.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# Cane History
|
2
|
+
|
3
|
+
## 1.3.0 - 20 April 2012 (c166dfa0)
|
4
|
+
|
5
|
+
* Remove dependency on tailor. Fewer styles checks are performed, but the three
|
6
|
+
remaining are the only ones I've found useful.
|
7
|
+
|
8
|
+
## 1.2.0 - 31 March 2012 (adce51b9)
|
9
|
+
|
10
|
+
* Gracefully handle files with invalid syntax (#1)
|
11
|
+
* Included class methods in ABC check (#8)
|
12
|
+
* Can disable style and doc checks from rake task (#9)
|
13
|
+
|
14
|
+
## 1.1.0 - 24 March 2012 (ba8a74fc)
|
15
|
+
|
16
|
+
* `app` added to default globs
|
17
|
+
* Added `cane/rake_task`
|
18
|
+
* `class << obj` syntax ignore by documentation check
|
19
|
+
* Line length checks no longer include trailing new lines
|
20
|
+
* Add support for a `.cane` file for setting per-project default options.
|
21
|
+
|
22
|
+
## 1.0.0 - 14 January 2012 (4e400534)
|
23
|
+
|
24
|
+
* Initial release.
|
data/LICENSE
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
Copyright 2012 Square Inc.
|
3
|
+
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
you may not use this file except in compliance with the License.
|
6
|
+
You may obtain a copy of the License at
|
7
|
+
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
See the License for the specific language governing permissions and
|
14
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
# Cane
|
2
|
+
|
3
|
+
Fails your build if code quality thresholds are not met.
|
4
|
+
|
5
|
+
> Discipline will set you free.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
gem install cane
|
10
|
+
cane --abc-glob '{lib,spec}/**/*.rb' --abc-max 15
|
11
|
+
|
12
|
+
Your main build task should run this, probably via `bundle exec`. It will have
|
13
|
+
a non-zero exit code if any quality checks fail. Also, a report:
|
14
|
+
|
15
|
+
> cane
|
16
|
+
|
17
|
+
Methods exceeded maximum allowed ABC complexity (2):
|
18
|
+
|
19
|
+
lib/cane.rb Cane > sample 23
|
20
|
+
lib/cane.rb Cane > sample_2 17
|
21
|
+
|
22
|
+
Lines violated style requirements (2):
|
23
|
+
|
24
|
+
lib/cane.rb:20 Line length >80
|
25
|
+
lib/cane.rb:42 Trailing whitespace
|
26
|
+
|
27
|
+
Classes are not documented (1):
|
28
|
+
lib/cane:3 SomeClass
|
29
|
+
|
30
|
+
Customize behaviour with a wealth of options:
|
31
|
+
|
32
|
+
> cane --help
|
33
|
+
Usage: cane [options]
|
34
|
+
|
35
|
+
You can also put these options in a .cane file.
|
36
|
+
|
37
|
+
--abc-glob GLOB Glob to run ABC metrics over (default: {app,lib}/**/*.rb)
|
38
|
+
--abc-max VALUE Ignore methods under this complexity (default: 15)
|
39
|
+
--no-abc Disable ABC checking
|
40
|
+
|
41
|
+
--style-glob GLOB Glob to run style metrics over (default: {app,lib,spec}/**/*.rb)
|
42
|
+
--style-measure VALUE Max line length (default: 80)
|
43
|
+
--no-style Disable style checking
|
44
|
+
|
45
|
+
--doc-glob GLOB Glob to run documentation checks over (default: {app,lib}/**/*.rb)
|
46
|
+
--no-doc Disable documentation checking
|
47
|
+
|
48
|
+
--gte FILE,THRESHOLD If FILE contains a number, verify it is >= to THRESHOLD.
|
49
|
+
|
50
|
+
--max-violations VALUE Max allowed violations (default: 0)
|
51
|
+
|
52
|
+
--version Show version
|
53
|
+
-h, --help Show this message
|
54
|
+
|
55
|
+
Set default options into a `.cane` file:
|
56
|
+
|
57
|
+
> cat .cane
|
58
|
+
--no-doc
|
59
|
+
--abc-glob **/*.rb
|
60
|
+
> cane
|
61
|
+
|
62
|
+
It works just like this:
|
63
|
+
|
64
|
+
> cane --no-doc --abc-glob '**/*.rb'
|
65
|
+
|
66
|
+
## Integrating with Rake
|
67
|
+
|
68
|
+
begin
|
69
|
+
require 'cane/rake_task'
|
70
|
+
|
71
|
+
desc "Run cane to check quality metrics"
|
72
|
+
Cane::RakeTask.new(:quality) do |cane|
|
73
|
+
cane.abc_max = 10
|
74
|
+
cane.add_threshold 'coverage/covered_percent', :>=, 99
|
75
|
+
cane.no_style = true
|
76
|
+
end
|
77
|
+
|
78
|
+
task :default => :quality
|
79
|
+
rescue LoadError
|
80
|
+
warn "cane not available, quality task not provided."
|
81
|
+
end
|
82
|
+
|
83
|
+
Rescuing `LoadError` is a good idea, since `rake -T` failing is totally
|
84
|
+
frustrating.
|
85
|
+
|
86
|
+
## Adding to a legacy project
|
87
|
+
|
88
|
+
Cane can be configured to still pass in the presence of a set number of
|
89
|
+
violations using the `--max-violations` option. This is ideal for retrofitting
|
90
|
+
on to an existing application that may already have many violations. By setting
|
91
|
+
the maximum to the current number, no immediate changes will be required to
|
92
|
+
your existing code base, but you will be protected from things getting worse.
|
93
|
+
|
94
|
+
## Integrating with SimpleCov
|
95
|
+
|
96
|
+
Any value in a file can be used as a threshold:
|
97
|
+
|
98
|
+
> echo "89" > coverage/covered_percent
|
99
|
+
> cane --gte 'coverage/covered_percent,90'
|
100
|
+
|
101
|
+
Quality threshold crossed
|
102
|
+
|
103
|
+
coverage/covered_percent is 89, should be >= 90
|
104
|
+
|
105
|
+
You can use a `SimpleCov` formatter to create the required file:
|
106
|
+
|
107
|
+
class SimpleCov::Formatter::QualityFormatter
|
108
|
+
def format(result)
|
109
|
+
SimpleCov::Formatter::HTMLFormatter.new.format(result)
|
110
|
+
File.open("coverage/covered_percent", "w") do |f|
|
111
|
+
f.puts result.source_files.covered_percent.to_f
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
SimpleCov.formatter = SimpleCov::Formatter::QualityFormatter
|
117
|
+
|
118
|
+
## Compatibility
|
119
|
+
|
120
|
+
Requires MRI 1.9, since it depends on the `ripper` library to calculate
|
121
|
+
complexity metrics. This only applies to the Ruby used to run Cane, not the
|
122
|
+
project it is being run against. In other words, you can run Cane against your
|
123
|
+
1.8 project.
|
124
|
+
|
125
|
+
## Support
|
126
|
+
|
127
|
+
[Ask questions on Stack
|
128
|
+
Overflow](http://stackoverflow.com/questions/ask?tags=ruby+cane). We keep an
|
129
|
+
eye on new cane questions.
|
130
|
+
|
131
|
+
## Contributing
|
132
|
+
|
133
|
+
Fork and patch! Before any changes are merged to master, we need you to sign an
|
134
|
+
[Individual Contributor
|
135
|
+
Agreement](https://spreadsheets.google.com/a/squareup.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1)
|
136
|
+
(Google Form).
|
data/bin/cane
ADDED
data/cane.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/cane/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Xavier Shay, Mike Emery"]
|
6
|
+
gem.email = ["xavier@squareup.com, mike@sparqcode.com"]
|
7
|
+
gem.description =
|
8
|
+
%q{Fails your build if code quality thresholds are not met}
|
9
|
+
gem.summary = %q{
|
10
|
+
Fails your build if code quality thresholds are not met. Provides
|
11
|
+
complexity and style checkers built-in, and allows integration with with
|
12
|
+
custom quality metrics. SPARQCode changed it so that instead of ABC checks it does cyclomatic complexity.
|
13
|
+
}
|
14
|
+
gem.homepage = "http://github.com/square/cane"
|
15
|
+
|
16
|
+
gem.files = Dir.glob("{spec,lib}/**/*.rb") + %w(
|
17
|
+
README.md
|
18
|
+
HISTORY.md
|
19
|
+
LICENSE
|
20
|
+
cane.gemspec
|
21
|
+
)
|
22
|
+
gem.test_files = Dir.glob("spec/**/*.rb")
|
23
|
+
gem.name = "sparqcode_cane"
|
24
|
+
gem.require_paths = ["lib"]
|
25
|
+
gem.bindir = "bin"
|
26
|
+
gem.executables << "cane"
|
27
|
+
gem.version = Cane::VERSION
|
28
|
+
gem.has_rdoc = false
|
29
|
+
gem.add_development_dependency 'rspec', '~> 2.0'
|
30
|
+
gem.add_development_dependency 'rake'
|
31
|
+
gem.add_development_dependency 'simplecov'
|
32
|
+
end
|
data/lib/cane.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'cane/abc_check'
|
2
|
+
require 'cane/style_check'
|
3
|
+
require 'cane/doc_check'
|
4
|
+
require 'cane/threshold_check'
|
5
|
+
require 'cane/violation_formatter'
|
6
|
+
|
7
|
+
module Cane
|
8
|
+
def run(opts)
|
9
|
+
Runner.new(opts).run
|
10
|
+
end
|
11
|
+
module_function :run
|
12
|
+
|
13
|
+
# Orchestrates the running of checks per the provided configuration, and
|
14
|
+
# hands the result to a formatter for display. This is the core of the
|
15
|
+
# application, but for the actual entry point see `Cane::CLI`.
|
16
|
+
class Runner
|
17
|
+
CHECKERS = {
|
18
|
+
abc: AbcCheck,
|
19
|
+
style: StyleCheck,
|
20
|
+
doc: DocCheck,
|
21
|
+
threshold: ThresholdCheck
|
22
|
+
}
|
23
|
+
|
24
|
+
def initialize(opts)
|
25
|
+
@opts = opts
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
outputter.print ViolationFormatter.new(violations)
|
30
|
+
|
31
|
+
violations.length <= opts.fetch(:max_violations)
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
attr_reader :opts
|
37
|
+
|
38
|
+
def violations
|
39
|
+
@violations ||= CHECKERS.
|
40
|
+
select { |key, _| opts.has_key?(key) }.
|
41
|
+
map { |key, check| check.new(opts.fetch(key)).violations }.
|
42
|
+
flatten
|
43
|
+
end
|
44
|
+
|
45
|
+
def outputter
|
46
|
+
opts.fetch(:out, $stdout)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'ripper'
|
2
|
+
|
3
|
+
require 'cane/abc_max_violation'
|
4
|
+
require 'cane/syntax_violation'
|
5
|
+
|
6
|
+
module Cane
|
7
|
+
|
8
|
+
# Creates violations for methods that are too complicated using a simple
|
9
|
+
# algorithm run against the parse tree of a file to count assignments,
|
10
|
+
# branches, and conditionals. Borrows heavily from metric_abc.
|
11
|
+
class AbcCheck < Struct.new(:opts)
|
12
|
+
def violations
|
13
|
+
order file_names.map { |file_name|
|
14
|
+
find_violations(file_name)
|
15
|
+
}.flatten
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def find_violations(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
|
29
|
+
|
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
|
35
|
+
end
|
36
|
+
|
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) }
|
43
|
+
end
|
44
|
+
|
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_cyclomatic(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
|
62
|
+
end
|
63
|
+
|
64
|
+
def calculate_cyclomatic(method_node)
|
65
|
+
cyclomatic = real_cyclomatic(method_node)
|
66
|
+
cyclomatic.size + 1
|
67
|
+
end
|
68
|
+
|
69
|
+
CONDITIONAL_NODES = [:if, :else, :elsif, :'||', :'&&', 'each', :rescue, :when]
|
70
|
+
|
71
|
+
def real_cyclomatic(node)
|
72
|
+
relevant_nodes = []
|
73
|
+
|
74
|
+
if(node.is_a? Array)
|
75
|
+
node.each do |e|
|
76
|
+
relevant_nodes.concat real_cyclomatic(e)
|
77
|
+
end
|
78
|
+
elsif(CONDITIONAL_NODES.include?(node))
|
79
|
+
relevant_nodes << node
|
80
|
+
end
|
81
|
+
|
82
|
+
relevant_nodes
|
83
|
+
end
|
84
|
+
|
85
|
+
def calculate_abc(method_node)
|
86
|
+
a = count_nodes(method_node, assignment_nodes)
|
87
|
+
b = count_nodes(method_node, branch_nodes) + 1
|
88
|
+
c = count_nodes(method_node, condition_nodes)
|
89
|
+
abc = Math.sqrt(a**2 + b**2 + c**2).round
|
90
|
+
abc
|
91
|
+
end
|
92
|
+
|
93
|
+
def label_for(node)
|
94
|
+
# A default case is deliberately omitted since I know of no way this
|
95
|
+
# could fail and want it to fail fast.
|
96
|
+
node.detect {|x|
|
97
|
+
[:@ident, :@op, :@kw, :@const, :@backtick].include?(x[0])
|
98
|
+
}[1]
|
99
|
+
end
|
100
|
+
|
101
|
+
def count_nodes(node, types)
|
102
|
+
node.flatten.select { |n| types.include?(n) }.length
|
103
|
+
end
|
104
|
+
|
105
|
+
def assignment_nodes
|
106
|
+
[:assign, :opassign]
|
107
|
+
end
|
108
|
+
|
109
|
+
def method_nodes
|
110
|
+
[:def, :defs]
|
111
|
+
end
|
112
|
+
|
113
|
+
def container_nodes
|
114
|
+
[:class, :module]
|
115
|
+
end
|
116
|
+
|
117
|
+
def branch_nodes
|
118
|
+
[:call, :fcall, :brace_block, :do_block]
|
119
|
+
end
|
120
|
+
|
121
|
+
def condition_nodes
|
122
|
+
[:==, :===, :"<>", :"<=", :">=", :"=~", :>, :<, :else, :"<=>"]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def file_names
|
127
|
+
Dir[opts.fetch(:files)]
|
128
|
+
end
|
129
|
+
|
130
|
+
def order(result)
|
131
|
+
result.sort_by(&:sort_index).reverse
|
132
|
+
end
|
133
|
+
|
134
|
+
def max_allowed_complexity
|
135
|
+
opts.fetch(:max)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Cane
|
2
|
+
|
3
|
+
# Value object used by AbcCheck for a method that is too complicated.
|
4
|
+
class AbcMaxViolation < Struct.new(:file_name, :detail, :complexity)
|
5
|
+
def columns
|
6
|
+
[file_name, detail, complexity]
|
7
|
+
end
|
8
|
+
|
9
|
+
def description
|
10
|
+
"Methods exceeded maximum allowed ABC complexity"
|
11
|
+
end
|
12
|
+
|
13
|
+
alias_method :sort_index, :complexity
|
14
|
+
end
|
15
|
+
end
|
data/lib/cane/cli.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'cane'
|
2
|
+
require 'cane/version'
|
3
|
+
|
4
|
+
require 'cane/cli/spec'
|
5
|
+
require 'cane/cli/translator'
|
6
|
+
|
7
|
+
module Cane
|
8
|
+
module CLI
|
9
|
+
|
10
|
+
def run(args)
|
11
|
+
opts = Spec.new.parse(args)
|
12
|
+
if opts
|
13
|
+
Cane.run(opts)
|
14
|
+
else
|
15
|
+
true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
module_function :run
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|