sandi_meter 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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: []