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
@@ -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
|