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.
- 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
@@ -5,14 +5,34 @@ module RailsBestPractices
|
|
5
5
|
module Checks
|
6
6
|
# Check db/schema.rb file to make sure every reference key has a database index.
|
7
7
|
#
|
8
|
-
#
|
8
|
+
# See the best practice details here http://rails-bestpractices.com/posts/21-always-add-db-index
|
9
|
+
#
|
10
|
+
# Implementation:
|
11
|
+
#
|
12
|
+
# Prepare process:
|
13
|
+
# none
|
14
|
+
#
|
15
|
+
# Review process:
|
16
|
+
# only check the call nodes and at the end of iter node in db/schema file,
|
17
|
+
# if the subject of call node is :create_table, then remember the table names
|
18
|
+
# if the subject of call node is :integer, then remember it as foreign key
|
19
|
+
# if the sujbect of call node is :string, the name of it is _type suffixed and there is an integer column _id suffixed, then remember it as polymorphic foreign key
|
20
|
+
# if the subject of call node is :add_index, then remember the index columns
|
21
|
+
# after all of these, at the end of iter node
|
22
|
+
#
|
23
|
+
# ActiveRecord::Schema.define(:version => 20101201111111) do
|
24
|
+
# ......
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# if there are any foreign keys not existed in index columns,
|
28
|
+
# then the foreign keys should add db index.
|
9
29
|
class AlwaysAddDbIndexCheck < Check
|
10
30
|
|
11
|
-
def
|
12
|
-
[:
|
31
|
+
def interesting_review_nodes
|
32
|
+
[:call, :iter]
|
13
33
|
end
|
14
34
|
|
15
|
-
def
|
35
|
+
def interesting_review_files
|
16
36
|
/db\/schema.rb/
|
17
37
|
end
|
18
38
|
|
@@ -23,23 +43,76 @@ module RailsBestPractices
|
|
23
43
|
@table_nodes = {}
|
24
44
|
end
|
25
45
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
46
|
+
# check call node in review process.
|
47
|
+
#
|
48
|
+
# if the message of call node is :create_table,
|
49
|
+
# then remember the table name (@table_nodes) like
|
50
|
+
# {
|
51
|
+
# "comments" =>
|
52
|
+
# s(:call, nil, :create_table, s(:arglist, s(:str, "comments"), s(:hash, s(:lit, :force), s(:true))))
|
53
|
+
# }
|
54
|
+
#
|
55
|
+
# if the message of call node is :integer,
|
56
|
+
# then remember it as a foreign key of last create table name.
|
57
|
+
#
|
58
|
+
# if the message of call node is :type and the name of argument is _type suffixed,
|
59
|
+
# then remember it with _id suffixed column as polymorphic foreign key.
|
60
|
+
#
|
61
|
+
# the remember foreign keys (@foreign_keys) like
|
62
|
+
#
|
63
|
+
# {
|
64
|
+
# "taggings" =>
|
65
|
+
# ["tag_id", ["taggable_id", "taggable_type"]]
|
66
|
+
# }
|
67
|
+
#
|
68
|
+
# if the message of call node is :add_index,
|
69
|
+
# then remember it as index columns (@index_columns) like
|
70
|
+
#
|
71
|
+
# {
|
72
|
+
# "comments" =>
|
73
|
+
# ["post_id", "user_id"]
|
74
|
+
# }
|
75
|
+
def review_start_call(node)
|
76
|
+
case node.message
|
77
|
+
when :create_table
|
78
|
+
remember_table_nodes(node)
|
79
|
+
when :integer, :string
|
80
|
+
remember_foreign_key_columns(node)
|
81
|
+
when :add_index
|
82
|
+
remember_index_columns(node)
|
83
|
+
else
|
38
84
|
end
|
39
85
|
end
|
40
86
|
|
41
|
-
|
42
|
-
|
87
|
+
# check at the end of iter node, like
|
88
|
+
#
|
89
|
+
# s(:iter,
|
90
|
+
# s(:call,
|
91
|
+
# s(:colon2, s(:const, :ActiveRecord), :Schema),
|
92
|
+
# :define,
|
93
|
+
# s(:arglist, s(:hash, s(:lit, :version), s(:lit, 20100603080629)))
|
94
|
+
# ),
|
95
|
+
# nil,
|
96
|
+
# s(:iter,
|
97
|
+
# s(:call, nil, :create_table,
|
98
|
+
# s(:arglist, s(:str, "comments"), s(:hash, s(:lit, :force), s(:true)))
|
99
|
+
# ),
|
100
|
+
# s(:lasgn, :t),
|
101
|
+
# s(:block,
|
102
|
+
# s(:call, s(:lvar, :t), :string, s(:arglist, s(:str, "content")))
|
103
|
+
# )
|
104
|
+
# )
|
105
|
+
# )
|
106
|
+
#
|
107
|
+
# if the subject of iter node is with subject ActiveRecord::Schema,
|
108
|
+
# it means we have completed the foreign keys and index columns parsing,
|
109
|
+
# then we compare foreign keys and index columns.
|
110
|
+
#
|
111
|
+
# if there are any foreign keys not existed in index columns,
|
112
|
+
# then we should add db index for that foreign keys.
|
113
|
+
def review_end_iter(node)
|
114
|
+
first_node = node.subject
|
115
|
+
if :call == first_node.node_type && s(:colon2, s(:const, :ActiveRecord), :Schema) == first_node.subject
|
43
116
|
remove_only_type_foreign_keys
|
44
117
|
@foreign_keys.each do |table, foreign_key|
|
45
118
|
table_node = @table_nodes[table]
|
@@ -53,20 +126,63 @@ module RailsBestPractices
|
|
53
126
|
end
|
54
127
|
|
55
128
|
private
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
129
|
+
# remember the node as index columns
|
130
|
+
#
|
131
|
+
# s(:call, nil, :add_index,
|
132
|
+
# s(:arglist,
|
133
|
+
# s(:str, "comments"),
|
134
|
+
# s(:array, s(:str, "post_id")),
|
135
|
+
# s(:hash, s(:lit, :name), s(:str, "index_comments_on_post_id"))
|
136
|
+
# )
|
137
|
+
# )
|
138
|
+
#
|
139
|
+
# the remember index columns are like
|
140
|
+
# {
|
141
|
+
# "comments" =>
|
142
|
+
# ["post_id", "user_id"]
|
143
|
+
# }
|
144
|
+
def remember_index_columns(node)
|
145
|
+
table_name = node.arguments[1].to_s
|
146
|
+
index_column = eval(node.arguments[2].to_s)
|
63
147
|
|
64
|
-
def add_index_column(table_name, index_column)
|
65
148
|
@index_columns[table_name] ||= []
|
66
149
|
@index_columns[table_name] << (index_column.size == 1 ? index_column[0] : index_column)
|
67
150
|
end
|
68
151
|
|
69
|
-
|
152
|
+
# remember table nodes
|
153
|
+
#
|
154
|
+
# if the node is
|
155
|
+
#
|
156
|
+
# s(:call, nil, :create_table,
|
157
|
+
# s(:arglist, s(:str, "comments"), s(:hash, s(:lit, :force), s(:true))))
|
158
|
+
#
|
159
|
+
# then the table nodes will be
|
160
|
+
#
|
161
|
+
# {
|
162
|
+
# "comments" =>
|
163
|
+
# s(:call, nil, :create_table, s(:arglist, s(:str, "comments"), s(:hash, s(:lit, :force), s(:true))))
|
164
|
+
# }
|
165
|
+
def remember_table_nodes(node)
|
166
|
+
@table_name = node.arguments[1].to_s
|
167
|
+
@table_nodes[@table_name] = node
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
# remember foreign key columns
|
172
|
+
#
|
173
|
+
# if the message of node is :integer,
|
174
|
+
# then it is a foreign key, like
|
175
|
+
#
|
176
|
+
# s(:call, s(:lvar, :t), :integer, s(:arglist, s(:str, "post_id")))
|
177
|
+
#
|
178
|
+
# if the message of node is :string, with _type suffixed and there is a _id suffixed column,
|
179
|
+
# then they are polymorphic foreign key
|
180
|
+
#
|
181
|
+
# s(:call, s(:lvar, :t), :integer, s(:arglist, s(:str, "taggable_id")))
|
182
|
+
# s(:call, s(:lvar, :t), :string, s(:arglist, s(:str, "taggable_type")))
|
183
|
+
def remember_foreign_key_columns(node)
|
184
|
+
table_name = @table_name
|
185
|
+
foreign_key_column = node.arguments[1].to_s
|
70
186
|
@foreign_keys[table_name] ||= []
|
71
187
|
if foreign_key_column =~ /(.*?)_id$/
|
72
188
|
if @foreign_keys[table_name].delete("#{$1}_type")
|
@@ -83,18 +199,21 @@ module RailsBestPractices
|
|
83
199
|
end
|
84
200
|
end
|
85
201
|
|
202
|
+
# remove the non foreign keys with only _type column.
|
86
203
|
def remove_only_type_foreign_keys
|
87
204
|
@foreign_keys.delete_if { |table, foreign_key|
|
88
205
|
foreign_key.size == 1 && foreign_key[0] =~ /_type$/
|
89
206
|
}
|
90
207
|
end
|
91
208
|
|
209
|
+
# check if the table's column is indexed.
|
92
210
|
def indexed?(table, column)
|
93
211
|
index_columns = @index_columns[table]
|
94
|
-
!index_columns || !index_columns.any? { |e|
|
212
|
+
!index_columns || !index_columns.any? { |e| greater_than_or_equal(Array(e), Array(column)) }
|
95
213
|
end
|
96
214
|
|
97
|
-
|
215
|
+
# check if more_array is greater than less_array or equal to less_array.
|
216
|
+
def greater_than_or_equal(more_array, less_array)
|
98
217
|
more_size = more_array.size
|
99
218
|
less_size = less_array.size
|
100
219
|
(more_array - less_array).size == more_size - less_size
|
@@ -3,14 +3,21 @@ require 'rails_best_practices/core/error'
|
|
3
3
|
|
4
4
|
module RailsBestPractices
|
5
5
|
module Checks
|
6
|
+
# A Check class that takes charge of reviewing one rails best practice.
|
7
|
+
# One check contains two process:
|
8
|
+
# 1. prepare process (optional), in this process, one check will do some preparation, such as analyzing the model associations.The check only does the preparation for the nodes (defined in interesting_prepare_nodes) in the files (defined in interesting_prepare_files).
|
9
|
+
# 2. review process, in this process, one check will really review your rails codes. The check only review the nodes (defined in interesting_review_nodes) in the files # (defined in interesting_review_files).
|
6
10
|
class Check
|
7
|
-
|
11
|
+
# only nodes whose node_type is in NODE_TYPE will be reviewed.
|
12
|
+
NODE_TYPES = [:call, :defn, :defs, :if, :class, :lasgn, :iasgn, :ivar, :lvar, :block, :iter, :const]
|
8
13
|
|
9
14
|
CONTROLLER_FILES = /_controller\.rb$/
|
10
15
|
MIGRATION_FILES = /db\/migrate\/.*\.rb$/
|
11
|
-
|
16
|
+
MODEL_FILES = /models\/.*\.rb$/
|
17
|
+
MAILER_FILES = /models\/.*\.rb$|mailers\/.*\.rb/
|
12
18
|
VIEW_FILES = /views\/.*\.(erb|haml)$/
|
13
19
|
PARTIAL_VIEW_FILES = /views\/.*\/_.*\.(erb|haml)$/
|
20
|
+
ROUTE_FILE = /config\/routes.rb/
|
14
21
|
|
15
22
|
attr_reader :errors
|
16
23
|
|
@@ -18,82 +25,178 @@ module RailsBestPractices
|
|
18
25
|
@errors = []
|
19
26
|
end
|
20
27
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
28
|
+
# define default interesting_prepare_nodes, interesting_review_nodes, interesting_prepare_files and interesting_review_files.
|
29
|
+
[:prepare, :review].each do |process|
|
30
|
+
class_eval <<-EOS
|
31
|
+
def interesting_#{process}_nodes # def interesting_review_nodes
|
32
|
+
[] # []
|
33
|
+
end # end
|
34
|
+
#
|
35
|
+
def interesting_#{process}_files # def interesting_review_files
|
36
|
+
/.*/ # /.*/
|
37
|
+
end # end
|
38
|
+
EOS
|
39
|
+
end
|
40
|
+
|
41
|
+
# define method prepare_node_start, prepare_node_end, review_node_start and review_node_end.
|
42
|
+
#
|
43
|
+
# they delegate the node to special process method, like
|
44
|
+
#
|
45
|
+
# review_node_start(call_node) => review_start_call(call_node)
|
46
|
+
# review_node_end(defn_node) => review_end_defn(defn_node)
|
47
|
+
# prepare_node_start(calss_node) => prepare_start_class(class_node)
|
48
|
+
# prepare_node_end(if_node) => prepare_end_if(if_node)
|
49
|
+
[:prepare, :review].each do |process|
|
50
|
+
class_eval <<-EOS
|
51
|
+
def #{process}_node_start(node) # def review_node_start(node)
|
52
|
+
@node = node # @node = node
|
53
|
+
method = "#{process}_start_" + node.node_type.to_s # method = "review_start_" + node.node_type.to_s
|
54
|
+
self.send(method, node) # self.send(method, node)
|
55
|
+
end # end
|
56
|
+
#
|
57
|
+
def #{process}_node_end(node) # def review_node_end(node)
|
58
|
+
@node = node # @node = node
|
59
|
+
method = "#{process}_end_" + node.node_type.to_s # method = "review_end_" + node.node_type.to_s
|
60
|
+
self.send(method, node) # self.send(method, node)
|
61
|
+
end # end
|
62
|
+
EOS
|
63
|
+
end
|
64
|
+
|
65
|
+
# define all start and end process for each node type, like
|
66
|
+
#
|
67
|
+
# prepare_start_defn
|
68
|
+
# prepare_end_defn
|
69
|
+
# review_start_call
|
70
|
+
# review_end_call
|
71
|
+
[:prepare, :review].each do |process|
|
72
|
+
NODE_TYPES.each do |node|
|
73
|
+
class_eval <<-EOS
|
74
|
+
def #{process}_start_#{node}(node) # def review_start_defn(node)
|
75
|
+
end # end
|
76
|
+
#
|
77
|
+
def #{process}_end_#{node}(node) # def review_end_defn(node)
|
78
|
+
end # end
|
79
|
+
EOS
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# remember the model names and model associations in prepare process.
|
84
|
+
def self.prepare_model_associations
|
85
|
+
class_eval <<-EOS
|
86
|
+
def initialize
|
87
|
+
super
|
88
|
+
@klazzes = []
|
89
|
+
@associations = {}
|
90
|
+
end
|
91
|
+
|
92
|
+
def interesting_prepare_nodes
|
93
|
+
[:class, :call]
|
94
|
+
end
|
95
|
+
|
96
|
+
def interesting_prepare_files
|
97
|
+
MODEL_FILES
|
98
|
+
end
|
99
|
+
|
100
|
+
# check class node to remember all class name in prepare process.
|
101
|
+
#
|
102
|
+
# the remembered class names (@klazzes) are like
|
103
|
+
# [ :User, :Post ]
|
104
|
+
def prepare_start_class(node)
|
105
|
+
remember_klazz(node)
|
106
|
+
end
|
107
|
+
|
108
|
+
# check call node to remember all assoication names in prepare process.
|
109
|
+
#
|
110
|
+
# the remembered association names (@associations) are like
|
111
|
+
# { :User => [":projects", ":location"], :Post => [":comments"] }
|
112
|
+
def prepare_start_call(node)
|
113
|
+
remember_association(node) if association_methods.include? node.message
|
114
|
+
end
|
115
|
+
|
116
|
+
# remember class models, just the subject of class node.
|
117
|
+
def remember_klazz(class_node)
|
118
|
+
@klazzes << class_node.class_name
|
119
|
+
end
|
120
|
+
|
121
|
+
# remember associations, with class to association names.
|
122
|
+
def remember_association(association_node)
|
123
|
+
@associations[@klazzes.last] ||= []
|
124
|
+
@associations[@klazzes.last] << association_node.arguments[1].to_s
|
125
|
+
end
|
126
|
+
|
127
|
+
# default rails association methods.
|
128
|
+
def association_methods
|
129
|
+
[:belongs_to, :has_one, :has_many, :has_and_belongs_to_many]
|
130
|
+
end
|
131
|
+
|
132
|
+
EOS
|
133
|
+
end
|
134
|
+
|
135
|
+
# add error if source code violates rails best practice.
|
136
|
+
# error is the string message for violation of the rails best practice
|
137
|
+
# file is the filename of source code
|
138
|
+
# line is the line number of the source code which is reviewing
|
139
|
+
def add_error(error, file = @node.file, line = @node.line)
|
92
140
|
@errors << RailsBestPractices::Core::Error.new("#{file}", "#{line}", error)
|
93
141
|
end
|
94
142
|
|
95
|
-
|
96
|
-
|
143
|
+
# remember use count for the local or instance variable in the call or attrasgn node.
|
144
|
+
#
|
145
|
+
# find the local variable or instance variable in the call or attrasgn node,
|
146
|
+
# then save it to as key in @variable_use_count hash, and add the call count (hash value).
|
147
|
+
def remember_variable_use_count(node)
|
148
|
+
variable_node = variable(node)
|
149
|
+
if variable_node
|
150
|
+
variable_use_count[variable_node] ||= 0
|
151
|
+
variable_use_count[variable_node] += 1
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# return @variable_use_count hash.
|
156
|
+
def variable_use_count
|
157
|
+
@variable_use_count ||= {}
|
158
|
+
end
|
159
|
+
|
160
|
+
# reset @variable_use_count hash.
|
161
|
+
def reset_variable_use_count
|
162
|
+
@variable_use_count = nil
|
163
|
+
end
|
164
|
+
|
165
|
+
# find local variable or instance variable in the most inner call node, e.g.
|
166
|
+
#
|
167
|
+
# if the call node is
|
168
|
+
#
|
169
|
+
# s(:call, s(:ivar, :@post), :editors, s(:arglist)),
|
170
|
+
#
|
171
|
+
# or it is
|
172
|
+
#
|
173
|
+
# s(:call,
|
174
|
+
# s(:call, s(:ivar, :@post), :editors, s(:arglist)),
|
175
|
+
# :include?,
|
176
|
+
# s(:arglist, s(:call, nil, :current_user, s(:arglist)))
|
177
|
+
# )
|
178
|
+
#
|
179
|
+
# then the variable both are s(:ivar, :@post).
|
180
|
+
#
|
181
|
+
def variable(node)
|
182
|
+
while node.subject.node_type == :call
|
183
|
+
node = node.subject
|
184
|
+
end
|
185
|
+
subject_node = node.subject
|
186
|
+
if [:ivar, :lvar].include?(subject_node.node_type) and subject_node[1] != :_erbout
|
187
|
+
subject_node
|
188
|
+
else
|
189
|
+
nil
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# compare two sexp nodes' to_s.
|
194
|
+
# equal?(":test", :test) => true
|
195
|
+
# equai?("@test", :test) => true
|
196
|
+
def equal?(node, expected_node)
|
197
|
+
actual = node.to_s.downcase
|
198
|
+
expected = expected_node.to_s.downcase
|
199
|
+
actual == expected || actual == ':' + expected || actual == '@' + expected
|
97
200
|
end
|
98
201
|
end
|
99
202
|
end
|