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.
Files changed (38) hide show
  1. data/LICENSE +20 -0
  2. data/README.textile +41 -0
  3. data/Rakefile +31 -0
  4. data/VERSION +1 -0
  5. data/bin/rails_best_practices +6 -0
  6. data/lib/rails_best_practices.rb +5 -0
  7. data/lib/rails_best_practices/checks.rb +9 -0
  8. data/lib/rails_best_practices/checks/add_model_virtual_attribute_check.rb +57 -0
  9. data/lib/rails_best_practices/checks/check.rb +59 -0
  10. data/lib/rails_best_practices/checks/many_to_many_collection_check.rb +12 -0
  11. data/lib/rails_best_practices/checks/move_finder_to_named_scope_check.rb +33 -0
  12. data/lib/rails_best_practices/checks/move_model_logic_into_model_check.rb +48 -0
  13. data/lib/rails_best_practices/checks/nested_model_forms_check.rb +12 -0
  14. data/lib/rails_best_practices/checks/replace_complex_creation_with_factory_method_check.rb +55 -0
  15. data/lib/rails_best_practices/checks/use_model_association_check.rb +47 -0
  16. data/lib/rails_best_practices/checks/use_model_callback_check.rb +12 -0
  17. data/lib/rails_best_practices/checks/use_scope_access_check.rb +38 -0
  18. data/lib/rails_best_practices/command.rb +32 -0
  19. data/lib/rails_best_practices/core.rb +5 -0
  20. data/lib/rails_best_practices/core/checking_visitor.rb +26 -0
  21. data/lib/rails_best_practices/core/core_ext.rb +11 -0
  22. data/lib/rails_best_practices/core/error.rb +17 -0
  23. data/lib/rails_best_practices/core/runner.rb +59 -0
  24. data/lib/rails_best_practices/core/visitable_sexp.rb +102 -0
  25. data/rails_best_practices.yml +7 -0
  26. data/spec/rails_best_practices/checks/add_model_virtual_attribute_check_spec.rb +60 -0
  27. data/spec/rails_best_practices/checks/many_to_many_collection_check_spec.rb +11 -0
  28. data/spec/rails_best_practices/checks/move_finder_to_named_scope_check_spec.rb +82 -0
  29. data/spec/rails_best_practices/checks/move_model_logic_into_model_check_spec.rb +49 -0
  30. data/spec/rails_best_practices/checks/nested_model_forms_check_spec.rb +11 -0
  31. data/spec/rails_best_practices/checks/overuse_route_customizations_check_spec.rb +21 -0
  32. data/spec/rails_best_practices/checks/replace_complex_creation_with_factory_method_check_spec.rb +76 -0
  33. data/spec/rails_best_practices/checks/use_model_association_check_spec.rb +71 -0
  34. data/spec/rails_best_practices/checks/use_model_callback_check_spec.rb +11 -0
  35. data/spec/rails_best_practices/checks/use_scope_access_check_spec.rb +171 -0
  36. data/spec/spec.opts +8 -0
  37. data/spec/spec_helper.rb +5 -0
  38. metadata +101 -0
@@ -0,0 +1,12 @@
1
+ require 'rails_best_practices/checks/check'
2
+
3
+ module RailsBestPractices
4
+ module Checks
5
+ # TODO: unknown how to realize it.
6
+ class UseModelCallbackCheck < Check
7
+
8
+ def interesting_nodes
9
+ end
10
+ end
11
+ end
12
+ end
@@ -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,5 @@
1
+ require 'rails_best_practices/core/core_ext'
2
+ require 'rails_best_practices/core/runner'
3
+ require 'rails_best_practices/core/checking_visitor'
4
+ require 'rails_best_practices/core/error'
5
+ require 'rails_best_practices/core/visitable_sexp'
@@ -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,11 @@
1
+ module Enumerable
2
+ def dups
3
+ inject({}) {|h,v| h[v]=h[v].to_i+1; h}.reject{|k,v| v==1}.keys
4
+ end
5
+ end
6
+
7
+ class NilClass
8
+ def method_missing(method_sym, *arguments, &block)
9
+ return nil
10
+ end
11
+ 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