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,19 +3,42 @@ require 'rails_best_practices/checks/check'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
- # Check a model creation to make sure using model association.
6
+ # Check a controller file to make sure to use model association instead of foreign key id assignment.
7
7
  #
8
- # Implementation:
9
- # 1. check :attrasgn, if xxx_id is assigned to a variable, set the value of the assigned variable to true.
10
- # 2. check :call, if call message :save and caller is included in variables, add error.
8
+ # See the best practice details here http://rails-bestpractices.com/posts/2-use-model-association.
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # none
14
+ #
15
+ # Review process:
16
+ # check model define nodes in all controller files,
17
+ # if there is an attribute assignment node with message xxx_id=,
18
+ # and after it, there is a call node with message :save or :save!,
19
+ # and the subjects of attribute assignment node and call node are the same,
20
+ # then model association should be used instead of xxx_id assignment.
11
21
  class UseModelAssociationCheck < Check
12
-
13
- def interesting_nodes
22
+
23
+ def interesting_review_nodes
14
24
  [:defn]
15
25
  end
16
26
 
17
- def evaluate_start(node)
18
- @variables = {}
27
+ def interesting_review_files
28
+ CONTROLLER_FILES
29
+ end
30
+
31
+ # check method define nodes to see if there are some attribute assignments that can use model association instead in review process.
32
+ #
33
+ # it will check attribute assignment node with message xxx_id=, and call node with message :save or :save!
34
+ #
35
+ # 1. if there is an attribute assignment node with message xxx_id=,
36
+ # then remember the subject of attribute assignment node.
37
+ # 2. after assignment, if there is a call node with message :save or :save!,
38
+ # and the subject of call node is one of the subject of attribute assignment node,
39
+ # then the attribute assignment should be replaced by using model association.
40
+ def review_start_defn(node)
41
+ @attrasgns = {}
19
42
  node.recursive_children do |child|
20
43
  case child.node_type
21
44
  when :attrasgn
@@ -25,24 +48,40 @@ module RailsBestPractices
25
48
  else
26
49
  end
27
50
  end
28
- @variables = nil
51
+ @attrasgns = nil
29
52
  end
30
-
53
+
31
54
  private
32
-
33
- def attribute_assignment(node)
34
- if node.message.to_s =~ /_id=$/
35
- variable = node.subject[1]
36
- @variables[variable] = true
55
+ # check an attribute assignment node, if its message is xxx_id, like
56
+ #
57
+ # s(:attrasgn, s(:ivar, :@post), :user_id=,
58
+ # s(:arglist,
59
+ # s(:call, s(:call, nil, :current_user, s(:arglist)), :id, s(:arglist))
60
+ # )
61
+ # )
62
+ #
63
+ # then remember the subject of the attribute assignment in @attrasgns.
64
+ #
65
+ # @attrasgns => { s(:ivar, :@post) => true }
66
+ def attribute_assignment(node)
67
+ if node.message.to_s =~ /_id=$/
68
+ subject = node.subject
69
+ @attrasgns[subject] = true
70
+ end
37
71
  end
38
- end
39
-
40
- def call_assignment(node)
41
- if node.message == :save
42
- variable = node.subject[1]
43
- add_error "use model association (for #{node.subject})" if @variables[variable]
72
+
73
+ # check a call node with message :save or :save!,
74
+ # if the subject of call node exists in @attrasgns, like
75
+ #
76
+ # s(:call, s(:ivar, :@post), :save, s(:arglist))
77
+ #
78
+ # then the attribute assignment should be replaced by using model association.
79
+ def call_assignment(node)
80
+ if [:save, :save!].include? node.message
81
+ subject = node.subject
82
+ add_error "use model association (for #{subject})" if @attrasgns[subject]
83
+ end
44
84
  end
45
- end
46
85
  end
47
86
  end
48
87
  end
@@ -3,51 +3,153 @@ require 'rails_best_practices/checks/check'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
- # Check a model file to make sure mail deliver method is in observer not callback.
6
+ # Make sure to use observer (sorry we only check the mailer deliver now).
7
7
  #
8
- # Implementation:
9
- # Record :after_create callback
10
- # Check method define, if it is a callback and call deliver_xxx message in method body, then it should use observer.
8
+ # See the best practice details here http://rails-bestpractices.com/posts/19-use-observer.
9
+ #
10
+ # TODO: we need a better solution, any suggestion?
11
+ #
12
+ # Implementation:
13
+ #
14
+ # Prepare process:
15
+ # check all class nodes to see if they are the subclass of ActionMailer::Base,
16
+ # if so, remember the class name.
17
+ #
18
+ # Review process:
19
+ # check all call nodes to see if they are callback definitions, like after_create, before_destroy,
20
+ # if so, remember the callback methods.
21
+ #
22
+ # check all method define nodes to see
23
+ # if the method is a callback method,
24
+ # and there is a mailer deliver call,
25
+ # then the method should be replaced by using observer.
11
26
  class UseObserverCheck < Check
12
27
 
13
- def interesting_nodes
28
+ def interesting_prepare_nodes
29
+ [:class]
30
+ end
31
+
32
+ def interesting_review_nodes
14
33
  [:defn, :call]
15
34
  end
16
35
 
17
- def interesting_files
18
- MODLE_FILES
36
+ def interesting_prepare_files
37
+ /#{MAILER_FILES}|#{MODEL_FILES}/
38
+ end
39
+
40
+ def interesting_review_files
41
+ MODEL_FILES
19
42
  end
20
43
 
21
44
  def initialize
22
45
  super
23
46
  @callbacks = []
47
+ @mailer_names = []
48
+ end
49
+
50
+ # check class node in prepare process.
51
+ #
52
+ # if it is a subclass of ActionMailer::Base,
53
+ # then remember its class name.
54
+ def prepare_start_class(node)
55
+ remember_mailer_names(node)
56
+ end
57
+
58
+ # check a call node in review process.
59
+ #
60
+ # if it is a callback definition, like
61
+ #
62
+ # after_create :send_create_notification
63
+ # before_destroy :send_destroy_notification
64
+ #
65
+ # then remember its callback methods (:send_create_notification).
66
+ def review_start_call(node)
67
+ remember_callback(node)
24
68
  end
25
69
 
26
- def evaluate_start(node)
27
- if :after_create == node.message
28
- remember_callbacks(node)
29
- elsif :defn == node.node_type and @callbacks.find { |callback| equal?(callback, node.message_name) }
30
- add_error "use observer" if use_observer?(node)
70
+ # check a method define node in prepare process.
71
+ #
72
+ # if it is callback method,
73
+ # and there is a actionmailer deliver call in the method define node,
74
+ # then it should be replaced by using observer.
75
+ def review_start_defn(node)
76
+ if callback_method?(node) and deliver_mailer?(node)
77
+ add_error "use observer"
31
78
  end
32
79
  end
33
80
 
34
81
  private
82
+ # check a class node, if its base class is ActionMailer::Base, like
83
+ #
84
+ # s(:class, :ProjectMailer,
85
+ # s(:colon2, s(:const, :ActionMailer), :Base),
86
+ # s(:scope)
87
+ # )
88
+ #
89
+ # then save the class name in @mailer_names
90
+ def remember_mailer_names(node)
91
+ if s(:colon2, s(:const, :ActionMailer), :Base) == node.base_class
92
+ @mailer_names << node.class_name.to_s
93
+ end
94
+ end
35
95
 
36
- def remember_callbacks(node)
37
- node.arguments[1..-1].each do |argument|
38
- # ignore callback like after_create Comment.new
39
- if :lit == argument.node_type
40
- @callbacks << argument.to_s
96
+ # check a call node, if it is a callback definition, such as after_create, before_create, like
97
+ #
98
+ # s(:call, nil, :after_create,
99
+ # s(:arglist, s(:lit, :send_create_notification))
100
+ # )
101
+ #
102
+ # then save the callback methods in @callbacks
103
+ #
104
+ # @callbacks => [:send_create_notification]
105
+ def remember_callback(node)
106
+ if node.message.to_s =~ /^after_|^before_/
107
+ node.arguments[1..-1].each do |argument|
108
+ # ignore callback like after_create Comment.new
109
+ @callbacks << argument.to_s if :lit == argument.node_type
110
+ end
41
111
  end
42
112
  end
43
- end
44
113
 
45
- def use_observer?(node)
46
- node.recursive_children do |child|
47
- return true if :call == child.node_type and :const == child.subject.node_type and child.message.to_s =~ /^deliver_/
114
+ # check a defn node to see if the method name exists in the @callbacks.
115
+ def callback_method?(node)
116
+ @callbacks.find { |callback| equal?(callback, node.method_name) }
117
+ end
118
+
119
+ # check a defn node to see if it contains a actionmailer deliver call.
120
+ #
121
+ # for rails2
122
+ #
123
+ # if the message of call node is deliver_xxx,
124
+ # and the subject of the call node exists in @callbacks, like
125
+ #
126
+ # s(:call, s(:const, :ProjectMailer), :deliver_notification,
127
+ # s(:arglist, s(:self), s(:lvar, :member))
128
+ # )
129
+ #
130
+ # for rails3
131
+ #
132
+ # if the message of call node is deliver,
133
+ # and the subject of the call node is with subject node who exists in @callbacks, like
134
+ #
135
+ # s(:call,
136
+ # s(:call, s(:const, :ProjectMailer), :notification,
137
+ # s(:arglist, s(:self), s(:lvar, :member))
138
+ # ),
139
+ # :deliver,
140
+ # s(:arglist)
141
+ # )
142
+ #
143
+ # then the call node is actionmailer deliver call.
144
+ def deliver_mailer?(node)
145
+ node.grep_nodes(:node_type => :call) do |child_node|
146
+ # rails2 actionmailer deliver
147
+ return true if child_node.message.to_s =~ /^deliver_/ && @mailer_names.include?(child_node.subject.to_s)
148
+ # rails3 actionmailer deliver
149
+ return true if :deliver == child_node.message && @mailer_names.include?(child_node.subject.subject.to_s)
150
+ end
151
+ false
48
152
  end
49
- false
50
- end
51
153
  end
52
154
  end
53
155
  end
@@ -3,43 +3,46 @@ require 'rails_best_practices/checks/check'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
- # Check to make sure use query attribute instead of nil?, blank? and present?.
6
+ # Make sure to use query attribute instead of nil?, blank? and present?.
7
+ #
8
+ # See the best practice details here http://rails-bestpractices.com/posts/56-use-query-attribute.
7
9
  #
8
10
  # Implementation:
9
- # 1. check all models to save model names and association names.
10
- # model names are used for detecting
11
- # association name should not be detected as query attribute
12
- # 2. check all method calls, if their subjects are model names and their messages are one of nil?,
13
- # blank?, present? or == "", not pluralize and not in the association names,
14
- # then they need to use query attribute.
11
+ #
12
+ # Prepare process:
13
+ # only check all model files to save model names and association names,
14
+ # model names are saved as only when subject of call method is equal to one of the model name, then the call method may use query attribute instead,
15
+ # association names are saved as association attributes should not be detected as query attributes.
16
+ #
17
+ # Review process:
18
+ # check all method calls within conditional statements, like @user.login.nil?
19
+ # if their subjects are one of the model names
20
+ # and their messages of first call are not pluralize and not in any of the association names
21
+ # and their messages of second call are one of nil?, blank?, present?, or they are == ""
22
+ # then you can use query attribute instead.
15
23
  class UseQueryAttributeCheck < Check
16
24
 
17
25
  QUERY_METHODS = [:nil?, :blank?, :present?]
18
- ASSOCIATION_METHODS = [:belongs_to, :has_one, :has_many, :has_and_belongs_to_many]
19
26
 
20
- def interesting_nodes
21
- [:if, :class, :call]
22
- end
23
-
24
- def interesting_prepare_files
25
- MODLE_FILES
26
- end
27
+ prepare_model_associations
27
28
 
28
- def initialize
29
- super
30
- @klazzes = []
31
- @associations = {}
29
+ def interesting_review_nodes
30
+ [:if]
32
31
  end
33
32
 
34
- def prepare_start_class(node)
35
- remember_klazz(node)
36
- end
37
-
38
- def prepare_start_call(node)
39
- remember_association(node) if ASSOCIATION_METHODS.include? node.message
40
- end
41
-
42
- def evaluate_start_if(node)
33
+ # check if node to see whose conditional statement nodes contain nodes that can use query attribute instead in review process.
34
+ #
35
+ # it will check every call nodes in the if nodes. If the call node is
36
+ #
37
+ # 1. two method calls, like @user.login.nil?
38
+ # 2. the subject is one of the model names
39
+ # 3. the message of first call is the model's attribute,
40
+ # the message is not in any of associations name and is not pluralize
41
+ # 4. the message of second call is one of nil?, blank? or present? or
42
+ # the message is == and the argument is ""
43
+ #
44
+ # then the call node can use query attribute instead.
45
+ def review_start_if(node)
43
46
  if node = query_attribute_node(node.conditional_statement)
44
47
  subject_node = node.subject
45
48
  add_error "use query attribute (#{subject_node.subject}.#{subject_node.message}?)", node.file, node.line
@@ -47,17 +50,7 @@ module RailsBestPractices
47
50
  end
48
51
 
49
52
  private
50
- def remember_klazz(class_node)
51
- if class_node.file =~ MODLE_FILES
52
- @klazzes << class_node.subject
53
- end
54
- end
55
-
56
- def remember_association(association_node)
57
- @associations[@klazzes.last] ||= []
58
- @associations[@klazzes.last] << association_node.arguments[1].to_s
59
- end
60
-
53
+ # recursively check conditional statement nodes to see if there is a call node that may be possible query attribute.
61
54
  def query_attribute_node(conditional_statement_node)
62
55
  case conditional_statement_node.node_type
63
56
  when :and, :or
@@ -65,25 +58,60 @@ module RailsBestPractices
65
58
  when :not
66
59
  return query_attribute_node(conditional_statement_node[1])
67
60
  when :call
68
- return conditional_statement_node if query_method?(conditional_statement_node) or compare_with_empty_string?(conditional_statement_node)
61
+ return conditional_statement_node if possible_query_attribute?(conditional_statement_node)
69
62
  end
70
63
  nil
71
64
  end
72
65
 
73
- def query_method?(node)
66
+ # check if the node may use query attribute instead.
67
+ #
68
+ # if the node contains two method calls, e.g. @user.login.nil?
69
+ #
70
+ # for the first call, the subject should be one of the class names and
71
+ # the message should not be one of the association name and the message should not be pluralize.
72
+ #
73
+ # for the second call, the message should be one of nil?, blank? or present? or
74
+ # it is compared with an empty string.
75
+ #
76
+ # the node that may use query attribute is like
77
+ #
78
+ # s(:call, s(:call, s(:ivar, :@user), :login, s(:arglist)), :nil?, s(:arglist))
79
+ #
80
+ #
81
+ def possible_query_attribute?(node)
74
82
  return false unless :call == node.subject.node_type
75
83
  subject = node.subject.subject
76
84
  message = node.subject.message
77
- subject_ruby = subject.to_s
78
85
 
79
- subject_ruby && node.subject.arguments.size == 1 &&
80
- @klazzes.find { |klazz| subject_ruby =~ %r|#{klazz.to_s.underscore}| and !@associations[klazz].find { |association| equal?(association, message) } } &&
81
- message && message.to_s.pluralize != message.to_s &&
82
- QUERY_METHODS.include?(node.message)
86
+ [:arglist] == node.subject.arguments && class_attribute?(subject, message) && !pluralize?(message.to_s) &&
87
+ (QUERY_METHODS.include?(node.message) || compare_with_empty_string?(node))
88
+ end
89
+
90
+ # check if the subject and message is one of the model's attribute.
91
+ # the subject should match one of the class model name, and the message should not match any of association name.
92
+ #
93
+ # subject, subject of call node, like
94
+ # s(:ivar, @user)
95
+ #
96
+ # message, message of call node, like
97
+ # :login
98
+ def class_attribute?(subject, message)
99
+ @klazzes.find do |klazz|
100
+ subject.to_s =~ %r|#{klazz.to_s.underscore}| && !@associations[klazz].find { |association| equal?(association, message) }
101
+ end
102
+ end
103
+
104
+
105
+ # check if the str is a pluralize string.
106
+ def pluralize?(str)
107
+ str.pluralize == str
83
108
  end
84
109
 
110
+ # check if the node is with node type :call, node message :== and node arguments {:arglist, (:str, "")}
111
+ #
112
+ # @user.login == "" => true
85
113
  def compare_with_empty_string?(node)
86
- :== == node.message and [:arglist, [:str, ""]] == node.arguments
114
+ :call == node.node_type && :== == node.message && [:arglist, [:str, ""]] == node.arguments
87
115
  end
88
116
  end
89
117
  end
@@ -3,26 +3,75 @@ require 'rails_best_practices/checks/check'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
- # Check a migration file to make sure to use say_with_time for customized data changes to produce a more readable output.
6
+ # Check a migration file to make sure to use say or say_with_time for customized data changes to produce a more readable output.
7
7
  #
8
- # Implementation: check if there are any first level messages called in self.up and self.down except say_with_time and default migration messages (such as :add_column and :create_table)
8
+ # See the best practice detials here http://rails-bestpractices.com/posts/46-use-say-and-say_with_time-in-migrations-to-make-a-useful-migration-log.
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # none
14
+ #
15
+ # Review process:
16
+ # check class method define nodes (self.up or self.down).
17
+ # if there is a method call in the class method definition,
18
+ # and the message of method call is not say, say_with_time and default migration methods (such as add_column and create_table),
19
+ # then the method call should be wrapped by say or say_with_time.
9
20
  class UseSayWithTimeInMigrationsCheck < Check
10
21
 
11
- DEFAULT_MIGRATION_MESSAGES = [:add_column, :add_index, :add_timestamps, :change_column, :change_column_default, :change_table, :create_table, :drop_table, :remove_column, :remove_index, :remove_timestamps, :rename_column, :rename_index, :rename_table]
22
+ DEFAULT_MIGRATION_METHODS = [:add_column, :add_index, :add_timestamps, :change_column, :change_column_default, :change_table, :create_table, :drop_table, :remove_column, :remove_index, :remove_timestamps, :rename_column, :rename_index, :rename_table]
23
+ WITH_SAY_METHODS = DEFAULT_MIGRATION_METHODS + [:say, :say_with_time]
24
+
12
25
 
13
- def interesting_nodes
26
+ def interesting_review_nodes
14
27
  [:defs]
15
28
  end
16
29
 
17
- def interesting_files
30
+ def interesting_review_files
18
31
  MIGRATION_FILES
19
32
  end
20
33
 
21
- def evaluate_start(node)
22
- block_body = node.grep_nodes(:node_type => :block).first.body
23
- block_body.each do |iter|
24
- if :iter == iter.node_type and :call == iter[1].node_type and !(DEFAULT_MIGRATION_MESSAGES << :say_with_time).include? iter[1].message
25
- add_error("use say with time in migrations", iter[1].file, iter[1].line)
34
+ # check a class method define node to see if there are method calls that need to be wrapped by :say or :say_with_time in review process.
35
+ #
36
+ # it will check the first block node,
37
+ # if any method call whose message is not default migration methods in the block node, like
38
+ #
39
+ # s(:defs, s(:self), :up, s(:args),
40
+ # s(:scope,
41
+ # s(:block,
42
+ # s(:iter,
43
+ # s(:call, s(:const, :User), :find_each, s(:arglist)),
44
+ # s(:lasgn, :user),
45
+ # s(:block,
46
+ # s(:masgn,
47
+ # s(:array,
48
+ # s(:attrasgn, s(:lvar, :user), :first_name=, s(:arglist)),
49
+ # s(:attrasgn, s(:lvar, :user), :last_name=, s(:arglist))
50
+ # ),
51
+ # s(:to_ary,
52
+ # s(:call,
53
+ # s(:call, s(:lvar, :user), :full_name, s(:arglist)),
54
+ # :split,
55
+ # s(:arglist, s(:str, " "))
56
+ # )
57
+ # )
58
+ # ),
59
+ # s(:call, s(:lvar, :user), :save, s(:arglist))
60
+ # )
61
+ # )
62
+ # )
63
+ # )
64
+ # )
65
+ #
66
+ # then such method call should be wrapped by say or say_with_time
67
+ def review_start_defs(node)
68
+ block_node = node.grep_node(:node_type => :block)
69
+ block_node.children.each do |child_node|
70
+ if :iter == child_node.node_type
71
+ subject_node = child_node.subject
72
+ if :call == subject_node.node_type && !WITH_SAY_METHODS.include?(subject_node.message)
73
+ add_error("use say with time in migrations", subject_node.file, subject_node.line)
74
+ end
26
75
  end
27
76
  end
28
77
  end