sandi_meter 0.0.1 → 0.0.2
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 +8 -8
- data/README.md +28 -2
- data/bin/sandi_meter +7 -3
- data/lib/sandi_meter/analyzer.rb +188 -0
- data/lib/sandi_meter/calculator.rb +93 -0
- data/lib/sandi_meter/file_scanner.rb +41 -0
- data/lib/sandi_meter/formatter.rb +29 -0
- data/lib/sandi_meter/loc_checker.rb +19 -0
- data/lib/sandi_meter/method_arguments_counter.rb +38 -0
- data/lib/sandi_meter/version.rb +1 -1
- data/lib/sandi_meter/warning_scanner.rb +57 -0
- data/spec/analyzer_spec.rb +3 -3
- data/spec/loc_checker_spec.rb +5 -5
- data/spec/method_arguments_counter_spec.rb +4 -4
- data/spec/warning_scanner_spec.rb +3 -3
- metadata +9 -8
- data/lib/analyzer.rb +0 -186
- data/lib/calculator.rb +0 -103
- data/lib/file_scanner.rb +0 -39
- data/lib/loc_checker.rb +0 -17
- data/lib/method_arguments_counter.rb +0 -36
- data/lib/warning_scanner.rb +0 -55
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
NzI2Nzc1NDI0OWYyNTExZTJjMGViMWQ2YmNkNzU3YmNhNWU5YmM0MQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
Njg4MzcxYmMyMDhiOTBhNTJjYTE1MzFhYWM5Yjc0ZWU2YjgyZGE0NA==
|
7
7
|
!binary "U0hBNTEy":
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
YTI5YzRhNDExYTg5MTg2ZGUyM2E4NGI0ODE5MjI5YjY1MTg5MTQ1MDNmYTM1
|
10
|
+
NzM2NWU2Y2ZlZGQxNmZlMzIyMmE4ZDI3MjgzY2E1MDYzMWIzZTRjMTI4MDQ4
|
11
|
+
M2Y5MWZhYTA5MzZlM2NjMDk3ZTM2OTI4Y2UyMWM4ZWUwMmNjYWU=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
ZDExZWRiZTA3ODQ1YWY5YTA0NGY4NzZkYzc5YWE5NTNhODBhYzI5M2RlYTkz
|
14
|
+
NzMwMzE1NDJhZjE5OTI3MWVkNzhmMTVmOGY0NmM0Y2MwYmI5MmZjYTJlYmRl
|
15
|
+
MWE4NTVlNmRkNDI2NzJiYzU0NWE0MWEwMTFjMDVmZGZiNDA4YTc=
|
data/README.md
CHANGED
@@ -5,11 +5,37 @@ Static analysis tool for checking your Ruby code for [Sandi Metz' for rules](htt
|
|
5
5
|
* 100 lines per class
|
6
6
|
* 5 lines per method
|
7
7
|
* 4 params per method call (and don't even try cheating with hash params)
|
8
|
-
*
|
8
|
+
* 1 instance variables per controller' action
|
9
9
|
|
10
|
-
##
|
10
|
+
## CLI mode
|
11
11
|
|
12
12
|
~~~
|
13
13
|
gem install sandi_meter
|
14
14
|
sandi_meter ~/your/ruby/or/rails/project
|
15
|
+
|
16
|
+
1. 94% of classes are under 100 lines.
|
17
|
+
2. 53% of methods are under 5 lines.
|
18
|
+
3. 98% of methods calls accepts are less than 4 parameters.
|
19
|
+
4. 21% of controllers have one instance variable per action.
|
20
|
+
~~~
|
21
|
+
|
22
|
+
## Ruby script mode
|
23
|
+
|
24
|
+
~~~ruby
|
25
|
+
require 'sandi_meter/file_scanner'
|
26
|
+
require 'pp'
|
27
|
+
|
28
|
+
scanner = SandiMeter::FileScanner.new
|
29
|
+
data = scanner.scan(PATH_TO_PROJECT)
|
30
|
+
pp data
|
31
|
+
# {:first_rule=>
|
32
|
+
# {:small_classes_amount=>916,
|
33
|
+
# :total_classes_amount=>937,
|
34
|
+
# :missindented_classes_amount=>1},
|
35
|
+
# :second_rule=>
|
36
|
+
# {:small_methods_amount=>1144,
|
37
|
+
# :total_methods_amount=>1833,
|
38
|
+
# :missindented_methods_amount=>0},
|
39
|
+
# :third_rule=>{:proper_method_calls=>5857, :total_method_calls=>5894},
|
40
|
+
# :fourth_rule=>{:proper_controllers_amount=>17, :total_controllers_amount=>94}}
|
15
41
|
~~~
|
data/bin/sandi_meter
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require_relative '../lib/file_scanner'
|
3
|
+
require_relative '../lib/sandi_meter/file_scanner'
|
4
|
+
require_relative '../lib/sandi_meter/formatter'
|
4
5
|
|
5
|
-
scanner = FileScanner.new(ARGV[1] == "--log")
|
6
|
-
scanner.scan(ARGV[0])
|
6
|
+
scanner = SandiMeter::FileScanner.new(ARGV[1] == "--log")
|
7
|
+
data = scanner.scan(ARGV[0])
|
8
|
+
formatter = SandiMeter::Formatter.new
|
9
|
+
|
10
|
+
formatter.print_data(data)
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'ripper'
|
2
|
+
require_relative 'warning_scanner'
|
3
|
+
require_relative 'loc_checker'
|
4
|
+
require_relative 'method_arguments_counter'
|
5
|
+
|
6
|
+
module SandiMeter
|
7
|
+
class Analyzer
|
8
|
+
attr_reader :classes, :missindented_classes, :methods, :missindented_methods, :method_calls, :instance_variables
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@classes = []
|
12
|
+
@missindented_classes = []
|
13
|
+
@missindented_methods = {}
|
14
|
+
@methods = {}
|
15
|
+
@method_calls = []
|
16
|
+
@instance_variables = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def analyze(file_path)
|
20
|
+
@file_path = file_path
|
21
|
+
@file_body = File.read(file_path)
|
22
|
+
@file_lines = @file_body.split(/$/).map { |l| l.gsub("\n", '') }
|
23
|
+
@indentation_warnings = indentation_warnings
|
24
|
+
# TODO
|
25
|
+
# add better determination wheter file is controller
|
26
|
+
@scan_instance_variables = !!(file_path =~ /\w+_controller.rb$/)
|
27
|
+
|
28
|
+
sexp = Ripper.sexp(@file_body)
|
29
|
+
scan_sexp(sexp)
|
30
|
+
|
31
|
+
output
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def output
|
36
|
+
loc_checker = SandiMeter::LOCChecker.new(@file_lines)
|
37
|
+
|
38
|
+
@classes.map! do |klass_params|
|
39
|
+
klass_params << loc_checker.check(klass_params, 'class')
|
40
|
+
end
|
41
|
+
|
42
|
+
@methods.each_pair do |klass, methods|
|
43
|
+
methods.each do |method_params|
|
44
|
+
method_params << loc_checker.check(method_params, 'def')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
{
|
49
|
+
classes: @classes,
|
50
|
+
missindented_classes: @missindented_classes,
|
51
|
+
methods: @methods,
|
52
|
+
missindented_methods: @missindented_methods,
|
53
|
+
method_calls: @method_calls,
|
54
|
+
instance_variables: @instance_variables
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_class_params(sexp, current_namespace)
|
59
|
+
flat_sexp = sexp[1].flatten
|
60
|
+
const_indexes = flat_sexp.each_index.select{ |i| flat_sexp[i] == :@const }
|
61
|
+
|
62
|
+
line_number = flat_sexp[const_indexes.first + 2]
|
63
|
+
class_tokens = const_indexes.map { |i| flat_sexp[i + 1] }
|
64
|
+
class_tokens.insert(0, current_namespace) unless current_namespace.empty?
|
65
|
+
class_name = class_tokens.join('::')
|
66
|
+
|
67
|
+
[class_name, line_number]
|
68
|
+
end
|
69
|
+
|
70
|
+
# MOVE
|
71
|
+
# to method scanner class
|
72
|
+
def number_of_arguments(method_sexp)
|
73
|
+
arguments = method_sexp[2]
|
74
|
+
arguments = arguments[1] if arguments.first == :paren
|
75
|
+
|
76
|
+
arguments[1] == nil ? 0 : arguments[1].size
|
77
|
+
end
|
78
|
+
|
79
|
+
def find_method_params(sexp)
|
80
|
+
sexp[1].flatten[1,2]
|
81
|
+
end
|
82
|
+
|
83
|
+
def find_last_line(params, token = 'class')
|
84
|
+
token_name, line = params
|
85
|
+
|
86
|
+
token_indentation = @file_lines[line - 1].index(token)
|
87
|
+
# TODO
|
88
|
+
# add check for trailing spaces
|
89
|
+
last_line = @file_lines[line..-1].index { |l| l =~ %r(\A\s{#{token_indentation}}end\s*\z) }
|
90
|
+
|
91
|
+
last_line ? last_line + line + 1 : nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def scan_class_sexp(element, current_namespace = '')
|
95
|
+
case element.first
|
96
|
+
when :module
|
97
|
+
module_params = find_class_params(element, current_namespace)
|
98
|
+
module_params += [find_last_line(module_params, 'module')]
|
99
|
+
current_namespace = module_params.first
|
100
|
+
|
101
|
+
scan_sexp(element, current_namespace)
|
102
|
+
when :class
|
103
|
+
class_params = find_class_params(element, current_namespace)
|
104
|
+
|
105
|
+
if @indentation_warnings['class'] && @indentation_warnings['class'].any? { |first_line, last_line| first_line == class_params.last }
|
106
|
+
class_params << nil
|
107
|
+
@missindented_classes << class_params
|
108
|
+
else
|
109
|
+
class_params += [find_last_line(class_params)]
|
110
|
+
|
111
|
+
# in case of one liner class last line will be nil
|
112
|
+
(class_params.last == nil ? @missindented_classes : @classes) << class_params
|
113
|
+
end
|
114
|
+
|
115
|
+
current_namespace = class_params.first
|
116
|
+
scan_sexp(element, current_namespace)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def find_args_add_block(method_call_sexp)
|
121
|
+
return unless method_call_sexp.kind_of?(Array)
|
122
|
+
|
123
|
+
method_call_sexp.each do |sexp|
|
124
|
+
next unless sexp.kind_of?(Array)
|
125
|
+
|
126
|
+
if sexp.first == :args_add_block
|
127
|
+
counter = SandiMeter::MethodArgumentsCounter.new
|
128
|
+
arguments_count, line = counter.count(sexp)
|
129
|
+
|
130
|
+
@method_calls << [arguments_count, line]
|
131
|
+
|
132
|
+
find_args_add_block(sexp)
|
133
|
+
else
|
134
|
+
find_args_add_block(sexp)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def scan_def_for_ivars(current_namespace, method_name, method_sexp)
|
140
|
+
return unless method_sexp.kind_of?(Array)
|
141
|
+
|
142
|
+
method_sexp.each do |sexp|
|
143
|
+
next unless sexp.kind_of?(Array)
|
144
|
+
|
145
|
+
if sexp.first == :assign
|
146
|
+
@instance_variables[current_namespace] ||= {}
|
147
|
+
@instance_variables[current_namespace][method_name] ||= []
|
148
|
+
@instance_variables[current_namespace][method_name] << sexp[1][1][1]
|
149
|
+
else
|
150
|
+
scan_def_for_ivars(current_namespace, method_name, sexp)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def scan_sexp(sexp, current_namespace = '')
|
156
|
+
sexp.each do |element|
|
157
|
+
next unless element.kind_of?(Array)
|
158
|
+
|
159
|
+
case element.first
|
160
|
+
when :def
|
161
|
+
method_params = find_method_params(element)
|
162
|
+
if @indentation_warnings['def'] && @indentation_warnings['def'].any? { |first_line, last_line| first_line == method_params.last }
|
163
|
+
method_params << nil
|
164
|
+
method_params << number_of_arguments(element)
|
165
|
+
@missindented_methods[current_namespace] ||= []
|
166
|
+
@missindented_methods[current_namespace] << method_params
|
167
|
+
else
|
168
|
+
method_params += [find_last_line(method_params, 'def')]
|
169
|
+
method_params << number_of_arguments(element)
|
170
|
+
@methods[current_namespace] ||= []
|
171
|
+
@methods[current_namespace] << method_params
|
172
|
+
end
|
173
|
+
scan_def_for_ivars(current_namespace, method_params.first, element) if @scan_instance_variables
|
174
|
+
find_args_add_block(element)
|
175
|
+
when :module, :class
|
176
|
+
scan_class_sexp(element, current_namespace)
|
177
|
+
else
|
178
|
+
scan_sexp(element, current_namespace)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def indentation_warnings
|
184
|
+
warning_scanner = SandiMeter::WarningScanner.new
|
185
|
+
warning_scanner.scan(@file_body)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module SandiMeter
|
2
|
+
class Calculator
|
3
|
+
def initialize
|
4
|
+
@data = {}
|
5
|
+
@output = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def push(data)
|
9
|
+
data.each_pair do |key, value|
|
10
|
+
if value.kind_of?(Array)
|
11
|
+
@data[key] ||= []
|
12
|
+
@data[key] += value
|
13
|
+
elsif value.kind_of?(Hash)
|
14
|
+
@data[key] ||= {}
|
15
|
+
@data[key].merge!(value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def calculate!
|
21
|
+
check_first_rule
|
22
|
+
check_second_rule
|
23
|
+
check_third_rule
|
24
|
+
check_fourth_rule
|
25
|
+
|
26
|
+
@output
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def check_first_rule
|
31
|
+
total_classes_amount = @data[:classes].size
|
32
|
+
small_classes_amount = @data[:classes].inject(0) do |sum, class_params|
|
33
|
+
sum += 1 if class_params.last == true
|
34
|
+
sum
|
35
|
+
end
|
36
|
+
missindented_classes_amount = @data[:missindented_classes].size
|
37
|
+
|
38
|
+
@output[:first_rule] ||= {}
|
39
|
+
@output[:first_rule][:small_classes_amount] = small_classes_amount
|
40
|
+
@output[:first_rule][:total_classes_amount] = total_classes_amount
|
41
|
+
@output[:first_rule][:missindented_classes_amount] = missindented_classes_amount
|
42
|
+
end
|
43
|
+
|
44
|
+
def check_second_rule
|
45
|
+
total_methods_amount = 0
|
46
|
+
small_methods_amount = 0
|
47
|
+
|
48
|
+
@data[:methods].each_pair do |klass, methods|
|
49
|
+
small_methods_amount += methods.select { |m| m.last == true }.size
|
50
|
+
total_methods_amount += methods.size
|
51
|
+
end
|
52
|
+
|
53
|
+
missindented_methods_amount = 0
|
54
|
+
@data[:missindented_methods].each_pair do |klass, methods|
|
55
|
+
missindented_methods_amount += methods.size
|
56
|
+
end
|
57
|
+
|
58
|
+
@output[:second_rule] ||= {}
|
59
|
+
@output[:second_rule][:small_methods_amount] = small_methods_amount
|
60
|
+
@output[:second_rule][:total_methods_amount] = total_methods_amount
|
61
|
+
@output[:second_rule][:missindented_methods_amount] = missindented_methods_amount
|
62
|
+
end
|
63
|
+
|
64
|
+
# TODO
|
65
|
+
# count method definitions argumets too
|
66
|
+
def check_third_rule
|
67
|
+
total_method_calls = @data[:method_calls].size
|
68
|
+
|
69
|
+
proper_method_calls = @data[:method_calls].inject(0) do |sum, params|
|
70
|
+
sum += 1 unless params.first > 4
|
71
|
+
sum
|
72
|
+
end
|
73
|
+
|
74
|
+
@output[:third_rule] ||= {}
|
75
|
+
@output[:third_rule][:proper_method_calls] = proper_method_calls
|
76
|
+
@output[:third_rule][:total_method_calls] = total_method_calls
|
77
|
+
end
|
78
|
+
|
79
|
+
def check_fourth_rule
|
80
|
+
proper_controllers_amount = 0
|
81
|
+
total_controllers_amount = 0
|
82
|
+
|
83
|
+
@data[:instance_variables].each_pair do |controller, methods|
|
84
|
+
total_controllers_amount += 1
|
85
|
+
proper_controllers_amount += 1 unless methods.values.map(&:size).any? { |v| v > 1 }
|
86
|
+
end
|
87
|
+
|
88
|
+
@output[:fourth_rule] ||= {}
|
89
|
+
@output[:fourth_rule][:proper_controllers_amount] = proper_controllers_amount
|
90
|
+
@output[:fourth_rule][:total_controllers_amount] = total_controllers_amount
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative 'analyzer'
|
2
|
+
require_relative 'calculator'
|
3
|
+
|
4
|
+
module SandiMeter
|
5
|
+
class FileScanner
|
6
|
+
def initialize(log_errors = false)
|
7
|
+
@log_errors = log_errors
|
8
|
+
@calculator = SandiMeter::Calculator.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def scan(path)
|
12
|
+
if File.directory?(path)
|
13
|
+
scan_dir(path)
|
14
|
+
else
|
15
|
+
scan_file(path)
|
16
|
+
end
|
17
|
+
|
18
|
+
@calculator.calculate!
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def scan_dir(path)
|
23
|
+
Dir["#{path}/**/*.rb"].each do |file|
|
24
|
+
scan_file(file)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def scan_file(path)
|
29
|
+
begin
|
30
|
+
analyzer = SandiMeter::Analyzer.new
|
31
|
+
data = analyzer.analyze(path)
|
32
|
+
@calculator.push(data)
|
33
|
+
rescue Exception => e
|
34
|
+
if @log_errors
|
35
|
+
puts "Checkout #{path} for:"
|
36
|
+
puts "\t#{e.message}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module SandiMeter
|
2
|
+
class Formatter
|
3
|
+
def print_data(data)
|
4
|
+
if data[:first_rule][:total_classes_amount] > 0
|
5
|
+
puts "1. #{data[:first_rule][:small_classes_amount] * 100 / data[:first_rule][:total_classes_amount]}% of classes are under 100 lines."
|
6
|
+
else
|
7
|
+
puts "1. No classes to analize."
|
8
|
+
end
|
9
|
+
|
10
|
+
if data[:second_rule][:total_methods_amount] > 0
|
11
|
+
puts "2. #{data[:second_rule][:small_methods_amount] * 100 / data[:second_rule][:total_methods_amount]}% of methods are under 5 lines."
|
12
|
+
else
|
13
|
+
puts "2. No methods to analize."
|
14
|
+
end
|
15
|
+
|
16
|
+
if data[:third_rule][:total_method_calls] > 0
|
17
|
+
puts "3. #{data[:third_rule][:proper_method_calls] * 100 / data[:third_rule][:total_method_calls]}% of methods calls accepts are less than 4 parameters."
|
18
|
+
else
|
19
|
+
puts "3. No method calls to analize."
|
20
|
+
end
|
21
|
+
|
22
|
+
if data[:fourth_rule][:total_controllers_amount] > 0
|
23
|
+
puts "4. #{data[:fourth_rule][:proper_controllers_amount] * 100 / data[:fourth_rule][:total_controllers_amount]}% of controllers have one instance variable per action."
|
24
|
+
else
|
25
|
+
puts "4. No controllers to analize."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SandiMeter
|
2
|
+
class LOCChecker < Struct.new(:file_lines)
|
3
|
+
|
4
|
+
MAX_LOC = {
|
5
|
+
'def' => 5,
|
6
|
+
'class' => 100
|
7
|
+
}
|
8
|
+
|
9
|
+
def check(params, token)
|
10
|
+
_, first_line, last_line = params
|
11
|
+
locs_size(first_line, last_line) <= MAX_LOC[token]
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def locs_size(first_line, last_line)
|
16
|
+
file_lines[first_line - 1..last_line - 1].map(&:strip).reject(&:empty?).size
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module SandiMeter
|
2
|
+
class MethodArgumentsCounter
|
3
|
+
def initialize
|
4
|
+
reset!
|
5
|
+
end
|
6
|
+
|
7
|
+
def count(args_add_block_sexp)
|
8
|
+
reset!
|
9
|
+
|
10
|
+
@count += args_add_block_sexp[1].size
|
11
|
+
@count += 1 if args_add_block_sexp.last == true
|
12
|
+
bypass_sexp(args_add_block_sexp)
|
13
|
+
|
14
|
+
return [@count, @lines.uniq.sort.first]
|
15
|
+
end
|
16
|
+
|
17
|
+
def reset!
|
18
|
+
@count = 0
|
19
|
+
@lines = []
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def bypass_sexp(args_add_block_sexp)
|
24
|
+
args_add_block_sexp.each do |sexp|
|
25
|
+
next unless sexp.kind_of?(Array)
|
26
|
+
|
27
|
+
case sexp.first
|
28
|
+
when :bare_assoc_hash
|
29
|
+
@count += sexp[1].size - 1
|
30
|
+
when :@int, :@ident
|
31
|
+
@lines << sexp.last.first
|
32
|
+
end
|
33
|
+
|
34
|
+
bypass_sexp(sexp)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/sandi_meter/version.rb
CHANGED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module SandiMeter
|
4
|
+
class WarningScanner
|
5
|
+
attr_reader :indentation_warnings
|
6
|
+
|
7
|
+
INDENTATION_WARNING_REGEXP = /at 'end' with '(def|class|module)' at (\d+)\z/
|
8
|
+
|
9
|
+
def scan(source)
|
10
|
+
status, @warnings, process = if defined? Bundler
|
11
|
+
Bundler.with_clean_env do
|
12
|
+
validate(source)
|
13
|
+
end
|
14
|
+
else
|
15
|
+
validate(source)
|
16
|
+
end
|
17
|
+
|
18
|
+
check_syntax(status)
|
19
|
+
@indentation_warnings = parse_warnings
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def validate(source)
|
24
|
+
Open3.capture3('ruby -wc', stdin_data: source)
|
25
|
+
end
|
26
|
+
|
27
|
+
def check_syntax(status)
|
28
|
+
raise SyntaxError, @warnings unless !!(status =~ /Syntax\sOK/)
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_token_lines(token, line_num, end_line_num)
|
32
|
+
raise 'No valid end line number' unless end_line_num =~ /^\d+$/
|
33
|
+
raise 'No valid line number' unless line_num =~ /^\d+$/
|
34
|
+
raise 'No valid token ("def" or "class")' unless token =~ /^def|class|module$/
|
35
|
+
end
|
36
|
+
|
37
|
+
def extract_indentation_mismatch(warning_line)
|
38
|
+
_, end_line_num, warning_type, warning_body = warning_line.split(':').map(&:strip)
|
39
|
+
return nil unless warning_type == 'warning'
|
40
|
+
return nil unless warning_body =~ /at 'end' with '(def|class|module)' at (\d+)\z/
|
41
|
+
|
42
|
+
res = warning_body.match(INDENTATION_WARNING_REGEXP)[1..2] << end_line_num
|
43
|
+
check_token_lines(*res)
|
44
|
+
|
45
|
+
res
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse_warnings
|
49
|
+
@warnings.split("\n").inject({}) do |warnings, warning|
|
50
|
+
token, line, end_line = extract_indentation_mismatch(warning)
|
51
|
+
warnings[token] ||= []
|
52
|
+
warnings[token] << [line.to_i, end_line.to_i]
|
53
|
+
warnings
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/spec/analyzer_spec.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
require 'test_helper'
|
2
|
-
require_relative '../lib/analyzer'
|
2
|
+
require_relative '../lib/sandi_meter/analyzer'
|
3
3
|
|
4
|
-
describe Analyzer do
|
5
|
-
let(:analyzer) { Analyzer.new }
|
4
|
+
describe SandiMeter::Analyzer do
|
5
|
+
let(:analyzer) { SandiMeter::Analyzer.new }
|
6
6
|
|
7
7
|
describe 'finds properly indended classes with lines' do
|
8
8
|
let(:test_class) { test_file_path(3) }
|
data/spec/loc_checker_spec.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
require 'test_helper'
|
2
|
-
require_relative '../lib/loc_checker'
|
2
|
+
require_relative '../lib/sandi_meter/loc_checker'
|
3
3
|
|
4
|
-
describe LOCChecker do
|
5
|
-
let(:checker) { LOCChecker.new([]) }
|
4
|
+
describe SandiMeter::LOCChecker do
|
5
|
+
let(:checker) { SandiMeter::LOCChecker.new([]) }
|
6
6
|
|
7
7
|
describe '#check' do
|
8
8
|
context 'for short code' do
|
9
9
|
before do
|
10
|
-
stub_const('LOCChecker::MAX_LOC', { 'blah' => 10 })
|
10
|
+
stub_const('SandiMeter::LOCChecker::MAX_LOC', { 'blah' => 10 })
|
11
11
|
checker.stub(:locs_size).and_return(rand(0..10))
|
12
12
|
end
|
13
13
|
|
@@ -20,7 +20,7 @@ describe LOCChecker do
|
|
20
20
|
|
21
21
|
context 'for large code' do
|
22
22
|
before do
|
23
|
-
stub_const('LOCChecker::MAX_LOC', { 'blah' => 10 })
|
23
|
+
stub_const('SandiMeter::LOCChecker::MAX_LOC', { 'blah' => 10 })
|
24
24
|
checker.stub(:locs_size).and_return(rand(11..100))
|
25
25
|
end
|
26
26
|
|
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'test_helper'
|
2
|
-
require_relative '../lib/method_arguments_counter'
|
2
|
+
require_relative '../lib/sandi_meter/method_arguments_counter'
|
3
3
|
|
4
|
-
describe MethodArgumentsCounter do
|
5
|
-
let(:test_loader) { ArgsLoader.new }
|
6
|
-
let(:analyzer) { MethodArgumentsCounter.new }
|
4
|
+
describe SandiMeter::MethodArgumentsCounter do
|
5
|
+
let(:test_loader) { SandiMeter::ArgsLoader.new }
|
6
|
+
let(:analyzer) { SandiMeter::MethodArgumentsCounter.new }
|
7
7
|
|
8
8
|
context 'when variable/method arguments' do
|
9
9
|
let(:args_add_block_1) { load_args_block('blah arg1, arg2')}
|
@@ -1,8 +1,8 @@
|
|
1
1
|
require 'test_helper'
|
2
|
-
require_relative '../lib/warning_scanner'
|
2
|
+
require_relative '../lib/sandi_meter/warning_scanner'
|
3
3
|
|
4
|
-
describe WarningScanner do
|
5
|
-
let(:scanner) { WarningScanner.new }
|
4
|
+
describe SandiMeter::WarningScanner do
|
5
|
+
let(:scanner) { SandiMeter::WarningScanner.new }
|
6
6
|
|
7
7
|
describe 'scanning class with indentation warnings' do
|
8
8
|
let(:test_class) { read_test_file(1) }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sandi_meter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anatoli Makarevich
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-09-
|
11
|
+
date: 2013-09-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -50,13 +50,14 @@ files:
|
|
50
50
|
- README.md
|
51
51
|
- Rakefile
|
52
52
|
- sandi_meter.gemspec
|
53
|
-
- lib/analyzer.rb
|
54
|
-
- lib/calculator.rb
|
55
|
-
- lib/file_scanner.rb
|
56
|
-
- lib/
|
57
|
-
- lib/
|
53
|
+
- lib/sandi_meter/analyzer.rb
|
54
|
+
- lib/sandi_meter/calculator.rb
|
55
|
+
- lib/sandi_meter/file_scanner.rb
|
56
|
+
- lib/sandi_meter/formatter.rb
|
57
|
+
- lib/sandi_meter/loc_checker.rb
|
58
|
+
- lib/sandi_meter/method_arguments_counter.rb
|
58
59
|
- lib/sandi_meter/version.rb
|
59
|
-
- lib/warning_scanner.rb
|
60
|
+
- lib/sandi_meter/warning_scanner.rb
|
60
61
|
- bin/sandi_meter
|
61
62
|
- spec/analyzer_spec.rb
|
62
63
|
- spec/loc_checker_spec.rb
|
data/lib/analyzer.rb
DELETED
@@ -1,186 +0,0 @@
|
|
1
|
-
require 'ripper'
|
2
|
-
require_relative 'warning_scanner'
|
3
|
-
require_relative 'loc_checker'
|
4
|
-
require_relative 'method_arguments_counter'
|
5
|
-
|
6
|
-
class Analyzer
|
7
|
-
attr_reader :classes, :missindented_classes, :methods, :missindented_methods, :method_calls, :instance_variables
|
8
|
-
|
9
|
-
def initialize
|
10
|
-
@classes = []
|
11
|
-
@missindented_classes = []
|
12
|
-
@missindented_methods = {}
|
13
|
-
@methods = {}
|
14
|
-
@method_calls = []
|
15
|
-
@instance_variables = {}
|
16
|
-
end
|
17
|
-
|
18
|
-
def analyze(file_path)
|
19
|
-
@file_path = file_path
|
20
|
-
@file_body = File.read(file_path)
|
21
|
-
@file_lines = @file_body.split(/$/).map { |l| l.gsub("\n", '') }
|
22
|
-
@indentation_warnings = indentation_warnings
|
23
|
-
# TODO
|
24
|
-
# add better determination wheter file is controller
|
25
|
-
@scan_instance_variables = !!(file_path =~ /\w+_controller.rb$/)
|
26
|
-
|
27
|
-
sexp = Ripper.sexp(@file_body)
|
28
|
-
scan_sexp(sexp)
|
29
|
-
|
30
|
-
output
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
def output
|
35
|
-
loc_checker = LOCChecker.new(@file_lines)
|
36
|
-
|
37
|
-
@classes.map! do |klass_params|
|
38
|
-
klass_params << loc_checker.check(klass_params, 'class')
|
39
|
-
end
|
40
|
-
|
41
|
-
@methods.each_pair do |klass, methods|
|
42
|
-
methods.each do |method_params|
|
43
|
-
method_params << loc_checker.check(method_params, 'def')
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
{
|
48
|
-
classes: @classes,
|
49
|
-
missindented_classes: @missindented_classes,
|
50
|
-
methods: @methods,
|
51
|
-
missindented_methods: @missindented_methods,
|
52
|
-
method_calls: @method_calls,
|
53
|
-
instance_variables: @instance_variables
|
54
|
-
}
|
55
|
-
end
|
56
|
-
|
57
|
-
def find_class_params(sexp, current_namespace)
|
58
|
-
flat_sexp = sexp[1].flatten
|
59
|
-
const_indexes = flat_sexp.each_index.select{ |i| flat_sexp[i] == :@const }
|
60
|
-
|
61
|
-
line_number = flat_sexp[const_indexes.first + 2]
|
62
|
-
class_tokens = const_indexes.map { |i| flat_sexp[i + 1] }
|
63
|
-
class_tokens.insert(0, current_namespace) unless current_namespace.empty?
|
64
|
-
class_name = class_tokens.join('::')
|
65
|
-
|
66
|
-
[class_name, line_number]
|
67
|
-
end
|
68
|
-
|
69
|
-
# MOVE
|
70
|
-
# to method scanner class
|
71
|
-
def number_of_arguments(method_sexp)
|
72
|
-
arguments = method_sexp[2]
|
73
|
-
arguments = arguments[1] if arguments.first == :paren
|
74
|
-
|
75
|
-
arguments[1] == nil ? 0 : arguments[1].size
|
76
|
-
end
|
77
|
-
|
78
|
-
def find_method_params(sexp)
|
79
|
-
sexp[1].flatten[1,2]
|
80
|
-
end
|
81
|
-
|
82
|
-
def find_last_line(params, token = 'class')
|
83
|
-
token_name, line = params
|
84
|
-
|
85
|
-
token_indentation = @file_lines[line - 1].index(token)
|
86
|
-
# TODO
|
87
|
-
# add check for trailing spaces
|
88
|
-
last_line = @file_lines[line..-1].index { |l| l =~ %r(\A\s{#{token_indentation}}end\s*\z) }
|
89
|
-
|
90
|
-
last_line ? last_line + line + 1 : nil
|
91
|
-
end
|
92
|
-
|
93
|
-
def scan_class_sexp(element, current_namespace = '')
|
94
|
-
case element.first
|
95
|
-
when :module
|
96
|
-
module_params = find_class_params(element, current_namespace)
|
97
|
-
module_params += [find_last_line(module_params, 'module')]
|
98
|
-
current_namespace = module_params.first
|
99
|
-
|
100
|
-
scan_sexp(element, current_namespace)
|
101
|
-
when :class
|
102
|
-
class_params = find_class_params(element, current_namespace)
|
103
|
-
|
104
|
-
if @indentation_warnings['class'] && @indentation_warnings['class'].any? { |first_line, last_line| first_line == class_params.last }
|
105
|
-
class_params << nil
|
106
|
-
@missindented_classes << class_params
|
107
|
-
else
|
108
|
-
class_params += [find_last_line(class_params)]
|
109
|
-
|
110
|
-
# in case of one liner class last line will be nil
|
111
|
-
(class_params.last == nil ? @missindented_classes : @classes) << class_params
|
112
|
-
end
|
113
|
-
|
114
|
-
current_namespace = class_params.first
|
115
|
-
scan_sexp(element, current_namespace)
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def find_args_add_block(method_call_sexp)
|
120
|
-
return unless method_call_sexp.kind_of?(Array)
|
121
|
-
|
122
|
-
method_call_sexp.each do |sexp|
|
123
|
-
next unless sexp.kind_of?(Array)
|
124
|
-
|
125
|
-
if sexp.first == :args_add_block
|
126
|
-
counter = MethodArgumentsCounter.new
|
127
|
-
arguments_count, line = counter.count(sexp)
|
128
|
-
|
129
|
-
@method_calls << [arguments_count, line]
|
130
|
-
|
131
|
-
find_args_add_block(sexp)
|
132
|
-
else
|
133
|
-
find_args_add_block(sexp)
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def scan_def_for_ivars(current_namespace, method_name, method_sexp)
|
139
|
-
return unless method_sexp.kind_of?(Array)
|
140
|
-
|
141
|
-
method_sexp.each do |sexp|
|
142
|
-
next unless sexp.kind_of?(Array)
|
143
|
-
|
144
|
-
if sexp.first == :assign
|
145
|
-
@instance_variables[current_namespace] ||= {}
|
146
|
-
@instance_variables[current_namespace][method_name] ||= []
|
147
|
-
@instance_variables[current_namespace][method_name] << sexp[1][1][1]
|
148
|
-
else
|
149
|
-
scan_def_for_ivars(current_namespace, method_name, sexp)
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
def scan_sexp(sexp, current_namespace = '')
|
155
|
-
sexp.each do |element|
|
156
|
-
next unless element.kind_of?(Array)
|
157
|
-
|
158
|
-
case element.first
|
159
|
-
when :def
|
160
|
-
method_params = find_method_params(element)
|
161
|
-
if @indentation_warnings['def'] && @indentation_warnings['def'].any? { |first_line, last_line| first_line == method_params.last }
|
162
|
-
method_params << nil
|
163
|
-
method_params << number_of_arguments(element)
|
164
|
-
@missindented_methods[current_namespace] ||= []
|
165
|
-
@missindented_methods[current_namespace] << method_params
|
166
|
-
else
|
167
|
-
method_params += [find_last_line(method_params, 'def')]
|
168
|
-
method_params << number_of_arguments(element)
|
169
|
-
@methods[current_namespace] ||= []
|
170
|
-
@methods[current_namespace] << method_params
|
171
|
-
end
|
172
|
-
scan_def_for_ivars(current_namespace, method_params.first, element) if @scan_instance_variables
|
173
|
-
find_args_add_block(element)
|
174
|
-
when :module, :class
|
175
|
-
scan_class_sexp(element, current_namespace)
|
176
|
-
else
|
177
|
-
scan_sexp(element, current_namespace)
|
178
|
-
end
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
182
|
-
def indentation_warnings
|
183
|
-
warning_scanner = WarningScanner.new
|
184
|
-
warning_scanner.scan(@file_body)
|
185
|
-
end
|
186
|
-
end
|
data/lib/calculator.rb
DELETED
@@ -1,103 +0,0 @@
|
|
1
|
-
class Calculator
|
2
|
-
def initialize
|
3
|
-
@data = {}
|
4
|
-
end
|
5
|
-
|
6
|
-
def push(data)
|
7
|
-
data.each_pair do |key, value|
|
8
|
-
if value.kind_of?(Array)
|
9
|
-
@data[key] ||= []
|
10
|
-
@data[key] += value
|
11
|
-
elsif value.kind_of?(Hash)
|
12
|
-
@data[key] ||= {}
|
13
|
-
@data[key].merge!(value)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def calculate!
|
19
|
-
check_first_rule
|
20
|
-
check_second_rule
|
21
|
-
check_third_rule
|
22
|
-
check_fourth_rule
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
def check_first_rule
|
27
|
-
total_classes_amount = @data[:classes].size
|
28
|
-
small_classes_amount = @data[:classes].inject(0) do |sum, class_params|
|
29
|
-
sum += 1 if class_params.last == true
|
30
|
-
sum
|
31
|
-
end
|
32
|
-
missindented_classes_amount = @data[:missindented_classes].size
|
33
|
-
|
34
|
-
puts "#{small_classes_amount * 100/ total_classes_amount}% of classes are under 100 lines."
|
35
|
-
|
36
|
-
# TODO uncomment when missindented location will be implemented
|
37
|
-
#
|
38
|
-
# if missindented_classes_amount > 0
|
39
|
-
# puts "Pay attention to #{missindented_classes_amount} missindented classes."
|
40
|
-
# end
|
41
|
-
end
|
42
|
-
|
43
|
-
def check_second_rule
|
44
|
-
total_methods_amount = 0
|
45
|
-
small_methods_amount = 0
|
46
|
-
|
47
|
-
@data[:methods].each_pair do |klass, methods|
|
48
|
-
small_methods_amount += methods.select { |m| m.last == true }.size
|
49
|
-
total_methods_amount += methods.size
|
50
|
-
end
|
51
|
-
|
52
|
-
missindented_methods_amount = 0
|
53
|
-
@data[:missindented_methods].each_pair do |klass, methods|
|
54
|
-
missindented_methods_amount += methods.size
|
55
|
-
end
|
56
|
-
|
57
|
-
puts "#{small_methods_amount * 100 / total_methods_amount}% of methods are under 5 lines."
|
58
|
-
|
59
|
-
# TODO uncomment when missindented location will be implemented
|
60
|
-
#
|
61
|
-
# if missindented_methods_amount > 0
|
62
|
-
# puts "Pay attention to #{missindented_methods_amount} missindented methods."
|
63
|
-
# end
|
64
|
-
end
|
65
|
-
|
66
|
-
# TODO
|
67
|
-
# count method definitions argumets too
|
68
|
-
def check_third_rule
|
69
|
-
total_method_calls = @data[:method_calls].size
|
70
|
-
|
71
|
-
proper_method_calls = @data[:method_calls].inject(0) do |sum, params|
|
72
|
-
sum += 1 unless params.first > 4
|
73
|
-
sum
|
74
|
-
end
|
75
|
-
|
76
|
-
missindented_methods_amount = 0
|
77
|
-
@data[:missindented_methods].each_pair do |klass, methods|
|
78
|
-
missindented_methods_amount += methods.size
|
79
|
-
end
|
80
|
-
|
81
|
-
if total_method_calls > 0
|
82
|
-
puts "#{proper_method_calls * 100 / total_method_calls}% of methods calls accepts are less than 4 parameters."
|
83
|
-
else
|
84
|
-
puts "Seems like there no method calls. WAT?!"
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def check_fourth_rule
|
89
|
-
proper_controllers_amount = 0
|
90
|
-
total_controllers_amount = 0
|
91
|
-
|
92
|
-
@data[:instance_variables].each_pair do |controller, methods|
|
93
|
-
total_controllers_amount += 1
|
94
|
-
proper_controllers_amount += 1 unless methods.values.map(&:size).any? { |v| v > 1 }
|
95
|
-
end
|
96
|
-
|
97
|
-
if total_controllers_amount > 0
|
98
|
-
puts "#{proper_controllers_amount * 100 / total_controllers_amount}% of controllers have one instance varible per action."
|
99
|
-
else
|
100
|
-
puts "Seems like there are no controllers :)"
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
data/lib/file_scanner.rb
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
require_relative 'analyzer'
|
2
|
-
require_relative 'calculator'
|
3
|
-
|
4
|
-
class FileScanner
|
5
|
-
def initialize(log_errors = false)
|
6
|
-
@log_errors = log_errors
|
7
|
-
@calculator = Calculator.new
|
8
|
-
end
|
9
|
-
|
10
|
-
def scan(path)
|
11
|
-
if File.directory?(path)
|
12
|
-
scan_dir(path)
|
13
|
-
else
|
14
|
-
scan_file(path)
|
15
|
-
end
|
16
|
-
|
17
|
-
@calculator.calculate!
|
18
|
-
end
|
19
|
-
|
20
|
-
private
|
21
|
-
def scan_dir(path)
|
22
|
-
Dir["#{path}/**/*.rb"].each do |file|
|
23
|
-
scan_file(file)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def scan_file(path)
|
28
|
-
begin
|
29
|
-
analyzer = Analyzer.new
|
30
|
-
data = analyzer.analyze(path)
|
31
|
-
@calculator.push(data)
|
32
|
-
rescue Exception => e
|
33
|
-
if @log_errors
|
34
|
-
puts "Checkout #{path} for:"
|
35
|
-
puts "\t#{e.message}"
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
data/lib/loc_checker.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
class LOCChecker < Struct.new(:file_lines)
|
2
|
-
|
3
|
-
MAX_LOC = {
|
4
|
-
'def' => 5,
|
5
|
-
'class' => 100
|
6
|
-
}
|
7
|
-
|
8
|
-
def check(params, token)
|
9
|
-
_, first_line, last_line = params
|
10
|
-
locs_size(first_line, last_line) <= MAX_LOC[token]
|
11
|
-
end
|
12
|
-
|
13
|
-
private
|
14
|
-
def locs_size(first_line, last_line)
|
15
|
-
file_lines[first_line - 1..last_line - 1].map(&:strip).reject(&:empty?).size
|
16
|
-
end
|
17
|
-
end
|
@@ -1,36 +0,0 @@
|
|
1
|
-
class MethodArgumentsCounter
|
2
|
-
def initialize
|
3
|
-
reset!
|
4
|
-
end
|
5
|
-
|
6
|
-
def count(args_add_block_sexp)
|
7
|
-
reset!
|
8
|
-
|
9
|
-
@count += args_add_block_sexp[1].size
|
10
|
-
@count += 1 if args_add_block_sexp.last == true
|
11
|
-
bypass_sexp(args_add_block_sexp)
|
12
|
-
|
13
|
-
return [@count, @lines.uniq.sort.first]
|
14
|
-
end
|
15
|
-
|
16
|
-
def reset!
|
17
|
-
@count = 0
|
18
|
-
@lines = []
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
def bypass_sexp(args_add_block_sexp)
|
23
|
-
args_add_block_sexp.each do |sexp|
|
24
|
-
next unless sexp.kind_of?(Array)
|
25
|
-
|
26
|
-
case sexp.first
|
27
|
-
when :bare_assoc_hash
|
28
|
-
@count += sexp[1].size - 1
|
29
|
-
when :@int, :@ident
|
30
|
-
@lines << sexp.last.first
|
31
|
-
end
|
32
|
-
|
33
|
-
bypass_sexp(sexp)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
data/lib/warning_scanner.rb
DELETED
@@ -1,55 +0,0 @@
|
|
1
|
-
require 'open3'
|
2
|
-
|
3
|
-
class WarningScanner
|
4
|
-
attr_reader :indentation_warnings
|
5
|
-
|
6
|
-
INDENTATION_WARNING_REGEXP = /at 'end' with '(def|class|module)' at (\d+)\z/
|
7
|
-
|
8
|
-
def scan(source)
|
9
|
-
status, @warnings, process = if defined? Bundler
|
10
|
-
Bundler.with_clean_env do
|
11
|
-
validate(source)
|
12
|
-
end
|
13
|
-
else
|
14
|
-
validate(source)
|
15
|
-
end
|
16
|
-
|
17
|
-
check_syntax(status)
|
18
|
-
@indentation_warnings = parse_warnings
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
def validate(source)
|
23
|
-
Open3.capture3('ruby -wc', stdin_data: source)
|
24
|
-
end
|
25
|
-
|
26
|
-
def check_syntax(status)
|
27
|
-
raise SyntaxError, @warnings unless !!(status =~ /Syntax\sOK/)
|
28
|
-
end
|
29
|
-
|
30
|
-
def check_token_lines(token, line_num, end_line_num)
|
31
|
-
raise 'No valid end line number' unless end_line_num =~ /^\d+$/
|
32
|
-
raise 'No valid line number' unless line_num =~ /^\d+$/
|
33
|
-
raise 'No valid token ("def" or "class")' unless token =~ /^def|class|module$/
|
34
|
-
end
|
35
|
-
|
36
|
-
def extract_indentation_mismatch(warning_line)
|
37
|
-
_, end_line_num, warning_type, warning_body = warning_line.split(':').map(&:strip)
|
38
|
-
return nil unless warning_type == 'warning'
|
39
|
-
return nil unless warning_body =~ /at 'end' with '(def|class|module)' at (\d+)\z/
|
40
|
-
|
41
|
-
res = warning_body.match(INDENTATION_WARNING_REGEXP)[1..2] << end_line_num
|
42
|
-
check_token_lines(*res)
|
43
|
-
|
44
|
-
res
|
45
|
-
end
|
46
|
-
|
47
|
-
def parse_warnings
|
48
|
-
@warnings.split("\n").inject({}) do |warnings, warning|
|
49
|
-
token, line, end_line = extract_indentation_mismatch(warning)
|
50
|
-
warnings[token] ||= []
|
51
|
-
warnings[token] << [line.to_i, end_line.to_i]
|
52
|
-
warnings
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|