rails_best_practices 1.1.0 → 1.2.0

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.
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