sugarcane 0.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.
- checksums.yaml +7 -0
- data/HISTORY.md +126 -0
- data/LICENSE +14 -0
- data/README.md +235 -0
- data/bin/sugarcane +7 -0
- data/lib/cane.rb +4 -0
- data/lib/sugarcane/abc_check.rb +216 -0
- data/lib/sugarcane/cli/options.rb +30 -0
- data/lib/sugarcane/cli/parser.rb +191 -0
- data/lib/sugarcane/cli.rb +21 -0
- data/lib/sugarcane/default_checks.rb +17 -0
- data/lib/sugarcane/doc_check.rb +156 -0
- data/lib/sugarcane/encoding_aware_iterator.rb +30 -0
- data/lib/sugarcane/ext/array.rb +24 -0
- data/lib/sugarcane/file.rb +30 -0
- data/lib/sugarcane/json_formatter.rb +17 -0
- data/lib/sugarcane/menu.rb +228 -0
- data/lib/sugarcane/rake_task.rb +78 -0
- data/lib/sugarcane/runner.rb +59 -0
- data/lib/sugarcane/style_check.rb +91 -0
- data/lib/sugarcane/task_runner.rb +20 -0
- data/lib/sugarcane/threshold_check.rb +83 -0
- data/lib/sugarcane/version.rb +3 -0
- data/lib/sugarcane/violation_formatter.rb +75 -0
- data/spec/abc_check_spec.rb +164 -0
- data/spec/cane_spec.rb +136 -0
- data/spec/cli_spec.rb +23 -0
- data/spec/doc_check_spec.rb +163 -0
- data/spec/encoding_aware_iterator_spec.rb +32 -0
- data/spec/file_spec.rb +25 -0
- data/spec/json_formatter_spec.rb +10 -0
- data/spec/parser_spec.rb +161 -0
- data/spec/rake_task_spec.rb +68 -0
- data/spec/runner_spec.rb +23 -0
- data/spec/spec_helper.rb +71 -0
- data/spec/style_check_spec.rb +57 -0
- data/spec/threshold_check_spec.rb +101 -0
- data/spec/violation_formatter_spec.rb +38 -0
- data/sugarcane.gemspec +41 -0
- metadata +196 -0
@@ -0,0 +1,228 @@
|
|
1
|
+
require "ncursesw"
|
2
|
+
|
3
|
+
module SugarCane
|
4
|
+
|
5
|
+
# Produces a ncurses menu that the user can navigate with:
|
6
|
+
# J/ K: Move up/down
|
7
|
+
# Q: Quit
|
8
|
+
# Enter: Open violation in text editor
|
9
|
+
#
|
10
|
+
# Constructor Parameters:
|
11
|
+
# checks: like ones produced from style_check.rb or doc_check.rb
|
12
|
+
# opts: command-line parsed options applied to each check
|
13
|
+
# height: the maximum number of items that can be in the menu
|
14
|
+
class Menu
|
15
|
+
|
16
|
+
TITLE = <<-'SUGARCANE'
|
17
|
+
___ _ _ __ _ __ _ _ __ ___ __ _ _ __ ___
|
18
|
+
/ __| | | |/ _` |/ _` | '__/ __/ _` | '_ \ / _ \
|
19
|
+
\__ \ |_| | (_| | (_| | | | (_| (_| | | | | __/
|
20
|
+
|___/\__,_|\__, |\__,_|_| \___\__,_|_| |_|\___|
|
21
|
+
|___/
|
22
|
+
SUGARCANE
|
23
|
+
|
24
|
+
# Don't trust ncursew keys as they don't always work
|
25
|
+
KEY_C = 99
|
26
|
+
KEY_Q = 113
|
27
|
+
KEY_X = 120
|
28
|
+
KEY_J = 106
|
29
|
+
KEY_K = 107
|
30
|
+
KEY_W = 119
|
31
|
+
KEY_S = 115
|
32
|
+
KEY_O = 111
|
33
|
+
KEY_UP = 259
|
34
|
+
KEY_DOWN = 258
|
35
|
+
KEY_ENTER = 13
|
36
|
+
KEY_SPACE = 32
|
37
|
+
|
38
|
+
def initialize(checks, options, height = 30)
|
39
|
+
@checks = checks
|
40
|
+
@options = options
|
41
|
+
@height = height
|
42
|
+
check_violations
|
43
|
+
end
|
44
|
+
|
45
|
+
def run
|
46
|
+
if @data.nil? or @data.empty?
|
47
|
+
return nil
|
48
|
+
end
|
49
|
+
begin
|
50
|
+
# can't go in separate function because redeclares constants
|
51
|
+
Ncurses.initscr
|
52
|
+
init_ncurses
|
53
|
+
draw_menu(@menu, @menu_position)
|
54
|
+
draw_fix_window(@fix_window)
|
55
|
+
draw_title_window(@title_window)
|
56
|
+
while ch = @menu.wgetch
|
57
|
+
case ch
|
58
|
+
when KEY_K, KEY_W, KEY_UP
|
59
|
+
# draw menu, 'move up'
|
60
|
+
@menu_position -= 1 unless @menu_position == @min_position
|
61
|
+
@data_position -= 1 unless @data_position == 0
|
62
|
+
when KEY_J, KEY_S, KEY_DOWN
|
63
|
+
# draw_info 'move down'
|
64
|
+
@menu_position += 1 unless @menu_position == @max_position
|
65
|
+
@data_position += 1 unless @data_position == @size - 1
|
66
|
+
when KEY_O, KEY_ENTER, KEY_SPACE
|
67
|
+
clean_up
|
68
|
+
selected = @data[@data_position]
|
69
|
+
edit_file(selected[:file], selected[:line])
|
70
|
+
init_ncurses
|
71
|
+
check_violations
|
72
|
+
when KEY_Q, KEY_X
|
73
|
+
clean_up
|
74
|
+
break
|
75
|
+
end
|
76
|
+
# For cycling through the options but is buggy
|
77
|
+
# @data_position = @size - 1 if @data_position < 0
|
78
|
+
# @data_position = 0 if @data_position > @size - 1
|
79
|
+
draw_menu(@menu, @menu_position)
|
80
|
+
draw_fix_window(@fix_window)
|
81
|
+
draw_title_window(@title_window)
|
82
|
+
end
|
83
|
+
return @data[@data_position]
|
84
|
+
ensure
|
85
|
+
clean_up
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def draw_menu(menu, active_index=nil)
|
90
|
+
Ncurses.stdscr.border(*([0]*8))
|
91
|
+
Ncurses.stdscr.refresh
|
92
|
+
menu.clear
|
93
|
+
menu.border(*([0]*8))
|
94
|
+
@height.times do |i|
|
95
|
+
menu.move(i + 1, 1)
|
96
|
+
position = i + @data_position - @menu_position
|
97
|
+
file = @data[position][:file]
|
98
|
+
if @data[position][:line]
|
99
|
+
line = " #{@data[position][:line]}: "
|
100
|
+
else
|
101
|
+
line = " "
|
102
|
+
end
|
103
|
+
desc = @data[position][:menu_description] || ""
|
104
|
+
if desc.length > Ncurses.COLS - 10
|
105
|
+
desc << "..."
|
106
|
+
end
|
107
|
+
if i == active_index
|
108
|
+
style = Ncurses::A_STANDOUT
|
109
|
+
menu.attrset(style)
|
110
|
+
menu.addstr(file)
|
111
|
+
menu.addstr(line)
|
112
|
+
menu.addstr(desc)
|
113
|
+
menu.attrset(Ncurses::A_NORMAL)
|
114
|
+
else
|
115
|
+
menu.attrset(Ncurses.COLOR_PAIR(2))
|
116
|
+
menu.addstr(file)
|
117
|
+
menu.attrset(Ncurses.COLOR_PAIR(3))
|
118
|
+
menu.addstr(line)
|
119
|
+
menu.attrset(Ncurses.COLOR_PAIR(4))
|
120
|
+
menu.addstr(desc)
|
121
|
+
# menu.attrset(Ncurses.COLOR_PAIR(1))
|
122
|
+
menu.attrset(Ncurses::A_NORMAL)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
menu.refresh
|
126
|
+
Ncurses.keypad(menu, true)
|
127
|
+
end
|
128
|
+
|
129
|
+
def draw_title_window(window)
|
130
|
+
window.clear
|
131
|
+
# window.border(*([0]*8))
|
132
|
+
window.attrset(Ncurses.COLOR_PAIR(5))
|
133
|
+
window.addstr(TITLE)
|
134
|
+
window.attrset(Ncurses.COLOR_PAIR(1))
|
135
|
+
window.refresh
|
136
|
+
end
|
137
|
+
|
138
|
+
def draw_fix_window(window)
|
139
|
+
window.clear
|
140
|
+
window.border(*([0]*8))
|
141
|
+
window.move(1, 1)
|
142
|
+
line = "Violations left: #{@data.size}"
|
143
|
+
window.addstr(line)
|
144
|
+
window.refresh
|
145
|
+
end
|
146
|
+
|
147
|
+
def init_ncurses
|
148
|
+
Ncurses.cbreak
|
149
|
+
Ncurses.start_color
|
150
|
+
Ncurses.noecho
|
151
|
+
Ncurses.nonl
|
152
|
+
Ncurses.curs_set(0)
|
153
|
+
|
154
|
+
if Ncurses.has_colors?
|
155
|
+
@background_color = Ncurses::COLOR_BLACK
|
156
|
+
Ncurses.init_pair(1, Ncurses::COLOR_WHITE, @background_color)
|
157
|
+
Ncurses.init_pair(2, Ncurses::COLOR_BLUE, @background_color)
|
158
|
+
Ncurses.init_pair(3, Ncurses::COLOR_CYAN, @background_color)
|
159
|
+
Ncurses.init_pair(4, Ncurses::COLOR_RED, @background_color)
|
160
|
+
Ncurses.init_pair(5, Ncurses::COLOR_GREEN, @background_color)
|
161
|
+
end
|
162
|
+
|
163
|
+
@title_window = Ncurses::WINDOW.new(5, Ncurses.COLS - 2,2,1)
|
164
|
+
@menu = Ncurses::WINDOW.new(@height + 2, Ncurses.COLS - 2,7,1)
|
165
|
+
@fix_window = Ncurses::WINDOW.new(3, Ncurses.COLS - 2,@height+9,1)
|
166
|
+
end
|
167
|
+
|
168
|
+
def clean_up
|
169
|
+
Ncurses.stdscr.clear
|
170
|
+
Ncurses.stdscr.refresh
|
171
|
+
Ncurses.echo
|
172
|
+
Ncurses.nocbreak
|
173
|
+
Ncurses.nl
|
174
|
+
Ncurses.endwin
|
175
|
+
end
|
176
|
+
|
177
|
+
def edit_file(file, line)
|
178
|
+
if @options[:editor]
|
179
|
+
system("#{@options[:editor]} +#{line} #{file}")
|
180
|
+
# If someone purchased sublime, they probably want to use it
|
181
|
+
elsif program_exist? "sublimetext"
|
182
|
+
system("sublimetext #{file}:#{line}")
|
183
|
+
elsif ENV['VISUAL']
|
184
|
+
system("#{ENV['VISUAL']} +#{line} #{file}")
|
185
|
+
elsif program_exist? "vim"
|
186
|
+
system("vim +#{line} #{file}")
|
187
|
+
elsif program_exist? "gedit"
|
188
|
+
system("gedit +#{line} #{file}")
|
189
|
+
elsif program_exist? "nano"
|
190
|
+
system("nano +#{line} #{file}")
|
191
|
+
elsif program_exist? "geany"
|
192
|
+
system("geany +#{line} #{file}")
|
193
|
+
else
|
194
|
+
# :(
|
195
|
+
system("notepad.exe #{file}")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Allegedly cross-platform way to determine if an executable is in PATH
|
200
|
+
def program_exist?(command)
|
201
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
202
|
+
ENV['PATH'].split(::File::PATH_SEPARATOR).each do |path|
|
203
|
+
exts.each { |ext|
|
204
|
+
exe = ::File.join(path, "#{command}#{ext}")
|
205
|
+
return exe if ::File.executable? exe
|
206
|
+
}
|
207
|
+
end
|
208
|
+
return nil
|
209
|
+
end
|
210
|
+
|
211
|
+
def check_violations
|
212
|
+
violations = @checks.
|
213
|
+
map {|check| check.new(@options).violations }.
|
214
|
+
flatten
|
215
|
+
@data = violations
|
216
|
+
@height = [@data.size,@height].min
|
217
|
+
@size = @data.size
|
218
|
+
@min_position = 0
|
219
|
+
@max_position = @height - 1
|
220
|
+
@data_position ||= 0
|
221
|
+
@menu_position ||= 0
|
222
|
+
if @data_position > @size - 1
|
223
|
+
@data_position = @size - 1
|
224
|
+
end
|
225
|
+
return violations
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/tasklib'
|
3
|
+
|
4
|
+
require 'sugarcane/cli/options'
|
5
|
+
require 'sugarcane/cli/parser'
|
6
|
+
|
7
|
+
module SugarCane
|
8
|
+
# Creates a rake task to run cane with given configuration.
|
9
|
+
#
|
10
|
+
# Examples
|
11
|
+
#
|
12
|
+
# desc "Run code quality checks"
|
13
|
+
# Cane::RakeTask.new(:quality) do |cane|
|
14
|
+
# cane.abc_max = 10
|
15
|
+
# cane.doc_glob = 'lib/**/*.rb'
|
16
|
+
# cane.no_style = true
|
17
|
+
# cane.add_threshold 'coverage/covered_percent', :>=, 99
|
18
|
+
# end
|
19
|
+
class RakeTask < ::Rake::TaskLib
|
20
|
+
attr_accessor :name
|
21
|
+
attr_reader :options
|
22
|
+
|
23
|
+
SugarCane::CLI.default_options.keys.each do |name|
|
24
|
+
define_method(name) do
|
25
|
+
options.fetch(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
define_method("#{name}=") do |v|
|
29
|
+
options[name] = v
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Add a threshold check. If the file exists and it contains a number,
|
34
|
+
# compare that number with the given value using the operator.
|
35
|
+
def add_threshold(file, operator, value)
|
36
|
+
if operator == :>=
|
37
|
+
@options[:gte] << [file, value]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def use(check, options = {})
|
42
|
+
@options.merge!(options)
|
43
|
+
@options[:checks] = @options[:checks] + [check]
|
44
|
+
end
|
45
|
+
|
46
|
+
def canefile=(file)
|
47
|
+
canefile = SugarCane::CLI::Parser.new
|
48
|
+
canefile.parser.parse!(canefile.read_options_from_file(file))
|
49
|
+
options.merge! canefile.options
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(task_name = nil)
|
53
|
+
self.name = task_name || :cane
|
54
|
+
@gte = []
|
55
|
+
@options = SugarCane::CLI.default_options
|
56
|
+
@options[:report] = true
|
57
|
+
|
58
|
+
if block_given?
|
59
|
+
yield self
|
60
|
+
else
|
61
|
+
if File.exists?('./sugarcane')
|
62
|
+
self.canefile = './.sugarcane'
|
63
|
+
else
|
64
|
+
self.canefile = './.cane'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
unless ::Rake.application.last_comment
|
69
|
+
desc %(Check code quality metrics with cane)
|
70
|
+
end
|
71
|
+
|
72
|
+
task name do
|
73
|
+
require 'sugarcane/cli'
|
74
|
+
abort unless SugarCane.run(options)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'parallel'
|
2
|
+
|
3
|
+
require 'sugarcane/violation_formatter'
|
4
|
+
require 'sugarcane/json_formatter'
|
5
|
+
require 'sugarcane/menu'
|
6
|
+
|
7
|
+
# Accepts a parsed configuration and passes those options to a new Runner
|
8
|
+
module SugarCane
|
9
|
+
def run(*args)
|
10
|
+
Runner.new(*args).run
|
11
|
+
end
|
12
|
+
module_function :run
|
13
|
+
|
14
|
+
# Orchestrates the running of checks per the provided configuration, and
|
15
|
+
# hands the result to a formatter for display. This is the core of the
|
16
|
+
# application, but for the actual entry point see `SugarCane::CLI`.
|
17
|
+
class Runner
|
18
|
+
def initialize(spec)
|
19
|
+
@opts = spec
|
20
|
+
@checks = spec[:checks]
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
check_options(violations, opts)
|
25
|
+
violations.length <= opts.fetch(:max_violations)
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
attr_reader :opts, :checks
|
31
|
+
|
32
|
+
def violations
|
33
|
+
@violations ||= checks.
|
34
|
+
map { |check| check.new(opts).violations }.
|
35
|
+
flatten
|
36
|
+
end
|
37
|
+
|
38
|
+
def check_violations
|
39
|
+
@violations = checks.
|
40
|
+
map { |check| check.new(opts).violations }.
|
41
|
+
flatten
|
42
|
+
end
|
43
|
+
|
44
|
+
def check_options(violations, opts)
|
45
|
+
if opts[:report]
|
46
|
+
outputter.print ViolationFormatter.new(violations, opts)
|
47
|
+
elsif opts[:json]
|
48
|
+
outputter.print JsonFormatter.new(violations, opts)
|
49
|
+
else
|
50
|
+
menu = SugarCane::Menu.new(@checks, @opts)
|
51
|
+
menu.run
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def outputter
|
56
|
+
opts.fetch(:out, $stdout)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'sugarcane/file'
|
4
|
+
require 'sugarcane/task_runner'
|
5
|
+
|
6
|
+
module SugarCane
|
7
|
+
|
8
|
+
# Creates violations for files that do not meet style conventions. Only
|
9
|
+
# highly obvious, probable, and non-controversial checks are performed here.
|
10
|
+
# It is not the goal of the tool to provide an extensive style report, but
|
11
|
+
# only to prevent stupid mistakes.
|
12
|
+
class StyleCheck < Struct.new(:opts)
|
13
|
+
|
14
|
+
def self.key; :style; end
|
15
|
+
def self.name; "style checking"; end
|
16
|
+
def self.options
|
17
|
+
{
|
18
|
+
style_glob: ['Glob to run style checks over',
|
19
|
+
default: '{app,lib,spec}/**/*.rb',
|
20
|
+
variable: 'GLOB',
|
21
|
+
clobber: :no_style],
|
22
|
+
style_measure: ['Max line length',
|
23
|
+
default: 80,
|
24
|
+
cast: :to_i,
|
25
|
+
clobber: :no_style],
|
26
|
+
style_exclude: ['Exclude file or glob from style checking',
|
27
|
+
variable: 'GLOB',
|
28
|
+
type: Array,
|
29
|
+
default: [],
|
30
|
+
clobber: :no_style],
|
31
|
+
no_style: ['Disable style checking', cast: ->(x) { !x }]
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def violations
|
36
|
+
return [] if opts[:no_style]
|
37
|
+
|
38
|
+
worker.map(file_list) do |file_path|
|
39
|
+
map_lines(file_path) do |line, line_number|
|
40
|
+
violations_for_line(line.chomp).map {|message| {
|
41
|
+
file: file_path,
|
42
|
+
line: line_number + 1,
|
43
|
+
value: line.length,
|
44
|
+
label: message,
|
45
|
+
description: "Lines violated style requirements",
|
46
|
+
menu_description: message
|
47
|
+
}}
|
48
|
+
end
|
49
|
+
end.flatten
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def violations_for_line(line)
|
55
|
+
result = []
|
56
|
+
if line.length > measure
|
57
|
+
result << "Line is > #{measure} characters (#{line.length})"
|
58
|
+
end
|
59
|
+
result << "Line contains trailing whitespace" if line =~ /\s$/
|
60
|
+
result << "Line contains hard tabs" if line =~ /\t/
|
61
|
+
result
|
62
|
+
end
|
63
|
+
|
64
|
+
def file_list
|
65
|
+
Dir[opts.fetch(:style_glob)].reject {|f| excluded?(f) }
|
66
|
+
end
|
67
|
+
|
68
|
+
def measure
|
69
|
+
opts.fetch(:style_measure)
|
70
|
+
end
|
71
|
+
|
72
|
+
def map_lines(file_path, &block)
|
73
|
+
SugarCane::File.iterator(file_path).map.with_index(&block)
|
74
|
+
end
|
75
|
+
|
76
|
+
def exclusions
|
77
|
+
@exclusions ||= opts.fetch(:style_exclude, []).flatten.map do |i|
|
78
|
+
Dir[i]
|
79
|
+
end.flatten.to_set
|
80
|
+
end
|
81
|
+
|
82
|
+
def excluded?(file)
|
83
|
+
exclusions.include?(file)
|
84
|
+
end
|
85
|
+
|
86
|
+
def worker
|
87
|
+
SugarCane.task_runner(opts)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Provides a SimpleTaskRunner or Parallel task runner based on configuration
|
2
|
+
module SugarCane
|
3
|
+
def task_runner(opts)
|
4
|
+
if opts[:parallel]
|
5
|
+
Parallel
|
6
|
+
else
|
7
|
+
SimpleTaskRunner
|
8
|
+
end
|
9
|
+
end
|
10
|
+
module_function :task_runner
|
11
|
+
|
12
|
+
# Mirrors the Parallel gem's interface but does not provide any parallelism.
|
13
|
+
# This is faster for smaller tasks since it doesn't incur any overhead for
|
14
|
+
# creating new processes and communicating between them.
|
15
|
+
class SimpleTaskRunner
|
16
|
+
def self.map(enumerable, &block)
|
17
|
+
enumerable.map(&block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'sugarcane/file'
|
2
|
+
|
3
|
+
module SugarCane
|
4
|
+
|
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
|
+
THRESHOLDS = {
|
9
|
+
lt: :<,
|
10
|
+
lte: :<=,
|
11
|
+
eq: :==,
|
12
|
+
gte: :>=,
|
13
|
+
gt: :>
|
14
|
+
}
|
15
|
+
|
16
|
+
def self.key; :threshold; end
|
17
|
+
def self.options
|
18
|
+
THRESHOLDS.each_with_object({}) do |(key, value), h|
|
19
|
+
h[key] = ["Check the number in FILE is #{value} to THRESHOLD " +
|
20
|
+
"(a number or another file name)",
|
21
|
+
variable: "FILE,THRESHOLD",
|
22
|
+
type: Array]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def violations
|
27
|
+
thresholds.map do |operator, file, threshold|
|
28
|
+
value = normalized_limit(file)
|
29
|
+
limit = normalized_limit(threshold)
|
30
|
+
|
31
|
+
if !limit.real?
|
32
|
+
{
|
33
|
+
description: 'Quality threshold could not be read',
|
34
|
+
label: "%s is not a number or a file" % [
|
35
|
+
threshold
|
36
|
+
]
|
37
|
+
}
|
38
|
+
elsif !value.send(operator, limit)
|
39
|
+
{
|
40
|
+
description: 'Quality threshold crossed',
|
41
|
+
label: "%s is %s, should be %s %s" % [
|
42
|
+
file, value, operator, limit
|
43
|
+
]
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end.compact
|
47
|
+
end
|
48
|
+
|
49
|
+
def normalized_limit(limit)
|
50
|
+
Float(limit)
|
51
|
+
rescue ArgumentError
|
52
|
+
value_from_file(limit)
|
53
|
+
end
|
54
|
+
|
55
|
+
def value_from_file(file)
|
56
|
+
begin
|
57
|
+
contents = SugarCane::File.contents(file).scan(/\d+\.?\d*/).first.to_f
|
58
|
+
rescue Errno::ENOENT
|
59
|
+
UnavailableValue.new
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def thresholds
|
64
|
+
THRESHOLDS.map do |k, v|
|
65
|
+
opts.fetch(k, []).map do |x|
|
66
|
+
x.unshift(v)
|
67
|
+
end
|
68
|
+
end.reduce(:+)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Null object for all cases when the value to be compared against cannot be
|
72
|
+
# read.
|
73
|
+
class UnavailableValue
|
74
|
+
def <(_); false; end
|
75
|
+
def >(_); false; end
|
76
|
+
def <=(_); false; end
|
77
|
+
def >=(_); false; end
|
78
|
+
def to_s; 'unavailable'; end
|
79
|
+
def real?; false; end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module SugarCane
|
5
|
+
|
6
|
+
# Computes a string to be displayed as output from an array of violations
|
7
|
+
# computed by the checks.
|
8
|
+
class ViolationFormatter
|
9
|
+
attr_reader :violations, :options
|
10
|
+
|
11
|
+
def initialize(violations, options = {})
|
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
|
+
|
19
|
+
@options = options
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
return "" if violations.empty?
|
24
|
+
|
25
|
+
string = violations.group_by {|x| x[:description] }.map do |d, vs|
|
26
|
+
format_group_header(d, vs) +
|
27
|
+
format_violations(vs)
|
28
|
+
end.join("\n") + "\n\n" + totals + "\n\n"
|
29
|
+
|
30
|
+
if violations.count > options.fetch(:max_violations, 0)
|
31
|
+
string = colorize(string)
|
32
|
+
end
|
33
|
+
|
34
|
+
string
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def format_group_header(description, violations)
|
40
|
+
["", "%s (%i):" % [description, violations.length], ""]
|
41
|
+
end
|
42
|
+
|
43
|
+
def format_violations(violations)
|
44
|
+
columns = [:file_and_line, :label, :value]
|
45
|
+
|
46
|
+
widths = column_widths(violations, columns)
|
47
|
+
|
48
|
+
violations.map do |v|
|
49
|
+
format_violation(v, widths)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def column_widths(violations, columns)
|
54
|
+
columns.each_with_object({}) do |column, h|
|
55
|
+
h[column] = violations.map {|v| v[column].to_s.length }.max
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def format_violation(v, column_widths)
|
60
|
+
' ' + column_widths.keys.map {|column|
|
61
|
+
v[column].to_s.ljust(column_widths[column])
|
62
|
+
}.join(' ').strip
|
63
|
+
end
|
64
|
+
|
65
|
+
def colorize(string)
|
66
|
+
return string unless options[:color]
|
67
|
+
|
68
|
+
"\e[31m#{string}\e[0m"
|
69
|
+
end
|
70
|
+
|
71
|
+
def totals
|
72
|
+
"Total Violations: #{violations.length}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|