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.
@@ -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
- file_names.map { |file_name|
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.open(file_name, 'r:utf-8').lines.map.with_index do |line, number|
34
+ Cane::File.iterator(file_name).map_with_index do |line, number|
15
35
  result = if class_definition?(line) && !comment?(last_line)
16
- UndocumentedClassViolation.new(file_name, number + 1, line)
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(:files)]
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
@@ -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
@@ -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
- # 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
- # 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
- @threshold << [operator, file, value]
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
- @threshold = []
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(translated_options)
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
@@ -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 do |message|
15
- StyleViolation.new(file_path, line_number + 1, message)
16
- end
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(:files)].reject { |f| excluded?(f) }
62
+ Dir[opts.fetch(:style_glob)].reject {|f| excluded?(f) }
35
63
  end
36
64
 
37
65
  def measure
38
- opts.fetch(:measure)
66
+ opts.fetch(:style_measure)
39
67
  end
40
68
 
41
69
  def map_lines(file_path, &block)
42
- File.open(file_path).each_line.map.with_index(&block)
70
+ Cane::File.iterator(file_path).map_with_index(&block)
43
71
  end
44
72
 
45
73
  def exclusions
46
- @exclusions ||= opts.fetch(:exclusions, []).to_set
74
+ @exclusions ||= opts.fetch(:style_exclude, []).flatten.to_set
47
75
  end
48
76
 
49
77
  def excluded?(file)
@@ -1,30 +1,55 @@
1
- require 'cane/threshold_violation'
1
+ require 'cane/file'
2
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)
3
+ module Cane
9
4
 
10
- unless value.send(operator, limit.to_f)
11
- ThresholdViolation.new(file, operator, value, limit)
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.compact
14
- end
39
+ end
40
+
41
+ def thresholds
42
+ (opts[:gte] || []).map do |x|
43
+ x.unshift(:>=)
44
+ end
45
+ end
15
46
 
16
- def value_from_file(file)
17
- begin
18
- contents = File.read(file).chomp.to_f
19
- rescue Errno::ENOENT
20
- UnavailableValue.new
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
@@ -1,3 +1,3 @@
1
1
  module Cane
2
- VERSION = '1.4.0'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -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 < Struct.new(:violations)
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 '' if violations.empty?
21
+ return "" if violations.empty?
10
22
 
11
- grouped_violations.map do |description, violations|
12
- format_group_header(description, violations) +
13
- format_violations(violations)
14
- end.flatten.join("\n") + "\n\n" + totals + "\n\n"
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
- column_widths = calculate_columm_widths(violations)
36
+ columns = [:file_and_line, :label, :value]
37
+
38
+ widths = column_widths(violations, columns)
29
39
 
30
- violations.map do |violation|
31
- format_violation(violation, column_widths)
40
+ violations.map do |v|
41
+ format_violation(v, widths)
32
42
  end
33
43
  end
34
44
 
35
- def format_violation(violation, column_widths)
36
- [
37
- ' ' + violation.columns.map.with_index { |column, index|
38
- "%-#{column_widths[index]}s" % column
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 calculate_columm_widths(violations)
44
- violations.map { |violation|
45
- violation.columns.map { |x| x.to_s.length }
46
- }.transpose.map(&:max)
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 grouped_violations
50
- violations.group_by(&:description)
57
+ def totals
58
+ "Total Violations: #{violations.length}"
51
59
  end
52
60
  end
53
61
  end