rails_best_practices 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Richard Huang (flyerhzm@gmail.com)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
* Lesson 1. Move code from Controller to Model
|
2
|
+
## [-Move finder to named_scope-]
|
3
|
+
## [-Use model association-]
|
4
|
+
## [-Use scope access-]
|
5
|
+
## [-Add model virtual attribute-]
|
6
|
+
## Use model callback
|
7
|
+
## [-Replace Complex Creation with Factory Method-]
|
8
|
+
## [-Move Model Logic into the Model-]
|
9
|
+
## model.collection_model_ids (many-to-many)
|
10
|
+
## Nested Model Forms (one-to-one)
|
11
|
+
## Nested Model Forms (one-to-many)
|
12
|
+
|
13
|
+
* Lesson 2. RESTful Conventions
|
14
|
+
## Overuse route customizations
|
15
|
+
## Needless deep nesting
|
16
|
+
## Not use default route
|
17
|
+
|
18
|
+
* Lesson 3. Model
|
19
|
+
## Keep Finders on Their Own Model
|
20
|
+
## Love named_scope
|
21
|
+
## the Law of Demeter
|
22
|
+
## DRY: metaprogramming
|
23
|
+
## Extract into Module
|
24
|
+
## Extract to composed class
|
25
|
+
## Use Observer
|
26
|
+
|
27
|
+
* Lesson 4. Migration
|
28
|
+
## Isolating Seed Data
|
29
|
+
## Always add DB index
|
30
|
+
|
31
|
+
* Lesson 5. Controller
|
32
|
+
## Use before_filter
|
33
|
+
## DRY Controller
|
34
|
+
|
35
|
+
* Lesson 6. View
|
36
|
+
## Move code into controller
|
37
|
+
## Move code into model
|
38
|
+
## Move code into helper
|
39
|
+
## Replace instance variable with local variable
|
40
|
+
## Use Form Builder
|
41
|
+
## Organize Helper files
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'jeweler'
|
5
|
+
|
6
|
+
desc 'Default: run unit tests.'
|
7
|
+
task :default => :spec
|
8
|
+
|
9
|
+
desc 'Generate documentation for the sitemap plugin.'
|
10
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
11
|
+
rdoc.rdoc_dir = 'rdoc'
|
12
|
+
rdoc.title = 'Bullet'
|
13
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
14
|
+
rdoc.rdoc_files.include('README')
|
15
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Run all specs in spec directory"
|
19
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
20
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
21
|
+
end
|
22
|
+
|
23
|
+
Jeweler::Tasks.new do |gemspec|
|
24
|
+
gemspec.name = "rails_best_practices"
|
25
|
+
gemspec.summary = ""
|
26
|
+
gemspec.description = ""
|
27
|
+
gemspec.email = "flyerhzm@gmail.com"
|
28
|
+
gemspec.homepage = "http://github.com/flyerhzm/rails_best_practices"
|
29
|
+
gemspec.authors = ["Richard Huang"]
|
30
|
+
end
|
31
|
+
Jeweler::GemcutterTasks.new
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'rails_best_practices/checks/move_finder_to_named_scope_check'
|
2
|
+
require 'rails_best_practices/checks/use_model_association_check'
|
3
|
+
require 'rails_best_practices/checks/use_scope_access_check'
|
4
|
+
require 'rails_best_practices/checks/add_model_virtual_attribute_check'
|
5
|
+
require 'rails_best_practices/checks/use_model_callback_check'
|
6
|
+
require 'rails_best_practices/checks/replace_complex_creation_with_factory_method_check'
|
7
|
+
require 'rails_best_practices/checks/move_model_logic_into_model_check'
|
8
|
+
require 'rails_best_practices/checks/many_to_many_collection_check'
|
9
|
+
require 'rails_best_practices/checks/nested_model_forms_check'
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'rails_best_practices/checks/check'
|
2
|
+
|
3
|
+
module RailsBestPractices
|
4
|
+
module Checks
|
5
|
+
# Check a controller to make sure adding a model virual attribute to simplify model creation.
|
6
|
+
#
|
7
|
+
# Implementation: check arguments of params#[]= before calling save,
|
8
|
+
# if they have duplicated arguments, then the model may need to add a model virtual attribute.
|
9
|
+
class AddModelVirtualAttributeCheck < Check
|
10
|
+
|
11
|
+
def interesting_nodes
|
12
|
+
[:defn]
|
13
|
+
end
|
14
|
+
|
15
|
+
def interesting_files
|
16
|
+
/_controller.rb$/
|
17
|
+
end
|
18
|
+
|
19
|
+
def evaluate_start(node)
|
20
|
+
@variables = {}
|
21
|
+
node.recursive_children do |child|
|
22
|
+
case child.node_type
|
23
|
+
when :attrasgn
|
24
|
+
attribute_assignment(child)
|
25
|
+
when :call
|
26
|
+
call_assignment(child)
|
27
|
+
else
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@variables = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def attribute_assignment(node)
|
36
|
+
variable = node.subject
|
37
|
+
return if variable.nil?
|
38
|
+
@variables[variable] ||= []
|
39
|
+
@variables[variable] << {:message => node.message, :arguments => node.arguments}
|
40
|
+
end
|
41
|
+
|
42
|
+
def call_assignment(node)
|
43
|
+
if node.message == :save
|
44
|
+
variable = node.subject
|
45
|
+
add_error "add model virtual attribute" if params_dup?(@variables[variable].collect {|h| h[:arguments] })
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def params_dup?(nodes)
|
50
|
+
return false if nodes.nil?
|
51
|
+
params_nodes = nodes.collect {|node| node.grep_nodes({:subject => s(:call, nil, :params, s(:arglist)), :message => :[]}).first}.compact
|
52
|
+
params_arguments = params_nodes.collect(&:arguments)
|
53
|
+
!params_arguments.dups.empty?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'rails_best_practices/core/error'
|
2
|
+
|
3
|
+
module RailsBestPractices
|
4
|
+
module Checks
|
5
|
+
class Check
|
6
|
+
NODE_TYPES = [:call, :defn, :if, :unless]
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@errors = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def interesting_files
|
13
|
+
/.*/
|
14
|
+
end
|
15
|
+
|
16
|
+
NODE_TYPES.each do |node|
|
17
|
+
start_node_method = "evaluate_start_#{node}"
|
18
|
+
end_node_method = "evaluate_end_#{node}"
|
19
|
+
define_method(start_node_method) { } unless self.respond_to?(start_node_method)
|
20
|
+
define_method(end_node_method) { } unless self.respond_to?(end_node_method)
|
21
|
+
end
|
22
|
+
|
23
|
+
def position(offset = 0)
|
24
|
+
"#{@line[2]}:#{@line[1] + offset}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def evaluate_start(node)
|
28
|
+
end
|
29
|
+
|
30
|
+
def evaluate_end(node)
|
31
|
+
end
|
32
|
+
|
33
|
+
def evaluate_node(position, node)
|
34
|
+
@node = node
|
35
|
+
eval_method = "evaluate_#{position}_#{node.node_type}"
|
36
|
+
self.send(eval_method, node)
|
37
|
+
end
|
38
|
+
|
39
|
+
def evaluate_node_start(node)
|
40
|
+
evaluate_node(:start, node)
|
41
|
+
evaluate_start(node)
|
42
|
+
end
|
43
|
+
|
44
|
+
def evaluate_node_end(node)
|
45
|
+
evaluate_node(:end, node)
|
46
|
+
evaluate_end(node)
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_error(error, line = nil)
|
50
|
+
line = @node.line if line.nil?
|
51
|
+
@errors << RailsBestPractices::Core::Error.new("#{@node.file}", "#{line}", error)
|
52
|
+
end
|
53
|
+
|
54
|
+
def errors
|
55
|
+
@errors
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rails_best_practices/checks/check'
|
2
|
+
|
3
|
+
module RailsBestPractices
|
4
|
+
module Checks
|
5
|
+
# Check a controller file to make sure finder is simple.
|
6
|
+
#
|
7
|
+
# Complex finder in controller is a code smell, use namd_scope instead.
|
8
|
+
#
|
9
|
+
# Implementation: check method :find, :all, :first, :last with hash parameters.
|
10
|
+
class MoveFinderToNamedScopeCheck < Check
|
11
|
+
|
12
|
+
FINDER = [:find, :all, :first, :last]
|
13
|
+
|
14
|
+
def interesting_nodes
|
15
|
+
[:call]
|
16
|
+
end
|
17
|
+
|
18
|
+
def interesting_files
|
19
|
+
/_controller.rb$/
|
20
|
+
end
|
21
|
+
|
22
|
+
def evaluate_start(node)
|
23
|
+
add_error "move finder to named_scope" if finder?(node)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def finder?(node)
|
29
|
+
node.subject.node_type == :const && FINDER.include?(node.message) && node.arguments.children.any? {|node| node.node_type == :hash}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rails_best_practices/checks/check'
|
2
|
+
|
3
|
+
module RailsBestPractices
|
4
|
+
module Checks
|
5
|
+
# Check a controller file to make sure that model logic should not exist in controller, move it into a model.
|
6
|
+
#
|
7
|
+
# Implementation: check the count of method calling of a model,
|
8
|
+
# if it is more than defined called count, then it contains model logic.
|
9
|
+
class MoveModelLogicIntoModelCheck < Check
|
10
|
+
|
11
|
+
def interesting_nodes
|
12
|
+
[:defn]
|
13
|
+
end
|
14
|
+
|
15
|
+
def interesting_files
|
16
|
+
/_controller.rb$/
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(options = {})
|
20
|
+
super()
|
21
|
+
@called_count = options['called_count'] || 4
|
22
|
+
end
|
23
|
+
|
24
|
+
def evaluate_start(node)
|
25
|
+
@variables = {}
|
26
|
+
node.recursive_children do |child|
|
27
|
+
case child.node_type
|
28
|
+
when :attrasgn, :call
|
29
|
+
call_node(child)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
if @variables.values.any? { |count| count > @called_count }
|
33
|
+
add_error "move model logic into model"
|
34
|
+
end
|
35
|
+
@variables = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def call_node(node)
|
41
|
+
variable = node.subject
|
42
|
+
return if variable.nil?
|
43
|
+
@variables[variable] ||= 0
|
44
|
+
@variables[variable] += 1
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rails_best_practices/checks/check'
|
2
|
+
|
3
|
+
module RailsBestPractices
|
4
|
+
module Checks
|
5
|
+
# Check a controller file to make sure that complex model creation should not exist in controller, move it to model factory method
|
6
|
+
#
|
7
|
+
# Implementation: check the count of variable attribute assignment calling before saving,
|
8
|
+
# if more than defined attribute assignment count, then it's a complex creation.
|
9
|
+
class ReplaceComplexCreationWithFactoryMethodCheck < Check
|
10
|
+
|
11
|
+
def interesting_nodes
|
12
|
+
[:defn]
|
13
|
+
end
|
14
|
+
|
15
|
+
def interesting_files
|
16
|
+
/_controller.rb$/
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(options = {})
|
20
|
+
super()
|
21
|
+
@attrasgn_count = options['attribute_assignment_count'] || 2
|
22
|
+
end
|
23
|
+
|
24
|
+
def evaluate_start(node)
|
25
|
+
@variables = {}
|
26
|
+
node.recursive_children do |child|
|
27
|
+
case child.node_type
|
28
|
+
when :attrasgn
|
29
|
+
attribute_assignment(child)
|
30
|
+
when :call
|
31
|
+
call_assignment(child)
|
32
|
+
else
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@variables = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def attribute_assignment(node)
|
41
|
+
variable = node.subject
|
42
|
+
return if variable.nil?
|
43
|
+
@variables[variable] ||= 0
|
44
|
+
@variables[variable] += 1
|
45
|
+
end
|
46
|
+
|
47
|
+
def call_assignment(node)
|
48
|
+
if node.message == :save
|
49
|
+
variable = node.subject
|
50
|
+
add_error "replace complex creation with factory method" if @variables[variable] > @attrasgn_count
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rails_best_practices/checks/check'
|
2
|
+
|
3
|
+
module RailsBestPractices
|
4
|
+
module Checks
|
5
|
+
# Check a model creation to make sure using model association.
|
6
|
+
#
|
7
|
+
# Implementation:
|
8
|
+
# 1. check :attrasgn, if xxx_id is assigned to a variable, set the value of the assigned variable to true.
|
9
|
+
# 2. check :call, if call message :save and caller is included in variables, add error.
|
10
|
+
class UseModelAssociationCheck < Check
|
11
|
+
|
12
|
+
def interesting_nodes
|
13
|
+
[:defn]
|
14
|
+
end
|
15
|
+
|
16
|
+
def evaluate_start(node)
|
17
|
+
@variables = {}
|
18
|
+
node.recursive_children do |child|
|
19
|
+
case child.node_type
|
20
|
+
when :attrasgn
|
21
|
+
attribute_assignment(child)
|
22
|
+
when :call
|
23
|
+
call_assignment(child)
|
24
|
+
else
|
25
|
+
end
|
26
|
+
end
|
27
|
+
@variables = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def attribute_assignment(node)
|
33
|
+
if node.message.to_s =~ /_id=$/
|
34
|
+
variable = node.subject[1]
|
35
|
+
@variables[variable] = true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def call_assignment(node)
|
40
|
+
if node.message == :save
|
41
|
+
variable = node.subject[1]
|
42
|
+
add_error "use model association" if @variables[variable]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|