cane 1.4.0 → 2.0.0
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 +12 -0
- data/README.md +9 -25
- data/lib/cane.rb +7 -18
- data/lib/cane/abc_check.rb +85 -22
- data/lib/cane/cli.rb +3 -5
- data/lib/cane/cli/spec.rb +59 -53
- data/lib/cane/doc_check.rb +30 -16
- data/lib/cane/encoding_aware_iterator.rb +35 -0
- data/lib/cane/file.rb +22 -0
- data/lib/cane/rake_task.rb +12 -41
- data/lib/cane/style_check.rb +36 -8
- data/lib/cane/threshold_check.rb +47 -22
- data/lib/cane/version.rb +1 -1
- data/lib/cane/violation_formatter.rb +33 -25
- data/spec/abc_check_spec.rb +53 -21
- data/spec/cane_spec.rb +21 -12
- data/spec/doc_check_spec.rb +30 -9
- data/spec/encoding_aware_iterator_spec.rb +24 -0
- data/spec/rake_task_spec.rb +13 -0
- data/spec/style_check_spec.rb +10 -4
- data/spec/threshold_check_spec.rb +4 -3
- data/spec/violation_formatter_spec.rb +4 -5
- metadata +8 -7
- data/lib/cane/abc_max_violation.rb +0 -15
- data/lib/cane/cli/translator.rb +0 -71
- data/lib/cane/style_violation.rb +0 -10
- data/lib/cane/syntax_violation.rb +0 -20
- data/lib/cane/threshold_violation.rb +0 -15
data/lib/cane/doc_check.rb
CHANGED
@@ -1,19 +1,44 @@
|
|
1
|
+
require 'cane/file'
|
2
|
+
|
1
3
|
module Cane
|
2
4
|
|
3
5
|
# Creates violations for class definitions that do not have an explantory
|
4
6
|
# comment immediately preceeding.
|
5
7
|
class DocCheck < Struct.new(:opts)
|
8
|
+
|
9
|
+
def self.key; :doc; end
|
10
|
+
def self.name; "documentation checking"; end
|
11
|
+
def self.options
|
12
|
+
{
|
13
|
+
doc_glob: ['Glob to run doc checks over',
|
14
|
+
default: '{app,lib}/**/*.rb',
|
15
|
+
variable: 'GLOB',
|
16
|
+
clobber: :no_doc],
|
17
|
+
no_doc: ['Disable documentation checking', cast: ->(x) { !x }]
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Stolen from ERB source.
|
22
|
+
MAGIC_COMMENT_REGEX = %r"coding\s*[=:]\s*([[:alnum:]\-_]+)"
|
23
|
+
|
6
24
|
def violations
|
7
|
-
|
25
|
+
return [] if opts[:no_doc]
|
26
|
+
|
27
|
+
file_names.map {|file_name|
|
8
28
|
find_violations(file_name)
|
9
29
|
}.flatten
|
10
30
|
end
|
11
31
|
|
12
32
|
def find_violations(file_name)
|
13
33
|
last_line = ""
|
14
|
-
File.
|
34
|
+
Cane::File.iterator(file_name).map_with_index do |line, number|
|
15
35
|
result = if class_definition?(line) && !comment?(last_line)
|
16
|
-
|
36
|
+
{
|
37
|
+
file: file_name,
|
38
|
+
line: number + 1,
|
39
|
+
label: extract_class_name(line),
|
40
|
+
description: "Classes are not documented"
|
41
|
+
}
|
17
42
|
end
|
18
43
|
last_line = line
|
19
44
|
result
|
@@ -21,7 +46,7 @@ module Cane
|
|
21
46
|
end
|
22
47
|
|
23
48
|
def file_names
|
24
|
-
Dir[opts.fetch(:
|
49
|
+
Dir[opts.fetch(:doc_glob)]
|
25
50
|
end
|
26
51
|
|
27
52
|
def class_definition?(line)
|
@@ -29,18 +54,7 @@ module Cane
|
|
29
54
|
end
|
30
55
|
|
31
56
|
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)]
|
57
|
+
line =~ /^\s*#/ && !(MAGIC_COMMENT_REGEX =~ line)
|
44
58
|
end
|
45
59
|
|
46
60
|
def extract_class_name(line)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Cane
|
2
|
+
|
3
|
+
# Provides iteration over lines (from a file), correctly handling encoding.
|
4
|
+
class EncodingAwareIterator < Struct.new(:lines)
|
5
|
+
def map_with_index(&block)
|
6
|
+
lines.map.with_index do |line, index|
|
7
|
+
with_encoding_retry(line) do
|
8
|
+
block.call(line, index)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
# This is to avoid re-encoding every line, since most are valid. I should
|
16
|
+
# performance test this but haven't (maybe can just re-encode always but my
|
17
|
+
# hunch says no).
|
18
|
+
def with_encoding_retry(line, &block)
|
19
|
+
retried = false
|
20
|
+
begin
|
21
|
+
block.call(line)
|
22
|
+
rescue ArgumentError
|
23
|
+
if retried
|
24
|
+
# I haven't seen input that causes this to occur. Please report it!
|
25
|
+
raise
|
26
|
+
else
|
27
|
+
line.encode!('UTF-8', 'UTF-8', invalid: :replace)
|
28
|
+
retried = true
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
data/lib/cane/file.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'cane/encoding_aware_iterator'
|
2
|
+
|
3
|
+
module Cane
|
4
|
+
|
5
|
+
# An interface for interacting with files that ensures encoding is handled in
|
6
|
+
# a consistent manner.
|
7
|
+
class File
|
8
|
+
class << self
|
9
|
+
def iterator(path)
|
10
|
+
EncodingAwareIterator.new(open(path).lines)
|
11
|
+
end
|
12
|
+
|
13
|
+
def contents(path)
|
14
|
+
open(path).read
|
15
|
+
end
|
16
|
+
|
17
|
+
def open(path)
|
18
|
+
::File.open(path, 'r:utf-8')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/cane/rake_task.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'rake'
|
2
2
|
require 'rake/tasklib'
|
3
3
|
|
4
|
+
require 'cane/cli/spec'
|
5
|
+
|
4
6
|
module Cane
|
5
7
|
# Creates a rake task to run cane with given configuration.
|
6
8
|
#
|
@@ -15,35 +17,22 @@ module Cane
|
|
15
17
|
# end
|
16
18
|
class RakeTask < ::Rake::TaskLib
|
17
19
|
attr_accessor :name
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
# File containing list of exclusions in YAML format
|
36
|
-
attr_accessor :exclusions_file
|
20
|
+
OPTIONS = Cane::CLI::Spec::OPTIONS
|
21
|
+
OPTIONS.each do |name, value|
|
22
|
+
attr_accessor name
|
23
|
+
end
|
37
24
|
|
38
25
|
# Add a threshold check. If the file exists and it contains a number,
|
39
26
|
# compare that number with the given value using the operator.
|
40
27
|
def add_threshold(file, operator, value)
|
41
|
-
|
28
|
+
if operator == :>=
|
29
|
+
@gte << [file, value]
|
30
|
+
end
|
42
31
|
end
|
43
32
|
|
44
33
|
def initialize(task_name = nil)
|
45
34
|
self.name = task_name || :cane
|
46
|
-
@
|
35
|
+
@gte = []
|
47
36
|
yield self if block_given?
|
48
37
|
|
49
38
|
unless ::Rake.application.last_comment
|
@@ -52,34 +41,16 @@ module Cane
|
|
52
41
|
|
53
42
|
task name do
|
54
43
|
require 'cane/cli'
|
55
|
-
abort unless Cane.run(
|
44
|
+
abort unless Cane.run(OPTIONS.merge(options), Cane::CLI::Spec::CHECKS)
|
56
45
|
end
|
57
46
|
end
|
58
47
|
|
59
48
|
def options
|
60
|
-
|
61
|
-
:abc_glob,
|
62
|
-
:abc_max,
|
63
|
-
:doc_glob,
|
64
|
-
:no_doc,
|
65
|
-
:max_violations,
|
66
|
-
:style_glob,
|
67
|
-
:no_style,
|
68
|
-
:style_measure,
|
69
|
-
:exclusions_file
|
70
|
-
].inject(threshold: @threshold) do |opts, setting|
|
49
|
+
OPTIONS.keys.inject({}) do |opts, setting|
|
71
50
|
value = self.send(setting)
|
72
51
|
opts[setting] = value unless value.nil?
|
73
52
|
opts
|
74
53
|
end
|
75
54
|
end
|
76
|
-
|
77
|
-
def default_options
|
78
|
-
Cane::CLI::Spec::DEFAULTS
|
79
|
-
end
|
80
|
-
|
81
|
-
def translated_options
|
82
|
-
Cane::CLI::Translator.new(options, default_options).to_hash
|
83
|
-
end
|
84
55
|
end
|
85
56
|
end
|
data/lib/cane/style_check.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
require 'cane/style_violation'
|
2
1
|
require 'set'
|
3
2
|
|
3
|
+
require 'cane/file'
|
4
|
+
|
4
5
|
module Cane
|
5
6
|
|
6
7
|
# Creates violations for files that do not meet style conventions. Only
|
@@ -8,12 +9,39 @@ module Cane
|
|
8
9
|
# It is not the goal of the tool to provide an extensive style report, but
|
9
10
|
# only to prevent stupid mistakes.
|
10
11
|
class StyleCheck < Struct.new(:opts)
|
12
|
+
|
13
|
+
def self.key; :style; end
|
14
|
+
def self.name; "style checking"; end
|
15
|
+
def self.options
|
16
|
+
{
|
17
|
+
style_glob: ['Glob to run style checks over',
|
18
|
+
default: '{app,lib,spec}/**/*.rb',
|
19
|
+
variable: 'GLOB',
|
20
|
+
clobber: :no_style],
|
21
|
+
style_measure: ['Max line length',
|
22
|
+
default: 80,
|
23
|
+
cast: :to_i,
|
24
|
+
clobber: :no_style],
|
25
|
+
style_exclude: ['Exclude file from style checking',
|
26
|
+
variable: 'FILE',
|
27
|
+
type: Array,
|
28
|
+
default: [],
|
29
|
+
clobber: :no_style],
|
30
|
+
no_style: ['Disable style checking']
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
11
34
|
def violations
|
35
|
+
return [] if opts[:no_style]
|
36
|
+
|
12
37
|
file_list.map do |file_path|
|
13
38
|
map_lines(file_path) do |line, line_number|
|
14
|
-
violations_for_line(line.chomp).map
|
15
|
-
|
16
|
-
|
39
|
+
violations_for_line(line.chomp).map {|message| {
|
40
|
+
file: file_path,
|
41
|
+
line: line_number + 1,
|
42
|
+
label: message,
|
43
|
+
description: "Lines violated style requirements"
|
44
|
+
}}
|
17
45
|
end
|
18
46
|
end.flatten
|
19
47
|
end
|
@@ -31,19 +59,19 @@ module Cane
|
|
31
59
|
end
|
32
60
|
|
33
61
|
def file_list
|
34
|
-
Dir[opts.fetch(:
|
62
|
+
Dir[opts.fetch(:style_glob)].reject {|f| excluded?(f) }
|
35
63
|
end
|
36
64
|
|
37
65
|
def measure
|
38
|
-
opts.fetch(:
|
66
|
+
opts.fetch(:style_measure)
|
39
67
|
end
|
40
68
|
|
41
69
|
def map_lines(file_path, &block)
|
42
|
-
File.
|
70
|
+
Cane::File.iterator(file_path).map_with_index(&block)
|
43
71
|
end
|
44
72
|
|
45
73
|
def exclusions
|
46
|
-
@exclusions ||= opts.fetch(:
|
74
|
+
@exclusions ||= opts.fetch(:style_exclude, []).flatten.to_set
|
47
75
|
end
|
48
76
|
|
49
77
|
def excluded?(file)
|
data/lib/cane/threshold_check.rb
CHANGED
@@ -1,30 +1,55 @@
|
|
1
|
-
require 'cane/
|
1
|
+
require 'cane/file'
|
2
2
|
|
3
|
-
|
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)
|
3
|
+
module Cane
|
9
4
|
|
10
|
-
|
11
|
-
|
5
|
+
# Configurable check that allows the contents of a file to be compared against
|
6
|
+
# a given value.
|
7
|
+
class ThresholdCheck < Struct.new(:opts)
|
8
|
+
|
9
|
+
def self.key; :threshold; end
|
10
|
+
def self.options
|
11
|
+
{
|
12
|
+
gte: ["If FILE contains a number, verify it is >= to THRESHOLD",
|
13
|
+
variable: "FILE,THRESHOLD",
|
14
|
+
type: Array]
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def violations
|
19
|
+
thresholds.map do |operator, file, limit|
|
20
|
+
value = value_from_file(file)
|
21
|
+
|
22
|
+
unless value.send(operator, limit.to_f)
|
23
|
+
{
|
24
|
+
description: 'Quality threshold crossed',
|
25
|
+
label: "%s is %s, should be %s %s" % [
|
26
|
+
file, value, operator, limit
|
27
|
+
]
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end.compact
|
31
|
+
end
|
32
|
+
|
33
|
+
def value_from_file(file)
|
34
|
+
begin
|
35
|
+
contents = Cane::File.contents(file).chomp.to_f
|
36
|
+
rescue Errno::ENOENT
|
37
|
+
UnavailableValue.new
|
12
38
|
end
|
13
|
-
end
|
14
|
-
|
39
|
+
end
|
40
|
+
|
41
|
+
def thresholds
|
42
|
+
(opts[:gte] || []).map do |x|
|
43
|
+
x.unshift(:>=)
|
44
|
+
end
|
45
|
+
end
|
15
46
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
47
|
+
# Null object for all cases when the value to be compared against cannot be
|
48
|
+
# read.
|
49
|
+
class UnavailableValue
|
50
|
+
def >=(_); false end
|
51
|
+
def to_s; 'unavailable' end
|
21
52
|
end
|
22
53
|
end
|
23
54
|
|
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
55
|
end
|
data/lib/cane/version.rb
CHANGED
@@ -1,53 +1,61 @@
|
|
1
1
|
require 'stringio'
|
2
|
+
require 'ostruct'
|
2
3
|
|
3
4
|
module Cane
|
4
5
|
|
5
6
|
# Computes a string to be displayed as output from an array of violations
|
6
7
|
# computed by the checks.
|
7
|
-
class ViolationFormatter
|
8
|
+
class ViolationFormatter
|
9
|
+
attr_reader :violations
|
10
|
+
|
11
|
+
def initialize(violations)
|
12
|
+
@violations = violations.map do |v|
|
13
|
+
v.merge(file_and_line: v[:line] ?
|
14
|
+
"%s:%i" % v.values_at(:file, :line) :
|
15
|
+
v[:file]
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
8
20
|
def to_s
|
9
|
-
return
|
21
|
+
return "" if violations.empty?
|
10
22
|
|
11
|
-
|
12
|
-
format_group_header(
|
13
|
-
format_violations(
|
14
|
-
end.
|
23
|
+
violations.group_by {|x| x[:description] }.map do |d, vs|
|
24
|
+
format_group_header(d, vs) +
|
25
|
+
format_violations(vs)
|
26
|
+
end.join("\n") + "\n\n" + totals + "\n\n"
|
15
27
|
end
|
16
28
|
|
17
29
|
protected
|
18
30
|
|
19
|
-
def totals
|
20
|
-
"Total Violations: #{violations.count}"
|
21
|
-
end
|
22
|
-
|
23
31
|
def format_group_header(description, violations)
|
24
32
|
["", "%s (%i):" % [description, violations.length], ""]
|
25
33
|
end
|
26
34
|
|
27
35
|
def format_violations(violations)
|
28
|
-
|
36
|
+
columns = [:file_and_line, :label, :value]
|
37
|
+
|
38
|
+
widths = column_widths(violations, columns)
|
29
39
|
|
30
|
-
violations.map do |
|
31
|
-
format_violation(
|
40
|
+
violations.map do |v|
|
41
|
+
format_violation(v, widths)
|
32
42
|
end
|
33
43
|
end
|
34
44
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
}.join(' ')
|
40
|
-
]
|
45
|
+
def column_widths(violations, columns)
|
46
|
+
columns.each_with_object({}) do |column, h|
|
47
|
+
h[column] = violations.map {|v| v[column].to_s.length }.max
|
48
|
+
end
|
41
49
|
end
|
42
50
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
}.
|
51
|
+
def format_violation(v, column_widths)
|
52
|
+
' ' + column_widths.keys.map {|column|
|
53
|
+
v[column].to_s.ljust(column_widths[column])
|
54
|
+
}.join(' ').strip
|
47
55
|
end
|
48
56
|
|
49
|
-
def
|
50
|
-
violations.
|
57
|
+
def totals
|
58
|
+
"Total Violations: #{violations.length}"
|
51
59
|
end
|
52
60
|
end
|
53
61
|
end
|