sandi_meter 0.0.1 → 0.0.2

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