rails_best_practices 0.5.6 → 0.6.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.
Files changed (37) hide show
  1. data/README.md +161 -0
  2. data/lib/rails_best_practices.rb +158 -20
  3. data/lib/rails_best_practices/checks/add_model_virtual_attribute_check.rb +108 -34
  4. data/lib/rails_best_practices/checks/always_add_db_index_check.rb +148 -29
  5. data/lib/rails_best_practices/checks/check.rb +178 -75
  6. data/lib/rails_best_practices/checks/dry_bundler_in_capistrano_check.rb +26 -5
  7. data/lib/rails_best_practices/checks/isolate_seed_data_check.rb +66 -15
  8. data/lib/rails_best_practices/checks/keep_finders_on_their_own_model_check.rb +53 -12
  9. data/lib/rails_best_practices/checks/law_of_demeter_check.rb +59 -30
  10. data/lib/rails_best_practices/checks/move_code_into_controller_check.rb +35 -15
  11. data/lib/rails_best_practices/checks/move_code_into_helper_check.rb +56 -12
  12. data/lib/rails_best_practices/checks/move_code_into_model_check.rb +30 -32
  13. data/lib/rails_best_practices/checks/move_finder_to_named_scope_check.rb +45 -15
  14. data/lib/rails_best_practices/checks/move_model_logic_into_model_check.rb +31 -27
  15. data/lib/rails_best_practices/checks/needless_deep_nesting_check.rb +99 -38
  16. data/lib/rails_best_practices/checks/not_use_default_route_check.rb +43 -12
  17. data/lib/rails_best_practices/checks/overuse_route_customizations_check.rb +140 -28
  18. data/lib/rails_best_practices/checks/replace_complex_creation_with_factory_method_check.rb +44 -30
  19. data/lib/rails_best_practices/checks/replace_instance_variable_with_local_variable_check.rb +18 -7
  20. data/lib/rails_best_practices/checks/use_before_filter_check.rb +88 -18
  21. data/lib/rails_best_practices/checks/use_model_association_check.rb +61 -22
  22. data/lib/rails_best_practices/checks/use_observer_check.rb +125 -23
  23. data/lib/rails_best_practices/checks/use_query_attribute_check.rb +75 -47
  24. data/lib/rails_best_practices/checks/use_say_with_time_in_migrations_check.rb +59 -10
  25. data/lib/rails_best_practices/checks/use_scope_access_check.rb +78 -23
  26. data/lib/rails_best_practices/command.rb +19 -34
  27. data/lib/rails_best_practices/core.rb +4 -2
  28. data/lib/rails_best_practices/core/checking_visitor.rb +49 -19
  29. data/lib/rails_best_practices/core/error.rb +5 -2
  30. data/lib/rails_best_practices/core/runner.rb +79 -55
  31. data/lib/rails_best_practices/core/visitable_sexp.rb +325 -55
  32. data/lib/rails_best_practices/{core/core_ext.rb → core_ext/enumerable.rb} +3 -6
  33. data/lib/rails_best_practices/core_ext/nil_class.rb +8 -0
  34. data/lib/rails_best_practices/version.rb +1 -1
  35. data/rails_best_practices.yml +2 -2
  36. metadata +8 -7
  37. data/README.textile +0 -150
@@ -5,14 +5,34 @@ module RailsBestPractices
5
5
  module Checks
6
6
  # Check db/schema.rb file to make sure every reference key has a database index.
7
7
  #
8
- # Implementation: read all add_index method calls to get the indexed columns in table, then read integer method call in create_table block to get the reference columns in tables (or polymorphic index like [commentable_id, commentable_type]), compare with indexed columns, if not in the indexed columns, then it violates always_add_db_index_check.
8
+ # See the best practice details here http://rails-bestpractices.com/posts/21-always-add-db-index
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # none
14
+ #
15
+ # Review process:
16
+ # only check the call nodes and at the end of iter node in db/schema file,
17
+ # if the subject of call node is :create_table, then remember the table names
18
+ # if the subject of call node is :integer, then remember it as foreign key
19
+ # if the sujbect of call node is :string, the name of it is _type suffixed and there is an integer column _id suffixed, then remember it as polymorphic foreign key
20
+ # if the subject of call node is :add_index, then remember the index columns
21
+ # after all of these, at the end of iter node
22
+ #
23
+ # ActiveRecord::Schema.define(:version => 20101201111111) do
24
+ # ......
25
+ # end
26
+ #
27
+ # if there are any foreign keys not existed in index columns,
28
+ # then the foreign keys should add db index.
9
29
  class AlwaysAddDbIndexCheck < Check
10
30
 
11
- def interesting_nodes
12
- [:block, :call, :iter]
31
+ def interesting_review_nodes
32
+ [:call, :iter]
13
33
  end
14
34
 
15
- def interesting_files
35
+ def interesting_review_files
16
36
  /db\/schema.rb/
17
37
  end
18
38
 
@@ -23,23 +43,76 @@ module RailsBestPractices
23
43
  @table_nodes = {}
24
44
  end
25
45
 
26
- def evaluate_start(node)
27
- if :block == node.node_type
28
- find_index_columns(node)
29
- elsif :call == node.node_type
30
- case node.message
31
- when :create_table
32
- @table_name = node.arguments[1].to_s
33
- @table_nodes[@table_name] = node
34
- when :integer, :string
35
- column_name = node.arguments[1].to_s
36
- add_foreign_key_column(@table_name, column_name)
37
- end
46
+ # check call node in review process.
47
+ #
48
+ # if the message of call node is :create_table,
49
+ # then remember the table name (@table_nodes) like
50
+ # {
51
+ # "comments" =>
52
+ # s(:call, nil, :create_table, s(:arglist, s(:str, "comments"), s(:hash, s(:lit, :force), s(:true))))
53
+ # }
54
+ #
55
+ # if the message of call node is :integer,
56
+ # then remember it as a foreign key of last create table name.
57
+ #
58
+ # if the message of call node is :type and the name of argument is _type suffixed,
59
+ # then remember it with _id suffixed column as polymorphic foreign key.
60
+ #
61
+ # the remember foreign keys (@foreign_keys) like
62
+ #
63
+ # {
64
+ # "taggings" =>
65
+ # ["tag_id", ["taggable_id", "taggable_type"]]
66
+ # }
67
+ #
68
+ # if the message of call node is :add_index,
69
+ # then remember it as index columns (@index_columns) like
70
+ #
71
+ # {
72
+ # "comments" =>
73
+ # ["post_id", "user_id"]
74
+ # }
75
+ def review_start_call(node)
76
+ case node.message
77
+ when :create_table
78
+ remember_table_nodes(node)
79
+ when :integer, :string
80
+ remember_foreign_key_columns(node)
81
+ when :add_index
82
+ remember_index_columns(node)
83
+ else
38
84
  end
39
85
  end
40
86
 
41
- def evaluate_end(node)
42
- if :iter == node.node_type && :call == node.subject.node_type && s(:colon2, s(:const, :ActiveRecord), :Schema) == node.subject.subject
87
+ # check at the end of iter node, like
88
+ #
89
+ # s(:iter,
90
+ # s(:call,
91
+ # s(:colon2, s(:const, :ActiveRecord), :Schema),
92
+ # :define,
93
+ # s(:arglist, s(:hash, s(:lit, :version), s(:lit, 20100603080629)))
94
+ # ),
95
+ # nil,
96
+ # s(:iter,
97
+ # s(:call, nil, :create_table,
98
+ # s(:arglist, s(:str, "comments"), s(:hash, s(:lit, :force), s(:true)))
99
+ # ),
100
+ # s(:lasgn, :t),
101
+ # s(:block,
102
+ # s(:call, s(:lvar, :t), :string, s(:arglist, s(:str, "content")))
103
+ # )
104
+ # )
105
+ # )
106
+ #
107
+ # if the subject of iter node is with subject ActiveRecord::Schema,
108
+ # it means we have completed the foreign keys and index columns parsing,
109
+ # then we compare foreign keys and index columns.
110
+ #
111
+ # if there are any foreign keys not existed in index columns,
112
+ # then we should add db index for that foreign keys.
113
+ def review_end_iter(node)
114
+ first_node = node.subject
115
+ if :call == first_node.node_type && s(:colon2, s(:const, :ActiveRecord), :Schema) == first_node.subject
43
116
  remove_only_type_foreign_keys
44
117
  @foreign_keys.each do |table, foreign_key|
45
118
  table_node = @table_nodes[table]
@@ -53,20 +126,63 @@ module RailsBestPractices
53
126
  end
54
127
 
55
128
  private
56
- def find_index_columns(node)
57
- node.grep_nodes({:node_type => :call, :message => :add_index}).each do |index_node|
58
- table_name = index_node.arguments[1].to_s
59
- index_column = eval(index_node.arguments[2].to_s)
60
- add_index_column(table_name, index_column)
61
- end
62
- end
129
+ # remember the node as index columns
130
+ #
131
+ # s(:call, nil, :add_index,
132
+ # s(:arglist,
133
+ # s(:str, "comments"),
134
+ # s(:array, s(:str, "post_id")),
135
+ # s(:hash, s(:lit, :name), s(:str, "index_comments_on_post_id"))
136
+ # )
137
+ # )
138
+ #
139
+ # the remember index columns are like
140
+ # {
141
+ # "comments" =>
142
+ # ["post_id", "user_id"]
143
+ # }
144
+ def remember_index_columns(node)
145
+ table_name = node.arguments[1].to_s
146
+ index_column = eval(node.arguments[2].to_s)
63
147
 
64
- def add_index_column(table_name, index_column)
65
148
  @index_columns[table_name] ||= []
66
149
  @index_columns[table_name] << (index_column.size == 1 ? index_column[0] : index_column)
67
150
  end
68
151
 
69
- def add_foreign_key_column(table_name, foreign_key_column)
152
+ # remember table nodes
153
+ #
154
+ # if the node is
155
+ #
156
+ # s(:call, nil, :create_table,
157
+ # s(:arglist, s(:str, "comments"), s(:hash, s(:lit, :force), s(:true))))
158
+ #
159
+ # then the table nodes will be
160
+ #
161
+ # {
162
+ # "comments" =>
163
+ # s(:call, nil, :create_table, s(:arglist, s(:str, "comments"), s(:hash, s(:lit, :force), s(:true))))
164
+ # }
165
+ def remember_table_nodes(node)
166
+ @table_name = node.arguments[1].to_s
167
+ @table_nodes[@table_name] = node
168
+ end
169
+
170
+
171
+ # remember foreign key columns
172
+ #
173
+ # if the message of node is :integer,
174
+ # then it is a foreign key, like
175
+ #
176
+ # s(:call, s(:lvar, :t), :integer, s(:arglist, s(:str, "post_id")))
177
+ #
178
+ # if the message of node is :string, with _type suffixed and there is a _id suffixed column,
179
+ # then they are polymorphic foreign key
180
+ #
181
+ # s(:call, s(:lvar, :t), :integer, s(:arglist, s(:str, "taggable_id")))
182
+ # s(:call, s(:lvar, :t), :string, s(:arglist, s(:str, "taggable_type")))
183
+ def remember_foreign_key_columns(node)
184
+ table_name = @table_name
185
+ foreign_key_column = node.arguments[1].to_s
70
186
  @foreign_keys[table_name] ||= []
71
187
  if foreign_key_column =~ /(.*?)_id$/
72
188
  if @foreign_keys[table_name].delete("#{$1}_type")
@@ -83,18 +199,21 @@ module RailsBestPractices
83
199
  end
84
200
  end
85
201
 
202
+ # remove the non foreign keys with only _type column.
86
203
  def remove_only_type_foreign_keys
87
204
  @foreign_keys.delete_if { |table, foreign_key|
88
205
  foreign_key.size == 1 && foreign_key[0] =~ /_type$/
89
206
  }
90
207
  end
91
208
 
209
+ # check if the table's column is indexed.
92
210
  def indexed?(table, column)
93
211
  index_columns = @index_columns[table]
94
- !index_columns || !index_columns.any? { |e| greater_than(Array(e), Array(column)) }
212
+ !index_columns || !index_columns.any? { |e| greater_than_or_equal(Array(e), Array(column)) }
95
213
  end
96
214
 
97
- def greater_than(more_array, less_array)
215
+ # check if more_array is greater than less_array or equal to less_array.
216
+ def greater_than_or_equal(more_array, less_array)
98
217
  more_size = more_array.size
99
218
  less_size = less_array.size
100
219
  (more_array - less_array).size == more_size - less_size
@@ -3,14 +3,21 @@ require 'rails_best_practices/core/error'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
+ # A Check class that takes charge of reviewing one rails best practice.
7
+ # One check contains two process:
8
+ # 1. prepare process (optional), in this process, one check will do some preparation, such as analyzing the model associations.The check only does the preparation for the nodes (defined in interesting_prepare_nodes) in the files (defined in interesting_prepare_files).
9
+ # 2. review process, in this process, one check will really review your rails codes. The check only review the nodes (defined in interesting_review_nodes) in the files # (defined in interesting_review_files).
6
10
  class Check
7
- NODE_TYPES = [:call, :defn, :defs, :if, :unless, :class, :lasgn, :iasgn, :ivar, :lvar, :block, :iter, :const]
11
+ # only nodes whose node_type is in NODE_TYPE will be reviewed.
12
+ NODE_TYPES = [:call, :defn, :defs, :if, :class, :lasgn, :iasgn, :ivar, :lvar, :block, :iter, :const]
8
13
 
9
14
  CONTROLLER_FILES = /_controller\.rb$/
10
15
  MIGRATION_FILES = /db\/migrate\/.*\.rb$/
11
- MODLE_FILES = /models\/.*\.rb$/
16
+ MODEL_FILES = /models\/.*\.rb$/
17
+ MAILER_FILES = /models\/.*\.rb$|mailers\/.*\.rb/
12
18
  VIEW_FILES = /views\/.*\.(erb|haml)$/
13
19
  PARTIAL_VIEW_FILES = /views\/.*\/_.*\.(erb|haml)$/
20
+ ROUTE_FILE = /config\/routes.rb/
14
21
 
15
22
  attr_reader :errors
16
23
 
@@ -18,82 +25,178 @@ module RailsBestPractices
18
25
  @errors = []
19
26
  end
20
27
 
21
- def interesting_files
22
- /.*/
23
- end
24
-
25
- def interesting_prepare_files
26
- /.*/
27
- end
28
-
29
- NODE_TYPES.each do |node|
30
- start_node_method = "evaluate_start_#{node}"
31
- end_node_method = "evaluate_end_#{node}"
32
- define_method(start_node_method) { |node| } unless self.respond_to?(start_node_method)
33
- define_method(end_node_method) { |node| } unless self.respond_to?(end_node_method)
34
-
35
- prepare_start_node_method = "prepare_start_#{node}"
36
- prepare_end_node_method = "prepare_end_#{node}"
37
- define_method(prepare_start_node_method) { |node| } unless self.respond_to?(prepare_start_node_method)
38
- define_method(prepare_end_node_method) { |node| } unless self.respond_to?(prepare_end_node_method)
39
- end
40
-
41
- def position(offset = 0)
42
- "#{@line[2]}:#{@line[1] + offset}"
43
- end
44
-
45
- def prepare_start(node)
46
- end
47
-
48
- def prepare_end(node)
49
- end
50
-
51
- def evaluate_start(node)
52
- end
53
-
54
- def evaluate_end(node)
55
- end
56
-
57
- def prepare_node(position, node)
58
- @node = node
59
- prepare_method = "prepare_#{position}_#{node.node_type}"
60
- self.send(prepare_method, node)
61
- end
62
-
63
- def evaluate_node(position, node)
64
- @node = node
65
- eval_method = "evaluate_#{position}_#{node.node_type}"
66
- self.send(eval_method, node)
67
- end
68
-
69
- def prepare_node_start(node)
70
- prepare_node(:start, node)
71
- prepare_start(node)
72
- end
73
-
74
- def prepare_node_end(node)
75
- prepare_node(:end, node)
76
- prepare_end(node)
77
- end
78
-
79
- def evaluate_node_start(node)
80
- evaluate_node(:start, node)
81
- evaluate_start(node)
82
- end
83
-
84
- def evaluate_node_end(node)
85
- evaluate_node(:end, node)
86
- evaluate_end(node)
87
- end
88
-
89
- def add_error(error, file = nil, line = nil)
90
- file ||= @node.file
91
- line ||= @node.line
28
+ # define default interesting_prepare_nodes, interesting_review_nodes, interesting_prepare_files and interesting_review_files.
29
+ [:prepare, :review].each do |process|
30
+ class_eval <<-EOS
31
+ def interesting_#{process}_nodes # def interesting_review_nodes
32
+ [] # []
33
+ end # end
34
+ #
35
+ def interesting_#{process}_files # def interesting_review_files
36
+ /.*/ # /.*/
37
+ end # end
38
+ EOS
39
+ end
40
+
41
+ # define method prepare_node_start, prepare_node_end, review_node_start and review_node_end.
42
+ #
43
+ # they delegate the node to special process method, like
44
+ #
45
+ # review_node_start(call_node) => review_start_call(call_node)
46
+ # review_node_end(defn_node) => review_end_defn(defn_node)
47
+ # prepare_node_start(calss_node) => prepare_start_class(class_node)
48
+ # prepare_node_end(if_node) => prepare_end_if(if_node)
49
+ [:prepare, :review].each do |process|
50
+ class_eval <<-EOS
51
+ def #{process}_node_start(node) # def review_node_start(node)
52
+ @node = node # @node = node
53
+ method = "#{process}_start_" + node.node_type.to_s # method = "review_start_" + node.node_type.to_s
54
+ self.send(method, node) # self.send(method, node)
55
+ end # end
56
+ #
57
+ def #{process}_node_end(node) # def review_node_end(node)
58
+ @node = node # @node = node
59
+ method = "#{process}_end_" + node.node_type.to_s # method = "review_end_" + node.node_type.to_s
60
+ self.send(method, node) # self.send(method, node)
61
+ end # end
62
+ EOS
63
+ end
64
+
65
+ # define all start and end process for each node type, like
66
+ #
67
+ # prepare_start_defn
68
+ # prepare_end_defn
69
+ # review_start_call
70
+ # review_end_call
71
+ [:prepare, :review].each do |process|
72
+ NODE_TYPES.each do |node|
73
+ class_eval <<-EOS
74
+ def #{process}_start_#{node}(node) # def review_start_defn(node)
75
+ end # end
76
+ #
77
+ def #{process}_end_#{node}(node) # def review_end_defn(node)
78
+ end # end
79
+ EOS
80
+ end
81
+ end
82
+
83
+ # remember the model names and model associations in prepare process.
84
+ def self.prepare_model_associations
85
+ class_eval <<-EOS
86
+ def initialize
87
+ super
88
+ @klazzes = []
89
+ @associations = {}
90
+ end
91
+
92
+ def interesting_prepare_nodes
93
+ [:class, :call]
94
+ end
95
+
96
+ def interesting_prepare_files
97
+ MODEL_FILES
98
+ end
99
+
100
+ # check class node to remember all class name in prepare process.
101
+ #
102
+ # the remembered class names (@klazzes) are like
103
+ # [ :User, :Post ]
104
+ def prepare_start_class(node)
105
+ remember_klazz(node)
106
+ end
107
+
108
+ # check call node to remember all assoication names in prepare process.
109
+ #
110
+ # the remembered association names (@associations) are like
111
+ # { :User => [":projects", ":location"], :Post => [":comments"] }
112
+ def prepare_start_call(node)
113
+ remember_association(node) if association_methods.include? node.message
114
+ end
115
+
116
+ # remember class models, just the subject of class node.
117
+ def remember_klazz(class_node)
118
+ @klazzes << class_node.class_name
119
+ end
120
+
121
+ # remember associations, with class to association names.
122
+ def remember_association(association_node)
123
+ @associations[@klazzes.last] ||= []
124
+ @associations[@klazzes.last] << association_node.arguments[1].to_s
125
+ end
126
+
127
+ # default rails association methods.
128
+ def association_methods
129
+ [:belongs_to, :has_one, :has_many, :has_and_belongs_to_many]
130
+ end
131
+
132
+ EOS
133
+ end
134
+
135
+ # add error if source code violates rails best practice.
136
+ # error is the string message for violation of the rails best practice
137
+ # file is the filename of source code
138
+ # line is the line number of the source code which is reviewing
139
+ def add_error(error, file = @node.file, line = @node.line)
92
140
  @errors << RailsBestPractices::Core::Error.new("#{file}", "#{line}", error)
93
141
  end
94
142
 
95
- def equal?(node, expected)
96
- node.to_s == expected or node.to_s == ':' + expected.to_s
143
+ # remember use count for the local or instance variable in the call or attrasgn node.
144
+ #
145
+ # find the local variable or instance variable in the call or attrasgn node,
146
+ # then save it to as key in @variable_use_count hash, and add the call count (hash value).
147
+ def remember_variable_use_count(node)
148
+ variable_node = variable(node)
149
+ if variable_node
150
+ variable_use_count[variable_node] ||= 0
151
+ variable_use_count[variable_node] += 1
152
+ end
153
+ end
154
+
155
+ # return @variable_use_count hash.
156
+ def variable_use_count
157
+ @variable_use_count ||= {}
158
+ end
159
+
160
+ # reset @variable_use_count hash.
161
+ def reset_variable_use_count
162
+ @variable_use_count = nil
163
+ end
164
+
165
+ # find local variable or instance variable in the most inner call node, e.g.
166
+ #
167
+ # if the call node is
168
+ #
169
+ # s(:call, s(:ivar, :@post), :editors, s(:arglist)),
170
+ #
171
+ # or it is
172
+ #
173
+ # s(:call,
174
+ # s(:call, s(:ivar, :@post), :editors, s(:arglist)),
175
+ # :include?,
176
+ # s(:arglist, s(:call, nil, :current_user, s(:arglist)))
177
+ # )
178
+ #
179
+ # then the variable both are s(:ivar, :@post).
180
+ #
181
+ def variable(node)
182
+ while node.subject.node_type == :call
183
+ node = node.subject
184
+ end
185
+ subject_node = node.subject
186
+ if [:ivar, :lvar].include?(subject_node.node_type) and subject_node[1] != :_erbout
187
+ subject_node
188
+ else
189
+ nil
190
+ end
191
+ end
192
+
193
+ # compare two sexp nodes' to_s.
194
+ # equal?(":test", :test) => true
195
+ # equai?("@test", :test) => true
196
+ def equal?(node, expected_node)
197
+ actual = node.to_s.downcase
198
+ expected = expected_node.to_s.downcase
199
+ actual == expected || actual == ':' + expected || actual == '@' + expected
97
200
  end
98
201
  end
99
202
  end