sandi_meter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,12 @@
1
+ require 'rake/testtask'
2
+ require 'bundler'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ Rake::TestTask.new do |test|
7
+ test.verbose = true
8
+ test.libs << "spec"
9
+ test.test_files = FileList['spec/**/*_spec.rb']
10
+ end
11
+
12
+ task default: :test
data/bin/sandi_meter ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/file_scanner'
4
+
5
+ scanner = FileScanner.new(ARGV[1] == "--log")
6
+ scanner.scan(ARGV[0])
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
@@ -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
@@ -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,3 @@
1
+ module SandiMeter
2
+ VERSION = "0.0.1"
3
+ 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
@@ -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,6 @@
1
+ module MyApp
2
+ class TestClass
3
+ def blah
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ class User
2
+ def initialize
3
+ @name = Faker::Lorem.word
4
+ end
5
+
6
+ def hi
7
+ puts @name
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class TestClass
2
+ def blah
3
+ call :a, :b, :c, :d, key: :value
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ class TestClass
2
+ def blah
3
+ end
@@ -0,0 +1,5 @@
1
+ class TestClass
2
+ def blah
3
+ User.blah(1,2,3,4,5)
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class FirstTestClass
2
+ def first_meth(blah)
3
+ end
4
+ end
5
+
6
+ class SecondTestClass
7
+ def second_meth blah
8
+ end
9
+ end
@@ -0,0 +1 @@
1
+ class OneLinerClass; end
@@ -0,0 +1,9 @@
1
+ module MyApp::Blah
2
+ def module_meth
3
+ end
4
+
5
+ class User
6
+ def class_meth
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ module MyApp::Blah
2
+ def module_meth
3
+ end
4
+
5
+ class User
6
+ def class_meth
7
+ end
8
+
9
+ class SubUser
10
+ def sub_meth
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ class RailsController
2
+ def index
3
+ end
4
+
5
+ def destroy
6
+ end
7
+
8
+ private
9
+ def private_meth
10
+ end
11
+
12
+ end
@@ -0,0 +1,5 @@
1
+ class UsersController
2
+ def index
3
+ @users = User.page(params[:page])
4
+ end
5
+ end
@@ -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: []