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