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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- YTQyYTQ2NWQ4ODBhYjNmMzVmNGU5NzJkOTI1ZjhkZTBkODZkNDEyNA==
4
+ NzI2Nzc1NDI0OWYyNTExZTJjMGViMWQ2YmNkNzU3YmNhNWU5YmM0MQ==
5
5
  data.tar.gz: !binary |-
6
- Yzk5MjM2ZTdiODY5ZjcyN2YyNTNjYWRjODBiMWI3MzQyYzkxMzdkNg==
6
+ Njg4MzcxYmMyMDhiOTBhNTJjYTE1MzFhYWM5Yjc0ZWU2YjgyZGE0NA==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- NmNlNWU0MGQzODYwY2RmMDQ4ZTY4NjZmNWJhN2I5MDU2MzhlMzhjNmI0ZTM1
10
- NmIwNzhmNWViNTFiMjExZjQ2ZWZhY2Y1N2JhMGE4ZmEzYWY4OWRmMTdlMTE5
11
- NjZmMjcxNzJkNTAwYjcwNWFjY2IwMzk5YTAxZjI3YTU2ZWFiMjI=
9
+ YTI5YzRhNDExYTg5MTg2ZGUyM2E4NGI0ODE5MjI5YjY1MTg5MTQ1MDNmYTM1
10
+ NzM2NWU2Y2ZlZGQxNmZlMzIyMmE4ZDI3MjgzY2E1MDYzMWIzZTRjMTI4MDQ4
11
+ M2Y5MWZhYTA5MzZlM2NjMDk3ZTM2OTI4Y2UyMWM4ZWUwMmNjYWU=
12
12
  data.tar.gz: !binary |-
13
- ZDBlYmZiZjI0OGI0Y2Y0MTAzY2YxNTE4YjNhZTgxYzdhOTdhMzc2MzI1M2M2
14
- OTdmMWU0NzZjNWE1NTZmNGViMjA2Y2IwOTkyMGY3ZmUyN2ZlY2RkZTQ4ZDA0
15
- MmEyYzUxZGU3ZjE3NWU5NmVkZGU1MDUzY2M0NTFmOWZmYjczZTQ=
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
- * 2 instance variables per controller' action
8
+ * 1 instance variables per controller' action
9
9
 
10
- ## As simple as
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
@@ -1,3 +1,3 @@
1
1
  module SandiMeter
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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
@@ -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) }
@@ -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.1
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-15 00:00:00.000000000 Z
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/loc_checker.rb
57
- - lib/method_arguments_counter.rb
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
@@ -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