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