sandi_meter 0.0.6 → 1.0.0

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
- MjEzMjdjMjMzYWE4NzkwMWQyYmNiOTE0ZjY0N2NkZDdmNTM3NWIwMQ==
4
+ MjRmZDFmOTBlY2IxMzJlMjBhNTQwMDIwMjMzMmNlNWQyZDM2ODEyYQ==
5
5
  data.tar.gz: !binary |-
6
- NjU1M2UzMTIwNjNhMzkyYzZhMGYyNGJlNjVmOTZhYTI3YjcwNzM2Yg==
6
+ ZjQyYWJiNDNlY2RlNjk0YTIzYTQ2MGU2YTUwNDAwMWU2OWQ3M2ZjZQ==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- NWJhODU3OWQ4ZmNiYzg1ZWIzMDc5MTBkYTE1MTc3MWEyNWNhMDFkNjZjZDIw
10
- NzIzOGFmMmMzMmQ2MjZlMDQ1ZjFlM2I4MmYwMjExMDI1M2E1Yzc5NzFiNjc1
11
- M2JmOTllMmI0ZDBkZDM2ZDlmZDZjYzE3MzQxZmJhOWE5YzY1NDM=
9
+ MzgxY2NjZGZjNTJhYmVhNmZhMDUxYmQ5MWQ2ZWEzNGMwZjA1ZTEyYTdjMjUy
10
+ MmQ1ZTZiOGE5OTAyMzBmOTA3MGFmNmRlZjkzNjA1ZWI2MzJlMWUzMDdmNDNi
11
+ MGJjZGFiMzNiNTI2OWIyOTI1YzBkMDNkZGQ0ZDg3ZTc1N2U2Zjc=
12
12
  data.tar.gz: !binary |-
13
- ZDI5OTYwNjAyZDQyZGQ1YzhkN2FhMWY0ZTllNjc2NmJmZDA5OTgzNzE1MTlk
14
- OWRjM2Q4MDMyOTdlZjAxZjI1ZDU5OGE4Yjk3YjhlOTNjMmU2Y2YxNzhkZjk3
15
- MThhZDE4ZjNhMWNiMDViNDM2NjNiNTYyY2QxZDJiOGQ0YzAzNDQ=
13
+ ZDMwMTA4NTA1ZTI3ZjU4OGVmYjI3MDMzMzQ2MGQ5Zjc3OGY0ZGZhYzA2Yjlm
14
+ NzQ1ODg5NmYzMzc0NjZhMmU5ZjFiYmY1YmEyM2FjMWRmNjg5MTRjY2FmNjU4
15
+ MDg1NmJmYjE1Mjc0NzU0YmQ1Y2M5ZDY0ZGRkMjM2YjlkMzMxZGY=
data/README.md CHANGED
@@ -15,25 +15,58 @@ Static analysis tool for checking your Ruby code for [Sandi Metz' four rules](ht
15
15
  gem install sandi_meter
16
16
 
17
17
  sandi_meter --help
18
- -g, --graph Create folder and log data to graph
19
- -l, --log Show syntax error and indentation log output
20
- -p, --path PATH Path to folder or file to analyze
21
- -r, --rules Show rules
22
- -h, --help Help
23
-
24
- sandi_meter -p ~/your/ruby/or/rails/project
25
-
26
- 1. 94% of classes are under 100 lines.
27
- 2. 53% of methods are under 5 lines.
28
- 3. 98% of methods calls accepts are less than 4 parameters.
29
- 4. 21% of controllers have one instance variable per action.
18
+ -d, --details CLI mode. Show details (path, line number)
19
+ -g, --graph HTML mode. Create folder, log data and output stats to HTML file.
20
+ -l, --log Show syntax error and indentation log output
21
+ -p, --path PATH Path to folder or file to analyze (default is ".")
22
+ -r, --rules Show rules
23
+ -h, --help Help
24
+
25
+ cd ~/your/ruby/or/rails/project
26
+ sandi_meter -d
27
+
28
+ 1. 85% of classes are under 100 lines.
29
+ 2. 45% of methods are under 5 lines.
30
+ 3. 99% of method calls accepted are less than 4 parameters.
31
+ 4. 66% of controllers have one instance variable per action.
32
+
33
+ Classes with 100+ lines
34
+ Class name | Size | Path
35
+ SandiMeter::Analyzer | 219 | ./lib/sandi_meter/analyzer.rb:7
36
+ SandiMeter::Calculator | 172 | ./lib/sandi_meter/calculator.rb:2
37
+ SandiMeter::HtmlGenerator | 135 | ./lib/sandi_meter/html_generator.rb:5
38
+ Valera | 109 | ./spec/test_classes/12.rb:1
39
+
40
+ Missindented classes
41
+ Class name | Path
42
+ MyApp::TestClass | ./spec/test_classes/1.rb:2
43
+ OneLinerClass | ./spec/test_classes/5.rb:1
44
+
45
+ Methods with 5+ lines
46
+ Class name | Method name | Size | Path
47
+ SandiMeter::Analyzer | initialize | 10 | ./lib/sandi_meter/analyzer.rb:10
48
+ SandiMeter::Analyzer | analyze | 13 | ./lib/sandi_meter/analyzer.rb:22
49
+
50
+ Missindented methods
51
+ Class name | Method name | Path
52
+ MyApp::TestClass | blah | ./spec/test_classes/1.rb:3
53
+
54
+ Method calls with 4+ arguments
55
+ # of arguments | Path
56
+ 5 | ./lib/sandi_meter/html_generator.rb:55
57
+ 5 | ./lib/sandi_meter/html_generator.rb:71
58
+
59
+ Controllers with 1+ instance variables
60
+ Controller name | Action name | Instance variables
61
+ AnotherUsersController | index | @users, @excess_variable
30
62
  ~~~
31
63
 
32
64
  ## HTML mode
33
65
 
34
66
  Try using gem with `-g (--graph)` option, so it will create a folder with beautiful html output and log file with results of any scan.
35
67
 
36
- ![SandiMeter HTML mode](http://img545.imageshack.us/img545/5601/t8qk.png)
68
+ ![SandiMeter HTML mode pie charts](http://imageshack.us/a/img823/7653/hns3.png)
69
+ ![SandiMeter HTML mode details](http://imageshack.us/a/img820/8711/bygo.png)
37
70
 
38
71
  ## Ruby script mode
39
72
 
@@ -47,11 +80,11 @@ pp data
47
80
  # {:first_rule=>
48
81
  # {:small_classes_amount=>916,
49
82
  # :total_classes_amount=>937,
50
- # :missindented_classes_amount=>1},
83
+ # :misindented_classes_amount=>1},
51
84
  # :second_rule=>
52
85
  # {:small_methods_amount=>1144,
53
86
  # :total_methods_amount=>1833,
54
- # :missindented_methods_amount=>0},
87
+ # :misindented_methods_amount=>0},
55
88
  # :third_rule=>{:proper_method_calls=>5857, :total_method_calls=>5894},
56
89
  # :fourth_rule=>{:proper_controllers_amount=>17, :total_controllers_amount=>94}}
57
90
  ~~~
@@ -0,0 +1,8 @@
1
+ <table class="details-stats">
2
+ <thead>
3
+ <% head %>
4
+ </thead>
5
+ <tbody>
6
+ <% rows %>
7
+ </tbody>
8
+ </table>
data/html/forkme.png ADDED
Binary file
data/html/index.html CHANGED
@@ -13,7 +13,7 @@
13
13
  </script>
14
14
  </head>
15
15
  <body>
16
- <a href="https://github.com/makaroni4/sandi_meter"><img style="position: absolute; top: 0; left: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_left_green_007200.png" alt="Fork me on GitHub"></a>
16
+ <a href="https://github.com/makaroni4/sandi_meter"><img style="position: absolute; top: 0; left: 0; border: 0;" src="assets/forkme.png" alt="Fork me on GitHub"></a>
17
17
  <div class="container">
18
18
  <header>
19
19
  <h1 class="main-title">Sandi Metz rules</h1>
@@ -26,7 +26,10 @@
26
26
  <div id="pie4" class="pie-charts-item"></div>
27
27
  <div class="clearfix"></div>
28
28
  </section>
29
- <section class="plot-charts">
29
+ <nav class="main-menu">
30
+ <a href="#" class="main-menu-active js-menu-item" data-rel=".js-charts">Charts</a><a href="#" class="js-menu-item" data-rel=".js-details">Details</a>
31
+ </nav>
32
+ <section class="plot-charts js-charts js-tab-item">
30
33
  <div class="plot-charts-item">
31
34
  <div class="plot-charts-item-caption">Number of 100 line classes vs time</div>
32
35
  <div class="plot-charts-item-graph" id="plot1"></div>
@@ -46,10 +49,13 @@
46
49
  </div>
47
50
  <div class="clearfix"></div>
48
51
  </section>
52
+ <section class="details js-details js-tab-item">
53
+ <% details %>
54
+ </section>
49
55
  <footer class="main-footer">
50
- Scanned with love by <a href="https://github.com/makaroni4/sandi_meter">sandi_meter</a> gem.
51
- Please, <a href="https://github.com/makaroni4/sandi_meter/issues/new">leave an issue</a> or
52
- <a href="email:makaroni4@gmail.com">email</a> some feedback!
56
+ Scanned with love by <a href="https://github.com/makaroni4/sandi_meter">sandi_meter</a> gem.
57
+ Please, <a href="https://github.com/makaroni4/sandi_meter/issues/new">leave an issue</a> or
58
+ <a href="mailto:makaroni4@gmail.com">email</a> some feedback!
53
59
  </footer>
54
60
  </div>
55
61
  </body>
data/html/script.js CHANGED
@@ -61,4 +61,15 @@ $(document).ready(function(){
61
61
  plotLine('plot2', data, ['r20', 'r21'], ['under 5 lines', 'more than 5 lines']);
62
62
  plotLine('plot3', data, ['r30', 'r31'], ['less than 4 params', 'more than 4 params']);
63
63
  plotLine('plot4', data, ['r40', 'r41'], ['one instance variable', 'many instance variables']);
64
+
65
+ var $tabs = $(".js-tab-item");
66
+ var $menuItems = $(".js-menu-item")
67
+ $menuItems.on("click", function(e){
68
+ var rel = $(this).data("rel");
69
+ $tabs.hide();
70
+ $menuItems.removeClass("main-menu-active");
71
+ $(rel).show();
72
+ $(this).addClass("main-menu-active");
73
+ e.preventDefault();
74
+ });
64
75
  })
data/html/style.css CHANGED
@@ -51,3 +51,78 @@ body {
51
51
  .plot-charts-item-graph {
52
52
  height: 280px;
53
53
  }
54
+
55
+ .toggle_report.current_tab {
56
+ text-decoration: none;
57
+ cursor: default;
58
+ pointer-events: none;
59
+ }
60
+
61
+ .details {
62
+ display: none;
63
+ }
64
+
65
+ .details h2 {
66
+ margin: 0 0 14px;
67
+ }
68
+
69
+ .details-stats {
70
+ font-family: monospace;
71
+ margin-bottom: 40px;
72
+ }
73
+
74
+ .details-stats td {
75
+ padding-right: 20px;
76
+ line-height: 18px;
77
+ }
78
+
79
+ .details-stats td.warning {
80
+ color: red;
81
+ }
82
+
83
+ .main-menu {
84
+ margin-bottom: 30px;
85
+ }
86
+
87
+ .main-menu {
88
+ text-align: center;
89
+ }
90
+
91
+ .main-menu a {
92
+ display: inline-block;
93
+ padding: 4px 16px;
94
+ font-size: 18px;
95
+ text-decoration: none;
96
+ color: black;
97
+ border: 1px solid rgb(158, 158, 158);
98
+ }
99
+
100
+ .main-menu a:hover {
101
+ color: rgb(65, 131, 196);;
102
+ }
103
+
104
+ .main-menu a:first-child {
105
+ border-right: none;
106
+ border-top-left-radius: 4px;
107
+ border-bottom-left-radius: 4px;
108
+ }
109
+
110
+ .main-menu a:last-child {
111
+ border-top-right-radius: 4px;
112
+ border-bottom-right-radius: 4px;
113
+ }
114
+
115
+ .main-menu-active {
116
+ background: rgb(240, 240, 240);
117
+ }
118
+
119
+ .main-menu .main-menu-active:hover {
120
+ color: black;
121
+ }
122
+
123
+ .hint {
124
+ margin: -40px 0 40px 0;
125
+ font-family: monospace;
126
+ font-style: italic;
127
+ font-weight: normal;
128
+ }
@@ -5,15 +5,18 @@ require_relative 'method_arguments_counter'
5
5
 
6
6
  module SandiMeter
7
7
  class Analyzer
8
- attr_reader :classes, :missindented_classes, :methods, :missindented_methods, :method_calls, :instance_variables
8
+ attr_reader :classes, :misindented_classes, :methods, :misindented_methods, :method_calls, :instance_variables
9
9
 
10
10
  def initialize
11
11
  @classes = []
12
- @missindented_classes = []
13
- @missindented_methods = {}
12
+ @misindented_classes = []
13
+ @misindented_methods = {}
14
14
  @methods = {}
15
15
  @method_calls = []
16
16
  @instance_variables = {}
17
+
18
+ @parent_token = nil
19
+ @private_or_protected = false
17
20
  end
18
21
 
19
22
  def analyze(file_path)
@@ -37,19 +40,31 @@ module SandiMeter
37
40
 
38
41
  @classes.map! do |klass_params|
39
42
  klass_params << loc_checker.check(klass_params, 'class')
43
+ klass_params << "#{@file_path}:#{klass_params[1]}"
44
+ end
45
+
46
+ @misindented_classes.map! do |klass_params|
47
+ klass_params << "#{@file_path}:#{klass_params[1]}"
40
48
  end
41
49
 
42
50
  @methods.each_pair do |klass, methods|
43
51
  methods.each do |method_params|
44
52
  method_params << loc_checker.check(method_params, 'def')
53
+ method_params << "#{@file_path}:#{method_params[1]}"
54
+ end
55
+ end
56
+
57
+ @misindented_methods.each_pair do |klass, methods|
58
+ methods.each do |method_params|
59
+ method_params << "#{@file_path}:#{method_params[1]}"
45
60
  end
46
61
  end
47
62
 
48
63
  {
49
64
  classes: @classes,
50
- missindented_classes: @missindented_classes,
65
+ misindented_classes: @misindented_classes,
51
66
  methods: @methods,
52
- missindented_methods: @missindented_methods,
67
+ misindented_methods: @misindented_methods,
53
68
  method_calls: @method_calls,
54
69
  instance_variables: @instance_variables
55
70
  }
@@ -104,12 +119,12 @@ module SandiMeter
104
119
 
105
120
  if @indentation_warnings['class'] && @indentation_warnings['class'].any? { |first_line, last_line| first_line == class_params.last }
106
121
  class_params << nil
107
- @missindented_classes << class_params
122
+ @misindented_classes << class_params
108
123
  else
109
124
  class_params += [find_last_line(class_params)]
110
125
 
111
126
  # in case of one liner class last line will be nil
112
- (class_params.last == nil ? @missindented_classes : @classes) << class_params
127
+ (class_params.last == nil ? @misindented_classes : @classes) << class_params
113
128
  end
114
129
 
115
130
  current_namespace = class_params.first
@@ -127,7 +142,7 @@ module SandiMeter
127
142
  counter = SandiMeter::MethodArgumentsCounter.new
128
143
  arguments_count, line = counter.count(sexp)
129
144
 
130
- @method_calls << [arguments_count, line]
145
+ @method_calls << [arguments_count, "#{@file_path}:#{line}"]
131
146
 
132
147
  find_args_add_block(sexp)
133
148
  else
@@ -145,7 +160,7 @@ module SandiMeter
145
160
  if sexp.first == :assign
146
161
  @instance_variables[current_namespace] ||= {}
147
162
  @instance_variables[current_namespace][method_name] ||= []
148
- @instance_variables[current_namespace][method_name] << sexp[1][1][1]
163
+ @instance_variables[current_namespace][method_name] << sexp[1][1][1] if sexp[1][1][0] == :@ivar
149
164
  else
150
165
  scan_def_for_ivars(current_namespace, method_name, sexp)
151
166
  end
@@ -156,24 +171,41 @@ module SandiMeter
156
171
  sexp.each do |element|
157
172
  next unless element.kind_of?(Array)
158
173
 
174
+ @parent_token = element.first
159
175
  case element.first
160
176
  when :def
161
177
  method_params = find_method_params(element)
162
178
  if @indentation_warnings['def'] && @indentation_warnings['def'].any? { |first_line, last_line| first_line == method_params.last }
163
179
  method_params << nil
164
180
  method_params << number_of_arguments(element)
165
- @missindented_methods[current_namespace] ||= []
166
- @missindented_methods[current_namespace] << method_params
181
+ @misindented_methods[current_namespace] ||= []
182
+ @misindented_methods[current_namespace] << method_params
167
183
  else
168
184
  method_params += [find_last_line(method_params, 'def')]
169
185
  method_params << number_of_arguments(element)
170
186
  @methods[current_namespace] ||= []
171
187
  @methods[current_namespace] << method_params
172
188
  end
173
- scan_def_for_ivars(current_namespace, method_params.first, element) if @scan_instance_variables
189
+ if @scan_instance_variables && !@private_or_protected
190
+ scan_def_for_ivars(current_namespace, method_params.first, element)
191
+ end
192
+
174
193
  find_args_add_block(element)
175
194
  when :module, :class
176
195
  scan_class_sexp(element, current_namespace)
196
+ when :vcall
197
+ if element[1].first == :@ident
198
+ case element[1][1]
199
+ when "private"
200
+ @private_or_protected = true
201
+ when "public"
202
+ @private_or_protected = false
203
+ else
204
+ scan_sexp(element, current_namespace)
205
+ end
206
+ else
207
+ scan_sexp(element, current_namespace)
208
+ end
177
209
  else
178
210
  scan_sexp(element, current_namespace)
179
211
  end
@@ -17,7 +17,9 @@ module SandiMeter
17
17
  end
18
18
  end
19
19
 
20
- def calculate!
20
+ def calculate!(store_details = false)
21
+ @store_details = store_details
22
+
21
23
  check_first_rule
22
24
  check_second_rule
23
25
  check_third_rule
@@ -27,18 +29,90 @@ module SandiMeter
27
29
  end
28
30
 
29
31
  private
32
+ def log_first_rule
33
+ @output[:first_rule][:log] ||= {}
34
+ @output[:first_rule][:log][:classes] = @data[:classes].inject([]) do |log, class_params|
35
+ # TODO
36
+ # wrap each class params into class and get params with
37
+ # verbose name instead of array keys (class_params[2] should be klass.line_count)
38
+ log << [class_params.first, class_params[2], class_params.last] if class_params[-2] == false
39
+ log
40
+ end
41
+
42
+ @output[:first_rule][:log][:misindented_classes] = @data[:misindented_classes].inject([]) do |log, class_params|
43
+ log << [class_params.first, nil, class_params.last]
44
+ log
45
+ end
46
+ end
47
+
48
+ def log_second_rule
49
+ @output[:second_rule][:log] ||= {}
50
+ @output[:second_rule][:log][:methods] ||= []
51
+ @output[:second_rule][:log][:misindented_methods] ||= []
52
+
53
+ @data[:methods].each_pair do |klass, methods|
54
+ methods.select { |m| m[-2] == false }.each do |method_params|
55
+ params = [klass, method_params.first]
56
+ # TODO
57
+ # wrap method param to method class so method_params[1] becomes method.first_line
58
+ # and method_params[2] method.last_line
59
+ params << method_params[2] - method_params[1]
60
+ params << method_params.last
61
+
62
+ @output[:second_rule][:log][:methods] << params
63
+ end
64
+ end
65
+
66
+ @data[:misindented_methods].each_pair do |klass, methods|
67
+ methods.each do |method_params|
68
+ params = [klass, method_params.first]
69
+ params << nil
70
+ params << method_params.last
71
+
72
+ @output[:second_rule][:log][:misindented_methods] << params
73
+ end
74
+ end
75
+ end
76
+
77
+ def log_third_rule
78
+ @output[:third_rule][:log] ||={}
79
+ @output[:third_rule][:log][:method_calls] ||= []
80
+
81
+ # TODO
82
+ # add name of method being called
83
+ proper_method_calls = @data[:method_calls].inject(0) do |sum, params|
84
+ @output[:third_rule][:log][:method_calls] << params if params.first > 4
85
+ end
86
+ end
87
+
88
+ def log_fourth_rule
89
+ @output[:fourth_rule][:log] ||={}
90
+ @output[:fourth_rule][:log][:controllers] ||= []
91
+
92
+ @data[:instance_variables].each_pair do |controller, methods|
93
+ methods.each_pair do |method, instance_variables|
94
+ if instance_variables.size > 1
95
+ @output[:fourth_rule][:log][:controllers] << [controller, method, instance_variables]
96
+ end
97
+ end
98
+ end
99
+ end
100
+
30
101
  def check_first_rule
31
102
  total_classes_amount = @data[:classes].size
32
103
  small_classes_amount = @data[:classes].inject(0) do |sum, class_params|
33
- sum += 1 if class_params.last == true
104
+ sum += 1 if class_params[-2] == true
34
105
  sum
35
106
  end
36
- missindented_classes_amount = @data[:missindented_classes].size
107
+
108
+ misindented_classes_amount = @data[:misindented_classes].size
37
109
 
38
110
  @output[:first_rule] ||= {}
39
111
  @output[:first_rule][:small_classes_amount] = small_classes_amount
40
112
  @output[:first_rule][:total_classes_amount] = total_classes_amount
41
- @output[:first_rule][:missindented_classes_amount] = missindented_classes_amount
113
+ @output[:first_rule][:misindented_classes_amount] = misindented_classes_amount
114
+
115
+ log_first_rule if @store_details
42
116
  end
43
117
 
44
118
  def check_second_rule
@@ -46,19 +120,21 @@ module SandiMeter
46
120
  small_methods_amount = 0
47
121
 
48
122
  @data[:methods].each_pair do |klass, methods|
49
- small_methods_amount += methods.select { |m| m.last == true }.size
123
+ small_methods_amount += methods.select { |m| m[-2] == true }.size
50
124
  total_methods_amount += methods.size
51
125
  end
52
126
 
53
- missindented_methods_amount = 0
54
- @data[:missindented_methods].each_pair do |klass, methods|
55
- missindented_methods_amount += methods.size
127
+ misindented_methods_amount = 0
128
+ @data[:misindented_methods].each_pair do |klass, methods|
129
+ misindented_methods_amount += methods.size
56
130
  end
57
131
 
58
132
  @output[:second_rule] ||= {}
59
133
  @output[:second_rule][:small_methods_amount] = small_methods_amount
60
134
  @output[:second_rule][:total_methods_amount] = total_methods_amount
61
- @output[:second_rule][:missindented_methods_amount] = missindented_methods_amount
135
+ @output[:second_rule][:misindented_methods_amount] = misindented_methods_amount
136
+
137
+ log_second_rule if @store_details
62
138
  end
63
139
 
64
140
  # TODO
@@ -74,6 +150,8 @@ module SandiMeter
74
150
  @output[:third_rule] ||= {}
75
151
  @output[:third_rule][:proper_method_calls] = proper_method_calls
76
152
  @output[:third_rule][:total_method_calls] = total_method_calls
153
+
154
+ log_third_rule if @store_details
77
155
  end
78
156
 
79
157
  def check_fourth_rule
@@ -88,6 +166,8 @@ module SandiMeter
88
166
  @output[:fourth_rule] ||= {}
89
167
  @output[:fourth_rule][:proper_controllers_amount] = proper_controllers_amount
90
168
  @output[:fourth_rule][:total_controllers_amount] = total_controllers_amount
169
+
170
+ log_fourth_rule if @store_details
91
171
  end
92
172
  end
93
173
  end
@@ -20,10 +20,16 @@ module SandiMeter
20
20
  description: "Show syntax error and indentation log output",
21
21
  boolean: true
22
22
 
23
+ option :details,
24
+ short: "-d",
25
+ long: "--details",
26
+ description: "CLI mode. Show details (path, line number)",
27
+ boolean: true
28
+
23
29
  option :graph,
24
30
  short: "-g",
25
31
  long: "--graph",
26
- description: "Create folder and log data to graph",
32
+ description: "HTML mode. Create folder, log data and output stats to HTML file.",
27
33
  boolean: true
28
34
 
29
35
  option :help,
@@ -53,18 +59,26 @@ module SandiMeter
53
59
  end
54
60
 
55
61
  scanner = SandiMeter::FileScanner.new(cli.config[:log])
56
- data = scanner.scan(cli.config[:path])
62
+ data = scanner.scan(cli.config[:path], cli.config[:details] || cli.config[:graph])
63
+
57
64
  formatter = SandiMeter::Formatter.new
58
65
 
59
66
  formatter.print_data(data)
60
67
 
61
68
  if cli.config[:graph]
62
- logger = SandiMeter::Logger.new
63
- logger.log!(cli.config[:path], data)
69
+ if File.directory?(cli.config[:path])
70
+ logger = SandiMeter::Logger.new
71
+ logger.log!(cli.config[:path], data)
72
+
73
+ html_generator = SandiMeter::HtmlGenerator.new
74
+ html_generator.copy_assets!(cli.config[:path])
75
+ html_generator.generate_data!(cli.config[:path])
76
+ html_generator.generate_details!(cli.config[:path], data)
64
77
 
65
- html_generator = SandiMeter::HtmlGenerator.new
66
- html_generator.copy_assets!(cli.config[:path])
67
- html_generator.generate_data!(cli.config[:path])
78
+ system "open sandi_meter/index.html"
79
+ else
80
+ puts "WARNING!!! HTML mode works only if you scan folder."
81
+ end
68
82
  end
69
83
  end
70
84
 
@@ -77,4 +91,4 @@ module SandiMeter
77
91
  )
78
92
  end
79
93
  end
80
- end
94
+ end
@@ -8,14 +8,14 @@ module SandiMeter
8
8
  @calculator = SandiMeter::Calculator.new
9
9
  end
10
10
 
11
- def scan(path)
11
+ def scan(path, store_details = false)
12
12
  if File.directory?(path)
13
13
  scan_dir(path)
14
14
  else
15
15
  scan_file(path)
16
16
  end
17
17
 
18
- @calculator.calculate!
18
+ @calculator.calculate!(store_details)
19
19
  end
20
20
 
21
21
  private
@@ -32,6 +32,8 @@ module SandiMeter
32
32
  @calculator.push(data)
33
33
  rescue Exception => e
34
34
  if @log_errors
35
+ # TODO
36
+ # add backtrace
35
37
  puts "Checkout #{path} for:"
36
38
  puts "\t#{e.message}"
37
39
  end
@@ -4,25 +4,81 @@ module SandiMeter
4
4
  if data[:first_rule][:total_classes_amount] > 0
5
5
  puts "1. #{data[:first_rule][:small_classes_amount] * 100 / data[:first_rule][:total_classes_amount]}% of classes are under 100 lines."
6
6
  else
7
- puts "1. No classes to analize."
7
+ puts "1. No classes to analyze."
8
8
  end
9
9
 
10
10
  if data[:second_rule][:total_methods_amount] > 0
11
11
  puts "2. #{data[:second_rule][:small_methods_amount] * 100 / data[:second_rule][:total_methods_amount]}% of methods are under 5 lines."
12
12
  else
13
- puts "2. No methods to analize."
13
+ puts "2. No methods to analyze."
14
14
  end
15
15
 
16
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."
17
+ puts "3. #{data[:third_rule][:proper_method_calls] * 100 / data[:third_rule][:total_method_calls]}% of method calls accepted are less than 4 parameters."
18
18
  else
19
- puts "3. No method calls to analize."
19
+ puts "3. No method calls to analyze."
20
20
  end
21
21
 
22
22
  if data[:fourth_rule][:total_controllers_amount] > 0
23
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
24
  else
25
- puts "4. No controllers to analize."
25
+ puts "4. No controllers to analyze."
26
+ end
27
+
28
+ print_log(data)
29
+ end
30
+
31
+ def print_log(data)
32
+ return unless data[:first_rule][:log] || data[:second_rule][:log] || data[:fourth_rule][:log]
33
+
34
+ if data[:first_rule][:log][:classes].any?
35
+ puts "\nClasses with 100+ lines"
36
+ print_array_of_arrays [["Class name", "Size", "Path"]] + data[:first_rule][:log][:classes]
37
+ end
38
+
39
+ if data[:first_rule][:log][:misindented_classes].any?
40
+ puts "\nMissindented classes"
41
+ print_array_of_arrays [["Class name", "Path"]] + data[:first_rule][:log][:misindented_classes].map { |row| row.delete_at(1); row } # 1 – size, which nil for misindented_classes
42
+ end
43
+
44
+ if data[:second_rule][:log][:methods].any?
45
+ puts "\nMethods with 5+ lines"
46
+ print_array_of_arrays [["Class name", "Method name", "Size", "Path"]] + data[:second_rule][:log][:methods]
47
+ end
48
+
49
+ if data[:second_rule][:log][:misindented_methods].any?
50
+ puts "\nMissindented methods"
51
+ print_array_of_arrays [["Class name", "Method name", "Path"]] + data[:second_rule][:log][:misindented_methods].map { |row| row.delete_at(2); row } # 2 – size, which nil for misindented_methods
52
+ end
53
+
54
+ if data[:third_rule][:log][:method_calls].any?
55
+ puts "\nMethod calls with 4+ arguments"
56
+ print_array_of_arrays [["# of arguments", "Path"]] + data[:third_rule][:log][:method_calls]
57
+ end
58
+
59
+ if data[:fourth_rule][:log][:controllers].any?
60
+ puts "\nControllers with 1+ instance variables"
61
+ print_array_of_arrays [["Controller name", "Action name", "Instance variables"]] + data[:fourth_rule][:log][:controllers]
62
+ end
63
+ end
64
+
65
+ private
66
+ # TODO
67
+ # sort output by number of lines or any param
68
+ def print_array_of_arrays(nested_array)
69
+ nested_sizes = nested_array.map do |row|
70
+ row.map { |element| element.to_s.size }
71
+ end
72
+
73
+ sizes = nested_sizes.transpose.map { |row| row.max }
74
+
75
+ nested_array.each do |row|
76
+ line_elements = row.each_with_index.map do |element, index|
77
+ element_string = element.kind_of?(Array) ? element.join(', ') : element.to_s
78
+ element_string.ljust(sizes[index] + 1, ' ')
79
+ end
80
+
81
+ puts line_elements.join(' | ').prepend(" ")
26
82
  end
27
83
  end
28
84
  end
@@ -8,7 +8,7 @@ module SandiMeter
8
8
  FileUtils.mkdir(asset_dir_path) unless Dir.exists?(asset_dir_path)
9
9
 
10
10
 
11
- Dir[File.join(File.dirname(__FILE__), "../../html/*.{js,css}")].each do |file|
11
+ Dir[File.join(File.dirname(__FILE__), "../../html/*.{js,css,png}")].each do |file|
12
12
  FileUtils.cp file, File.join(asset_dir_path, File.basename(file))
13
13
  end
14
14
 
@@ -39,5 +39,98 @@ module SandiMeter
39
39
  file.write(index)
40
40
  end
41
41
  end
42
+
43
+ def generate_details!(path, data)
44
+ details = ""
45
+
46
+ if data[:first_rule][:log][:classes].any?
47
+ data[:first_rule][:log][:misindented_classes] ||= []
48
+ data[:first_rule][:log][:misindented_classes].each do |class_params|
49
+ class_params.insert(1, nil)
50
+ end
51
+
52
+ details << string_to_h2("Classes with 100+ lines")
53
+ details << generate_details_block(
54
+ ["Class name", "Size", "Path"],
55
+ proper_data: data[:first_rule][:log][:classes],
56
+ warning_data: data[:first_rule][:log][:misindented_classes],
57
+ hint: "NOTE: Red classes are misindented. Start improving your project by fixing them.",
58
+ warning_message: 'Misindented classes'
59
+ )
60
+ end
61
+
62
+ if data[:second_rule][:log][:methods].any?
63
+ data[:second_rule][:log][:misindented_methods] ||= []
64
+ data[:second_rule][:log][:misindented_methods].each do |method_params|
65
+ method_params.insert(2, nil)
66
+ end
67
+
68
+ details << string_to_h2("Methods with 5+ lines")
69
+ details << generate_details_block(
70
+ ["Class name", "Method name", "Size", "Path"],
71
+ proper_data: data[:second_rule][:log][:methods].sort_by { |a| -a[2].to_i },
72
+ warning_data: data[:second_rule][:log][:misindented_methods].sort_by { |a| -a[1].to_i },
73
+ hint: "NOTE: Red methods are misindented. Continue your way to perfect code by fixing them.",
74
+ warning_message: 'Misindented methods'
75
+ )
76
+ end
77
+
78
+ if data[:third_rule][:log][:method_calls].any?
79
+ details << string_to_h2("Method calls with 4+ arguments")
80
+ details << generate_details_block(
81
+ ["# of arguments", "Path"],
82
+ proper_data: data[:third_rule][:log][:method_calls]
83
+ )
84
+ end
85
+
86
+ if data[:fourth_rule][:log][:controllers].any?
87
+ details << string_to_h2("Controllers with 1+ instance variables")
88
+ details << generate_details_block(
89
+ ["Controller name", "Action name", "Instance variables"],
90
+ proper_data: data[:fourth_rule][:log][:controllers]
91
+ )
92
+ end
93
+
94
+ index_file = File.join(path, 'sandi_meter', 'index.html')
95
+ index = File.read(index_file)
96
+ index.gsub!('<% details %>', details)
97
+
98
+ File.open(index_file, 'w') do |file|
99
+ file.write(index)
100
+ end
101
+ end
102
+
103
+ private
104
+ def generate_details_block(head_row, data)
105
+ block_partial = File.read File.join(File.dirname(__FILE__), "../../html", "_detail_block.html")
106
+ block_partial.gsub!('<% head %>', array_to_tr(head_row, cell: "th"))
107
+
108
+ table_rows = data[:proper_data].map { |row| array_to_tr(row) }.join('')
109
+
110
+ if data[:warning_data]
111
+ table_rows << data[:warning_data].map { |row| array_to_tr(row, css_class: 'warning', tip: data[:warning_message]) }.join('')
112
+ end
113
+
114
+ block_partial.gsub!('<% rows %>', table_rows)
115
+ block_partial << hint(data[:hint]) if data[:hint]
116
+ block_partial
117
+ end
118
+
119
+ def hint(string)
120
+ %(<div class="hint">#{string}</div>)
121
+ end
122
+
123
+ def string_to_h2(string)
124
+ "<h2>#{string}</h2>\n"
125
+ end
126
+
127
+ def cell_to_s(element)
128
+ element.kind_of?(Array) ? element.join(', ') : element.to_s
129
+ end
130
+
131
+ def array_to_tr(array, params = {})
132
+ cell = params[:cell] || "td"
133
+ array.map { |element| "<#{cell} class=\"#{params[:css_class]}\" title=\"#{params[:tip]}\">#{cell_to_s(element)}</#{cell}>\n" }.join('').prepend("<tr>\n").concat("</tr>\n")
134
+ end
42
135
  end
43
136
  end
@@ -1,3 +1,3 @@
1
1
  module SandiMeter
2
- VERSION = "0.0.6"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -12,21 +12,21 @@ describe SandiMeter::Analyzer do
12
12
  end
13
13
 
14
14
  it 'finds indentation warnings for method' do
15
- analyzer.classes.should eq([["TestClass", 1, 5, true]])
16
- analyzer.missindented_classes.should be_empty
15
+ analyzer.classes.should eq([["TestClass", 1, 5, true, "#{test_file_path(3)}:1"]])
16
+ analyzer.misindented_classes.should be_empty
17
17
  end
18
18
 
19
19
  it 'finds methods' do
20
- analyzer.methods.should eq({"TestClass"=>[["blah", 2, 4, 0, true]]})
21
- analyzer.missindented_methods.should be_empty
20
+ analyzer.methods.should eq({"TestClass"=>[["blah", 2, 4, 0, true, "#{test_file_path(3)}:2"]]})
21
+ analyzer.misindented_methods.should be_empty
22
22
  end
23
23
 
24
24
  it 'finds method calls that brakes third rule' do
25
- analyzer.method_calls.should eq([[5,3]])
25
+ analyzer.method_calls.should eq([[5, "#{test_file_path(3)}:3"]])
26
26
  end
27
27
  end
28
28
 
29
- describe 'finds missindented classes without last line' do
29
+ describe 'finds misindented classes without last line' do
30
30
  let(:test_class) { test_file_path(1) }
31
31
 
32
32
  before do
@@ -35,12 +35,12 @@ describe SandiMeter::Analyzer do
35
35
 
36
36
  it 'finds indentation warnings for method' do
37
37
  analyzer.classes.should be_empty
38
- analyzer.missindented_classes.should eq([["MyApp::TestClass", 2, nil]])
38
+ analyzer.misindented_classes.should eq([["MyApp::TestClass", 2, nil, "#{test_file_path(1)}:2"]])
39
39
  end
40
40
 
41
41
  it 'finds methods' do
42
42
  analyzer.methods.should be_empty
43
- analyzer.missindented_methods.should eq({"MyApp::TestClass"=>[["blah", 3, nil, 0]]})
43
+ analyzer.misindented_methods.should eq({"MyApp::TestClass"=>[["blah", 3, nil, 0, "#{test_file_path(1)}:3"]]})
44
44
  end
45
45
  end
46
46
 
@@ -52,15 +52,15 @@ describe SandiMeter::Analyzer do
52
52
  end
53
53
 
54
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
55
+ analyzer.classes.should include(["FirstTestClass", 1, 4, true, "#{test_file_path(4)}:1"])
56
+ analyzer.classes.should include(["SecondTestClass", 6, 9, true, "#{test_file_path(4)}:6"])
57
+ analyzer.misindented_classes.should be_empty
58
58
  end
59
59
 
60
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
61
+ analyzer.methods["FirstTestClass"].should eq([["first_meth", 2, 3, 1, true, "#{test_file_path(4)}:2"]])
62
+ analyzer.methods["SecondTestClass"].should eq([["second_meth", 7, 8, 1, true, "#{test_file_path(4)}:7"]])
63
+ analyzer.misindented_methods.should be_empty
64
64
  end
65
65
  end
66
66
 
@@ -72,13 +72,13 @@ describe SandiMeter::Analyzer do
72
72
  end
73
73
 
74
74
  it 'finds classes' do
75
- analyzer.missindented_classes.should eq([["OneLinerClass", 1, nil]])
75
+ analyzer.misindented_classes.should eq([["OneLinerClass", 1, nil, "#{test_file_path(5)}:1"]])
76
76
  analyzer.classes.should be_empty
77
77
  end
78
78
 
79
79
  it 'finds methods' do
80
80
  analyzer.methods.should be_empty
81
- analyzer.missindented_methods.should be_empty
81
+ analyzer.misindented_methods.should be_empty
82
82
  end
83
83
  end
84
84
 
@@ -90,16 +90,16 @@ describe SandiMeter::Analyzer do
90
90
  end
91
91
 
92
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
93
+ analyzer.classes.should include(["MyApp::Blah::User", 5, 13, true, "#{test_file_path(7)}:5"])
94
+ analyzer.classes.should include(["MyApp::Blah::User::SubUser", 9, 12, true, "#{test_file_path(7)}:9"])
95
+ analyzer.misindented_classes.should be_empty
96
96
  end
97
97
 
98
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
99
+ analyzer.methods["MyApp::Blah"].should eq([["module_meth", 2, 3, 0, true, "#{test_file_path(7)}:2"]])
100
+ analyzer.methods["MyApp::Blah::User"].should eq([["class_meth", 6, 7, 0, true, "#{test_file_path(7)}:6"]])
101
+ analyzer.methods["MyApp::Blah::User::SubUser"].should eq([["sub_meth", 10, 11, 0, true, "#{test_file_path(7)}:10"]])
102
+ analyzer.misindented_methods.should be_empty
103
103
  end
104
104
  end
105
105
 
@@ -111,15 +111,15 @@ describe SandiMeter::Analyzer do
111
111
  end
112
112
 
113
113
  it 'finds class and subclass' do
114
- analyzer.classes.should include(["RailsController", 1, 12, true])
115
- analyzer.missindented_classes.should be_empty
114
+ analyzer.classes.should include(["RailsController", 1, 12, true, "#{test_file_path(8)}:1"])
115
+ analyzer.misindented_classes.should be_empty
116
116
  end
117
117
 
118
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
119
+ analyzer.methods["RailsController"].should include(["index", 2, 3, 0, true, "#{test_file_path(8)}:2"])
120
+ analyzer.methods["RailsController"].should include(["destroy", 5, 6, 0, true, "#{test_file_path(8)}:5"])
121
+ analyzer.methods["RailsController"].should include(["private_meth", 9, 10, 0, true, "#{test_file_path(8)}:9"])
122
+ analyzer.misindented_methods.should be_empty
123
123
  end
124
124
  end
125
125
 
@@ -136,6 +136,18 @@ describe SandiMeter::Analyzer do
136
136
  end
137
137
  end
138
138
 
139
+ context 'in controller class with non instance variables' do
140
+ let(:test_class) { test_file_path('14_controller') }
141
+
142
+ before do
143
+ analyzer.analyze(test_class)
144
+ end
145
+
146
+ it 'does not find instance variables' do
147
+ analyzer.instance_variables.should eq({"GuestController"=>{"create_guest_user"=>[]}})
148
+ end
149
+ end
150
+
139
151
  context 'not in controller class' do
140
152
  let(:test_class) { test_file_path(10) }
141
153
 
@@ -147,6 +159,32 @@ describe SandiMeter::Analyzer do
147
159
  analyzer.instance_variables.should be_empty
148
160
  end
149
161
  end
162
+
163
+ context 'in controller class with private method' do
164
+ let(:test_class) { test_file_path("15_controller") }
165
+
166
+ before do
167
+ analyzer.analyze(test_class)
168
+ end
169
+
170
+ it 'finds method defined after public keyword' do
171
+ analyzer.instance_variables["UsersController"].should have_key("create")
172
+ end
173
+
174
+ it 'omits actions without instance variables' do
175
+ analyzer.instance_variables["UsersController"].should_not have_key("show")
176
+ end
177
+
178
+ it 'omits private methods' do
179
+ analyzer.instance_variables["UsersController"].should_not have_key("find_user")
180
+ end
181
+
182
+ it 'omits protected methods' do
183
+ analyzer.instance_variables["UsersController"].should_not have_key("protected_find_user")
184
+ end
185
+
186
+
187
+ end
150
188
  end
151
189
 
152
190
  describe 'hash method arguments' do
@@ -157,7 +195,7 @@ describe SandiMeter::Analyzer do
157
195
  end
158
196
 
159
197
  it 'counts arguments' do
160
- analyzer.method_calls.should eq([[5, 3]])
198
+ analyzer.method_calls.should eq([[5, "#{test_file_path(11)}:3"]])
161
199
  end
162
200
  end
163
201
 
@@ -169,11 +207,11 @@ describe SandiMeter::Analyzer do
169
207
  end
170
208
 
171
209
  it 'are count for class definition' do
172
- analyzer.classes.should eq([["Valera", 1, 109, false]])
210
+ analyzer.classes.should eq([["Valera", 1, 109, false, "#{test_file_path(12)}:1"]])
173
211
  end
174
212
 
175
213
  it 'are count for method definition' do
176
- analyzer.methods.should eq({"Valera"=>[["doodle", 2, 9, 0, false]]})
214
+ analyzer.methods.should eq({"Valera"=>[["doodle", 2, 9, 0, false, "#{test_file_path(12)}:2"]]})
177
215
  end
178
216
  end
179
217
 
@@ -0,0 +1,6 @@
1
+ class AnotherUsersController
2
+ def index
3
+ @users = User.page(params[:page])
4
+ @excess_variable = 'blah blah'
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ class GuestController
2
+ def create_guest_user
3
+ u = User.new_guest
4
+ u.save
5
+ session[:guest_user_id] = u.id
6
+ u
7
+ end
8
+ end
@@ -0,0 +1,21 @@
1
+ class UsersController
2
+ before_filter :find_user
3
+
4
+ def show
5
+ end
6
+
7
+ private
8
+ def find_user
9
+ @user = User.find(params[:id])
10
+ end
11
+
12
+ protected
13
+ def protected_find_user
14
+ @user = User.find(params[:id])
15
+ end
16
+
17
+ public
18
+ def create
19
+ @user = User.new(params[:user])
20
+ end
21
+ end
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.6
4
+ version: 1.0.0
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-26 00:00:00.000000000 Z
11
+ date: 2013-10-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -64,7 +64,9 @@ files:
64
64
  - README.md
65
65
  - Rakefile
66
66
  - sandi_meter.gemspec
67
+ - html/_detail_block.html
67
68
  - html/chart.js
69
+ - html/forkme.png
68
70
  - html/index.html
69
71
  - html/jquery.js
70
72
  - html/morris.css
@@ -88,9 +90,12 @@ files:
88
90
  - spec/method_arguments_counter_spec.rb
89
91
  - spec/test_classes/1.rb
90
92
  - spec/test_classes/10.rb
93
+ - spec/test_classes/10_controller.rb
91
94
  - spec/test_classes/11.rb
92
95
  - spec/test_classes/12.rb
93
96
  - spec/test_classes/13.rb
97
+ - spec/test_classes/14_controller.rb
98
+ - spec/test_classes/15_controller.rb
94
99
  - spec/test_classes/2.rb
95
100
  - spec/test_classes/3.rb
96
101
  - spec/test_classes/4.rb