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
@@ -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
|