sparqcode_cane 1.3.0.1
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 +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
|