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