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.
- data/README.md +161 -0
- data/lib/rails_best_practices.rb +158 -20
- data/lib/rails_best_practices/checks/add_model_virtual_attribute_check.rb +108 -34
- data/lib/rails_best_practices/checks/always_add_db_index_check.rb +148 -29
- data/lib/rails_best_practices/checks/check.rb +178 -75
- data/lib/rails_best_practices/checks/dry_bundler_in_capistrano_check.rb +26 -5
- data/lib/rails_best_practices/checks/isolate_seed_data_check.rb +66 -15
- data/lib/rails_best_practices/checks/keep_finders_on_their_own_model_check.rb +53 -12
- data/lib/rails_best_practices/checks/law_of_demeter_check.rb +59 -30
- data/lib/rails_best_practices/checks/move_code_into_controller_check.rb +35 -15
- data/lib/rails_best_practices/checks/move_code_into_helper_check.rb +56 -12
- data/lib/rails_best_practices/checks/move_code_into_model_check.rb +30 -32
- data/lib/rails_best_practices/checks/move_finder_to_named_scope_check.rb +45 -15
- data/lib/rails_best_practices/checks/move_model_logic_into_model_check.rb +31 -27
- data/lib/rails_best_practices/checks/needless_deep_nesting_check.rb +99 -38
- data/lib/rails_best_practices/checks/not_use_default_route_check.rb +43 -12
- data/lib/rails_best_practices/checks/overuse_route_customizations_check.rb +140 -28
- data/lib/rails_best_practices/checks/replace_complex_creation_with_factory_method_check.rb +44 -30
- data/lib/rails_best_practices/checks/replace_instance_variable_with_local_variable_check.rb +18 -7
- data/lib/rails_best_practices/checks/use_before_filter_check.rb +88 -18
- data/lib/rails_best_practices/checks/use_model_association_check.rb +61 -22
- data/lib/rails_best_practices/checks/use_observer_check.rb +125 -23
- data/lib/rails_best_practices/checks/use_query_attribute_check.rb +75 -47
- data/lib/rails_best_practices/checks/use_say_with_time_in_migrations_check.rb +59 -10
- data/lib/rails_best_practices/checks/use_scope_access_check.rb +78 -23
- data/lib/rails_best_practices/command.rb +19 -34
- data/lib/rails_best_practices/core.rb +4 -2
- data/lib/rails_best_practices/core/checking_visitor.rb +49 -19
- data/lib/rails_best_practices/core/error.rb +5 -2
- data/lib/rails_best_practices/core/runner.rb +79 -55
- data/lib/rails_best_practices/core/visitable_sexp.rb +325 -55
- data/lib/rails_best_practices/{core/core_ext.rb → core_ext/enumerable.rb} +3 -6
- data/lib/rails_best_practices/core_ext/nil_class.rb +8 -0
- data/lib/rails_best_practices/version.rb +1 -1
- data/rails_best_practices.yml +2 -2
- metadata +8 -7
- 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
|
6
|
+
# Check a controller to make sure to use scope access instead of manually checking current_user and redirect.
|
7
7
|
#
|
8
|
-
#
|
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
|
12
|
-
[:if
|
22
|
+
|
23
|
+
def interesting_review_nodes
|
24
|
+
[:if]
|
13
25
|
end
|
14
|
-
|
15
|
-
def
|
26
|
+
|
27
|
+
def interesting_review_files
|
16
28
|
CONTROLLER_FILES
|
17
29
|
end
|
18
|
-
|
19
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
(
|
30
|
-
(
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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.
|
40
|
-
|
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
|
-
|
61
|
-
runner.check_file(file)
|
62
|
-
bar.inc unless options['debug']
|
52
|
+
opts.parse!
|
63
53
|
end
|
64
|
-
bar.finish
|
65
54
|
|
66
|
-
|
67
|
-
|
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
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
14
|
-
|
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
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|