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
@@ -3,37 +3,92 @@ require 'rails_best_practices/checks/check'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
- # Check a controller to make sure using scope access
6
+ # Check a controller to make sure to use scope access instead of manually checking current_user and redirect.
7
7
  #
8
- # Implementation: simply check if or unless compare with current_user or current_user.id and there is a redirect_to message in if or unless block
8
+ # See the best practice details here http://rails-bestpractices.com/posts/3-use-scope-access.
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # none
14
+ #
15
+ # Review process:
16
+ # check all if nodes to see
17
+ #
18
+ # if they are compared with current_user or current_user.id,
19
+ # and there is redirect_to method call in if block body,
20
+ # then it should be replaced by using scope access.
9
21
  class UseScopeAccessCheck < Check
10
-
11
- def interesting_nodes
12
- [:if, :unless]
22
+
23
+ def interesting_review_nodes
24
+ [:if]
13
25
  end
14
-
15
- def interesting_files
26
+
27
+ def interesting_review_files
16
28
  CONTROLLER_FILES
17
29
  end
18
-
19
- def evaluate_start(node)
30
+
31
+ # check if node in review process.
32
+ #
33
+ # if it is a method call compared with current_user or current_user.id,
34
+ # and there is a redirect_to method call in the block body, like
35
+ #
36
+ # unless @post.user == current_user
37
+ # falsh[:error] = "Access Denied"
38
+ # redirect_to posts_url
39
+ # end
40
+ #
41
+ # then it should be replaced by using scope access.
42
+ def review_start_if(node)
20
43
  add_error "use scope access" if current_user_redirect?(node)
21
44
  end
22
-
45
+
23
46
  private
24
-
25
- def current_user_redirect?(node)
26
- condition_node = node.call
27
-
28
- condition_node.message == :== and
29
- (current_user?(condition_node.arguments.call) or current_user?(condition_node.subject)) and
30
- (node.false_node.body.any? {|n| n.message == :redirect_to} or node.true_node.method_body.any? {|n| n.message == :redirect_to})
31
- end
32
-
33
- def current_user?(call_node)
34
- call_node.node_type == :call and (call_node.message == :current_user or (call_node.subject.message == :current_user and call_node.message == :id))
35
- end
36
-
47
+ # check a if node to see
48
+ #
49
+ # if the conditional statement is compared with current_user or current_user.id,
50
+ # and there is a redirect_to method call in the block body, like
51
+ #
52
+ # s(:if,
53
+ # s(:call,
54
+ # s(:call, s(:ivar, :@post), :user, s(:arglist)),
55
+ # :==,
56
+ # s(:arglist, s(:call, nil, :current_user, s(:arglist)))
57
+ # ),
58
+ # nil,
59
+ # s(:block,
60
+ # s(:attrasgn,
61
+ # s(:call, nil, :flash, s(:arglist)),
62
+ # :[]=,
63
+ # s(:arglist, s(:lit, :warning), s(:str, "Access Denied"))
64
+ # ),
65
+ # s(:call, nil, :redirect_to,
66
+ # s(:arglist, s(:call, nil, :posts_url, s(:arglist)))
67
+ # )
68
+ # )
69
+ # )
70
+ #
71
+ # then it should be replaced by using scope access.
72
+ def current_user_redirect?(node)
73
+ condition_node = node.conditional_statement
74
+
75
+ condition_node.message == :== &&
76
+ (current_user?(condition_node.arguments[1]) || current_user?(condition_node.subject)) &&
77
+ (node.false_node.grep_node(:message => :redirect_to) || node.true_node.grep_node(:message => :redirect_to))
78
+ end
79
+
80
+ # check a call node to see if it uses current_user, or current_user.id.
81
+ #
82
+ # the expected call node may be
83
+ #
84
+ # s(:call, nil, :current_user, s(:arglist))
85
+ #
86
+ # or
87
+ #
88
+ # s(:call, s(:call, nil, :current_user, s(:arglist)), :id, s(:arglist))
89
+ def current_user?(call_node)
90
+ call_node.message == :current_user || (call_node.subject.message == :current_user && call_node.message == :id)
91
+ end
37
92
  end
38
93
  end
39
94
  end
@@ -1,11 +1,20 @@
1
1
  # encoding: utf-8
2
2
  require 'optparse'
3
- require 'progressbar'
4
- require 'colored'
5
3
 
4
+ # Usage: rails_best_practices [options] path
5
+ # -d, --debug Debug mode
6
+ # --vendor include vendor files
7
+ # --spec include spec files
8
+ # --test include test files
9
+ # --features include features files
10
+ # -x, --exclude PATTERNS Don't analyze files matching a pattern
11
+ # (comma-separated regexp list)
12
+ # -g, --generate Generate configuration yaml
13
+ # -v, --version Show this version
14
+ # -h, --help Show this message
6
15
  options = {}
7
16
  OptionParser.new do |opts|
8
- opts.banner = "Usage: rails_best_practices [options]"
17
+ opts.banner = "Usage: rails_best_practices [options] path"
9
18
 
10
19
  opts.on("-d", "--debug", "Debug mode") do
11
20
  options['debug'] = true
@@ -36,39 +45,15 @@ OptionParser.new do |opts|
36
45
  end
37
46
  end
38
47
 
39
- opts.parse!
40
- end
41
-
42
- runner = RailsBestPractices::Core::Runner.new
43
- runner.set_debug if options['debug']
44
-
45
- prepare_files = RailsBestPractices::prepare_files(ARGV)
46
- files = RailsBestPractices::analyze_files(ARGV, options)
47
-
48
- if runner.checks.find { |check| check.is_a? RailsBestPractices::Checks::AlwaysAddDbIndexCheck } &&
49
- !files.find { |file| file.index "db\/schema.rb" }
50
- puts "AlwaysAddDbIndexCheck is disabled as there is no db/schema.rb file in your rails project.".blue
51
- end
52
-
53
- bar = ProgressBar.new('Analyzing', prepare_files.size + files.size)
54
-
55
- prepare_files.each do |file|
56
- runner.prepare_file(file)
57
- bar.inc unless options['debug']
58
- end
48
+ opts.on("-g", "--generate", "Generate configuration yaml") do
49
+ options[:generate] = true
50
+ end
59
51
 
60
- files.each do |file|
61
- runner.check_file(file)
62
- bar.inc unless options['debug']
52
+ opts.parse!
63
53
  end
64
- bar.finish
65
54
 
66
- runner.errors.each { |error| puts error.to_s.red }
67
- puts "\nPlease go to http://rails-bestpractices.com to see more useful Rails Best Practices.".green
68
- if runner.errors.empty?
69
- puts "\nNo error found. Cool!".green
55
+ if options[:generate]
56
+ RailsBestPractices.generate(ARGV.first)
70
57
  else
71
- puts "\nFound #{runner.errors.size} errors.".red
58
+ RailsBestPractices.start(ARGV.first, options)
72
59
  end
73
-
74
- exit runner.errors.size
@@ -1,6 +1,8 @@
1
1
  # encoding: utf-8
2
- require 'rails_best_practices/core/core_ext'
3
2
  require 'rails_best_practices/core/runner'
4
3
  require 'rails_best_practices/core/checking_visitor'
5
4
  require 'rails_best_practices/core/error'
6
- require 'rails_best_practices/core/visitable_sexp'
5
+ require 'rails_best_practices/core/visitable_sexp'
6
+
7
+ require 'rails_best_practices/core_ext/enumerable'
8
+ require 'rails_best_practices/core_ext/nil_class'
@@ -1,32 +1,62 @@
1
1
  # encoding: utf-8
2
2
  module RailsBestPractices
3
3
  module Core
4
+ # CheckingVisitor is a visitor class.
5
+ #
6
+ # it remembers all the checks for prepare and review processes according to interesting_prepare_nodes and interesting_review_nodes,
7
+ # then recursively iterate all sexp nodes,
8
+ #
9
+ # for prepare process
10
+ # if the node_type and the node filename match the interesting_prepare_nodes and interesting_prepare_files,
11
+ # then run the prepare for that node.
12
+ #
13
+ # for review process
14
+ # if the node_type and the node filename match the interesting_review_nodes and interesting_review_files,
15
+ # then run the reivew for that node.
4
16
  class CheckingVisitor
17
+ # remember all the checks for prepare and review processes according to interesting_prepare_nodes and interesting_review_nodes,
5
18
  def initialize(checks)
6
- @checks ||= {}
7
- checks.each do |check|
8
- nodes = check.interesting_nodes
9
- nodes.each do |node|
10
- @checks[node] ||= []
11
- @checks[node] << check
12
- @checks[node].uniq!
19
+ [:prepare, :review].each do |process|
20
+ instance_variable_set("@#{process}_checks", {}) # @review_checks = {}
21
+ checks.each do |check| # checks.each do |check|
22
+ check.send("interesting_#{process}_nodes").each do |node| # check.interesting_review_nodes.each do |node|
23
+ instance_variable_get("@#{process}_checks")[node] ||= [] # @review_checks[node] ||= []
24
+ instance_variable_get("@#{process}_checks")[node] << check # @review_checks[node] << check
25
+ instance_variable_get("@#{process}_checks")[node].uniq! # @review_checks[node].uniq!
26
+ end # end
13
27
  end
14
28
  end
15
29
  end
16
30
 
17
- def prepare(node)
18
- checks = @checks[node.node_type]
19
- checks.each {|check| check.prepare_node_start(node) if node.file =~ check.interesting_prepare_files} unless checks.nil?
20
- node.visitable_children.each {|sexp| sexp.prepare(self)}
21
- checks.each {|check| check.prepare_node_end(node) if node.file =~ check.interesting_prepare_files} unless checks.nil?
31
+ # for prepare process
32
+ # if the node_type and the node filename match the interesting_prepare_nodes and interesting_prepare_files,
33
+ # then run the prepare for that node.
34
+ #
35
+ # for review process
36
+ # if the node_type and the node filename match the interesting_review_nodes and interesting_review_files,
37
+ # then run the reivew for that node.
38
+ [:prepare, :review].each do |process|
39
+ class_eval <<-EOS
40
+ def #{process}(node) # def review(node)
41
+ checks = @#{process}_checks[node.node_type] # checks = @review_checks[node.node_type]
42
+ if checks # if checks
43
+ checks.each { |check| # checks.each { |check|
44
+ if node.file =~ check.interesting_#{process}_files # if node.file =~ check.interesting_review_files
45
+ check.#{process}_node_start(node) # check.review_node_start(node)
46
+ end # end
47
+ } # }
48
+ end # end
49
+ node.children.each {|sexp| sexp.#{process}(self)} # node.children.each {|sexp| sexp.review(self)}
50
+ if checks # if checks
51
+ checks.each { |check| # checks.each { |check|
52
+ if node.file =~ check.interesting_#{process}_files # if node.file =~ check.interesting_review_files
53
+ check.#{process}_node_end(node) # check.review_node_end(node)
54
+ end # end
55
+ } # }
56
+ end # end
57
+ end # end
58
+ EOS
22
59
  end
23
-
24
- def visit(node)
25
- checks = @checks[node.node_type]
26
- checks.each {|check| check.evaluate_node_start(node) if node.file =~ check.interesting_files} unless checks.nil?
27
- node.visitable_children.each {|sexp| sexp.accept(self)}
28
- checks.each {|check| check.evaluate_node_end(node) if node.file =~ check.interesting_files} unless checks.nil?
29
- end
30
60
  end
31
61
  end
32
62
  end
@@ -1,15 +1,18 @@
1
1
  # encoding: utf-8
2
2
  module RailsBestPractices
3
3
  module Core
4
+ # Error is the violation to rails best practice.
5
+ #
6
+ # it indicates the filenname, line number and error message for the violation.
4
7
  class Error
5
8
  attr_reader :filename, :line_number, :message
6
-
9
+
7
10
  def initialize(filename, line_number, message)
8
11
  @filename = filename
9
12
  @line_number = line_number
10
13
  @message = message
11
14
  end
12
-
15
+
13
16
  def to_s
14
17
  "#{@filename}:#{@line_number} - #{@message}"
15
18
  end
@@ -7,45 +7,62 @@ require 'active_support/inflector'
7
7
 
8
8
  module RailsBestPractices
9
9
  module Core
10
+ # Runner is the main class, it can check source code of a filename with all checks (according to the configuration).
11
+ #
12
+ # the check process is partitioned into two parts,
13
+ #
14
+ # 1. prepare process, it will do some preparations for further checking, such as remember the model associations.
15
+ # 2. review process, it does real check, if the source code violates some best practices, the violations will be notified.
10
16
  class Runner
11
17
  attr_reader :checks
18
+ attr_accessor :debug
12
19
 
13
- DEFAULT_CONFIG = File.join(File.dirname(__FILE__), "..", "..", "..", "rails_best_practices.yml")
14
- CUSTOM_CONFIG = File.join('config', 'rails_best_practices.yml')
20
+ # set the base path.
21
+ #
22
+ # @param [String] path the base path
23
+ def self.base_path=(path)
24
+ @base_path = path
25
+ end
26
+
27
+ # get the base path, by default, the base path is current path.
28
+ #
29
+ # @return [String] the base path
30
+ def self.base_path
31
+ @base_path || "."
32
+ end
15
33
 
16
34
  def initialize(*checks)
17
- @config = File.exists?(CUSTOM_CONFIG) ? CUSTOM_CONFIG : DEFAULT_CONFIG
35
+ custom_config = File.join(Runner.base_path, 'config/rails_best_practices.yml')
36
+ @config = File.exists?(custom_config) ? custom_config : RailsBestPractices::DEFAULT_CONFIG
18
37
  @checks = checks unless checks.empty?
19
38
  @checks ||= load_checks
20
39
  @checker ||= CheckingVisitor.new(@checks)
21
40
  @debug = false
22
41
  end
23
42
 
24
- def set_debug
25
- @debug = true
26
- end
27
-
28
- def check(filename, content)
29
- puts filename if @debug
30
- content = parse_erb_or_haml(filename, content)
31
- node = parse_ruby(filename, content)
32
- node.accept(@checker) if node
33
- end
34
-
35
- def prepare(filename, content)
36
- puts filename if @debug
37
- node = parse_ruby(filename, content)
38
- node.prepare(@checker) if node
39
- end
40
-
41
- def check_file(filename)
42
- check(filename, File.read(filename))
43
- end
44
-
45
- def prepare_file(filename)
46
- prepare(filename, File.read(filename))
43
+ # prepare and review a file's content with filename.
44
+ # the file may be a ruby, erb or haml file.
45
+ #
46
+ # filename is the filename of the code.
47
+ # content is the source code.
48
+ [:prepare, :review].each do |process|
49
+ class_eval <<-EOS
50
+ def #{process}(filename, content) # def review(filename, content)
51
+ puts filename if @debug # puts filename if @debug
52
+ content = parse_erb_or_haml(filename, content) # content = parse_erb_or_haml(filename, content)
53
+ node = parse_ruby(filename, content) # node = parse_ruby(filename, content)
54
+ node.#{process}(@checker) if node # node.review(@checker) if node
55
+ end # end
56
+ #
57
+ def #{process}_file(filename) # def review_file(filename)
58
+ #{process}(filename, File.read(filename)) # review(filename, File.read(filename))
59
+ end # end
60
+ EOS
47
61
  end
48
62
 
63
+ # get all errors from checks.
64
+ #
65
+ # @return [Array] all errors from checks
49
66
  def errors
50
67
  @checks ||= []
51
68
  all_errors = @checks.collect {|check| check.errors}
@@ -53,41 +70,48 @@ module RailsBestPractices
53
70
  end
54
71
 
55
72
  private
56
-
57
- def parse_ruby(filename, content)
58
- begin
59
- RubyParser.new.parse(content, filename)
60
- rescue Exception => e
61
- puts "#{filename} looks like it's not a valid Ruby file. Skipping..." if @debug
62
- nil
73
+ # parse ruby code.
74
+ #
75
+ # filename is the filename of the ruby code.
76
+ # content is the source code of ruby file.
77
+ def parse_ruby(filename, content)
78
+ begin
79
+ RubyParser.new.parse(content, filename)
80
+ rescue Exception => e
81
+ puts "#{filename} looks like it's not a valid Ruby file. Skipping...".red if @debug
82
+ nil
83
+ end
63
84
  end
64
- end
65
85
 
66
- def parse_erb_or_haml(filename, content)
67
- if filename =~ /.*\.erb$/
68
- content = Erubis::Eruby.new(content).src
69
- end
70
- if filename =~ /.*\.haml$/
71
- begin
72
- require 'haml'
73
- content = Haml::Engine.new(content).precompiled
74
- # remove \xxx characters
75
- content.gsub!(/\\\d{3}/, '')
76
- rescue Haml::SyntaxError
86
+ # parse erb or html code.
87
+ #
88
+ # filename is the filename of the erb or haml code.
89
+ # content is the source code of erb or haml file.
90
+ def parse_erb_or_haml(filename, content)
91
+ if filename =~ /.*\.erb$/
92
+ content = Erubis::Eruby.new(content).src
93
+ elsif filename =~ /.*\.haml$/
94
+ begin
95
+ require 'haml'
96
+ content = Haml::Engine.new(content).precompiled
97
+ # remove \xxx characters
98
+ content.gsub!(/\\\d{3}/, '')
99
+ rescue Haml::SyntaxError
100
+ end
77
101
  end
102
+ content
78
103
  end
79
- content
80
- end
81
104
 
82
- def load_checks
83
- check_objects = []
84
- checks = YAML.load_file @config
85
- checks.each do |check|
86
- klass = eval("RailsBestPractices::Checks::#{check[0]}")
87
- check_objects << (check[1].empty? ? klass.new : klass.new(check[1]))
105
+ # load all checks according to configuration.
106
+ def load_checks
107
+ check_objects = []
108
+ checks = YAML.load_file @config
109
+ checks.each do |check|
110
+ klass = RailsBestPractices::Checks.const_get(check[0])
111
+ check_objects << (check[1].empty? ? klass.new : klass.new(check[1]))
112
+ end
113
+ check_objects
88
114
  end
89
- check_objects
90
- end
91
115
  end
92
116
  end
93
117
  end