rubocop-rails 2.0.1 → 2.19.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +52 -5
  4. data/config/default.yml +726 -32
  5. data/config/obsoletion.yml +17 -0
  6. data/lib/rubocop/cop/mixin/active_record_helper.rb +106 -0
  7. data/lib/rubocop/cop/mixin/active_record_migrations_helper.rb +32 -0
  8. data/lib/rubocop/cop/mixin/class_send_node_helper.rb +20 -0
  9. data/lib/rubocop/cop/mixin/enforce_superclass.rb +40 -0
  10. data/lib/rubocop/cop/mixin/index_method.rb +165 -0
  11. data/lib/rubocop/cop/mixin/migrations_helper.rb +26 -0
  12. data/lib/rubocop/cop/rails/action_controller_flash_before_render.rb +112 -0
  13. data/lib/rubocop/cop/rails/action_controller_test_case.rb +47 -0
  14. data/lib/rubocop/cop/rails/action_filter.rb +11 -21
  15. data/lib/rubocop/cop/rails/action_order.rb +116 -0
  16. data/lib/rubocop/cop/rails/active_record_aliases.rb +23 -24
  17. data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +143 -0
  18. data/lib/rubocop/cop/rails/active_record_override.rb +3 -6
  19. data/lib/rubocop/cop/rails/active_support_aliases.rb +13 -22
  20. data/lib/rubocop/cop/rails/active_support_on_load.rb +70 -0
  21. data/lib/rubocop/cop/rails/add_column_index.rb +61 -0
  22. data/lib/rubocop/cop/rails/after_commit_override.rb +81 -0
  23. data/lib/rubocop/cop/rails/application_controller.rb +36 -0
  24. data/lib/rubocop/cop/rails/application_job.rb +9 -4
  25. data/lib/rubocop/cop/rails/application_mailer.rb +39 -0
  26. data/lib/rubocop/cop/rails/application_record.rb +9 -9
  27. data/lib/rubocop/cop/rails/arel_star.rb +47 -0
  28. data/lib/rubocop/cop/rails/assert_not.rb +8 -10
  29. data/lib/rubocop/cop/rails/attribute_default_block_value.rb +90 -0
  30. data/lib/rubocop/cop/rails/belongs_to.rb +12 -24
  31. data/lib/rubocop/cop/rails/blank.rb +40 -36
  32. data/lib/rubocop/cop/rails/bulk_change_table.rb +40 -35
  33. data/lib/rubocop/cop/rails/compact_blank.rb +111 -0
  34. data/lib/rubocop/cop/rails/content_tag.rb +93 -0
  35. data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +22 -15
  36. data/lib/rubocop/cop/rails/date.rb +41 -36
  37. data/lib/rubocop/cop/rails/default_scope.rb +61 -0
  38. data/lib/rubocop/cop/rails/delegate.rb +33 -29
  39. data/lib/rubocop/cop/rails/delegate_allow_blank.rb +9 -10
  40. data/lib/rubocop/cop/rails/deprecated_active_model_errors_methods.rb +168 -0
  41. data/lib/rubocop/cop/rails/dot_separated_keys.rb +71 -0
  42. data/lib/rubocop/cop/rails/duplicate_association.rb +56 -0
  43. data/lib/rubocop/cop/rails/duplicate_scope.rb +46 -0
  44. data/lib/rubocop/cop/rails/duration_arithmetic.rb +98 -0
  45. data/lib/rubocop/cop/rails/dynamic_find_by.rb +76 -31
  46. data/lib/rubocop/cop/rails/eager_evaluation_log_message.rb +82 -0
  47. data/lib/rubocop/cop/rails/enum_hash.rb +75 -0
  48. data/lib/rubocop/cop/rails/enum_uniqueness.rb +30 -12
  49. data/lib/rubocop/cop/rails/environment_comparison.rb +70 -22
  50. data/lib/rubocop/cop/rails/environment_variable_access.rb +67 -0
  51. data/lib/rubocop/cop/rails/exit.rb +7 -13
  52. data/lib/rubocop/cop/rails/expanded_date_range.rb +102 -0
  53. data/lib/rubocop/cop/rails/file_path.rb +48 -31
  54. data/lib/rubocop/cop/rails/find_by.rb +43 -24
  55. data/lib/rubocop/cop/rails/find_by_id.rb +94 -0
  56. data/lib/rubocop/cop/rails/find_each.rb +42 -18
  57. data/lib/rubocop/cop/rails/freeze_time.rb +79 -0
  58. data/lib/rubocop/cop/rails/has_and_belongs_to_many.rb +4 -3
  59. data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +62 -25
  60. data/lib/rubocop/cop/rails/helper_instance_variable.rb +32 -4
  61. data/lib/rubocop/cop/rails/http_positional_arguments.rb +61 -32
  62. data/lib/rubocop/cop/rails/http_status.rb +27 -23
  63. data/lib/rubocop/cop/rails/i18n_lazy_lookup.rb +96 -0
  64. data/lib/rubocop/cop/rails/i18n_locale_assignment.rb +37 -0
  65. data/lib/rubocop/cop/rails/i18n_locale_texts.rb +110 -0
  66. data/lib/rubocop/cop/rails/ignored_columns_assignment.rb +50 -0
  67. data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +9 -16
  68. data/lib/rubocop/cop/rails/index_by.rb +65 -0
  69. data/lib/rubocop/cop/rails/index_with.rb +68 -0
  70. data/lib/rubocop/cop/rails/inquiry.rb +39 -0
  71. data/lib/rubocop/cop/rails/inverse_of.rb +33 -27
  72. data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +62 -32
  73. data/lib/rubocop/cop/rails/link_to_blank.rb +31 -32
  74. data/lib/rubocop/cop/rails/mailer_name.rb +90 -0
  75. data/lib/rubocop/cop/rails/match_route.rb +120 -0
  76. data/lib/rubocop/cop/rails/migration_class_name.rb +63 -0
  77. data/lib/rubocop/cop/rails/negate_include.rb +42 -0
  78. data/lib/rubocop/cop/rails/not_null_column.rb +16 -12
  79. data/lib/rubocop/cop/rails/order_by_id.rb +51 -0
  80. data/lib/rubocop/cop/rails/output.rb +29 -10
  81. data/lib/rubocop/cop/rails/output_safety.rb +9 -4
  82. data/lib/rubocop/cop/rails/pick.rb +64 -0
  83. data/lib/rubocop/cop/rails/pluck.rb +96 -0
  84. data/lib/rubocop/cop/rails/pluck_id.rb +59 -0
  85. data/lib/rubocop/cop/rails/pluck_in_where.rb +71 -0
  86. data/lib/rubocop/cop/rails/pluralization_grammar.rb +14 -19
  87. data/lib/rubocop/cop/rails/presence.rb +54 -26
  88. data/lib/rubocop/cop/rails/present.rb +40 -37
  89. data/lib/rubocop/cop/rails/rake_environment.rb +112 -0
  90. data/lib/rubocop/cop/rails/read_write_attribute.rb +56 -18
  91. data/lib/rubocop/cop/rails/redundant_allow_nil.rb +33 -45
  92. data/lib/rubocop/cop/rails/redundant_foreign_key.rb +77 -0
  93. data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +257 -0
  94. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +34 -32
  95. data/lib/rubocop/cop/rails/redundant_travel_back.rb +57 -0
  96. data/lib/rubocop/cop/rails/reflection_class_name.rb +56 -7
  97. data/lib/rubocop/cop/rails/refute_methods.rb +56 -35
  98. data/lib/rubocop/cop/rails/relative_date_constant.rb +52 -33
  99. data/lib/rubocop/cop/rails/render_inline.rb +41 -0
  100. data/lib/rubocop/cop/rails/render_plain_text.rb +71 -0
  101. data/lib/rubocop/cop/rails/request_referer.rb +10 -11
  102. data/lib/rubocop/cop/rails/require_dependency.rb +38 -0
  103. data/lib/rubocop/cop/rails/response_parsed_body.rb +57 -0
  104. data/lib/rubocop/cop/rails/reversible_migration.rb +122 -82
  105. data/lib/rubocop/cop/rails/reversible_migration_method_definition.rb +66 -0
  106. data/lib/rubocop/cop/rails/root_join_chain.rb +72 -0
  107. data/lib/rubocop/cop/rails/root_pathname_methods.rb +238 -0
  108. data/lib/rubocop/cop/rails/root_public_path.rb +59 -0
  109. data/lib/rubocop/cop/rails/safe_navigation.rb +55 -43
  110. data/lib/rubocop/cop/rails/safe_navigation_with_blank.rb +50 -0
  111. data/lib/rubocop/cop/rails/save_bang.rb +89 -63
  112. data/lib/rubocop/cop/rails/schema_comment.rb +104 -0
  113. data/lib/rubocop/cop/rails/scope_args.rb +8 -3
  114. data/lib/rubocop/cop/rails/short_i18n.rb +71 -0
  115. data/lib/rubocop/cop/rails/skips_model_validations.rb +53 -16
  116. data/lib/rubocop/cop/rails/squished_sql_heredocs.rb +87 -0
  117. data/lib/rubocop/cop/rails/strip_heredoc.rb +56 -0
  118. data/lib/rubocop/cop/rails/table_name_assignment.rb +44 -0
  119. data/lib/rubocop/cop/rails/three_state_boolean_column.rb +73 -0
  120. data/lib/rubocop/cop/rails/time_zone.rb +83 -67
  121. data/lib/rubocop/cop/rails/time_zone_assignment.rb +37 -0
  122. data/lib/rubocop/cop/rails/to_formatted_s.rb +46 -0
  123. data/lib/rubocop/cop/rails/to_s_with_argument.rb +78 -0
  124. data/lib/rubocop/cop/rails/top_level_hash_with_indifferent_access.rb +49 -0
  125. data/lib/rubocop/cop/rails/transaction_exit_statement.rb +99 -0
  126. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +40 -49
  127. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +172 -0
  128. data/lib/rubocop/cop/rails/unknown_env.rb +52 -21
  129. data/lib/rubocop/cop/rails/unused_ignored_columns.rb +76 -0
  130. data/lib/rubocop/cop/rails/validation.rb +54 -23
  131. data/lib/rubocop/cop/rails/where_equals.rb +102 -0
  132. data/lib/rubocop/cop/rails/where_exists.rb +138 -0
  133. data/lib/rubocop/cop/rails/where_missing.rb +118 -0
  134. data/lib/rubocop/cop/rails/where_not.rb +101 -0
  135. data/lib/rubocop/cop/rails/where_not_with_multiple_conditions.rb +55 -0
  136. data/lib/rubocop/cop/rails_cops.rb +78 -8
  137. data/lib/rubocop/rails/inject.rb +1 -1
  138. data/lib/rubocop/rails/schema_loader/schema.rb +191 -0
  139. data/lib/rubocop/rails/schema_loader.rb +61 -0
  140. data/lib/rubocop/rails/version.rb +5 -1
  141. data/lib/rubocop/rails.rb +3 -1
  142. data/lib/rubocop-rails.rb +22 -0
  143. metadata +120 -19
  144. data/bin/setup +0 -7
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Rails
6
- # This cop checks whether the change method of the migration file is
6
+ # Checks whether the change method of the migration file is
7
7
  # reversible.
8
8
  #
9
9
  # @example
@@ -16,23 +16,15 @@ module RuboCop
16
16
  #
17
17
  # # good
18
18
  # def change
19
- # create_table :users do |t|
20
- # t.string :name
19
+ # change_table :users do |t|
20
+ # t.remove :name, :string
21
21
  # end
22
22
  # end
23
23
  #
24
24
  # # good
25
25
  # def change
26
- # reversible do |dir|
27
- # change_table :users do |t|
28
- # dir.up do
29
- # t.column :name, :string
30
- # end
31
- #
32
- # dir.down do
33
- # t.remove :name
34
- # end
35
- # end
26
+ # create_table :users do |t|
27
+ # t.string :name
36
28
  # end
37
29
  # end
38
30
  #
@@ -90,6 +82,11 @@ module RuboCop
90
82
  # remove_foreign_key :accounts, :branches
91
83
  # end
92
84
  #
85
+ # # good
86
+ # def change
87
+ # remove_foreign_key :accounts, to_table: :branches
88
+ # end
89
+ #
93
90
  # @example
94
91
  # # change_table
95
92
  #
@@ -109,71 +106,106 @@ module RuboCop
109
106
  # end
110
107
  # end
111
108
  #
109
+ # @example
110
+ # # remove_columns
111
+ #
112
+ # # bad
113
+ # def change
114
+ # remove_columns :users, :name, :email
115
+ # end
116
+ #
112
117
  # # good
113
118
  # def change
114
119
  # reversible do |dir|
115
- # change_table :users do |t|
116
- # dir.up do
117
- # t.change :price, :string
118
- # end
120
+ # dir.up do
121
+ # remove_columns :users, :name, :email
122
+ # end
119
123
  #
120
- # dir.down do
121
- # t.change :price, :integer
122
- # end
124
+ # dir.down do
125
+ # add_column :users, :name, :string
126
+ # add_column :users, :email, :string
123
127
  # end
124
128
  # end
125
129
  # end
126
130
  #
127
- # @see https://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html
128
- class ReversibleMigration < Cop
131
+ # # good (Rails >= 6.1, see https://github.com/rails/rails/pull/36589)
132
+ # def change
133
+ # remove_columns :users, :name, :email, type: :string
134
+ # end
135
+ #
136
+ # @example
137
+ # # remove_index
138
+ #
139
+ # # bad
140
+ # def change
141
+ # remove_index :users, name: :index_users_on_email
142
+ # end
143
+ #
144
+ # # good
145
+ # def change
146
+ # remove_index :users, :email
147
+ # end
148
+ #
149
+ # # good
150
+ # def change
151
+ # remove_index :users, column: :email
152
+ # end
153
+ class ReversibleMigration < Base
154
+ include MigrationsHelper
155
+
129
156
  MSG = '%<action>s is not reversible.'
130
- IRREVERSIBLE_CHANGE_TABLE_CALLS = %i[
131
- change change_default remove
132
- ].freeze
133
157
 
134
- def_node_matcher :irreversible_schema_statement_call, <<-PATTERN
135
- (send nil? ${:change_table_comment :execute :remove_belongs_to} ...)
158
+ def_node_matcher :irreversible_schema_statement_call, <<~PATTERN
159
+ (send nil? ${:change_column :execute} ...)
136
160
  PATTERN
137
161
 
138
- def_node_matcher :drop_table_call, <<-PATTERN
162
+ def_node_matcher :drop_table_call, <<~PATTERN
139
163
  (send nil? :drop_table ...)
140
164
  PATTERN
141
165
 
142
- def_node_matcher :change_column_default_call, <<-PATTERN
143
- (send nil? :change_column_default {[(sym _) (sym _)] (splat _)} $...)
144
- PATTERN
145
-
146
- def_node_matcher :remove_column_call, <<-PATTERN
166
+ def_node_matcher :remove_column_call, <<~PATTERN
147
167
  (send nil? :remove_column $...)
148
168
  PATTERN
149
169
 
150
- def_node_matcher :remove_foreign_key_call, <<-PATTERN
170
+ def_node_matcher :remove_foreign_key_call, <<~PATTERN
151
171
  (send nil? :remove_foreign_key _ $_)
152
172
  PATTERN
153
173
 
154
- def_node_matcher :change_table_call, <<-PATTERN
174
+ def_node_matcher :change_table_call, <<~PATTERN
155
175
  (send nil? :change_table $_ ...)
156
176
  PATTERN
157
177
 
178
+ def_node_matcher :remove_columns_call, <<~PATTERN
179
+ (send nil? :remove_columns ... $_)
180
+ PATTERN
181
+
182
+ def_node_matcher :remove_index_call, <<~PATTERN
183
+ (send nil? :remove_index _ $_)
184
+ PATTERN
185
+
158
186
  def on_send(node)
159
- return unless within_change_method?(node)
187
+ return unless in_migration?(node) && within_change_method?(node)
160
188
  return if within_reversible_or_up_only_block?(node)
161
189
 
162
190
  check_irreversible_schema_statement_node(node)
163
191
  check_drop_table_node(node)
164
- check_change_column_default_node(node)
192
+ check_reversible_hash_node(node)
165
193
  check_remove_column_node(node)
166
194
  check_remove_foreign_key_node(node)
195
+ check_remove_columns_node(node)
196
+ check_remove_index_node(node)
167
197
  end
168
198
 
169
199
  def on_block(node)
170
- return unless within_change_method?(node)
200
+ return unless in_migration?(node) && within_change_method?(node)
171
201
  return if within_reversible_or_up_only_block?(node)
172
202
  return if node.body.nil?
173
203
 
174
204
  check_change_table_node(node.send_node, node.body)
175
205
  end
176
206
 
207
+ alias on_numblock on_block
208
+
177
209
  private
178
210
 
179
211
  def check_irreversible_schema_statement_node(node)
@@ -184,59 +216,35 @@ module RuboCop
184
216
 
185
217
  def check_drop_table_node(node)
186
218
  drop_table_call(node) do
187
- unless node.parent.block_type?
188
- add_offense(
189
- node,
190
- message: format(MSG, action: 'drop_table(without block)')
191
- )
219
+ unless node.parent.block_type? || node.last_argument.block_pass_type?
220
+ add_offense(node, message: format(MSG, action: 'drop_table(without block)'))
192
221
  end
193
222
  end
194
223
  end
195
224
 
196
- def check_change_column_default_node(node)
197
- change_column_default_call(node) do |args|
198
- unless all_hash_key?(args.last, :from, :to)
199
- add_offense(
200
- node,
201
- message: format(
202
- MSG, action: 'change_column_default(without :from and :to)'
203
- )
204
- )
205
- end
206
- end
225
+ def check_reversible_hash_node(node)
226
+ return if reversible_change_table_call?(node)
227
+
228
+ add_offense(node, message: format(MSG, action: "#{node.method_name}(without :from and :to)"))
207
229
  end
208
230
 
209
231
  def check_remove_column_node(node)
210
232
  remove_column_call(node) do |args|
211
- if args.to_a.size < 3
212
- add_offense(
213
- node,
214
- message: format(MSG, action: 'remove_column(without type)')
215
- )
216
- end
233
+ add_offense(node, message: format(MSG, action: 'remove_column(without type)')) if args.to_a.size < 3
217
234
  end
218
235
  end
219
236
 
220
237
  def check_remove_foreign_key_node(node)
221
238
  remove_foreign_key_call(node) do |arg|
222
- if arg.hash_type?
223
- add_offense(
224
- node,
225
- message: format(MSG,
226
- action: 'remove_foreign_key(without table)')
227
- )
239
+ if arg.hash_type? && !all_hash_key?(arg, :to_table)
240
+ add_offense(node, message: format(MSG, action: 'remove_foreign_key(without table)'))
228
241
  end
229
242
  end
230
243
  end
231
244
 
232
245
  def check_change_table_node(node, block)
233
246
  change_table_call(node) do |arg|
234
- if target_rails_version < 4.0
235
- add_offense(
236
- node,
237
- message: format(MSG, action: 'change_table')
238
- )
239
- elsif block.send_type?
247
+ if block.send_type?
240
248
  check_change_table_offense(arg, block)
241
249
  else
242
250
  block.each_child_node(:send) do |child_node|
@@ -246,15 +254,49 @@ module RuboCop
246
254
  end
247
255
  end
248
256
 
257
+ def check_remove_columns_node(node)
258
+ remove_columns_call(node) do |args|
259
+ unless all_hash_key?(args, :type) && target_rails_version >= 6.1
260
+ action = target_rails_version >= 6.1 ? 'remove_columns(without type)' : 'remove_columns'
261
+
262
+ add_offense(node, message: format(MSG, action: action))
263
+ end
264
+ end
265
+ end
266
+
267
+ def check_remove_index_node(node)
268
+ remove_index_call(node) do |args|
269
+ if args.hash_type? && !all_hash_key?(args, :column)
270
+ add_offense(node, message: format(MSG, action: 'remove_index(without column)'))
271
+ end
272
+ end
273
+ end
274
+
249
275
  def check_change_table_offense(receiver, node)
250
276
  method_name = node.method_name
251
- return if receiver != node.receiver &&
252
- !IRREVERSIBLE_CHANGE_TABLE_CALLS.include?(method_name)
277
+ return if receiver != node.receiver && reversible_change_table_call?(node)
278
+
279
+ action = if method_name == :remove
280
+ target_rails_version >= 6.1 ? 't.remove (without type)' : 't.remove'
281
+ else
282
+ "change_table(with #{method_name})"
283
+ end
253
284
 
254
- add_offense(
255
- node,
256
- message: format(MSG, action: "change_table(with #{method_name})")
257
- )
285
+ add_offense(node, message: format(MSG, action: action))
286
+ end
287
+
288
+ def reversible_change_table_call?(node)
289
+ case node.method_name
290
+ when :change
291
+ false
292
+ when :remove
293
+ target_rails_version >= 6.1 && all_hash_key?(node.arguments.last, :type)
294
+ when :change_default, :change_column_default, :change_table_comment,
295
+ :change_column_comment
296
+ all_hash_key?(node.arguments.last, :from, :to)
297
+ else
298
+ true
299
+ end
258
300
  end
259
301
 
260
302
  def within_change_method?(node)
@@ -265,9 +307,7 @@ module RuboCop
265
307
 
266
308
  def within_reversible_or_up_only_block?(node)
267
309
  node.each_ancestor(:block).any? do |ancestor|
268
- ancestor.block_type? &&
269
- ancestor.send_node.method?(:reversible) ||
270
- ancestor.send_node.method?(:up_only)
310
+ (ancestor.block_type? && ancestor.send_node.method?(:reversible)) || ancestor.send_node.method?(:up_only)
271
311
  end
272
312
  end
273
313
 
@@ -278,7 +318,7 @@ module RuboCop
278
318
  key.children.first.to_sym
279
319
  end
280
320
 
281
- hash_keys & keys == keys
321
+ (hash_keys & keys).sort == keys
282
322
  end
283
323
  end
284
324
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Checks whether the migration implements
7
+ # either a `change` method or both an `up` and a `down`
8
+ # method.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # class SomeMigration < ActiveRecord::Migration[6.0]
13
+ # def up
14
+ # # up migration
15
+ # end
16
+ #
17
+ # # <----- missing down method
18
+ # end
19
+ #
20
+ # class SomeMigration < ActiveRecord::Migration[6.0]
21
+ # # <----- missing up method
22
+ #
23
+ # def down
24
+ # # down migration
25
+ # end
26
+ # end
27
+ #
28
+ # # good
29
+ # class SomeMigration < ActiveRecord::Migration[6.0]
30
+ # def change
31
+ # # reversible migration
32
+ # end
33
+ # end
34
+ #
35
+ # # good
36
+ # class SomeMigration < ActiveRecord::Migration[6.0]
37
+ # def up
38
+ # # up migration
39
+ # end
40
+ #
41
+ # def down
42
+ # # down migration
43
+ # end
44
+ # end
45
+ class ReversibleMigrationMethodDefinition < Base
46
+ include MigrationsHelper
47
+
48
+ MSG = 'Migrations must contain either a `change` method, or both an `up` and a `down` method.'
49
+
50
+ def_node_matcher :change_method?, <<~PATTERN
51
+ `(def :change (args) _)
52
+ PATTERN
53
+
54
+ def_node_matcher :up_and_down_methods?, <<~PATTERN
55
+ [`(def :up (args) _) `(def :down (args) _)]
56
+ PATTERN
57
+
58
+ def on_class(node)
59
+ return if !migration_class?(node) || change_method?(node) || up_and_down_methods?(node)
60
+
61
+ add_offense(node)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Use a single `#join` instead of chaining on `Rails.root` or `Rails.public_path`.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # Rails.root.join('db').join('schema.rb')
11
+ # Rails.root.join('db').join(migrate).join('migration.rb')
12
+ # Rails.public_path.join('path').join('file.pdf')
13
+ # Rails.public_path.join('path').join(to).join('file.pdf')
14
+ #
15
+ # # good
16
+ # Rails.root.join('db', 'schema.rb')
17
+ # Rails.root.join('db', migrate, 'migration.rb')
18
+ # Rails.public_path.join('path', 'file.pdf')
19
+ # Rails.public_path.join('path', to, 'file.pdf')
20
+ #
21
+ class RootJoinChain < Base
22
+ extend AutoCorrector
23
+ include RangeHelp
24
+
25
+ MSG = 'Use `%<root>s.join(...)` instead of chaining `#join` calls.'
26
+
27
+ RESTRICT_ON_SEND = %i[join].to_set.freeze
28
+
29
+ # @!method rails_root?(node)
30
+ def_node_matcher :rails_root?, <<~PATTERN
31
+ (send (const {nil? cbase} :Rails) {:root :public_path})
32
+ PATTERN
33
+
34
+ # @!method join?(node)
35
+ def_node_matcher :join?, <<~PATTERN
36
+ (send _ :join $...)
37
+ PATTERN
38
+
39
+ def on_send(node)
40
+ evidence(node) do |rails_node, args|
41
+ add_offense(node, message: format(MSG, root: rails_node.source)) do |corrector|
42
+ range = range_between(rails_node.loc.selector.end_pos, node.source_range.end_pos)
43
+ replacement = ".join(#{args.map(&:source).join(', ')})"
44
+
45
+ corrector.replace(range, replacement)
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def evidence(node)
53
+ # Are we at the *end* of the join chain?
54
+ return if join?(node.parent)
55
+ # Is there only one join?
56
+ return if rails_root?(node.receiver)
57
+
58
+ all_args = []
59
+
60
+ while (args = join?(node))
61
+ all_args = args + all_args
62
+ node = node.receiver
63
+ end
64
+
65
+ rails_root?(node) do
66
+ yield(node, all_args)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Use `Rails.root` IO methods instead of passing it to `File`.
7
+ #
8
+ # `Rails.root` is an instance of `Pathname`
9
+ # so we can apply many IO methods directly.
10
+ #
11
+ # This cop works best when used together with
12
+ # `Style/FileRead`, `Style/FileWrite` and `Rails/RootJoinChain`.
13
+ #
14
+ # @safety
15
+ # This cop is unsafe for autocorrection because `Dir`'s `children`, `each_child`, `entries`, and `glob`
16
+ # methods return string element, but these methods of `Pathname` return `Pathname` element.
17
+ #
18
+ # @example
19
+ # # bad
20
+ # File.open(Rails.root.join('db', 'schema.rb'))
21
+ # File.open(Rails.root.join('db', 'schema.rb'), 'w')
22
+ # File.read(Rails.root.join('db', 'schema.rb'))
23
+ # File.binread(Rails.root.join('db', 'schema.rb'))
24
+ # File.write(Rails.root.join('db', 'schema.rb'), content)
25
+ # File.binwrite(Rails.root.join('db', 'schema.rb'), content)
26
+ #
27
+ # # good
28
+ # Rails.root.join('db', 'schema.rb').open
29
+ # Rails.root.join('db', 'schema.rb').open('w')
30
+ # Rails.root.join('db', 'schema.rb').read
31
+ # Rails.root.join('db', 'schema.rb').binread
32
+ # Rails.root.join('db', 'schema.rb').write(content)
33
+ # Rails.root.join('db', 'schema.rb').binwrite(content)
34
+ #
35
+ class RootPathnameMethods < Base
36
+ extend AutoCorrector
37
+ include RangeHelp
38
+
39
+ MSG = '`%<rails_root>s` is a `Pathname` so you can just append `#%<method>s`.'
40
+
41
+ DIR_METHODS = %i[children delete each_child empty? entries exist? glob mkdir open rmdir unlink].to_set.freeze
42
+
43
+ FILE_METHODS = %i[
44
+ atime
45
+ basename
46
+ binread
47
+ binwrite
48
+ birthtime
49
+ blockdev?
50
+ chardev?
51
+ chmod
52
+ chown
53
+ ctime
54
+ delete
55
+ directory?
56
+ dirname
57
+ empty?
58
+ executable?
59
+ executable_real?
60
+ exist?
61
+ expand_path
62
+ extname
63
+ file?
64
+ fnmatch
65
+ fnmatch?
66
+ ftype
67
+ grpowned?
68
+ join
69
+ lchmod
70
+ lchown
71
+ lstat
72
+ mtime
73
+ open
74
+ owned?
75
+ pipe?
76
+ read
77
+ readable?
78
+ readable_real?
79
+ readlines
80
+ readlink
81
+ realdirpath
82
+ realpath
83
+ rename
84
+ setgid?
85
+ setuid?
86
+ size
87
+ size?
88
+ socket?
89
+ split
90
+ stat
91
+ sticky?
92
+ symlink?
93
+ sysopen
94
+ truncate
95
+ unlink
96
+ utime
97
+ world_readable?
98
+ world_writable?
99
+ writable?
100
+ writable_real?
101
+ write
102
+ zero?
103
+ ].to_set.freeze
104
+
105
+ FILE_TEST_METHODS = %i[
106
+ blockdev?
107
+ chardev?
108
+ directory?
109
+ empty?
110
+ executable?
111
+ executable_real?
112
+ exist?
113
+ file?
114
+ grpowned?
115
+ owned?
116
+ pipe?
117
+ readable?
118
+ readable_real?
119
+ setgid?
120
+ setuid?
121
+ size
122
+ size?
123
+ socket?
124
+ sticky?
125
+ symlink?
126
+ world_readable?
127
+ world_writable?
128
+ writable?
129
+ writable_real?
130
+ zero?
131
+ ].to_set.freeze
132
+
133
+ FILE_UTILS_METHODS = %i[chmod chown mkdir mkpath rmdir rmtree].to_set.freeze
134
+
135
+ RESTRICT_ON_SEND = (DIR_METHODS + FILE_METHODS + FILE_TEST_METHODS + FILE_UTILS_METHODS).to_set.freeze
136
+
137
+ def_node_matcher :pathname_method, <<~PATTERN
138
+ {
139
+ (send (const {nil? cbase} :Dir) $DIR_METHODS $_ $...)
140
+ (send (const {nil? cbase} {:IO :File}) $FILE_METHODS $_ $...)
141
+ (send (const {nil? cbase} :FileTest) $FILE_TEST_METHODS $_ $...)
142
+ (send (const {nil? cbase} :FileUtils) $FILE_UTILS_METHODS $_ $...)
143
+ }
144
+ PATTERN
145
+
146
+ def_node_matcher :dir_glob?, <<~PATTERN
147
+ (send
148
+ (const {cbase nil?} :Dir) :glob ...)
149
+ PATTERN
150
+
151
+ def_node_matcher :rails_root_pathname?, <<~PATTERN
152
+ {
153
+ $#rails_root?
154
+ (send $#rails_root? :join ...)
155
+ }
156
+ PATTERN
157
+
158
+ # @!method rails_root?(node)
159
+ def_node_matcher :rails_root?, <<~PATTERN
160
+ (send (const {nil? cbase} :Rails) {:root :public_path})
161
+ PATTERN
162
+
163
+ def on_send(node)
164
+ evidence(node) do |method, path, args, rails_root|
165
+ add_offense(node, message: format(MSG, method: method, rails_root: rails_root.source)) do |corrector|
166
+ replacement = if dir_glob?(node)
167
+ build_path_glob_replacement(path, method)
168
+ else
169
+ build_path_replacement(path, method, args)
170
+ end
171
+
172
+ corrector.replace(node, replacement)
173
+ end
174
+ end
175
+ end
176
+
177
+ private
178
+
179
+ def evidence(node)
180
+ return if node.method?(:open) && node.parent&.send_type?
181
+ return unless (method, path, args = pathname_method(node)) && (rails_root = rails_root_pathname?(path))
182
+
183
+ yield(method, path, args, rails_root)
184
+ end
185
+
186
+ def build_path_glob_replacement(path, method)
187
+ receiver = range_between(path.source_range.begin_pos, path.children.first.loc.selector.end_pos).source
188
+
189
+ argument = path.arguments.one? ? path.first_argument.source : join_arguments(path.arguments)
190
+
191
+ "#{receiver}.#{method}(#{argument})"
192
+ end
193
+
194
+ def build_path_replacement(path, method, args)
195
+ path_replacement = path.source
196
+ if path.arguments? && !path.parenthesized_call?
197
+ path_replacement[' '] = '('
198
+ path_replacement << ')'
199
+ end
200
+
201
+ replacement = "#{path_replacement}.#{method}"
202
+ replacement += "(#{args.map(&:source).join(', ')})" unless args.empty?
203
+ replacement
204
+ end
205
+
206
+ def include_interpolation?(arguments)
207
+ arguments.any? do |argument|
208
+ argument.children.any? { |child| child.respond_to?(:begin_type?) && child.begin_type? }
209
+ end
210
+ end
211
+
212
+ def join_arguments(arguments)
213
+ use_interpolation = false
214
+
215
+ joined_arguments = arguments.map do |arg|
216
+ if arg.respond_to?(:value)
217
+ arg.value
218
+ else
219
+ use_interpolation = true
220
+ "\#{#{arg.source}}"
221
+ end
222
+ end.join('/')
223
+ quote = enforce_double_quotes? || include_interpolation?(arguments) || use_interpolation ? '"' : "'"
224
+
225
+ "#{quote}#{joined_arguments}#{quote}"
226
+ end
227
+
228
+ def enforce_double_quotes?
229
+ string_literals_config['EnforcedStyle'] == 'double_quotes'
230
+ end
231
+
232
+ def string_literals_config
233
+ config.for_cop('Style/StringLiterals')
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end