sandi_meter 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 +15 -0
- data/LICENSE +22 -0
- data/README.md +15 -0
- data/Rakefile +12 -0
- data/bin/sandi_meter +6 -0
- data/lib/analyzer.rb +186 -0
- data/lib/calculator.rb +103 -0
- data/lib/file_scanner.rb +39 -0
- data/lib/loc_checker.rb +17 -0
- data/lib/method_arguments_counter.rb +36 -0
- data/lib/sandi_meter/version.rb +3 -0
- data/lib/warning_scanner.rb +55 -0
- data/sandi_meter.gemspec +24 -0
- data/spec/analyzer_spec.rb +163 -0
- data/spec/loc_checker_spec.rb +32 -0
- data/spec/method_arguments_counter_spec.rb +59 -0
- data/spec/test_classes/1.rb +6 -0
- data/spec/test_classes/10.rb +9 -0
- data/spec/test_classes/11.rb +5 -0
- data/spec/test_classes/2.rb +3 -0
- data/spec/test_classes/3.rb +5 -0
- data/spec/test_classes/4.rb +9 -0
- data/spec/test_classes/5.rb +1 -0
- data/spec/test_classes/6.rb +9 -0
- data/spec/test_classes/7.rb +14 -0
- data/spec/test_classes/8.rb +12 -0
- data/spec/test_classes/9_controller.rb +5 -0
- data/spec/test_helper.rb +49 -0
- data/spec/warning_scanner_spec.rb +36 -0
- metadata +101 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YTQyYTQ2NWQ4ODBhYjNmMzVmNGU5NzJkOTI1ZjhkZTBkODZkNDEyNA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
Yzk5MjM2ZTdiODY5ZjcyN2YyNTNjYWRjODBiMWI3MzQyYzkxMzdkNg==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NmNlNWU0MGQzODYwY2RmMDQ4ZTY4NjZmNWJhN2I5MDU2MzhlMzhjNmI0ZTM1
|
10
|
+
NmIwNzhmNWViNTFiMjExZjQ2ZWZhY2Y1N2JhMGE4ZmEzYWY4OWRmMTdlMTE5
|
11
|
+
NjZmMjcxNzJkNTAwYjcwNWFjY2IwMzk5YTAxZjI3YTU2ZWFiMjI=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZDBlYmZiZjI0OGI0Y2Y0MTAzY2YxNTE4YjNhZTgxYzdhOTdhMzc2MzI1M2M2
|
14
|
+
OTdmMWU0NzZjNWE1NTZmNGViMjA2Y2IwOTkyMGY3ZmUyN2ZlY2RkZTQ4ZDA0
|
15
|
+
MmEyYzUxZGU3ZjE3NWU5NmVkZGU1MDUzY2M0NTFmOWZmYjczZTQ=
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Anatoli Makarevich
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# SandiMeter
|
2
|
+
|
3
|
+
Static analysis tool for checking your Ruby code for [Sandi Metz' for rules](http://robots.thoughtbot.com/post/50655960596/sandi-metz-rules-for-developers).
|
4
|
+
|
5
|
+
* 100 lines per class
|
6
|
+
* 5 lines per method
|
7
|
+
* 4 params per method call (and don't even try cheating with hash params)
|
8
|
+
* 2 instance variables per controller' action
|
9
|
+
|
10
|
+
## As simple as
|
11
|
+
|
12
|
+
~~~
|
13
|
+
gem install sandi_meter
|
14
|
+
sandi_meter ~/your/ruby/or/rails/project
|
15
|
+
~~~
|
data/Rakefile
ADDED
data/bin/sandi_meter
ADDED
data/lib/analyzer.rb
ADDED
@@ -0,0 +1,186 @@
|
|
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
ADDED
@@ -0,0 +1,103 @@
|
|
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
ADDED
@@ -0,0 +1,39 @@
|
|
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
ADDED
@@ -0,0 +1,17 @@
|
|
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
|
@@ -0,0 +1,36 @@
|
|
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
|
@@ -0,0 +1,55 @@
|
|
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
|
data/sandi_meter.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'sandi_meter/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "sandi_meter"
|
7
|
+
spec.version = SandiMeter::VERSION
|
8
|
+
spec.authors = ["Anatoli Makarevich"]
|
9
|
+
spec.email = ["makaroni4@gmail.com"]
|
10
|
+
spec.description = %q{Sandi Metz rules checker}
|
11
|
+
spec.summary = %q{Sandi Metz rules checker}
|
12
|
+
spec.homepage = "https://github.com/makaroni4/sandi_meter"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = %w(LICENSE README.md Rakefile sandi_meter.gemspec)
|
16
|
+
spec.files += Dir.glob("lib/**/*.rb")
|
17
|
+
spec.files += Dir.glob("bin/**/*")
|
18
|
+
spec.files += Dir.glob("spec/**/*")
|
19
|
+
spec.executables = ["sandi_meter"]
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require_relative '../lib/analyzer'
|
3
|
+
|
4
|
+
describe Analyzer do
|
5
|
+
let(:analyzer) { Analyzer.new }
|
6
|
+
|
7
|
+
describe 'finds properly indended classes with lines' do
|
8
|
+
let(:test_class) { test_file_path(3) }
|
9
|
+
|
10
|
+
before do
|
11
|
+
analyzer.analyze(test_class)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'finds indentation warnings for method' do
|
15
|
+
analyzer.classes.should eq([["TestClass", 1, 5, true]])
|
16
|
+
analyzer.missindented_classes.should be_empty
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'finds methods' do
|
20
|
+
analyzer.methods.should eq({"TestClass"=>[["blah", 2, 4, 0, true]]})
|
21
|
+
analyzer.missindented_methods.should be_empty
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'finds method calls that brakes third rule' do
|
25
|
+
analyzer.method_calls.should eq([[5,3]])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'finds missindented classes without last line' do
|
30
|
+
let(:test_class) { test_file_path(1) }
|
31
|
+
|
32
|
+
before do
|
33
|
+
analyzer.analyze(test_class)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'finds indentation warnings for method' do
|
37
|
+
analyzer.classes.should be_empty
|
38
|
+
analyzer.missindented_classes.should eq([["MyApp::TestClass", 2, nil]])
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'finds methods' do
|
42
|
+
analyzer.methods.should be_empty
|
43
|
+
analyzer.missindented_methods.should eq({"MyApp::TestClass"=>[["blah", 3, nil, 0]]})
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe 'finds properly indended classes in one file' do
|
48
|
+
let(:test_class) { test_file_path(4) }
|
49
|
+
|
50
|
+
before do
|
51
|
+
analyzer.analyze(test_class)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'finds classes' do
|
55
|
+
analyzer.classes.should include(["FirstTestClass", 1, 4, true])
|
56
|
+
analyzer.classes.should include(["SecondTestClass", 6, 9, true])
|
57
|
+
analyzer.missindented_classes.should be_empty
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'finds methods' do
|
61
|
+
analyzer.methods["FirstTestClass"].should eq([["first_meth", 2, 3, 1, true]])
|
62
|
+
analyzer.methods["SecondTestClass"].should eq([["second_meth", 7, 8, 1, true]])
|
63
|
+
analyzer.missindented_methods.should be_empty
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe 'finds one liner class' do
|
68
|
+
let(:test_class) { test_file_path(5) }
|
69
|
+
|
70
|
+
before do
|
71
|
+
analyzer.analyze(test_class)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'finds classes' do
|
75
|
+
analyzer.missindented_classes.should eq([["OneLinerClass", 1, nil]])
|
76
|
+
analyzer.classes.should be_empty
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'finds methods' do
|
80
|
+
analyzer.methods.should be_empty
|
81
|
+
analyzer.missindented_methods.should be_empty
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe 'finds subclass of a class' do
|
86
|
+
let(:test_class) { test_file_path(7) }
|
87
|
+
|
88
|
+
before do
|
89
|
+
analyzer.analyze(test_class)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'finds class and subclass' do
|
93
|
+
analyzer.classes.should include(["MyApp::Blah::User", 5, 13, true])
|
94
|
+
analyzer.classes.should include(["MyApp::Blah::User::SubUser", 9, 12, true])
|
95
|
+
analyzer.missindented_classes.should be_empty
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'finds methods' do
|
99
|
+
analyzer.methods["MyApp::Blah"].should eq([["module_meth", 2, 3, 0, true]])
|
100
|
+
analyzer.methods["MyApp::Blah::User"].should eq([["class_meth", 6, 7, 0, true]])
|
101
|
+
analyzer.methods["MyApp::Blah::User::SubUser"].should eq([["sub_meth", 10, 11, 0, true]])
|
102
|
+
analyzer.missindented_methods.should be_empty
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe 'finds class and methods with private methods' do
|
107
|
+
let(:test_class) { test_file_path(8) }
|
108
|
+
|
109
|
+
before do
|
110
|
+
analyzer.analyze(test_class)
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'finds class and subclass' do
|
114
|
+
analyzer.classes.should include(["RailsController", 1, 12, true])
|
115
|
+
analyzer.missindented_classes.should be_empty
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'finds methods' do
|
119
|
+
analyzer.methods["RailsController"].should include(["index", 2, 3, 0, true])
|
120
|
+
analyzer.methods["RailsController"].should include(["destroy", 5, 6, 0, true])
|
121
|
+
analyzer.methods["RailsController"].should include(["private_meth", 9, 10, 0, true])
|
122
|
+
analyzer.missindented_methods.should be_empty
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe 'instance variables in methods' do
|
127
|
+
context 'in controller class' do
|
128
|
+
let(:test_class) { test_file_path('9_controller') }
|
129
|
+
|
130
|
+
before do
|
131
|
+
analyzer.analyze(test_class)
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'finds instance variable' do
|
135
|
+
analyzer.instance_variables.should eq({"UsersController"=>{"index"=>["@users"]}})
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'not in controller class' do
|
140
|
+
let(:test_class) { test_file_path(10) }
|
141
|
+
|
142
|
+
before do
|
143
|
+
analyzer.analyze(test_class)
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'does not find instance variable' do
|
147
|
+
analyzer.instance_variables.should be_empty
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
describe 'hash method arguments' do
|
153
|
+
let(:test_class) { test_file_path(11) }
|
154
|
+
|
155
|
+
before do
|
156
|
+
analyzer.analyze(test_class)
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'counts arguments' do
|
160
|
+
analyzer.method_calls.should eq([[5, 3]])
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require_relative '../lib/loc_checker'
|
3
|
+
|
4
|
+
describe LOCChecker do
|
5
|
+
let(:checker) { LOCChecker.new([]) }
|
6
|
+
|
7
|
+
describe '#check' do
|
8
|
+
context 'for short code' do
|
9
|
+
before do
|
10
|
+
stub_const('LOCChecker::MAX_LOC', { 'blah' => 10 })
|
11
|
+
checker.stub(:locs_size).and_return(rand(0..10))
|
12
|
+
end
|
13
|
+
|
14
|
+
# REFACTOR
|
15
|
+
# avoid passing dumb arguments to tested methods
|
16
|
+
it 'passes the check' do
|
17
|
+
checker.check([1,2,3], 'blah').should be_true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'for large code' do
|
22
|
+
before do
|
23
|
+
stub_const('LOCChecker::MAX_LOC', { 'blah' => 10 })
|
24
|
+
checker.stub(:locs_size).and_return(rand(11..100))
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'does not pass the check' do
|
28
|
+
checker.check([1,2,3], 'blah').should be_false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require_relative '../lib/method_arguments_counter'
|
3
|
+
|
4
|
+
describe MethodArgumentsCounter do
|
5
|
+
let(:test_loader) { ArgsLoader.new }
|
6
|
+
let(:analyzer) { MethodArgumentsCounter.new }
|
7
|
+
|
8
|
+
context 'when variable/method arguments' do
|
9
|
+
let(:args_add_block_1) { load_args_block('blah arg1, arg2')}
|
10
|
+
let(:args_add_block_2) { load_args_block('blah(arg1, arg2)')}
|
11
|
+
|
12
|
+
it 'counts arguments' do
|
13
|
+
analyzer.count(args_add_block_1).should eq([2, 1])
|
14
|
+
analyzer.count(args_add_block_2).should eq([2, 1])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'when hash arguments' do
|
19
|
+
let(:args_add_block_1) { load_args_block('blah k: :v') }
|
20
|
+
let(:args_add_block_2) { load_args_block('blah(k: :v)') }
|
21
|
+
|
22
|
+
let(:args_add_block_3) { load_args_block('blah k1: :v1, k2: :v2') }
|
23
|
+
let(:args_add_block_4) { load_args_block('blah(k1: :v1, k2: :v2)') }
|
24
|
+
|
25
|
+
it 'counts arguments' do
|
26
|
+
analyzer.count(args_add_block_1).should eq([1, 1])
|
27
|
+
analyzer.count(args_add_block_2).should eq([1, 1])
|
28
|
+
analyzer.count(args_add_block_3).should eq([2, 1])
|
29
|
+
analyzer.count(args_add_block_4).should eq([2, 1])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'when variable/method with hash' do
|
34
|
+
let(:code_1) { load_args_block('blah arg_1, arg_2, k: :v') }
|
35
|
+
let(:code_2) { load_args_block('blah(arg_1, arg_2, k: :v)') }
|
36
|
+
let(:code_3) { load_args_block('blah arg_1, arg_2, k1: :v1, k2: :v2') }
|
37
|
+
let(:code_4) { load_args_block('blah(arg_1, arg_2, k1: :v1, k2: :v2)') }
|
38
|
+
|
39
|
+
it 'counts arguments' do
|
40
|
+
analyzer.count(code_1).should eq([3, 1])
|
41
|
+
analyzer.count(code_2).should eq([3, 1])
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'counts hash keys as argumets' do
|
45
|
+
analyzer.count(code_3).should eq([4, 1])
|
46
|
+
analyzer.count(code_4).should eq([4, 1])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'when argument with default value' do
|
51
|
+
let(:code_1) { load_args_block('blah arg_1 = "blah"') }
|
52
|
+
let(:code_2) { load_args_block('blah(arg_1 = "blah")') }
|
53
|
+
|
54
|
+
it 'counts arguments' do
|
55
|
+
analyzer.count(code_1).should eq([1, 1])
|
56
|
+
analyzer.count(code_2).should eq([1, 1])
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
class OneLinerClass; end
|
data/spec/test_helper.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rspec/autorun'
|
2
|
+
require 'ripper'
|
3
|
+
|
4
|
+
def test_file_path(file_name)
|
5
|
+
File.join(
|
6
|
+
File.dirname(__FILE__),
|
7
|
+
"test_classes/#{file_name}.rb"
|
8
|
+
)
|
9
|
+
end
|
10
|
+
|
11
|
+
def read_test_file(file_name)
|
12
|
+
File.read(
|
13
|
+
test_file_path(file_name)
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def load_args_block(method_call)
|
18
|
+
loader = ArgsLoader.new
|
19
|
+
loader.load(method_call)
|
20
|
+
end
|
21
|
+
|
22
|
+
class ArgsLoader
|
23
|
+
def reset!
|
24
|
+
@sexp = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def load(method_call)
|
28
|
+
reset!
|
29
|
+
load_method_add_arg(Ripper.sexp(method_call))
|
30
|
+
return @sexp
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def load_method_add_arg(method_sexp)
|
35
|
+
method_sexp.each do |s|
|
36
|
+
next unless s.kind_of?(Array)
|
37
|
+
|
38
|
+
s.each do |a|
|
39
|
+
next unless a.kind_of?(Array)
|
40
|
+
|
41
|
+
if a.first == :args_add_block
|
42
|
+
@sexp ||= a
|
43
|
+
else
|
44
|
+
load_method_add_arg(s)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require_relative '../lib/warning_scanner'
|
3
|
+
|
4
|
+
describe WarningScanner do
|
5
|
+
let(:scanner) { WarningScanner.new }
|
6
|
+
|
7
|
+
describe 'scanning class with indentation warnings' do
|
8
|
+
let(:test_class) { read_test_file(1) }
|
9
|
+
|
10
|
+
before do
|
11
|
+
scanner.scan(test_class)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'finds indentation warnings for method' do
|
15
|
+
scanner.indentation_warnings['def'].should eq([[3, 4]])
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'finds indentation warnings for class' do
|
19
|
+
scanner.indentation_warnings['class'].should eq([[2, 5]])
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'finds indentation warnings for module' do
|
23
|
+
scanner.indentation_warnings['module'].should eq([[1, 6]])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'scanning class with syntax error' do
|
28
|
+
let(:test_class) { read_test_file(2) }
|
29
|
+
|
30
|
+
it 'raise syntax error' do
|
31
|
+
expect {
|
32
|
+
scanner.scan(test_class)
|
33
|
+
}.to raise_error(SyntaxError)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sandi_meter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anatoli Makarevich
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-09-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Sandi Metz rules checker
|
42
|
+
email:
|
43
|
+
- makaroni4@gmail.com
|
44
|
+
executables:
|
45
|
+
- sandi_meter
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- LICENSE
|
50
|
+
- README.md
|
51
|
+
- Rakefile
|
52
|
+
- sandi_meter.gemspec
|
53
|
+
- lib/analyzer.rb
|
54
|
+
- lib/calculator.rb
|
55
|
+
- lib/file_scanner.rb
|
56
|
+
- lib/loc_checker.rb
|
57
|
+
- lib/method_arguments_counter.rb
|
58
|
+
- lib/sandi_meter/version.rb
|
59
|
+
- lib/warning_scanner.rb
|
60
|
+
- bin/sandi_meter
|
61
|
+
- spec/analyzer_spec.rb
|
62
|
+
- spec/loc_checker_spec.rb
|
63
|
+
- spec/method_arguments_counter_spec.rb
|
64
|
+
- spec/test_classes/1.rb
|
65
|
+
- spec/test_classes/10.rb
|
66
|
+
- spec/test_classes/11.rb
|
67
|
+
- spec/test_classes/2.rb
|
68
|
+
- spec/test_classes/3.rb
|
69
|
+
- spec/test_classes/4.rb
|
70
|
+
- spec/test_classes/5.rb
|
71
|
+
- spec/test_classes/6.rb
|
72
|
+
- spec/test_classes/7.rb
|
73
|
+
- spec/test_classes/8.rb
|
74
|
+
- spec/test_classes/9_controller.rb
|
75
|
+
- spec/test_helper.rb
|
76
|
+
- spec/warning_scanner_spec.rb
|
77
|
+
homepage: https://github.com/makaroni4/sandi_meter
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
metadata: {}
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 2.0.5
|
98
|
+
signing_key:
|
99
|
+
specification_version: 4
|
100
|
+
summary: Sandi Metz rules checker
|
101
|
+
test_files: []
|