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.
@@ -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.
@@ -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).
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'cane/cli'
4
+
5
+ result = Cane::CLI.run(ARGV)
6
+
7
+ exit(1) unless result
@@ -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
@@ -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
@@ -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