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
@@ -3,52 +3,164 @@ require 'rails_best_practices/checks/check'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
- # Check config/routes.rb to make sure there are no much route customizations.
6
+ # Check config/routes.rb file to make sure there are no overuse route customizations.
7
7
  #
8
- # Implementation: check member and collection route count, if more than customize_count, then it is overuse route customizations.
8
+ # See the best practice details here http://rails-bestpractices.com/posts/10-overuse-route-customizations.
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # none
14
+ #
15
+ # Review process:
16
+ # the check methods are different for rails2 and rails3 syntax.
17
+ #
18
+ # for rails2
19
+ #
20
+ # check all call nodes in route file.
21
+ # if the message of call node is resources,
22
+ # and the second argument of call node is a hash,
23
+ # and the count of the pair (key/value) in hash is greater than @customize_count,
24
+ # then these custom routes are overuse.
25
+ #
26
+ # for rails3
27
+ #
28
+ # check all iter nodes in route file.
29
+ # if the subject of iter node is with message resources,
30
+ # and in the block body of iter node, there are more than @customize_count call nodes,
31
+ # whose message is :get, :post, :update or :delete,
32
+ # then these custom routes are overuse.
9
33
  class OveruseRouteCustomizationsCheck < Check
10
-
11
- def interesting_nodes
34
+
35
+ VERBS = [:get, :post, :update, :delete]
36
+
37
+ def interesting_review_nodes
12
38
  [:call, :iter]
13
39
  end
14
-
15
- def interesting_files
16
- /config\/routes.rb/
40
+
41
+ def interesting_review_files
42
+ ROUTE_FILE
17
43
  end
18
44
 
19
45
  def initialize(options = {})
20
46
  super()
21
47
  @customize_count = options['customize_count'] || 3
22
48
  end
23
-
24
- def evaluate_start(node)
25
- add_error "overuse route customizations (customize_count > #{@customize_count})", node.file, node.subject.line if member_and_collection_count(node) > @customize_count
49
+
50
+ # check call node to see if the count of member and collection custom routes is more than @customize_count defined in review process.
51
+ # this is for rails2 syntax.
52
+ #
53
+ # if the message of call node is :resources,
54
+ # and the second argument of call node is a hash,
55
+ # and the count of the pair (key/value) in hash is greater than @customize_count, like
56
+ #
57
+ # map.resources :posts, :member => { :create_comment => :post,
58
+ # :update_comment => :update,
59
+ # :delete_comment => :delete },
60
+ # :collection => { :comments => :get }
61
+ #
62
+ # then they are overuse route customizations.
63
+ def review_start_call(node)
64
+ if member_and_collection_count_for_rails2(node) > @customize_count
65
+ add_error "overuse route customizations (customize_count > #{@customize_count})", node.file, node.subject.line
66
+ end
67
+ end
68
+
69
+ # check iter node to see if the count of member and collection custom routes is more than @customize_count defined in review process.
70
+ # this is for rails3 syntax.
71
+ #
72
+ # if the subject of iter node is with message :resources,
73
+ # and in the block body of iter node, there are more than @customize_count call nodes,
74
+ # whose message is :get, :post, :update or :delete, like
75
+ #
76
+ # resources :posts do
77
+ # member do
78
+ # post :create_comment
79
+ # update :update_comment
80
+ # delete :delete_comment
81
+ # end
82
+ #
83
+ # collection do
84
+ # get :comments
85
+ # end
86
+ # end
87
+ #
88
+ # then they are overuse route customizations.
89
+ def review_start_iter(node)
90
+ if member_and_collection_count_for_rails3(node) > @customize_count
91
+ add_error "overuse route customizations (customize_count > #{@customize_count})", node.file, node.subject.line
92
+ end
26
93
  end
27
94
 
28
95
  private
29
- def member_and_collection_count(node)
96
+ # check call node to calculate the count of member and collection custom routes.
97
+ # this is for rails2 syntax.
98
+ #
99
+ # if the message of call node is :resources,
100
+ # and the second argument is a hash,
101
+ # then calculate the pair (key/value) count,
102
+ # it is just the count of member and collection custom routes.
103
+ #
104
+ # s(:call, s(:lvar, :map), :resources,
105
+ # s(:arglist,
106
+ # s(:lit, :posts),
107
+ # s(:hash,
108
+ # s(:lit, :member),
109
+ # s(:hash,
110
+ # s(:lit, :create_comment),
111
+ # s(:lit, :post),
112
+ # s(:lit, :update_comment),
113
+ # s(:lit, :update),
114
+ # s(:lit, :delete_comment),
115
+ # s(:lit, :delete)
116
+ # ),
117
+ # s(:lit, :collection),
118
+ # s(:hash,
119
+ # s(:lit, :comments),
120
+ # s(:lit, :get)
121
+ # )
122
+ # )
123
+ # )
124
+ # )
125
+ def member_and_collection_count_for_rails2(node)
30
126
  if :resources == node.message
31
- member_and_collection_count_for_rails2(node)
32
- elsif :iter == node.node_type and :resources == node.subject.message
33
- member_and_collection_count_for_rails3(node)
127
+ hash_node = node.arguments[2]
128
+ if hash_node
129
+ (hash_node.grep_nodes_count(:node_type => :lit) - hash_node.grep_nodes_count(:node_type => :hash)) / 2
130
+ end
34
131
  end
35
132
  end
36
133
 
37
- # this is the checker for rails3 style routes
134
+ # check iter node to calculate the count of member and collection custom routes.
135
+ # this is for rails3 syntax.
136
+ #
137
+ # if its subject is with message :resources,
138
+ # then calculate the count of call nodes, whose message is :get, :post, :update or :delete,
139
+ # it is just the count of member and collection custom routes.
140
+ #
141
+ # s(:iter,
142
+ # s(:call, nil, :resources, s(:arglist, s(:lit, :posts))),
143
+ # nil,
144
+ # s(:block,
145
+ # s(:iter,
146
+ # s(:call, nil, :member, s(:arglist)),
147
+ # nil,
148
+ # s(:block,
149
+ # s(:call, nil, :post, s(:arglist, s(:lit, :create_comment))),
150
+ # s(:call, nil, :post, s(:arglist, s(:lit, :update_comment))),
151
+ # s(:call, nil, :post, s(:arglist, s(:lit, :delete_comment)))
152
+ # )
153
+ # ),
154
+ # s(:iter,
155
+ # s(:call, nil, :collection, s(:arglist)),
156
+ # nil,
157
+ # s(:call, nil, :get, s(:arglist, s(:lit, :comments)))
158
+ # )
159
+ # )
160
+ # )
38
161
  def member_and_collection_count_for_rails3(node)
39
- get_nodes = node.grep_nodes(:node_type => :call, :message => :get)
40
- post_nodes = node.grep_nodes(:node_type => :call, :message => :post)
41
- get_nodes.size + post_nodes.size
42
- end
43
-
44
- # this is the checker for rails2 style routes
45
- def member_and_collection_count_for_rails2(node)
46
- hash_nodes = node.grep_nodes(:node_type => :hash)
47
- return 0 if hash_nodes.empty?
48
- hash_key_node = hash_nodes.first[1]
49
- if :lit == hash_key_node.node_type and [:member, :collection].include? hash_key_node[1]
50
- customize_hash = eval(hash_nodes.first.to_s)
51
- (customize_hash[:member].size || 0) + (customize_hash[:collection].size || 0)
162
+ if :resources == node.subject.message
163
+ node.grep_nodes_count(:node_type => :call, :message => VERBS)
52
164
  end
53
165
  end
54
166
  end
@@ -3,54 +3,68 @@ require 'rails_best_practices/checks/check'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
- # Check a controller file to make sure that complex model creation should not exist in controller, move it to model factory method
6
+ # Check a controller file to make sure that complex model creation should not exist in controller, should be replaced with factory method.
7
7
  #
8
- # Implementation: check the count of variable attribute assignment calling before saving,
9
- # if more than defined attribute assignment count, then it's a complex creation.
8
+ # See the best practice details here http://rails-bestpractices.com/posts/6-replace-complex-creation-with-factory-method.
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # none
14
+ #
15
+ # Review process:
16
+ # check all method defines in the controller files,
17
+ # if there are multiple attribute assignments apply to one subject,
18
+ # and the subject is a local variable or an instance variable,
19
+ # and after them there is a call node with message :save or :save!,
20
+ # then these attribute assignments are complex creation, should be replaced with factory method.
10
21
  class ReplaceComplexCreationWithFactoryMethodCheck < Check
11
-
12
- def interesting_nodes
22
+
23
+ def interesting_review_nodes
13
24
  [:defn]
14
25
  end
15
-
16
- def interesting_files
26
+
27
+ def interesting_review_files
17
28
  CONTROLLER_FILES
18
29
  end
19
-
30
+
20
31
  def initialize(options = {})
21
32
  super()
22
33
  @attrasgn_count = options['attribute_assignment_count'] || 2
23
34
  end
24
-
25
- def evaluate_start(node)
26
- @variables = {}
27
- node.recursive_children do |child|
28
- case child.node_type
35
+
36
+ # check method define node to see if there are multiple attribute assignments, more than @attrasgn_count, on one local variable or instance variable before save in review process.
37
+ #
38
+ # it wll check every attrasgn nodes in method define node,
39
+ # if there are multiple attrasgn nodes who have the same subject,
40
+ # and the subject is a local variable or an instance variable,
41
+ # and after them, there is a call node with message :save or :save!,
42
+ # then these attribute assignments are complex creation, should be replaced with factory method.
43
+ def review_start_defn(node)
44
+ node.recursive_children do |child_node|
45
+ case child_node.node_type
29
46
  when :attrasgn
30
- attribute_assignment(child)
47
+ remember_variable_use_count(child_node)
31
48
  when :call
32
- call_assignment(child)
49
+ check_variable_save(child_node)
33
50
  else
34
51
  end
35
52
  end
36
- @variables = nil
53
+ reset_variable_use_count
37
54
  end
38
-
55
+
39
56
  private
40
-
41
- def attribute_assignment(node)
42
- variable = node.subject
43
- return if variable.nil? or ![:lvar, :ivar].include? node.subject.node_type
44
- @variables[variable] ||= 0
45
- @variables[variable] += 1
46
- end
47
-
48
- def call_assignment(node)
49
- if node.message == :save
50
- variable = node.subject
51
- add_error "replace complex creation with factory method (#{variable} attribute_assignment_count > #{@attrasgn_count})" if @variables[variable] > @attrasgn_count
57
+ # check the call node to see if it is with message :save or :save!,
58
+ # and the count attribute assignment on the subject of the call node is greater than @attrasgn_count defined,
59
+ # then it is a complex creation, should be replaced with factory method.
60
+ def check_variable_save(node)
61
+ if [:save, :save!].include? node.message
62
+ variable = node.subject
63
+ if variable_use_count[variable] > @attrasgn_count
64
+ add_error "replace complex creation with factory method (#{variable} attribute_assignment_count > #{@attrasgn_count})"
65
+ end
66
+ end
52
67
  end
53
- end
54
68
  end
55
69
  end
56
70
  end
@@ -5,18 +5,29 @@ module RailsBestPractices
5
5
  module Checks
6
6
  # Check a partail view file to make sure there is no instance variable.
7
7
  #
8
- # Implementation: Check all instance variable, if exists, then it should be replaced with local variable
8
+ # See the best practice details here http://rails-bestpractices.com/posts/27-replace-instance-variable-with-local-variable.
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # none
14
+ #
15
+ # Review process:
16
+ # check all instance variable in partial view files,
17
+ # if exist, then they should be replaced with local variable
9
18
  class ReplaceInstanceVariableWithLocalVariableCheck < Check
10
-
11
- def interesting_nodes
19
+
20
+ def interesting_review_nodes
12
21
  [:ivar]
13
22
  end
14
-
15
- def interesting_files
23
+
24
+ def interesting_review_files
16
25
  PARTIAL_VIEW_FILES
17
26
  end
18
-
19
- def evaluate_start(node)
27
+
28
+ # check ivar node in partial view file,
29
+ # it is an instance variable, and should be replaced with local variable.
30
+ def review_start_ivar(node)
20
31
  add_error "replace instance variable with local variable"
21
32
  end
22
33
  end
@@ -3,39 +3,109 @@ require 'rails_best_practices/checks/check'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
- # Check a controller file to make sure to use before_filter to remove duplicate call in different action.
6
+ # Check a controller file to make sure to use before_filter to remove duplicated first code line in different action.
7
7
  #
8
- # Implementation: Check all methods' first call, if they are duplicate, then should use before_filter.
8
+ # See the best practice detailed here http://rails-bestpractices.com/posts/22-use-before_filter.
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # none
14
+ #
15
+ # Review process:
16
+ # check all first code line in method definitions (actions),
17
+ # if they are duplicated, then they should be moved to before_filter.
9
18
  class UseBeforeFilterCheck < Check
10
19
 
11
- def interesting_nodes
20
+ def interesting_review_nodes
12
21
  [:class]
13
22
  end
14
23
 
15
- def interesting_files
24
+ def interesting_review_files
16
25
  CONTROLLER_FILES
17
26
  end
18
27
 
19
- def evaluate_start(node)
20
- @methods = {}
21
- node.grep_nodes({:node_type => :defn}).each { |method_node| remember_method(method_node) }
22
- @methods.each do |first_call, method_nodes|
23
- if method_nodes.size > 1
24
- add_error "use before_filter for #{method_nodes.collect{|method_node| method_node.message_name}.join(',')}",
25
- node.file, method_nodes.collect{|method_node| method_node.line}.join(',')
28
+ # check class define node to see if there are method define nodes whose first code line are duplicated in review process.
29
+ #
30
+ # it will every defn nodes in the class node,
31
+ # if there are defn nodes who have the same first code line, like
32
+ #
33
+ # s(:class, :PostsController, s(:const, :ApplicationController),
34
+ # s(:scope,
35
+ # s(:block,
36
+ # s(:defn, :show, s(:args),
37
+ # s(:scope,
38
+ # s(:block,
39
+ # s(:iasgn, :@post,
40
+ # s(:call,
41
+ # s(:call, s(:call, nil, :current_user, s(:arglist)), :posts, s(:arglist)),
42
+ # :find,
43
+ # s(:arglist,
44
+ # s(:call, s(:call, nil, :params, s(:arglist)), :[], s(:arglist, s(:lit, :id)))
45
+ # )
46
+ # )
47
+ # )
48
+ # )
49
+ # )
50
+ # ),
51
+ # s(:defn, :edit, s(:args),
52
+ # s(:scope,
53
+ # s(:block,
54
+ # s(:iasgn, :@post,
55
+ # s(:call,
56
+ # s(:call, s(:call, nil, :current_user, s(:arglist)), :posts, s(:arglist)),
57
+ # :find,
58
+ # s(:arglist,
59
+ # s(:call, s(:call, nil, :params, s(:arglist)), :[], s(:arglist, s(:lit, :id)))
60
+ # )
61
+ # )
62
+ # )
63
+ # )
64
+ # )
65
+ # )
66
+ # )
67
+ # )
68
+ # )
69
+ #
70
+ # then these duplicated first code lines should be moved to before_filter.
71
+ def review_start_class(class_node)
72
+ @first_sentences = {}
73
+ class_node.grep_nodes({:node_type => :defn}) { |defn_node| remember_first_sentence(defn_node) }
74
+ @first_sentences.each do |first_sentence, defn_nodes|
75
+ if defn_nodes.size > 1
76
+ add_error "use before_filter for #{defn_nodes.collect(&:method_name).join(',')}", class_node.file, defn_nodes.collect(&:line).join(',')
26
77
  end
27
78
  end
28
79
  end
29
80
 
30
81
  private
31
-
32
- def remember_method(method_node)
33
- first_call = method_node.body[1]
34
- unless first_call == s(:nil)
35
- @methods[first_call] ||= []
36
- @methods[first_call] << method_node
82
+ # check method define node, and remember the first sentence.
83
+ # first sentence may be :iasgn, :lasgn, :attrasgn, :call node, like
84
+ #
85
+ # s(:defn, :show, s(:args),
86
+ # s(:scope,
87
+ # s(:block,
88
+ # s(:iasgn, :@post,
89
+ # s(:call,
90
+ # s(:call, s(:call, nil, :current_user, s(:arglist)), :posts, s(:arglist)),
91
+ # :find,
92
+ # s(:arglist,
93
+ # s(:call, s(:call, nil, :params, s(:arglist)), :[], s(:arglist, s(:lit, :id)))
94
+ # )
95
+ # )
96
+ # )
97
+ # )
98
+ # )
99
+ # )
100
+ #
101
+ # the first sentence of defn node is :iasgn node.
102
+ def remember_first_sentence(defn_node)
103
+ first_sentence = defn_node.body[1]
104
+ unless first_sentence == s(:nil)
105
+ @first_sentences[first_sentence] ||= []
106
+ @first_sentences[first_sentence] << defn_node
107
+ end
37
108
  end
38
- end
39
109
  end
40
110
  end
41
111
  end