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
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'cane/cli/translator'
|
3
|
+
|
4
|
+
module Cane
|
5
|
+
module CLI
|
6
|
+
|
7
|
+
# Provides a specification for the command line interface that drives
|
8
|
+
# documentation, parsing, and default values.
|
9
|
+
class Spec
|
10
|
+
DEFAULTS = {
|
11
|
+
abc_glob: '{app,lib}/**/*.rb',
|
12
|
+
abc_max: '15',
|
13
|
+
style_glob: '{app,lib,spec}/**/*.rb',
|
14
|
+
style_measure: '80',
|
15
|
+
doc_glob: '{app,lib}/**/*.rb',
|
16
|
+
max_violations: '0',
|
17
|
+
}
|
18
|
+
|
19
|
+
# Exception to indicate that no further processing is required and the
|
20
|
+
# program can exit. This is used to handle --help and --version flags.
|
21
|
+
class OptionsHandled < RuntimeError; end
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
add_banner
|
25
|
+
|
26
|
+
add_abc_options
|
27
|
+
add_style_options
|
28
|
+
add_doc_options
|
29
|
+
add_threshold_options
|
30
|
+
add_cane_options
|
31
|
+
|
32
|
+
add_version
|
33
|
+
add_help
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse(args)
|
37
|
+
parser.parse!(get_default_options + args)
|
38
|
+
|
39
|
+
Translator.new(options, DEFAULTS).to_hash
|
40
|
+
rescue OptionsHandled
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_default_options
|
45
|
+
if File.exists?('./.cane')
|
46
|
+
File.read('./.cane').gsub("\n", ' ').split(' ')
|
47
|
+
else
|
48
|
+
[]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_banner
|
53
|
+
parser.banner = <<-BANNER
|
54
|
+
Usage: cane [options]
|
55
|
+
|
56
|
+
You can also put these options in a .cane file.
|
57
|
+
|
58
|
+
BANNER
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_abc_options
|
62
|
+
add_option %w(--abc-glob GLOB), "Glob to run ABC metrics over"
|
63
|
+
add_option %w(--abc-max VALUE), "Ignore methods under this complexity"
|
64
|
+
add_option %w(--no-abc), "Disable ABC checking"
|
65
|
+
|
66
|
+
parser.separator ""
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_style_options
|
70
|
+
add_option %w(--style-glob GLOB), "Glob to run style metrics over"
|
71
|
+
add_option %w(--style-measure VALUE), "Max line length"
|
72
|
+
add_option %w(--no-style), "Disable style checking"
|
73
|
+
|
74
|
+
parser.separator ""
|
75
|
+
end
|
76
|
+
|
77
|
+
def add_doc_options
|
78
|
+
add_option %w(--doc-glob GLOB), "Glob to run documentation checks over"
|
79
|
+
add_option %w(--no-doc), "Disable documentation checking"
|
80
|
+
|
81
|
+
parser.separator ""
|
82
|
+
end
|
83
|
+
|
84
|
+
def add_threshold_options
|
85
|
+
desc = "If FILE contains a number, verify it is >= to THRESHOLD."
|
86
|
+
parser.on("--gte FILE,THRESHOLD", Array, desc) do |opts|
|
87
|
+
(options[:threshold] ||= []) << opts.unshift(:>=)
|
88
|
+
end
|
89
|
+
|
90
|
+
parser.separator ""
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_cane_options
|
94
|
+
add_option %w(--max-violations VALUE), "Max allowed violations"
|
95
|
+
|
96
|
+
parser.separator ""
|
97
|
+
end
|
98
|
+
|
99
|
+
def add_version
|
100
|
+
parser.on_tail("--version", "Show version") do
|
101
|
+
puts Cane::VERSION
|
102
|
+
raise OptionsHandled
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def add_help
|
107
|
+
parser.on_tail("-h", "--help", "Show this message") do
|
108
|
+
puts parser
|
109
|
+
raise OptionsHandled
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def add_option(option, description)
|
114
|
+
option_key = option[0].gsub('--', '').tr('-', '_').to_sym
|
115
|
+
|
116
|
+
if DEFAULTS.has_key?(option_key)
|
117
|
+
description += " (default: %s)" % DEFAULTS[option_key]
|
118
|
+
end
|
119
|
+
|
120
|
+
parser.on(option.join(' '), description) do |v|
|
121
|
+
options[option_key] = v
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def options
|
126
|
+
@options ||= {}
|
127
|
+
end
|
128
|
+
|
129
|
+
def parser
|
130
|
+
@parser ||= OptionParser.new
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Cane
|
2
|
+
module CLI
|
3
|
+
|
4
|
+
# Translates CLI options with given defaults to a hash suitable to be
|
5
|
+
# passed to `Cane.run`.
|
6
|
+
class Translator < Struct.new(:options, :defaults)
|
7
|
+
def to_hash
|
8
|
+
result = {}
|
9
|
+
translate_abc_options(result)
|
10
|
+
translate_doc_options(result)
|
11
|
+
translate_style_options(result)
|
12
|
+
|
13
|
+
result[:threshold] = options.fetch(:threshold, [])
|
14
|
+
result[:max_violations] = option_with_default(:max_violations).to_i
|
15
|
+
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
def translate_abc_options(result)
|
20
|
+
result[:abc] = {
|
21
|
+
files: option_with_default(:abc_glob),
|
22
|
+
max: option_with_default(:abc_max).to_i
|
23
|
+
} unless check_disabled(:no_abc, [:abc_glob, :abc_max])
|
24
|
+
end
|
25
|
+
|
26
|
+
def translate_style_options(result)
|
27
|
+
result[:style] = {
|
28
|
+
files: option_with_default(:style_glob),
|
29
|
+
measure: option_with_default(:style_measure).to_i,
|
30
|
+
} unless check_disabled(:no_style, [:style_glob])
|
31
|
+
end
|
32
|
+
|
33
|
+
def translate_doc_options(result)
|
34
|
+
result[:doc] = {
|
35
|
+
files: option_with_default(:doc_glob),
|
36
|
+
} unless check_disabled(:no_doc, [:doc_glob])
|
37
|
+
end
|
38
|
+
|
39
|
+
def check_disabled(check, params)
|
40
|
+
relevant_options = options.keys & params + [check]
|
41
|
+
|
42
|
+
check == relevant_options[-1]
|
43
|
+
end
|
44
|
+
|
45
|
+
def option_with_default(key)
|
46
|
+
options.fetch(key, defaults.fetch(key))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Cane
|
2
|
+
|
3
|
+
# Creates violations for class definitions that do not have an explantory
|
4
|
+
# comment immediately preceeding.
|
5
|
+
class DocCheck < Struct.new(:opts)
|
6
|
+
def violations
|
7
|
+
file_names.map { |file_name|
|
8
|
+
find_violations(file_name)
|
9
|
+
}.flatten
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_violations(file_name)
|
13
|
+
last_line = ""
|
14
|
+
File.open(file_name, 'r:utf-8').lines.map.with_index do |line, number|
|
15
|
+
result = if class_definition?(line) && !comment?(last_line)
|
16
|
+
UndocumentedClassViolation.new(file_name, number + 1, line)
|
17
|
+
end
|
18
|
+
last_line = line
|
19
|
+
result
|
20
|
+
end.compact
|
21
|
+
end
|
22
|
+
|
23
|
+
def file_names
|
24
|
+
Dir[opts.fetch(:files)]
|
25
|
+
end
|
26
|
+
|
27
|
+
def class_definition?(line)
|
28
|
+
line =~ /^\s*class\s+/ and $'.index('<<') != 0
|
29
|
+
end
|
30
|
+
|
31
|
+
def comment?(line)
|
32
|
+
line =~ /^\s*#/
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Value object used by DocCheck.
|
37
|
+
class UndocumentedClassViolation < Struct.new(:file_name, :number, :line)
|
38
|
+
def description
|
39
|
+
"Classes are not documented"
|
40
|
+
end
|
41
|
+
|
42
|
+
def columns
|
43
|
+
["%s:%i" % [file_name, number], extract_class_name(line)]
|
44
|
+
end
|
45
|
+
|
46
|
+
def extract_class_name(line)
|
47
|
+
line.match(/class (\S+)/)[1]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/tasklib'
|
3
|
+
|
4
|
+
module Cane
|
5
|
+
# Creates a rake task to run cane with given configuration.
|
6
|
+
#
|
7
|
+
# Examples
|
8
|
+
#
|
9
|
+
# desc "Run code quality checks"
|
10
|
+
# Cane::RakeTask.new(:quality) do |cane|
|
11
|
+
# cane.abc_max = 10
|
12
|
+
# cane.doc_glob = 'lib/**/*.rb'
|
13
|
+
# cane.no_style = true
|
14
|
+
# cane.add_threshold 'coverage/covered_percent', :>=, 99
|
15
|
+
# end
|
16
|
+
class RakeTask < ::Rake::TaskLib
|
17
|
+
attr_accessor :name
|
18
|
+
|
19
|
+
# Glob to run ABC metrics over (default: "lib/**/*.rb")
|
20
|
+
attr_accessor :abc_glob
|
21
|
+
# Max complexity of methods to allow (default: 15)
|
22
|
+
attr_accessor :abc_max
|
23
|
+
# Glob to run style checks over (default: "{lib,spec}/**/*.rb")
|
24
|
+
attr_accessor :style_glob
|
25
|
+
# TRUE to disable style checks
|
26
|
+
attr_accessor :no_style
|
27
|
+
# Max line length (default: 80)
|
28
|
+
attr_accessor :style_measure
|
29
|
+
# Glob to run doc checks over (default: "lib/**/*.rb")
|
30
|
+
attr_accessor :doc_glob
|
31
|
+
# TRUE to disable doc checks
|
32
|
+
attr_accessor :no_doc
|
33
|
+
# Max violations to tolerate (default: 0)
|
34
|
+
attr_accessor :max_violations
|
35
|
+
|
36
|
+
# Add a threshold check. If the file exists and it contains a number,
|
37
|
+
# compare that number with the given value using the operator.
|
38
|
+
def add_threshold(file, operator, value)
|
39
|
+
@threshold << [operator, file, value]
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(task_name = nil)
|
43
|
+
self.name = task_name || :cane
|
44
|
+
@threshold = []
|
45
|
+
yield self if block_given?
|
46
|
+
|
47
|
+
unless ::Rake.application.last_comment
|
48
|
+
desc %(Check code quality metrics with cane)
|
49
|
+
end
|
50
|
+
|
51
|
+
task name do
|
52
|
+
require 'cane/cli'
|
53
|
+
abort unless Cane.run(translated_options)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def options
|
58
|
+
[
|
59
|
+
:abc_glob,
|
60
|
+
:abc_max,
|
61
|
+
:doc_glob,
|
62
|
+
:no_doc,
|
63
|
+
:max_violations,
|
64
|
+
:style_glob,
|
65
|
+
:no_style,
|
66
|
+
:style_measure
|
67
|
+
].inject(threshold: @threshold) do |opts, setting|
|
68
|
+
value = self.send(setting)
|
69
|
+
opts[setting] = value unless value.nil?
|
70
|
+
opts
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def default_options
|
75
|
+
Cane::CLI::Spec::DEFAULTS
|
76
|
+
end
|
77
|
+
|
78
|
+
def translated_options
|
79
|
+
Cane::CLI::Translator.new(options, default_options).to_hash
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'cane/style_violation'
|
2
|
+
|
3
|
+
module Cane
|
4
|
+
|
5
|
+
# Creates violations for files that do not meet style conventions. Only
|
6
|
+
# highly obvious, probable, and non-controversial checks are performed here.
|
7
|
+
# It is not the goal of the tool to provide an extensive style report, but
|
8
|
+
# only to prevent studid mistakes.
|
9
|
+
class StyleCheck < Struct.new(:opts)
|
10
|
+
def violations
|
11
|
+
file_list.map do |file_path|
|
12
|
+
map_lines(file_path) do |line, line_number|
|
13
|
+
violations_for_line(line.chomp).map do |message|
|
14
|
+
StyleViolation.new(file_path, line_number + 1, message)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end.flatten
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def violations_for_line(line)
|
23
|
+
result = []
|
24
|
+
if line.length > measure
|
25
|
+
result << "Line is >%i characters (%i)" % [measure, line.length]
|
26
|
+
end
|
27
|
+
result << "Line contains trailing whitespace" if line =~ /\s$/
|
28
|
+
result << "Line contains hard tabs" if line =~ /\t/
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
def file_list
|
33
|
+
Dir[opts.fetch(:files)]
|
34
|
+
end
|
35
|
+
|
36
|
+
def measure
|
37
|
+
opts.fetch(:measure)
|
38
|
+
end
|
39
|
+
|
40
|
+
def map_lines(file_path, &block)
|
41
|
+
File.open(file_path).each_line.map.with_index(&block)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -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
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'cane/threshold_violation'
|
2
|
+
|
3
|
+
# Configurable check that allows the contents of a file to be compared against
|
4
|
+
# a given value.
|
5
|
+
class ThresholdCheck < Struct.new(:checks)
|
6
|
+
def violations
|
7
|
+
checks.map do |operator, file, limit|
|
8
|
+
value = value_from_file(file)
|
9
|
+
|
10
|
+
unless value.send(operator, limit.to_f)
|
11
|
+
ThresholdViolation.new(file, operator, value, limit)
|
12
|
+
end
|
13
|
+
end.compact
|
14
|
+
end
|
15
|
+
|
16
|
+
def value_from_file(file)
|
17
|
+
begin
|
18
|
+
contents = File.read(file).chomp.to_f
|
19
|
+
rescue Errno::ENOENT
|
20
|
+
UnavailableValue.new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Null object for all cases when the value to be compared against cannot be
|
25
|
+
# read.
|
26
|
+
class UnavailableValue
|
27
|
+
def >=(_); false end
|
28
|
+
def to_s; 'unavailable' end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Value object used by ThresholdCheck.
|
2
|
+
class ThresholdViolation < Struct.new(:name, :operator, :value, :limit)
|
3
|
+
def description
|
4
|
+
"Quality threshold crossed"
|
5
|
+
end
|
6
|
+
|
7
|
+
def columns
|
8
|
+
["%s is %s, should be %s %s" % [
|
9
|
+
name,
|
10
|
+
value,
|
11
|
+
operator,
|
12
|
+
limit
|
13
|
+
]]
|
14
|
+
end
|
15
|
+
end
|