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,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,10 @@
1
+ # Value object used by StyleCheck.
2
+ class StyleViolation < Struct.new(:file_name, :line, :message)
3
+ def description
4
+ "Lines violated style requirements"
5
+ end
6
+
7
+ def columns
8
+ ["%s:%i" % [file_name, line], message]
9
+ end
10
+ 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