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