rails_best_practices 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile.lock +1 -1
  3. data/README.md +2 -0
  4. data/assets/result.html.erb +25 -2
  5. data/lib/rails_best_practices.rb +20 -9
  6. data/lib/rails_best_practices/core.rb +1 -0
  7. data/lib/rails_best_practices/core/check.rb +106 -25
  8. data/lib/rails_best_practices/core/controllers.rb +2 -1
  9. data/lib/rails_best_practices/core/error.rb +3 -2
  10. data/lib/rails_best_practices/core/klasses.rb +34 -0
  11. data/lib/rails_best_practices/core/mailers.rb +2 -1
  12. data/lib/rails_best_practices/core/methods.rb +113 -9
  13. data/lib/rails_best_practices/core/model_associations.rb +17 -0
  14. data/lib/rails_best_practices/core/model_attributes.rb +16 -0
  15. data/lib/rails_best_practices/core/models.rb +3 -2
  16. data/lib/rails_best_practices/core/nil.rb +9 -1
  17. data/lib/rails_best_practices/core/runner.rb +65 -26
  18. data/lib/rails_best_practices/core_ext/sexp.rb +57 -0
  19. data/lib/rails_best_practices/prepares.rb +12 -1
  20. data/lib/rails_best_practices/prepares/controller_prepare.rb +13 -8
  21. data/lib/rails_best_practices/prepares/mailer_prepare.rb +3 -3
  22. data/lib/rails_best_practices/prepares/model_prepare.rb +44 -16
  23. data/lib/rails_best_practices/reviews.rb +1 -0
  24. data/lib/rails_best_practices/reviews/needless_deep_nesting_review.rb +5 -2
  25. data/lib/rails_best_practices/reviews/remove_unused_methods_in_models_review.rb +77 -0
  26. data/lib/rails_best_practices/reviews/restrict_auto_generated_routes_review.rb +2 -2
  27. data/lib/rails_best_practices/reviews/review.rb +1 -1
  28. data/lib/rails_best_practices/version.rb +1 -1
  29. data/rails_best_practices.yml +1 -0
  30. data/spec/fixtures/lib/rails_best_practices/plugins/reviews/not_use_rails_root_review.rb +11 -0
  31. data/spec/rails_best_practices/core/check_spec.rb +22 -0
  32. data/spec/rails_best_practices/core/controllers_spec.rb +1 -1
  33. data/spec/rails_best_practices/core/error_spec.rb +1 -1
  34. data/spec/rails_best_practices/core/klasses_spec.rb +12 -0
  35. data/spec/rails_best_practices/core/mailers_spec.rb +5 -0
  36. data/spec/rails_best_practices/core/methods_spec.rb +26 -4
  37. data/spec/rails_best_practices/core/models_spec.rb +2 -2
  38. data/spec/rails_best_practices/core/runner_spec.rb +13 -0
  39. data/spec/rails_best_practices/core_ext/sexp_spec.rb +26 -2
  40. data/spec/rails_best_practices/prepares/controller_prepare_spec.rb +72 -60
  41. data/spec/rails_best_practices/prepares/mailer_prepare_spec.rb +1 -1
  42. data/spec/rails_best_practices/prepares/model_prepare_spec.rb +150 -59
  43. data/spec/rails_best_practices/reviews/move_model_logic_into_model_review_spec.rb +20 -3
  44. data/spec/rails_best_practices/reviews/needless_deep_nesting_review_spec.rb +14 -0
  45. data/spec/rails_best_practices/reviews/remove_unused_methods_in_models_review_spec.rb +387 -0
  46. metadata +15 -3
data/.gitignore CHANGED
@@ -9,3 +9,4 @@ pkg/**
9
9
  rdoc/**
10
10
  doc/**
11
11
  .yardoc/**
12
+ rails_best_practices_output.html
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rails_best_practices (1.1.0)
4
+ rails_best_practices (1.2.0)
5
5
  activesupport
6
6
  colored
7
7
  erubis
data/README.md CHANGED
@@ -133,6 +133,7 @@ Now you can customize this configuration file, the default configuration is as f
133
133
  RemoveEmptyHelpersCheck: {}
134
134
  RemoveTabCheck: {}
135
135
  RestrictAutoGeneratedRoutesCheck: { }
136
+ RemoveUnusedMethodsInModelsCheck: { except_methods: [] }
136
137
 
137
138
  You can remove or comment one review to disable it, and you can change the options.
138
139
 
@@ -161,6 +162,7 @@ Model
161
162
  2. the Law of Demeter
162
163
  3. Use Observer
163
164
  4. Use Query Attribute
165
+ 5. Remove Unused Methods In Models (Experiment, not available by default configuration)
164
166
 
165
167
  Mailer
166
168
 
@@ -3,7 +3,6 @@
3
3
  <head>
4
4
  <meta charset='UTF-8' />
5
5
  <title>Output of rails_best_practices</title>
6
-
7
6
  <style type="text/css">
8
7
  body {
9
8
  color: #333;
@@ -34,6 +33,10 @@
34
33
  table tr:hover {
35
34
  background-color: #FFFFC0;
36
35
  }
36
+ ul li {
37
+ list-style: none;
38
+ display: none;
39
+ }
37
40
  </style>
38
41
  </head>
39
42
  <body>
@@ -50,6 +53,11 @@
50
53
  Found <%= @errors.size %> errors.
51
54
  <% end %>
52
55
  </h2>
56
+ <ul>
57
+ <% @error_types.each do |error_type| %>
58
+ <li><input type="checkbox" value="<%= error_type.split(':').last %>" /><%= error_type.split(':').last %></li>
59
+ <% end %>
60
+ </ul>
53
61
  <table>
54
62
  <tr>
55
63
  <th>Filename</th>
@@ -57,7 +65,7 @@
57
65
  <th>Warning Message</th>
58
66
  </tr>
59
67
  <% @errors.each do |error| %>
60
- <tr>
68
+ <tr class="<%= error.type.split(':').last %>">
61
69
  <td class='filename'>
62
70
  <% if @textmate %>
63
71
  <a href='txmt://open/?url=file://<%= File.expand_path(error.filename) %>&amp;line=<%= error.line_number %>'><%= error.filename %></a>
@@ -74,5 +82,20 @@
74
82
  </tr>
75
83
  <% end %>
76
84
  </table>
85
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
86
+ <script type="text/javascript">
87
+ $(function() {
88
+ $('ul li').show();
89
+ $('input[type=checkbox]').prop('checked', true).click(function() {
90
+ if ($(this).attr('checked')) {
91
+ $(this).prop('checked', true);
92
+ $('.'+$(this).val()).show();
93
+ } else {
94
+ $(this).prop('checked', false);
95
+ $('.'+$(this).val()).hide();
96
+ }
97
+ });
98
+ });
99
+ </script>
77
100
  </body>
78
101
  </html>
@@ -83,6 +83,7 @@ module RailsBestPractices
83
83
 
84
84
  @bar = ProgressBar.new('Analyzing', lexical_files.size + prepare_files.size + review_files.size)
85
85
  ["lexical", "prepare", "review"].each { |process| send(:process, process) }
86
+ @runner.on_complete
86
87
  @bar.finish
87
88
 
88
89
  if @options['format'] == 'html'
@@ -202,7 +203,7 @@ module RailsBestPractices
202
203
  files.reject { |file| file.index(pattern) }
203
204
  end
204
205
 
205
- # output errors if exist.
206
+ # output errors on terminal.
206
207
  def output_terminal_errors
207
208
  @runner.errors.each { |error| plain_output(error.to_s, 'red') }
208
209
  plain_output("\nPlease go to http://rails-bestpractices.com to see more useful Rails Best Practices.", 'green')
@@ -213,6 +214,21 @@ module RailsBestPractices
213
214
  end
214
215
  end
215
216
 
217
+ # output errors with html format.
218
+ def output_html_errors
219
+ require 'erubis'
220
+ template = File.read(File.join(File.dirname(__FILE__), "..", "assets", "result.html.erb"))
221
+
222
+ File.open("rails_best_practices_output.html", "w+") do |file|
223
+ eruby = Erubis::Eruby.new(template)
224
+ file.puts eruby.evaluate(:errors => @runner.errors, :error_types => error_types, :textmate => @options["with-textmate"], :mvim => @options["with-mvim"])
225
+ end
226
+ end
227
+
228
+ # plain output with color.
229
+ #
230
+ # @param [String] message to output
231
+ # @param [String] color
216
232
  def plain_output(message, color)
217
233
  if @options["without-color"]
218
234
  puts message
@@ -221,14 +237,9 @@ module RailsBestPractices
221
237
  end
222
238
  end
223
239
 
224
- def output_html_errors
225
- require 'erubis'
226
- template = File.read(File.join(File.dirname(__FILE__), "..", "assets", "result.html.erb"))
227
-
228
- File.open("rails_best_practices_output.html", "w+") do |file|
229
- eruby = Erubis::Eruby.new(template)
230
- file.puts eruby.evaluate(:errors => @runner.errors, :textmate => @options["with-textmate"], :mvim => @options["with-mvim"])
231
- end
240
+ # unique error types.
241
+ def error_types
242
+ @runner.errors.map(&:type).uniq
232
243
  end
233
244
  end
234
245
  end
@@ -4,6 +4,7 @@ require 'rails_best_practices/core/runner'
4
4
  require 'rails_best_practices/core/checking_visitor'
5
5
  require 'rails_best_practices/core/error'
6
6
  require 'rails_best_practices/core/nil'
7
+ require 'rails_best_practices/core/klasses'
7
8
  require 'rails_best_practices/core/models'
8
9
  require 'rails_best_practices/core/model_associations'
9
10
  require 'rails_best_practices/core/model_attributes'
@@ -3,6 +3,7 @@ module RailsBestPractices
3
3
  module Core
4
4
  # A Check class that takes charge of checking the sexp.
5
5
  class Check
6
+
6
7
  CONTROLLER_FILES = /controllers\/.*\.rb$/
7
8
  MIGRATION_FILES = /db\/migrate\/.*\.rb$/
8
9
  MODEL_FILES = /models\/.*\.rb$/
@@ -13,12 +14,6 @@ module RailsBestPractices
13
14
  SCHEMA_FILE = /db\/schema\.rb/
14
15
  HELPER_FILES = /helpers.*\.rb$/
15
16
 
16
- attr_reader :errors
17
-
18
- def initialize
19
- @errors = []
20
- end
21
-
22
17
  # default interesting nodes.
23
18
  def interesting_nodes
24
19
  []
@@ -37,6 +32,9 @@ module RailsBestPractices
37
32
  # @param [Sexp] node
38
33
  def node_start(node)
39
34
  @node = node
35
+ Array(self.class.callbacks["start_#{node.sexp_type}"]).each do |callback|
36
+ self.instance_exec node, &callback
37
+ end
40
38
  self.send("start_#{node.sexp_type}", node)
41
39
  end
42
40
 
@@ -49,14 +47,23 @@ module RailsBestPractices
49
47
  def node_end(node)
50
48
  @node = node
51
49
  self.send("end_#{node.sexp_type}", node)
50
+ Array(self.class.callbacks["end_#{node.sexp_type}"]).each do |callback|
51
+ self.instance_exec node, &callback
52
+ end
52
53
  end
53
54
 
54
55
  # add error if source code violates rails best practice.
55
- # error is the string message for violation of the rails best practice
56
- # file is the filename of source code
57
- # line is the line number of the source code which is reviewing
58
- def add_error(error, file = @node.file, line = @node.line)
59
- @errors << RailsBestPractices::Core::Error.new("#{file}", "#{line}", error, url)
56
+ #
57
+ # @param [String] message, is the string message for violation of the rails best practice
58
+ # @param [String] file, is the filename of source code
59
+ # @param [Integer] line, is the line number of the source code which is reviewing
60
+ def add_error(message, file = @node.file, line = @node.line)
61
+ errors << RailsBestPractices::Core::Error.new("#{file}", "#{line}", message, self.class.to_s, url)
62
+ end
63
+
64
+ # errors that vialote the rails best practices.
65
+ def errors
66
+ @errors ||= []
60
67
  end
61
68
 
62
69
  # default url is empty.
@@ -84,31 +91,105 @@ module RailsBestPractices
84
91
  end
85
92
  end
86
93
 
87
- module Classable
88
- # remember module name.
89
- def start_module(node)
90
- modules << node.module_name
94
+ class <<self
95
+ # callbacks for start_xxx and end_xxx.
96
+ def callbacks
97
+ @callbacks ||= {}
91
98
  end
92
99
 
93
- # end of the module.
94
- def end_module(node)
95
- modules.pop
100
+ # add a callback.
101
+ #
102
+ # @param [String] name, callback name, can be start_xxx or end_xxx
103
+ # @param [Proc] block, be executed when callbacks are called
104
+ def add_callback(name, &block)
105
+ callbacks[name] ||= []
106
+ callbacks[name] << block
96
107
  end
108
+ end
109
+
110
+ # Helper to parse the class name.
111
+ module Klassable
112
+ def self.included(base)
113
+ base.class_eval do
114
+ # remember module name
115
+ add_callback "start_module" do |node|
116
+ modules << node.module_name.to_s
117
+ end
118
+
119
+ # end of the module.
120
+ add_callback "end_module" do |node|
121
+ modules.pop
122
+ end
97
123
 
98
- # get the class name with module name.
99
- def class_name(node)
100
- class_name = node.class_name.to_s
101
- if modules.empty?
102
- class_name
103
- else
104
- modules.map { |modu| "#{modu}::" }.join("") + class_name
124
+ # remember the class anem
125
+ add_callback "start_class" do |node|
126
+ @klass = Core::Klass.new(node.class_name.to_s, node.base_class.to_s, modules)
127
+ end
128
+
129
+ # end of the class
130
+ add_callback "end_class" do |node|
131
+ @klass = nil
132
+ end
105
133
  end
106
134
  end
107
135
 
136
+ # get the current class name.
137
+ def current_class_name
138
+ @klass.to_s
139
+ end
140
+
141
+ # get the current extend class name.
142
+ def current_extend_class_name
143
+ @klass.extend_class_name
144
+ end
145
+
146
+ # modules.
108
147
  def modules
109
148
  @moduels ||= []
110
149
  end
111
150
  end
151
+
152
+ # Helper to add callback after all files reviewed.
153
+ module Completeable
154
+ def self.included(base)
155
+ base.class_eval do
156
+ add_callback "end_class" do |node|
157
+ if "RailsBestPractices::Complete" == node.class_name.to_s
158
+ on_complete
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ # Helper to parse the access control.
166
+ module Accessable
167
+ def self.included(base)
168
+ base.class_eval do
169
+ # remember the current access control for methods.
170
+ add_callback "start_var_ref" do |node|
171
+ if %w(public protected private).include? node.to_s
172
+ @access_control = node.to_s
173
+ end
174
+ end
175
+
176
+ # set access control to "public" by default.
177
+ add_callback "start_class" do |node|
178
+ @access_control = "public"
179
+ end
180
+
181
+ # set access control to "public" by default.
182
+ add_callback "start_module" do |node|
183
+ @access_control = "public"
184
+ end
185
+ end
186
+
187
+ # get the current acces control.
188
+ def current_access_control
189
+ @access_control
190
+ end
191
+ end
192
+ end
112
193
  end
113
194
  end
114
195
  end
@@ -1,7 +1,8 @@
1
1
  # encoding: utf-8
2
2
  module RailsBestPractices
3
3
  module Core
4
- class Controllers < Array
4
+ # Controller classes.
5
+ class Controllers < Klasses
5
6
  end
6
7
  end
7
8
  end
@@ -5,12 +5,13 @@ module RailsBestPractices
5
5
  #
6
6
  # it indicates the filenname, line number and error message for the violation.
7
7
  class Error
8
- attr_reader :filename, :line_number, :message, :url
8
+ attr_reader :filename, :line_number, :message, :type, :url
9
9
 
10
- def initialize(filename, line_number, message, url = nil)
10
+ def initialize(filename, line_number, message, type, url = nil)
11
11
  @filename = filename
12
12
  @line_number = line_number
13
13
  @message = message
14
+ @type = type
14
15
  @url = url
15
16
  end
16
17
 
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+ module RailsBestPractices
3
+ module Core
4
+ # Klass container.
5
+ class Klasses < Array
6
+ # If include the class.
7
+ #
8
+ # @param [String] class name
9
+ # @return [Boolean] include or not
10
+ def include?(class_name)
11
+ find { |klass| klass.to_s == class_name }
12
+ end
13
+ end
14
+
15
+ # Class info includes clas name, extend class name and module names.
16
+ class Klass
17
+ attr_reader :class_name, :extend_class_name
18
+
19
+ def initialize(class_name, extend_class_name, modules)
20
+ @class_name = class_name
21
+ @extend_class_name = extend_class_name
22
+ @modules = modules.dup
23
+ end
24
+
25
+ def to_s
26
+ if @modules.empty?
27
+ @class_name
28
+ else
29
+ @modules.map { |modu| "#{modu}::" }.join("") + @class_name
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,7 +1,8 @@
1
1
  # encoding: utf-8
2
2
  module RailsBestPractices
3
3
  module Core
4
- class Mailers < Array
4
+ # Mailer classes.
5
+ class Mailers < Klasses
5
6
  end
6
7
  end
7
8
  end
@@ -1,24 +1,128 @@
1
1
  # encoding: utf-8
2
2
  module RailsBestPractices
3
3
  module Core
4
+ # Method container.
4
5
  class Methods
5
6
  def initialize
6
7
  @methods = {}
8
+ @possible_methods = {}
7
9
  end
8
10
 
9
- def add_method(model_name, method_name)
10
- @methods[model_name] ||= []
11
- @methods[model_name] << method_name
11
+ # Add a method.
12
+ #
13
+ # @param [String] class name
14
+ # @param [String] method name
15
+ # @param [Hash] method meta, file and line, {"file" => "app/models/post.rb", "line" => 5}
16
+ # @param [String] access control, public, protected or private
17
+ def add_method(class_name, method_name, meta={}, access_control="public")
18
+ return if class_name == ""
19
+ methods(class_name) << Method.new(class_name, method_name, access_control, meta)
20
+ if access_control == "public"
21
+ @possible_methods[method_name] = false
22
+ end
12
23
  end
13
24
 
14
- def get_methods(model_name)
15
- @methods[model_name] ||= []
16
- @methods[model_name].to_a
25
+ # Get methods of a class.
26
+ #
27
+ # @param [String] class name
28
+ # @param [String] access control
29
+ # @return [Array] all methods of a class for such access control, if access control is nil, return all public/protected/private methods
30
+ def get_methods(class_name, access_control=nil)
31
+ if access_control
32
+ methods(class_name).select { |method| method.access_control == access_control }
33
+ else
34
+ methods(class_name)
35
+ end
17
36
  end
18
37
 
19
- def has_method?(model_name, method_name)
20
- @methods[model_name] ||= []
21
- @methods[model_name].include? method_name
38
+ # If a class has a method.
39
+ #
40
+ # @param [String] class name
41
+ # @param [String] method name
42
+ # @param [String] access control
43
+ # @return [Boolean] has a method or not
44
+ def has_method?(class_name, method_name, access_control=nil)
45
+ if access_control
46
+ !!methods(class_name).find { |method| method.method_name == method_name && method.access_control == access_control }
47
+ else
48
+ !!methods(class_name).find { |method| method.method_name == method_name }
49
+ end
50
+ end
51
+
52
+ # Mark parent class' method as used.
53
+ #
54
+ # @param [String] class name
55
+ # @param [String] method name
56
+ def mark_extend_class_method_used(class_name, method_name)
57
+ klass = Prepares.klasses.find { |klass| klass.to_s == class_name }
58
+ if klass && klass.extend_class_name
59
+ mark_extend_class_method_used(klass.extend_class_name, method_name)
60
+ method = get_method(klass.extend_class_name, method_name)
61
+ method.mark_used if method
62
+ end
63
+ end
64
+
65
+ # remomber the method name, the method is probably be used for the class' public method.
66
+ #
67
+ # @param [String] method name
68
+ def possible_public_used(method_name)
69
+ @possible_methods[method_name] = true
70
+ end
71
+
72
+ # Get a method in a class.
73
+ #
74
+ # @param [String] class name
75
+ # @param [String] method name
76
+ # @param [String] access control
77
+ # @return [Method] Method object
78
+ def get_method(class_name, method_name, access_control=nil)
79
+ if access_control
80
+ methods(class_name).find { |method| method.method_name == method_name && method.access_control == access_control }
81
+ else
82
+ methods(class_name).find { |method| method.method_name == method_name }
83
+ end
84
+ end
85
+
86
+ # Get all unused methods.
87
+ #
88
+ # @param [String] access control
89
+ # @return [Array] array of Method
90
+ def get_all_unused_methods(access_control=nil)
91
+ @methods.inject([]) { |unused_methods, (class_name, methods)|
92
+ unused_methods += if access_control
93
+ methods.select { |method| method.access_control == access_control && !method.used }
94
+ else
95
+ methods.select { |method| !method.used }
96
+ end
97
+ }.reject { |method| method.access_control == "public" && @possible_methods[method.method_name] }
98
+ end
99
+
100
+ private
101
+ # Methods of a class.
102
+ #
103
+ # @param [String] class name
104
+ # @return [Array] array of methods
105
+ def methods(class_name)
106
+ @methods[class_name] ||= []
107
+ end
108
+ end
109
+
110
+ # Method info includes class name, method name, access control, file, line, used.
111
+ class Method
112
+ attr_reader :access_control, :class_name, :method_name, :used, :file, :line
113
+
114
+ def initialize(class_name, method_name, access_control, meta)
115
+ @class_name = class_name
116
+ @method_name = method_name
117
+ @file = meta["file"]
118
+ @line = meta["line"]
119
+ @access_control = access_control
120
+ @used = false
121
+ end
122
+
123
+ # Mark this method as used.
124
+ def mark_used
125
+ @used = true
22
126
  end
23
127
  end
24
128
  end