rails_best_practices 0.5.6 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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