sugarcane 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|