rails_best_practices 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile.lock +1 -1
  3. data/README.md +2 -0
  4. data/assets/result.html.erb +25 -2
  5. data/lib/rails_best_practices.rb +20 -9
  6. data/lib/rails_best_practices/core.rb +1 -0
  7. data/lib/rails_best_practices/core/check.rb +106 -25
  8. data/lib/rails_best_practices/core/controllers.rb +2 -1
  9. data/lib/rails_best_practices/core/error.rb +3 -2
  10. data/lib/rails_best_practices/core/klasses.rb +34 -0
  11. data/lib/rails_best_practices/core/mailers.rb +2 -1
  12. data/lib/rails_best_practices/core/methods.rb +113 -9
  13. data/lib/rails_best_practices/core/model_associations.rb +17 -0
  14. data/lib/rails_best_practices/core/model_attributes.rb +16 -0
  15. data/lib/rails_best_practices/core/models.rb +3 -2
  16. data/lib/rails_best_practices/core/nil.rb +9 -1
  17. data/lib/rails_best_practices/core/runner.rb +65 -26
  18. data/lib/rails_best_practices/core_ext/sexp.rb +57 -0
  19. data/lib/rails_best_practices/prepares.rb +12 -1
  20. data/lib/rails_best_practices/prepares/controller_prepare.rb +13 -8
  21. data/lib/rails_best_practices/prepares/mailer_prepare.rb +3 -3
  22. data/lib/rails_best_practices/prepares/model_prepare.rb +44 -16
  23. data/lib/rails_best_practices/reviews.rb +1 -0
  24. data/lib/rails_best_practices/reviews/needless_deep_nesting_review.rb +5 -2
  25. data/lib/rails_best_practices/reviews/remove_unused_methods_in_models_review.rb +77 -0
  26. data/lib/rails_best_practices/reviews/restrict_auto_generated_routes_review.rb +2 -2
  27. data/lib/rails_best_practices/reviews/review.rb +1 -1
  28. data/lib/rails_best_practices/version.rb +1 -1
  29. data/rails_best_practices.yml +1 -0
  30. data/spec/fixtures/lib/rails_best_practices/plugins/reviews/not_use_rails_root_review.rb +11 -0
  31. data/spec/rails_best_practices/core/check_spec.rb +22 -0
  32. data/spec/rails_best_practices/core/controllers_spec.rb +1 -1
  33. data/spec/rails_best_practices/core/error_spec.rb +1 -1
  34. data/spec/rails_best_practices/core/klasses_spec.rb +12 -0
  35. data/spec/rails_best_practices/core/mailers_spec.rb +5 -0
  36. data/spec/rails_best_practices/core/methods_spec.rb +26 -4
  37. data/spec/rails_best_practices/core/models_spec.rb +2 -2
  38. data/spec/rails_best_practices/core/runner_spec.rb +13 -0
  39. data/spec/rails_best_practices/core_ext/sexp_spec.rb +26 -2
  40. data/spec/rails_best_practices/prepares/controller_prepare_spec.rb +72 -60
  41. data/spec/rails_best_practices/prepares/mailer_prepare_spec.rb +1 -1
  42. data/spec/rails_best_practices/prepares/model_prepare_spec.rb +150 -59
  43. data/spec/rails_best_practices/reviews/move_model_logic_into_model_review_spec.rb +20 -3
  44. data/spec/rails_best_practices/reviews/needless_deep_nesting_review_spec.rb +14 -0
  45. data/spec/rails_best_practices/reviews/remove_unused_methods_in_models_review_spec.rb +387 -0
  46. metadata +15 -3
@@ -1,21 +1,38 @@
1
1
  # encoding: utf-8
2
2
  module RailsBestPractices
3
3
  module Core
4
+ # Model associations container.
4
5
  class ModelAssociations
5
6
  def initialize
6
7
  @associations = {}
7
8
  end
8
9
 
10
+ # Add a model association.
11
+ #
12
+ # @param [String] model name
13
+ # @param [String] association name
14
+ # @param [String] association meta, has_many, has_one, belongs_to and has_and_belongs_to_many
15
+ # @param [String] association class name
9
16
  def add_association(model_name, association_name, association_meta, association_class=nil)
10
17
  @associations[model_name] ||= {}
11
18
  @associations[model_name][association_name] = {"meta" => association_meta, "class_name" => association_class || association_name.classify}
12
19
  end
13
20
 
21
+ # Get a model association.
22
+ #
23
+ # @param [String] model name
24
+ # @param [String] association name
25
+ # @return [Hash] {"meta" => association_meta, "class_name" => association_class}
14
26
  def get_association(model_name, association_name)
15
27
  associations = @associations[model_name]
16
28
  associations and associations[association_name]
17
29
  end
18
30
 
31
+ # If it is a model's association.
32
+ #
33
+ # @param [String] model name
34
+ # @param [String] association name
35
+ # @return [Boolean] true if it is the model's association
19
36
  def is_association?(model_name, association_name)
20
37
  associations = @associations[model_name]
21
38
  associations && associations[association_name]
@@ -1,21 +1,37 @@
1
1
  # encoding: utf-8
2
2
  module RailsBestPractices
3
3
  module Core
4
+ # Model attributes container.
4
5
  class ModelAttributes
5
6
  def initialize
6
7
  @attributes = {}
7
8
  end
8
9
 
10
+ # Add a model attribute.
11
+ #
12
+ # @param [String] model name
13
+ # @param [String] attribute name
14
+ # @param [String] attribute type
9
15
  def add_attribute(model_name, attribute_name, attribute_type)
10
16
  @attributes[model_name] ||= {}
11
17
  @attributes[model_name][attribute_name] = attribute_type
12
18
  end
13
19
 
20
+ # Get attribute type.
21
+ #
22
+ # @param [String] model name
23
+ # @param [String] attribute name
24
+ # @return [String] attribute type
14
25
  def get_attribute_type(model_name, attribute_name)
15
26
  @attributes[model_name] ||= {}
16
27
  @attributes[model_name][attribute_name]
17
28
  end
18
29
 
30
+ # If it is a model's attribute.
31
+ #
32
+ # @param [String] model name
33
+ # @param [String] attribute name
34
+ # @return [Boolean] true if it is the model's attribute
19
35
  def is_attribute?(model_name, attribute_name)
20
36
  @attributes[model_name] ||= {}
21
37
  !!@attributes[model_name][attribute_name]
@@ -1,7 +1,8 @@
1
1
  # encoding: utf-8
2
2
  module RailsBestPractices
3
3
  module Core
4
- class Models < Array
4
+ # Model classes.
5
+ class Models < Klasses
5
6
  end
6
7
  end
7
- end
8
+ end
@@ -1,16 +1,24 @@
1
1
  # encoding: utf-8
2
2
  module RailsBestPractices
3
3
  module Core
4
+ # Fake nil.
4
5
  class Nil
6
+ # hash_size is 0.
5
7
  def hash_size
6
8
  0
7
9
  end
8
10
 
11
+ # array_size is 0.
12
+ def array_size
13
+ 0
14
+ end
15
+
16
+ # return self for to_s.
9
17
  def to_s
10
18
  self
11
19
  end
12
20
 
13
- # return self
21
+ # return self.
14
22
  def method_missing(method_sym, *arguments, &block)
15
23
  self
16
24
  end
@@ -38,9 +38,10 @@ module RailsBestPractices
38
38
  custom_config = File.join(Runner.base_path, 'config/rails_best_practices.yml')
39
39
  @config = File.exists?(custom_config) ? custom_config : RailsBestPractices::DEFAULT_CONFIG
40
40
 
41
+ lexicals = Array(options[:lexicals])
41
42
  prepares = Array(options[:prepares])
42
43
  reviews = Array(options[:reviews])
43
- @lexicals = load_lexicals
44
+ @lexicals = lexicals.empty? ? load_lexicals : lexicals
44
45
  @prepares = prepares.empty? ? load_prepares : prepares
45
46
  @reviews = reviews.empty? ? load_reviews : reviews
46
47
 
@@ -64,28 +65,48 @@ module RailsBestPractices
64
65
  #
65
66
  # @param [String] filename
66
67
  def lexical_file(filename)
67
- lexical(filename, File.open(filename, "r:UTF-8") { |f| f.read })
68
+ lexical(filename, read_file(filename))
68
69
  end
69
70
 
70
- # prepare and review a file's content with filename.
71
- # the file may be a ruby, erb or haml file.
71
+ # parepare a file's content with filename.
72
72
  #
73
- # filename is the filename of the code.
74
- # content is the source code.
75
- [:prepare, :review].each do |process|
76
- class_eval <<-EOS
77
- def #{process}(filename, content) # def review(filename, content)
78
- puts filename if @debug # puts filename if @debug
79
- content = parse_erb_or_haml(filename, content) # content = parse_erb_or_haml(filename, content)
80
- node = parse_ruby(content) # node = parse_ruby(content)
81
- node.file = filename # node.file = filename
82
- node.#{process}(@checker) if node # node.review(@checker) if node
83
- end # end
84
- #
85
- def #{process}_file(filename) # def review_file(filename)
86
- #{process}(filename, File.open(filename, "r:UTF-8") { |f| f.read }) # review(filename, File.open(filename, "r:UTF-8") { |f| f.read })
87
- end # end
88
- EOS
73
+ # @param [String] filename name of the file
74
+ # @param [String] content content of the file
75
+ def prepare(filename, content)
76
+ puts filename if @debug
77
+ node = parse_ruby(filename, content)
78
+ if node
79
+ node.file = filename
80
+ node.prepare(@checker)
81
+ end
82
+ end
83
+
84
+ # parapare the file.
85
+ #
86
+ # @param [String] filename
87
+ def prepare_file(filename)
88
+ prepare(filename, read_file(filename))
89
+ end
90
+
91
+ # review a file's content with filename.
92
+ #
93
+ # @param [String] filename name of the file
94
+ # @param [String] content content of the file
95
+ def review(filename, content)
96
+ puts filename if @debug
97
+ content = parse_erb_or_haml(filename, content)
98
+ node = parse_ruby(filename, content)
99
+ if node
100
+ node.file = filename
101
+ node.review(@checker)
102
+ end
103
+ end
104
+
105
+ # review the file.
106
+ #
107
+ # @param [String] filename
108
+ def review_file(filename)
109
+ review(filename, read_file(filename))
89
110
  end
90
111
 
91
112
  # get all errors from lexicals and reviews.
@@ -95,11 +116,21 @@ module RailsBestPractices
95
116
  (@reviews + @lexicals).collect {|check| check.errors}.flatten
96
117
  end
97
118
 
119
+ # provide a handler after all files reviewed.
120
+ def on_complete
121
+ filename = "rails_best_practices.complete"
122
+ content = "class RailsBestPractices::Complete; end"
123
+ node = parse_ruby(filename, content)
124
+ node.file = filename
125
+ node.review(@checker)
126
+ end
127
+
98
128
  private
99
129
  # parse ruby code.
100
130
  #
101
- # content is the source code of ruby file.
102
- def parse_ruby(content)
131
+ # @param [String] filename is the filename of ruby file.
132
+ # @param [String] content is the source code of ruby file.
133
+ def parse_ruby(filename, content)
103
134
  begin
104
135
  Sexp.from_array(Ripper::SexpBuilder.new(content).parse)
105
136
  rescue Exception => e
@@ -115,8 +146,8 @@ module RailsBestPractices
115
146
 
116
147
  # parse erb or html code.
117
148
  #
118
- # filename is the filename of the erb or haml code.
119
- # content is the source code of erb or haml file.
149
+ # @param [String] filename is the filename of the erb or haml code.
150
+ # @param [String] content is the source code of erb or haml file.
120
151
  def parse_erb_or_haml(filename, content)
121
152
  if filename =~ /.*\.erb|.*\.rhtml$/
122
153
  content = Erubis::Eruby.new(content).src
@@ -171,12 +202,12 @@ module RailsBestPractices
171
202
  # load all plugin reviews.
172
203
  def load_plugin_reviews
173
204
  begin
174
- plugins = "lib/rails_best_practices/plugins/reviews"
205
+ plugins = "#{Runner.base_path}lib/rails_best_practices/plugins/reviews"
175
206
  if File.directory?(plugins)
176
207
  Dir[File.expand_path(File.join(plugins, "*.rb"))].each do |review|
177
208
  require review
178
209
  end
179
- if RailsBestPractices.constants.include? :Plugins
210
+ if RailsBestPractices.constants.map(&:to_sym).include? :Plugins
180
211
  RailsBestPractices::Plugins::Reviews.constants.each do |review|
181
212
  @reviews << RailsBestPractices::Plugins::Reviews.const_get(review).new
182
213
  end
@@ -189,6 +220,14 @@ module RailsBestPractices
189
220
  def checks_from_config
190
221
  @checks ||= YAML.load_file @config
191
222
  end
223
+
224
+ # read the file content.
225
+ #
226
+ # @param [String] filename
227
+ # @return [String] file conent
228
+ def read_file(filename)
229
+ File.open(filename, "r:UTF-8") { |f| f.read }
230
+ end
192
231
  end
193
232
  end
194
233
  end
@@ -593,6 +593,39 @@ class Sexp
593
593
  end
594
594
  end
595
595
 
596
+ # Get the hash values.
597
+ #
598
+ # s(:hash,
599
+ # s(:assoclist_from_args,
600
+ # s(
601
+ # s(:assoc_new, s(:@label, "first_name:", s(1, 1)), s(:string_literal, s(:string_add, s(:string_content), s(:@tstring_content, "Richard", s(1, 14))))),
602
+ # s(:assoc_new, s(:@label, "last_name:", s(1, 24)), s(:string_literal, s(:string_add, s(:string_content), s(:@tstring_content, "Huang", s(1, 36)))))
603
+ # )
604
+ # )
605
+ # )
606
+ # => [
607
+ # s(:string_literal, s(:string_add, s(:string_content), s(:@tstring_content, "Richard", s(1, 14)))),
608
+ # s(:string_literal, s(:string_add, s(:string_content), s(:@tstring_content, "Huang", s(1, 36))))
609
+ # ]
610
+ #
611
+ # @return [Array] hash values
612
+ def hash_values
613
+ pair_nodes = case sexp_type
614
+ when :bare_assoc_hash
615
+ self[1]
616
+ when :hash
617
+ self[1][1]
618
+ else
619
+ end
620
+ if pair_nodes
621
+ values = []
622
+ pair_nodes.size.times do |i|
623
+ values << pair_nodes[i][2]
624
+ end
625
+ values
626
+ end
627
+ end
628
+
596
629
  # Get the array size.
597
630
  #
598
631
  # s(:array,
@@ -624,6 +657,30 @@ class Sexp
624
657
  end
625
658
  end
626
659
 
660
+ # Get the array values.
661
+ #
662
+ # s(:array,
663
+ # s(:args_add,
664
+ # s(:args_add, s(:args_new), s(:string_literal, s(:string_add, s(:string_content), s(:@tstring_content, "first_name", s(1, 2))))),
665
+ # s(:string_literal, s(:string_add, s(:string_content), s(:@tstring_content, "last_name", s(1, 16))))
666
+ # )
667
+ # )
668
+ # => [
669
+ # s(:string_literal, s(:string_add, s(:string_content), s(:@tstring_content, "first_name", s(1, 2)))),
670
+ # s(:string_literal, s(:string_add, s(:string_content), s(:@tstring_content, "last_name", s(1, 16))))
671
+ # ]
672
+ #
673
+ # @return [Array] array values
674
+ def array_values
675
+ if :array == sexp_type
676
+ if nil == self[1]
677
+ []
678
+ else
679
+ arguments.all
680
+ end
681
+ end
682
+ end
683
+
627
684
  # To object.
628
685
  #
629
686
  # s(:array,
@@ -9,6 +9,10 @@ module RailsBestPractices
9
9
  class <<self
10
10
  attr_writer :models, :model_associations, :model_attributes, :mailers
11
11
 
12
+ def klasses
13
+ models + mailers + controllers
14
+ end
15
+
12
16
  def models
13
17
  @models ||= Core::Models.new
14
18
  end
@@ -21,6 +25,10 @@ module RailsBestPractices
21
25
  @model_attributes ||= Core::ModelAttributes.new
22
26
  end
23
27
 
28
+ def model_methods
29
+ @model_methods ||= Core::Methods.new
30
+ end
31
+
24
32
  def mailers
25
33
  @mailers ||= Core::Mailers.new
26
34
  end
@@ -33,8 +41,11 @@ module RailsBestPractices
33
41
  @controller_methods ||= Core::Methods.new
34
42
  end
35
43
 
44
+ # Clear all prepare objects.
36
45
  def clear
37
- @models = @model_associations = @model_attributes = @mailers = @controllers = @controller_methods = nil
46
+ instance_variables.each do |instance_variable|
47
+ instance_variable_set(instance_variable, nil)
48
+ end
38
49
  end
39
50
  end
40
51
  end
@@ -5,7 +5,8 @@ module RailsBestPractices
5
5
  module Prepares
6
6
  # Remember controllers and controller methods
7
7
  class ControllerPrepare < Core::Check
8
- include Core::Check::Classable
8
+ include Core::Check::Klassable
9
+ include Core::Check::Accessable
9
10
 
10
11
  DEFAULT_ACTIONS = %w(index show new create edit update destroy)
11
12
 
@@ -26,9 +27,8 @@ module RailsBestPractices
26
27
  # check class node to remember the class name.
27
28
  # also check if the controller is inherit from InheritedResources::Base.
28
29
  def start_class(node)
29
- @class_name = class_name(node)
30
- @controllers << @class_name
31
- if "InheritedResources::Base" == node.base_class.to_s
30
+ @controllers << @klass
31
+ if "InheritedResources::Base" == current_extend_class_name
32
32
  @inherited_resources = true
33
33
  @actions = DEFAULT_ACTIONS
34
34
  end
@@ -38,7 +38,7 @@ module RailsBestPractices
38
38
  def end_class(node)
39
39
  if @inherited_resources
40
40
  @actions.each do |action|
41
- @methods.add_method(@class_name, action)
41
+ @methods.add_method(current_class_name, action)
42
42
  end
43
43
  end
44
44
  end
@@ -62,12 +62,17 @@ module RailsBestPractices
62
62
  #
63
63
  # the remembered methods (@methods) are like
64
64
  # {
65
- # "Post" => ["create", "destroy"],
66
- # "Comment" => ["create"]
65
+ # "PostsController" => {
66
+ # "save" => {"file" => "app/controllers/posts_controller.rb", "line" => 10, "unused" => false},
67
+ # "find" => {"file" => "app/controllers/posts_controller.rb", "line" => 10, "unused" => false}
68
+ # },
69
+ # "CommentsController" => {
70
+ # "create" => {"file" => "app/controllers/comments_controller.rb", "line" => 10, "unused" => false},
71
+ # }
67
72
  # }
68
73
  def start_def(node)
69
74
  method_name = node.method_name.to_s
70
- @methods.add_method(@class_name, method_name)
75
+ @methods.add_method(current_class_name, method_name, {"file" => node.file, "line" => node.line}, current_access_control)
71
76
  end
72
77
  end
73
78
  end
@@ -5,7 +5,7 @@ module RailsBestPractices
5
5
  module Prepares
6
6
  # Remember the mailer names.
7
7
  class MailerPrepare < Core::Check
8
- include Core::Check::Classable
8
+ include Core::Check::Klassable
9
9
 
10
10
  def interesting_nodes
11
11
  [:class, :module]
@@ -24,8 +24,8 @@ module RailsBestPractices
24
24
  # if it is a subclass of ActionMailer::Base,
25
25
  # then remember its class name.
26
26
  def start_class(node)
27
- if "ActionMailer::Base" == node.base_class.to_s
28
- @mailers << class_name(node)
27
+ if "ActionMailer::Base" == current_extend_class_name
28
+ @mailers << @klass
29
29
  end
30
30
  end
31
31
  end
@@ -5,12 +5,13 @@ module RailsBestPractices
5
5
  module Prepares
6
6
  # Remember models and model associations.
7
7
  class ModelPrepare < Core::Check
8
- include Core::Check::Classable
8
+ include Core::Check::Klassable
9
+ include Core::Check::Accessable
9
10
 
10
11
  ASSOCIATION_METHODS = %w(belongs_to has_one has_many has_and_belongs_to_many)
11
12
 
12
13
  def interesting_nodes
13
- [:class, :command, :module]
14
+ [:module, :class, :def, :command, :var_ref]
14
15
  end
15
16
 
16
17
  def interesting_files
@@ -20,15 +21,36 @@ module RailsBestPractices
20
21
  def initialize
21
22
  @models = Prepares.models
22
23
  @model_associations = Prepares.model_associations
24
+ @methods = Prepares.model_methods
23
25
  end
24
26
 
25
27
  # check class node to remember the last class name.
26
28
  def start_class(node)
27
- @class_name= class_name(node)
28
- @models << @class_name
29
+ if "ActionMailer::Base" != current_extend_class_name
30
+ @models << @klass
31
+ end
32
+ end
33
+
34
+ # check ref node to remember all methods.
35
+ #
36
+ # the remembered methods (@methods) are like
37
+ # {
38
+ # "Post" => {
39
+ # "save" => {"file" => "app/models/post.rb", "line" => 10, "unused" => false, "unused" => false},
40
+ # "find" => {"file" => "app/models/post.rb", "line" => 10, "unused" => false, "unused" => false}
41
+ # },
42
+ # "Comment" => {
43
+ # "create" => {"file" => "app/models/comment.rb", "line" => 10, "unused" => false, "unused" => false},
44
+ # }
45
+ # }
46
+ def start_def(node)
47
+ if @klass && "ActionMailer::Base" != current_extend_class_name
48
+ method_name = node.method_name.to_s
49
+ @methods.add_method(current_class_name, method_name, {"file" => node.file, "line" => node.line}, current_access_control)
50
+ end
29
51
  end
30
52
 
31
- # check command node to remember all assoications.
53
+ # check command node to remember all assoications or named_scope/scope methods.
32
54
  #
33
55
  # the remembered association names (@associations) are like
34
56
  # {
@@ -40,20 +62,26 @@ module RailsBestPractices
40
62
  # }
41
63
  # }
42
64
  def start_command(node)
43
- remember_association(node) if ASSOCIATION_METHODS.include? node.message.to_s
65
+ if %w(named_scope scope).include? node.message.to_s
66
+ method_name = node.arguments.all[0].to_s
67
+ @methods.add_method(current_class_name, method_name, {"file" => node.file, "line" => node.line}, current_access_control)
68
+ elsif ASSOCIATION_METHODS.include? node.message.to_s
69
+ remember_association(node)
70
+ end
44
71
  end
45
72
 
46
- # remember associations, with class to association names.
47
- def remember_association(node)
48
- association_meta = node.message.to_s
49
- association_name = node.arguments.all[0].to_s
50
- arguments_node = node.arguments.all[1]
51
- if arguments_node && :bare_assoc_hash == arguments_node.sexp_type
52
- association_class = arguments_node.hash_value("class_name").to_s
73
+ private
74
+ # remember associations, with class to association names.
75
+ def remember_association(node)
76
+ association_meta = node.message.to_s
77
+ association_name = node.arguments.all[0].to_s
78
+ arguments_node = node.arguments.all[1]
79
+ if arguments_node && :bare_assoc_hash == arguments_node.sexp_type
80
+ association_class = arguments_node.hash_value("class_name").to_s
81
+ end
82
+ association_class ||= association_name.classify
83
+ @model_associations.add_association(current_class_name, association_name, association_meta, association_class)
53
84
  end
54
- association_class ||= association_name.classify
55
- @model_associations.add_association(@class_name, association_name, association_meta, association_class)
56
- end
57
85
  end
58
86
  end
59
87
  end