rails_best_practices 0.1.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.
- data/LICENSE +20 -0
- data/README.textile +41 -0
- data/Rakefile +31 -0
- data/VERSION +1 -0
- data/bin/rails_best_practices +6 -0
- data/lib/rails_best_practices.rb +5 -0
- data/lib/rails_best_practices/checks.rb +9 -0
- data/lib/rails_best_practices/checks/add_model_virtual_attribute_check.rb +57 -0
- data/lib/rails_best_practices/checks/check.rb +59 -0
- data/lib/rails_best_practices/checks/many_to_many_collection_check.rb +12 -0
- data/lib/rails_best_practices/checks/move_finder_to_named_scope_check.rb +33 -0
- data/lib/rails_best_practices/checks/move_model_logic_into_model_check.rb +48 -0
- data/lib/rails_best_practices/checks/nested_model_forms_check.rb +12 -0
- data/lib/rails_best_practices/checks/replace_complex_creation_with_factory_method_check.rb +55 -0
- data/lib/rails_best_practices/checks/use_model_association_check.rb +47 -0
- data/lib/rails_best_practices/checks/use_model_callback_check.rb +12 -0
- data/lib/rails_best_practices/checks/use_scope_access_check.rb +38 -0
- data/lib/rails_best_practices/command.rb +32 -0
- data/lib/rails_best_practices/core.rb +5 -0
- data/lib/rails_best_practices/core/checking_visitor.rb +26 -0
- data/lib/rails_best_practices/core/core_ext.rb +11 -0
- data/lib/rails_best_practices/core/error.rb +17 -0
- data/lib/rails_best_practices/core/runner.rb +59 -0
- data/lib/rails_best_practices/core/visitable_sexp.rb +102 -0
- data/rails_best_practices.yml +7 -0
- data/spec/rails_best_practices/checks/add_model_virtual_attribute_check_spec.rb +60 -0
- data/spec/rails_best_practices/checks/many_to_many_collection_check_spec.rb +11 -0
- data/spec/rails_best_practices/checks/move_finder_to_named_scope_check_spec.rb +82 -0
- data/spec/rails_best_practices/checks/move_model_logic_into_model_check_spec.rb +49 -0
- data/spec/rails_best_practices/checks/nested_model_forms_check_spec.rb +11 -0
- data/spec/rails_best_practices/checks/overuse_route_customizations_check_spec.rb +21 -0
- data/spec/rails_best_practices/checks/replace_complex_creation_with_factory_method_check_spec.rb +76 -0
- data/spec/rails_best_practices/checks/use_model_association_check_spec.rb +71 -0
- data/spec/rails_best_practices/checks/use_model_callback_check_spec.rb +11 -0
- data/spec/rails_best_practices/checks/use_scope_access_check_spec.rb +171 -0
- data/spec/spec.opts +8 -0
- data/spec/spec_helper.rb +5 -0
- metadata +101 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rails_best_practices/checks/check'
|
2
|
+
|
3
|
+
module RailsBestPractices
|
4
|
+
module Checks
|
5
|
+
# Check a controller to make sure using scope access
|
6
|
+
#
|
7
|
+
# 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
|
+
class UseScopeAccessCheck < Check
|
9
|
+
|
10
|
+
def interesting_nodes
|
11
|
+
[:if, :unless]
|
12
|
+
end
|
13
|
+
|
14
|
+
def interesting_files
|
15
|
+
/_controller.rb$/
|
16
|
+
end
|
17
|
+
|
18
|
+
def evaluate_start(node)
|
19
|
+
add_error "use scope access" if current_user_redirect?(node)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def current_user_redirect?(node)
|
25
|
+
condition_node = node.call
|
26
|
+
|
27
|
+
condition_node.message == :== and
|
28
|
+
(current_user?(condition_node.arguments.call) or current_user?(condition_node.subject)) and
|
29
|
+
(node.false_node.method_body.any? {|n| n.message == :redirect_to} or node.true_node.method_body.any? {|n| n.message == :redirect_to})
|
30
|
+
end
|
31
|
+
|
32
|
+
def current_user?(call_node)
|
33
|
+
call_node.message == :current_user or (call_node.subject.message == :current_user and call_node.message == :id)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
def expand_dirs_to_files *dirs
|
4
|
+
extensions = ['rb']
|
5
|
+
|
6
|
+
dirs.flatten.map { |p|
|
7
|
+
if File.directory? p
|
8
|
+
Dir[File.join(p, '**', "*.{#{extensions.join(',')}}")]
|
9
|
+
else
|
10
|
+
p
|
11
|
+
end
|
12
|
+
}.flatten.sort
|
13
|
+
end
|
14
|
+
|
15
|
+
options = {}
|
16
|
+
OptionParser.new do |opts|
|
17
|
+
opts.banner = "Usage: rails_best_practices [options]"
|
18
|
+
|
19
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
20
|
+
puts opts
|
21
|
+
exit
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.parse!
|
25
|
+
end
|
26
|
+
|
27
|
+
runner = RailsBestPractices::Core::Runner.new
|
28
|
+
expand_dirs_to_files(ARGV).each.each { |file| runner.check_file(file) }
|
29
|
+
runner.errors.each {|error| puts error}
|
30
|
+
puts "\nFound #{runner.errors.size} errors."
|
31
|
+
|
32
|
+
exit runner.errors.size
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module RailsBestPractices
|
2
|
+
module Core
|
3
|
+
class CheckingVisitor
|
4
|
+
def initialize(*checks)
|
5
|
+
@checks ||= {}
|
6
|
+
checks.first.each do |check|
|
7
|
+
nodes = check.interesting_nodes
|
8
|
+
nodes.each do |node|
|
9
|
+
@checks[node] ||= []
|
10
|
+
@checks[node] << check
|
11
|
+
@checks[node].uniq!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def visit(node)
|
17
|
+
checks = @checks[node.node_type]
|
18
|
+
checks.each {|check| check.evaluate_node_start(node) if node.file =~ check.interesting_files} unless checks.nil?
|
19
|
+
|
20
|
+
node.visitable_children.each {|sexp| sexp.accept(self)}
|
21
|
+
|
22
|
+
checks.each {|check| check.evaluate_node_end(node) if node.file =~ check.interesting_files} unless checks.nil?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module RailsBestPractices
|
2
|
+
module Core
|
3
|
+
class Error
|
4
|
+
attr_reader :filename, :line_number, :message
|
5
|
+
|
6
|
+
def initialize(filename, line_number, message)
|
7
|
+
@filename = filename
|
8
|
+
@line_number = line_number
|
9
|
+
@message = message
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"#{@filename}:#{@line_number} - #{@message}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'ruby_parser'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module RailsBestPractices
|
6
|
+
module Core
|
7
|
+
class Runner
|
8
|
+
DEFAULT_CONFIG = File.join(File.dirname(__FILE__), "..", "..", "..", "rails_best_practices.yml")
|
9
|
+
|
10
|
+
def initialize(*checks)
|
11
|
+
@config = DEFAULT_CONFIG
|
12
|
+
@checks = checks unless checks.empty?
|
13
|
+
@checks ||= load_checks
|
14
|
+
@checker ||= CheckingVisitor.new(@checks)
|
15
|
+
@parser = RubyParser.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def check(filename, content)
|
19
|
+
node = parse(filename, content)
|
20
|
+
node.accept(@checker) if node
|
21
|
+
end
|
22
|
+
|
23
|
+
def check_content(content)
|
24
|
+
check("dummy-file.rb", content)
|
25
|
+
end
|
26
|
+
|
27
|
+
def check_file(filename)
|
28
|
+
check(filename, File.read(filename))
|
29
|
+
end
|
30
|
+
|
31
|
+
def errors
|
32
|
+
@checks ||= []
|
33
|
+
all_errors = @checks.collect {|check| check.errors}
|
34
|
+
all_errors.flatten
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def parse(filename, content)
|
40
|
+
begin
|
41
|
+
@parser.parse(content, filename)
|
42
|
+
rescue Exception => e
|
43
|
+
puts "#{filename} looks like it's not a valid Ruby file. Skipping..." if ENV["ROODI_DEBUG"]
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def load_checks
|
49
|
+
check_objects = []
|
50
|
+
checks = YAML.load_file @config
|
51
|
+
checks.each do |check|
|
52
|
+
klass = eval("RailsBestPractices::Checks::#{check[0]}")
|
53
|
+
check_objects << (check[1].empty? ? klass.new : klass.new(check[1]))
|
54
|
+
end
|
55
|
+
check_objects
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sexp'
|
3
|
+
|
4
|
+
class Sexp
|
5
|
+
def accept(visitor)
|
6
|
+
visitor.visit(self)
|
7
|
+
end
|
8
|
+
|
9
|
+
def node_type
|
10
|
+
first
|
11
|
+
end
|
12
|
+
|
13
|
+
def children
|
14
|
+
find_all { | sexp | Sexp === sexp }
|
15
|
+
end
|
16
|
+
|
17
|
+
def is_language_node?
|
18
|
+
first.class == Symbol
|
19
|
+
end
|
20
|
+
|
21
|
+
def visitable_children
|
22
|
+
parent = is_language_node? ? sexp_body : self
|
23
|
+
parent.children
|
24
|
+
end
|
25
|
+
|
26
|
+
def recursive_children(&handler)
|
27
|
+
visitable_children.each do |child|
|
28
|
+
handler.call child
|
29
|
+
child.recursive_children(&handler)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def grep_nodes(options)
|
34
|
+
return self if options.empty?
|
35
|
+
subject = options[:subject]
|
36
|
+
message = options[:message]
|
37
|
+
arguments = options[:arguments]
|
38
|
+
nodes = []
|
39
|
+
self.recursive_children do |child|
|
40
|
+
if (!subject or subject == child.subject) and (!message or message == child.message) and (!arguments or arguments == child.arguments)
|
41
|
+
nodes << child
|
42
|
+
end
|
43
|
+
end
|
44
|
+
nodes
|
45
|
+
end
|
46
|
+
|
47
|
+
def subject
|
48
|
+
case node_type
|
49
|
+
when :attrasgn, :call, :iasgn, :lasgn
|
50
|
+
self[1]
|
51
|
+
else
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def message
|
56
|
+
case node_type
|
57
|
+
when :attrasgn, :call
|
58
|
+
self[2]
|
59
|
+
else
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def arguments
|
64
|
+
case node_type
|
65
|
+
when :attrasgn, :call
|
66
|
+
self[3]
|
67
|
+
else
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def call
|
72
|
+
case node_type
|
73
|
+
when :if, :arglist
|
74
|
+
self[1]
|
75
|
+
else
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def true_node
|
80
|
+
case node_type
|
81
|
+
when :if
|
82
|
+
self[2]
|
83
|
+
else
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def false_node
|
88
|
+
case node_type
|
89
|
+
when :if
|
90
|
+
self[3]
|
91
|
+
else
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def method_body
|
96
|
+
case node_type
|
97
|
+
when :block
|
98
|
+
self[1..-1]
|
99
|
+
else
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
MoveFinderToNamedScopeCheck: { }
|
2
|
+
UseModelAssociationCheck: { }
|
3
|
+
UseScopeAccessCheck: { }
|
4
|
+
AddModelVirtualAttributeCheck: { }
|
5
|
+
# UseModelCallbackCheck: { }
|
6
|
+
ReplaceComplexCreationWithFactoryMethodCheck: { attribute_assignment_count: 2 }
|
7
|
+
MoveModelLogicIntoModelCheck: { called_count: 4 }
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe RailsBestPractices::Checks::AddModelVirtualAttributeCheck do
|
4
|
+
before(:each) do
|
5
|
+
@runner = RailsBestPractices::Core::Runner.new(RailsBestPractices::Checks::AddModelVirtualAttributeCheck.new)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should add model virtual attribute" do
|
9
|
+
content = <<-EOF
|
10
|
+
class UsersController < ApplicationController
|
11
|
+
|
12
|
+
def create
|
13
|
+
@user = User.new(params[:user])
|
14
|
+
@user.first_name = params[:full_name].split(' ', 2).first
|
15
|
+
@user.last_name = params[:full_name].split(' ', 2).last
|
16
|
+
@user.save
|
17
|
+
end
|
18
|
+
end
|
19
|
+
EOF
|
20
|
+
@runner.check('app/controller/users_controller.rb', content)
|
21
|
+
errors = @runner.errors
|
22
|
+
errors.should_not be_empty
|
23
|
+
errors[0].to_s.should == "app/controller/users_controller.rb:3 - add model virtual attribute"
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should not add model virtual attribute with differen param" do
|
27
|
+
content = <<-EOF
|
28
|
+
class UsersController < ApplicationController
|
29
|
+
|
30
|
+
def create
|
31
|
+
@user = User.new(params[:user])
|
32
|
+
@user.first_name = params[:first_name]
|
33
|
+
@user.last_name = params[:last_name]
|
34
|
+
@user.save
|
35
|
+
end
|
36
|
+
end
|
37
|
+
EOF
|
38
|
+
@runner.check('app/controller/users_controller.rb', content)
|
39
|
+
errors = @runner.errors
|
40
|
+
errors.should be_empty
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should not add model virtual attribute with read" do
|
44
|
+
content = <<-EOF
|
45
|
+
class UsersController < ApplicationController
|
46
|
+
|
47
|
+
def show
|
48
|
+
if params[:id]
|
49
|
+
@user = User.find(params[:id])
|
50
|
+
else
|
51
|
+
@user = current_user
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
EOF
|
56
|
+
@runner.check('app/controller/users_controller.rb', content)
|
57
|
+
errors = @runner.errors
|
58
|
+
errors.should be_empty
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe RailsBestPractices::Checks::ManyToManyCollectionCheck do
|
4
|
+
before(:each) do
|
5
|
+
@runner = RailsBestPractices::Core::Runner.new(RailsBestPractices::Checks::ManyToManyCollectionCheck.new)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should many to many collection check" do
|
9
|
+
pending
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe RailsBestPractices::Checks::MoveFinderToNamedScopeCheck do
|
4
|
+
before(:each) do
|
5
|
+
@runner = RailsBestPractices::Core::Runner.new(RailsBestPractices::Checks::MoveFinderToNamedScopeCheck.new)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should move finder to named_scope" do
|
9
|
+
content = <<-EOF
|
10
|
+
class PostsController < ActionController::Base
|
11
|
+
|
12
|
+
def index
|
13
|
+
@public_posts = Post.find(:all, :conditions => { :state => 'public' },
|
14
|
+
:limit => 10,
|
15
|
+
:order => 'created_at desc')
|
16
|
+
|
17
|
+
@draft_posts = Post.find(:all, :conditions => { :state => 'draft' },
|
18
|
+
:limit => 10,
|
19
|
+
:order => 'created_at desc')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
EOF
|
23
|
+
@runner.check('app/controller/posts_controller.rb', content)
|
24
|
+
errors = @runner.errors
|
25
|
+
errors.size.should == 2
|
26
|
+
errors[0].to_s.should == "app/controller/posts_controller.rb:4 - move finder to named_scope"
|
27
|
+
errors[1].to_s.should == "app/controller/posts_controller.rb:8 - move finder to named_scope"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should not move simple finder" do
|
31
|
+
content = <<-EOF
|
32
|
+
class PostsController < ActionController::Base
|
33
|
+
|
34
|
+
def index
|
35
|
+
@all_posts = Post.find(:all)
|
36
|
+
@another_all_posts = Post.all
|
37
|
+
@first_post = Post.find(:first)
|
38
|
+
@another_first_post = Post.first
|
39
|
+
@last_post = Post.find(:last)
|
40
|
+
@another_last_post = Post.last
|
41
|
+
end
|
42
|
+
end
|
43
|
+
EOF
|
44
|
+
@runner.check('app/controller/posts_controller.rb', content)
|
45
|
+
@runner.errors.should be_empty
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should not move namd_scope" do
|
49
|
+
content = <<-EOF
|
50
|
+
class PostsController < ActionController::Base
|
51
|
+
|
52
|
+
def index
|
53
|
+
@public_posts = Post.published
|
54
|
+
@draft_posts = Post.draft
|
55
|
+
end
|
56
|
+
end
|
57
|
+
EOF
|
58
|
+
@runner.check('app/controller/posts_controller.rb', content)
|
59
|
+
@runner.errors.should be_empty
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should not check model file" do
|
63
|
+
content = <<-EOF
|
64
|
+
class Post < ActiveRecord::Base
|
65
|
+
|
66
|
+
def published
|
67
|
+
Post.find(:all, :conditions => { :state => 'public' },
|
68
|
+
:limit => 10, :order => 'created_at desc')
|
69
|
+
end
|
70
|
+
|
71
|
+
def published
|
72
|
+
Post.find(:all, :conditions => { :state => 'draft' },
|
73
|
+
:limit => 10, :order => 'created_at desc')
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
EOF
|
78
|
+
@runner.check('app/model/post.rb', content)
|
79
|
+
@runner.errors.should be_empty
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|