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
@@ -5,19 +5,40 @@ module RailsBestPractices
5
5
  module Checks
6
6
  # Check config/deploy.rb file to make sure using the bundler's capistrano recipe.
7
7
  #
8
- # Implementation: check the method call,
9
- # if there is a method call "namespace" with argument ":bundler", then it should use bundler's capistrano recipe.
8
+ # See the best practice details here http://rails-bestpractices.com/posts/51-dry-bundler-in-capistrano
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # none
14
+ #
15
+ # Review process:
16
+ # only check the call nodes to see if there is bundler namespace in config/deploy.rb file,
17
+ #
18
+ # if the message of call node is :namespace and the arguments of the call node is :bundler,
19
+ # then it should use bundler's capistrano recipe.
10
20
  class DryBundlerInCapistranoCheck < Check
11
21
 
12
- def interesting_nodes
22
+ def interesting_review_nodes
13
23
  [:call]
14
24
  end
15
25
 
16
- def interesting_files
26
+ def interesting_review_files
17
27
  /config\/deploy.rb/
18
28
  end
19
29
 
20
- def evaluate_start(node)
30
+ # check call node in review process to see if it is with message :namespace and arguments :bundler.
31
+ #
32
+ # the ruby code is
33
+ #
34
+ # namespace :bundler do
35
+ # ...
36
+ # end
37
+ #
38
+ # then the call node is as follows
39
+ #
40
+ # s(:call, nil, :namespace, s(:arglist, s(:lit, :bundler)))
41
+ def review_start_call(node)
21
42
  if :namespace == node.message and equal?(node.arguments[1], "bundler")
22
43
  add_error "dry bundler in capistrano"
23
44
  end
@@ -3,16 +3,33 @@ require 'rails_best_practices/checks/check'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
- # Check a migration file to make sure not to insert data in migration, move them to seed file.
6
+ # Make sure not to insert data in migration, move them to seed file.
7
7
  #
8
- # Implementation: check if there are :create, :create!, and :new with :save or :save! exist, the migration file needs isolate seed data.
8
+ # See the best practice details here http://rails-bestpractices.com/posts/20-isolating-seed-data.
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # none
14
+ #
15
+ # Review process:
16
+ # 1. check all local assignment and instance assignment nodes,
17
+ # if the right value is a call node with message :new,
18
+ # then remember their left value as new variables.
19
+ #
20
+ # 2. check all call nodes,
21
+ # if the message is :create or :create!,
22
+ # then it should be isolated to db seed.
23
+ # if the message is :save or :save!,
24
+ # and the subject is included in new variables,
25
+ # then it should be isolated to db seed.
9
26
  class IsolateSeedDataCheck < Check
10
27
 
11
- def interesting_nodes
28
+ def interesting_review_nodes
12
29
  [:call, :lasgn, :iasgn]
13
30
  end
14
31
 
15
- def interesting_files
32
+ def interesting_review_files
16
33
  MIGRATION_FILES
17
34
  end
18
35
 
@@ -21,27 +38,61 @@ module RailsBestPractices
21
38
  @new_variables = []
22
39
  end
23
40
 
24
- def evaluate_start(node)
41
+ # check local assignment node in review process.
42
+ #
43
+ # if the right value of the node is a call node with :new message,
44
+ # then remember it as new variables (@new_variables).
45
+ def review_start_lasgn(node)
46
+ remember_new_variable(node)
47
+ end
48
+
49
+ # check instance assignment node in review process.
50
+ #
51
+ # if the right value of the node is a call node with :new message,
52
+ # then remember it as new variables (@new_variables).
53
+ def review_start_iasgn(node)
54
+ remember_new_variable(node)
55
+ end
56
+
57
+ # check the call node in review process.
58
+ #
59
+ # if the message of the call node is :create or :create!,
60
+ # then you should isolate it to seed data.
61
+ #
62
+ # if the message of the call node is :save or :save!,
63
+ # and the subject of the call node is included in @new_variables,
64
+ # then you should isolate it to seed data.
65
+ def review_start_call(node)
25
66
  if [:create, :create!].include? node.message
26
67
  add_error("isolate seed data")
27
- elsif [:lasgn, :iasgn].include? node.node_type
28
- remember_new_variable(node)
29
68
  elsif [:save, :save!].include? node.message
30
69
  add_error("isolate seed data") if new_record?(node)
31
70
  end
32
71
  end
33
72
 
34
73
  private
35
-
36
- def remember_new_variable(node)
37
- unless node.grep_nodes({:node_type => :call, :message => :new}).empty?
38
- @new_variables << node.left_value.to_s
74
+ # check local assignment or instance assignment node,
75
+ # if the right vavlue is a call node with message :new,
76
+ # then remember the left value as new variable.
77
+ #
78
+ # if the local variable node is
79
+ #
80
+ # s(:lasgn, :role, s(:call, s(:const, :Role), :new, s(:arglist, s(:hash, s(:lit, :name), s(:lvar, :name)))))
81
+ #
82
+ # then the new variables (@new_variables) is
83
+ #
84
+ # ["role"]
85
+ def remember_new_variable(node)
86
+ right_value = node.right_value
87
+ if :call == right_value.node_type && :new == right_value.message
88
+ @new_variables << node.left_value.to_s
89
+ end
39
90
  end
40
- end
41
91
 
42
- def new_record?(node)
43
- @new_variables.include? node.subject.to_s
44
- end
92
+ # see if the subject of the call node is included in the @new_varaibles.
93
+ def new_record?(node)
94
+ @new_variables.include? node.subject.to_s
95
+ end
45
96
  end
46
97
  end
47
98
  end
@@ -3,28 +3,69 @@ require 'rails_best_practices/checks/check'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
- # Check a model to make sure finders are on their own model.
6
+ # Check model files to ake sure finders are on their own model.
7
7
  #
8
- # Implementation: check if :find is called by other model.
8
+ # See the best practice details here http://rails-bestpractices.com/posts/13-keep-finders-on-their-own-model.
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # none
14
+ #
15
+ # Review process:
16
+ # check all call nodes in model files.
17
+ #
18
+ # if the call node is a finder (find, all, first or last),
19
+ # and the it calls the other model,
20
+ # and there is a hash argument for finder,
21
+ # then it should keep finders on its own model.
9
22
  class KeepFindersOnTheirOwnModelCheck < Check
10
-
11
- def interesting_nodes
23
+
24
+ FINDERS = [:find, :all, :first, :last]
25
+
26
+ def interesting_review_nodes
12
27
  [:call]
13
28
  end
14
29
 
15
- def interesting_files
16
- MODLE_FILES
30
+ def interesting_review_files
31
+ MODEL_FILES
17
32
  end
18
33
 
19
- def evaluate_start(node)
20
- add_error "keep finders on their own model" if others_finder?(node)
34
+ # check all the call nodes to see if there is a finder for other model.
35
+ #
36
+ # if the call node is
37
+ #
38
+ # 1. the message of call node is one of the :find, :all, :first or :last
39
+ # 2. the subject of call node is also a call node (it's the other model)
40
+ # 3. the any of its arguments is a hash (complex finder)
41
+ #
42
+ # then it should keep finders on its own model.
43
+ def review_start_call(node)
44
+ add_error "keep finders on their own model" if other_finder?(node)
21
45
  end
22
46
 
23
47
  private
24
-
25
- def others_finder?(node)
26
- [:find, :all, :first, :last].include? node.message and node.subject.node_type == :call and node.arguments.size > 1
27
- end
48
+ # check if the call node is the finder of other model.
49
+ #
50
+ # the message of the node should be one of :find, :all, :first or :last,
51
+ # and the subject of the node should be with message :call (this is the other model),
52
+ # and any of its arguments is a hash, like
53
+ #
54
+ # s(:call,
55
+ # s(:call, s(:self), :comment, s(:arglist)),
56
+ # :find,
57
+ # s(:arglist, s(:lit, :all),
58
+ # s(:hash,
59
+ # s(:lit, :conditions),
60
+ # s(:hash, s(:lit, :is_spam), s(:false)),
61
+ # s(:lit, :limit),
62
+ # s(:lit, 10)
63
+ # )
64
+ # )
65
+ # )
66
+ def other_finder?(node)
67
+ FINDERS.include?(node.message) && :call == node.subject.node_type && node.arguments.children.any? { |node| :hash == node.node_type }
68
+ end
28
69
  end
29
70
  end
30
71
  end
@@ -3,47 +3,76 @@ require 'rails_best_practices/checks/check'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
- # Check to make sure not avoid the law of demeter.
7
- #
8
- # Implementation:
9
- # 1. check all models to record belongs_to and has_one associations
10
- # 2. check if calling belongs_to and has_one association's method or attribute
6
+ # Check to make sure not to avoid the law of demeter.
7
+ #
8
+ # See the best practice details here http://rails-bestpractices.com/posts/15-the-law-of-demeter.
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # only check all model files to save model names and association names.
14
+ #
15
+ # Review process:
16
+ # check all method calls to see if there is method call to the association object.
17
+ # if there is a call node whose subject is an object of model (compare by name),
18
+ # and whose message is an association of that model (also compare by name),
19
+ # and outer the call node, it is also a call node,
20
+ # then it violate the law of demeter.
11
21
  class LawOfDemeterCheck < Check
12
-
13
- def interesting_nodes
14
- [:call, :class]
15
- end
16
22
 
17
- def initialize
18
- super
19
- @associations = {}
23
+ prepare_model_associations
24
+
25
+ def interesting_review_nodes
26
+ [:call]
20
27
  end
21
28
 
22
- def evaluate_start(node)
23
- if node.node_type == :class
24
- remember_association(node)
25
- elsif [:lvar, :ivar].include?(node.subject.subject.node_type) and node.subject != s(:lvar, :_erbout)
26
- add_error "law of demeter" if need_delegate?(node)
29
+ # check the call node in review process,
30
+ #
31
+ # if the subject of the call node is also a call node,
32
+ # and the subject of the subject call node matchs one of the class names,
33
+ # and the message of the subject call node matchs one of the association name with the class name, like
34
+ #
35
+ # s(:call,
36
+ # s(:call, s(:ivar, :@invoice), :user, s(:arglist)),
37
+ # :name,
38
+ # s(:arglist)
39
+ # )
40
+ #
41
+ # then it violates the law of demeter.
42
+ def review_start_call(node)
43
+ if [:lvar, :ivar].include?(node.subject.subject.node_type) && need_delegate?(node)
44
+ add_error "law of demeter"
27
45
  end
28
46
  end
29
47
 
30
48
  private
31
-
32
- # remember belongs_to or has_one node
33
- def remember_association(node)
34
- (node.body.grep_nodes(:message => :belongs_to) + node.body.grep_nodes(:message => :has_one)).collect do |body_node|
35
- class_name = node.subject.to_s.underscore
36
- @associations[class_name] ||= []
37
- @associations[class_name] << body_node.arguments[1].to_s
49
+ # check if the call node can use delegate to avoid violating law of demeter.
50
+ #
51
+ # if the subject of subject of the call node matchs any in model names,
52
+ # and the message of subject of the call node matchs any in association names,
53
+ # then it needs delegate.
54
+ #
55
+ # e.g. the source code is
56
+ #
57
+ # @invoic.user.name
58
+ #
59
+ # then the call node is
60
+ #
61
+ # s(:call, s(:call, s(:ivar, :@invoice), :user, s(:arglist)), :name, s(:arglist))
62
+ #
63
+ # as you see the subject of subject of the call node is [:ivar, @invoice],
64
+ # and the message of subject of the call node is :user
65
+ def need_delegate?(node)
66
+ @associations.each do |class_name, associations|
67
+ return true if equal?(node.subject.subject, class_name) && associations.find { |association| equal?(association, node.subject.message) }
68
+ end
69
+ false
38
70
  end
39
- end
40
71
 
41
- def need_delegate?(node)
42
- @associations.each do |class_name, associations|
43
- return true if node.subject.subject.to_s =~ /#{class_name}$/ and associations.find { |association| equal?(association, node.subject.message) }
72
+ # only check belongs_to and has_one association.
73
+ def association_methods
74
+ [:belongs_to, :has_one]
44
75
  end
45
- false
46
- end
47
76
  end
48
77
  end
49
78
  end
@@ -3,30 +3,50 @@ require 'rails_best_practices/checks/check'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
- # Check a view file to make sure there is no finder.
6
+ # Check a view file to make sure there is no finder, finder should be moved to controller.
7
7
  #
8
- # Implementation: Check if view file contains finder, then the code should move to controller.
8
+ # See the best practice details here http://rails-bestpractices.com/posts/24-move-code-into-controller.
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # none
14
+ #
15
+ # Review process:
16
+ # only check all view files to see if there are finders, then the finders should be moved to controller.
9
17
  class MoveCodeIntoControllerCheck < Check
10
-
11
- FINDER = [:find, :all, :first, :last]
12
-
13
- def interesting_nodes
18
+
19
+ FINDERS = [:find, :all, :first, :last]
20
+
21
+ def interesting_review_nodes
14
22
  [:call]
15
23
  end
16
-
17
- def interesting_files
24
+
25
+ def interesting_review_files
18
26
  VIEW_FILES
19
27
  end
20
-
21
- def evaluate_start(node)
28
+
29
+ # check call nodes in review process.
30
+ #
31
+ # if the subject of the call node is a constant,
32
+ # and the message of the call node is one of the :find, :all, :first and :last,
33
+ # then it is a finder and should be moved to controller.
34
+ def review_start_call(node)
22
35
  add_error "move code into controller" if finder?(node)
23
36
  end
24
-
25
- private
26
37
 
27
- def finder?(node)
28
- node.subject.node_type == :const && FINDER.include?(node.message)
29
- end
38
+ private
39
+ # check if the node is a finder call node.
40
+ # e.g. the following call node is a finder
41
+ #
42
+ # s(:call,
43
+ # s(:const, :Post),
44
+ # :find,
45
+ # s(:arglist, s(:lit, :all))
46
+ # )
47
+ def finder?(node)
48
+ :const == node.subject.node_type && FINDERS.include?(node.message)
49
+ end
30
50
  end
31
51
  end
32
52
  end
@@ -5,14 +5,29 @@ module RailsBestPractices
5
5
  module Checks
6
6
  # Check a view file to make sure there is no complex options_for_select message call.
7
7
  #
8
- # Implementation: Check if first argument of options_for_select is an array and contains more than two nodes, then it should be moved into helper.
8
+ # See the best practice details here http://rails-bestpractices.com/posts/26-move-code-into-helper.
9
+ #
10
+ # TODO: we need a better soluation, any suggestion?
11
+ #
12
+ # Implementation:
13
+ #
14
+ # Prepare process:
15
+ # none
16
+ #
17
+ # Review process:
18
+ # check al method calls to see if there is a complex options_for_select helper.
19
+ #
20
+ # if the message of the call node is options_for_select,
21
+ # and the first argument of the call node is array,
22
+ # and the size of the array is greater than array_count defined,
23
+ # then the options_for_select method should be moved into helper.
9
24
  class MoveCodeIntoHelperCheck < Check
10
-
11
- def interesting_nodes
25
+
26
+ def interesting_review_nodes
12
27
  [:call]
13
28
  end
14
-
15
- def interesting_files
29
+
30
+ def interesting_review_files
16
31
  VIEW_FILES
17
32
  end
18
33
 
@@ -21,15 +36,44 @@ module RailsBestPractices
21
36
  @array_count = options['array_count'] || 3
22
37
  end
23
38
 
24
- def evaluate_start(node)
39
+ # check call node with message options_for_select (sorry we only check options_for_select helper now).
40
+ #
41
+ # if the first argument of options_for_select method call is an array,
42
+ # and the size of the array is more than @array_count defined,
43
+ # then the options_for_select helper should be moved into helper.
44
+ def review_start_call(node)
25
45
  add_error "move code into helper (array_count >= #{@array_count})" if complex_select_options?(node)
26
46
  end
27
-
47
+
28
48
  private
29
-
30
- def complex_select_options?(node)
31
- :options_for_select == node.message and :array == node.arguments[1].node_type and node.arguments[1].size > @array_count
32
- end
49
+ # check if the arguments of options_for_select are complex.
50
+ #
51
+ # if the first argument is an array,
52
+ # and the size of array is greater than @array_count you defined,
53
+ # then it is complext, e.g.
54
+ #
55
+ # s(:call, nil, :options_for_select,
56
+ # s(:arglist,
57
+ # s(:array,
58
+ # s(:array,
59
+ # s(:call, nil, :t, s(:arglist, s(:lit, :draft))),
60
+ # s(:str, "draft")
61
+ # ),
62
+ # s(:array,
63
+ # s(:call, nil, :t, s(:arglist, s(:lit, :published))),
64
+ # s(:str, "published")
65
+ # )
66
+ # ),
67
+ # s(:call,
68
+ # s(:call, nil, :params, s(:arglist)),
69
+ # :[],
70
+ # s(:arglist, s(:lit, :default_state))
71
+ # )
72
+ # )
73
+ # )
74
+ def complex_select_options?(node)
75
+ :options_for_select == node.message and :array == node.arguments[1].node_type and node.arguments[1].size > @array_count
76
+ end
33
77
  end
34
78
  end
35
- end
79
+ end