rails_best_practices 1.1.0 → 1.2.0

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