rubocop-rails 2.20.2 → 2.24.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -7
  3. data/config/default.yml +72 -10
  4. data/lib/rubocop/cop/mixin/active_record_helper.rb +15 -3
  5. data/lib/rubocop/cop/mixin/database_type_resolvable.rb +66 -0
  6. data/lib/rubocop/cop/mixin/index_method.rb +2 -2
  7. data/lib/rubocop/cop/rails/action_controller_flash_before_render.rb +3 -1
  8. data/lib/rubocop/cop/rails/action_controller_test_case.rb +2 -2
  9. data/lib/rubocop/cop/rails/action_filter.rb +3 -0
  10. data/lib/rubocop/cop/rails/active_record_aliases.rb +2 -2
  11. data/lib/rubocop/cop/rails/active_support_aliases.rb +6 -5
  12. data/lib/rubocop/cop/rails/active_support_on_load.rb +21 -1
  13. data/lib/rubocop/cop/rails/after_commit_override.rb +1 -1
  14. data/lib/rubocop/cop/rails/bulk_change_table.rb +8 -41
  15. data/lib/rubocop/cop/rails/content_tag.rb +1 -1
  16. data/lib/rubocop/cop/rails/dangerous_column_names.rb +446 -0
  17. data/lib/rubocop/cop/rails/date.rb +1 -1
  18. data/lib/rubocop/cop/rails/duplicate_association.rb +69 -12
  19. data/lib/rubocop/cop/rails/dynamic_find_by.rb +3 -3
  20. data/lib/rubocop/cop/rails/eager_evaluation_log_message.rb +2 -2
  21. data/lib/rubocop/cop/rails/env_local.rb +46 -0
  22. data/lib/rubocop/cop/rails/expanded_date_range.rb +1 -1
  23. data/lib/rubocop/cop/rails/file_path.rb +9 -6
  24. data/lib/rubocop/cop/rails/find_by.rb +3 -3
  25. data/lib/rubocop/cop/rails/find_by_id.rb +9 -23
  26. data/lib/rubocop/cop/rails/freeze_time.rb +1 -1
  27. data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +1 -1
  28. data/lib/rubocop/cop/rails/helper_instance_variable.rb +1 -1
  29. data/lib/rubocop/cop/rails/http_status.rb +4 -3
  30. data/lib/rubocop/cop/rails/i18n_lazy_lookup.rb +63 -13
  31. data/lib/rubocop/cop/rails/inquiry.rb +1 -0
  32. data/lib/rubocop/cop/rails/inverse_of.rb +1 -1
  33. data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +7 -8
  34. data/lib/rubocop/cop/rails/not_null_column.rb +13 -3
  35. data/lib/rubocop/cop/rails/output.rb +3 -2
  36. data/lib/rubocop/cop/rails/pick.rb +6 -5
  37. data/lib/rubocop/cop/rails/pluck.rb +1 -1
  38. data/lib/rubocop/cop/rails/pluck_id.rb +2 -1
  39. data/lib/rubocop/cop/rails/pluck_in_where.rb +18 -5
  40. data/lib/rubocop/cop/rails/rake_environment.rb +22 -6
  41. data/lib/rubocop/cop/rails/redundant_active_record_all_method.rb +219 -0
  42. data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +7 -0
  43. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +1 -1
  44. data/lib/rubocop/cop/rails/response_parsed_body.rb +52 -10
  45. data/lib/rubocop/cop/rails/reversible_migration.rb +4 -4
  46. data/lib/rubocop/cop/rails/root_pathname_methods.rb +38 -4
  47. data/lib/rubocop/cop/rails/save_bang.rb +15 -8
  48. data/lib/rubocop/cop/rails/schema_comment.rb +16 -10
  49. data/lib/rubocop/cop/rails/select_map.rb +78 -0
  50. data/lib/rubocop/cop/rails/time_zone.rb +13 -5
  51. data/lib/rubocop/cop/rails/transaction_exit_statement.rb +29 -10
  52. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +12 -4
  53. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +2 -2
  54. data/lib/rubocop/cop/rails/unknown_env.rb +5 -1
  55. data/lib/rubocop/cop/rails/unused_render_content.rb +67 -0
  56. data/lib/rubocop/cop/rails/validation.rb +2 -2
  57. data/lib/rubocop/cop/rails/where_equals.rb +3 -2
  58. data/lib/rubocop/cop/rails/where_exists.rb +9 -9
  59. data/lib/rubocop/cop/rails/where_missing.rb +6 -2
  60. data/lib/rubocop/cop/rails/where_not.rb +8 -6
  61. data/lib/rubocop/cop/rails_cops.rb +6 -0
  62. data/lib/rubocop/rails/schema_loader/schema.rb +3 -2
  63. data/lib/rubocop/rails/schema_loader.rb +5 -15
  64. data/lib/rubocop/rails/version.rb +1 -1
  65. data/lib/rubocop-rails.rb +8 -0
  66. metadata +30 -4
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # TODO: In the future, please support only RuboCop 1.52+ and use `RuboCop::Cop::AllowedReceivers`:
7
+ # https://github.com/rubocop/rubocop/blob/v1.52.0/lib/rubocop/cop/mixin/allowed_receivers.rb
8
+ # At that time, this duplicated module implementation can be removed.
9
+ module AllowedReceivers
10
+ def allowed_receiver?(receiver)
11
+ receiver_name = receiver_name(receiver)
12
+
13
+ allowed_receivers.include?(receiver_name)
14
+ end
15
+
16
+ def receiver_name(receiver)
17
+ return receiver_name(receiver.receiver) if receiver.receiver && !receiver.receiver.const_type?
18
+
19
+ if receiver.send_type?
20
+ if receiver.receiver
21
+ "#{receiver_name(receiver.receiver)}.#{receiver.method_name}"
22
+ else
23
+ receiver.method_name.to_s
24
+ end
25
+ else
26
+ receiver.source
27
+ end
28
+ end
29
+
30
+ def allowed_receivers
31
+ cop_config.fetch('AllowedReceivers', [])
32
+ end
33
+ end
34
+
35
+ # Detect redundant `all` used as a receiver for Active Record query methods.
36
+ #
37
+ # For the methods `delete_all` and `destroy_all`, this cop will only check cases where the receiver is a model.
38
+ # It will ignore cases where the receiver is an association (e.g., `user.articles.all.delete_all`).
39
+ # This is because omitting `all` from an association changes the methods
40
+ # from `ActiveRecord::Relation` to `ActiveRecord::Associations::CollectionProxy`,
41
+ # which can affect their behavior.
42
+ #
43
+ # @safety
44
+ # This cop is unsafe for autocorrection if the receiver for `all` is not an Active Record object.
45
+ #
46
+ # @example
47
+ # # bad
48
+ # User.all.find(id)
49
+ # User.all.order(:created_at)
50
+ # users.all.where(id: ids)
51
+ # user.articles.all.order(:created_at)
52
+ #
53
+ # # good
54
+ # User.find(id)
55
+ # User.order(:created_at)
56
+ # users.where(id: ids)
57
+ # user.articles.order(:created_at)
58
+ #
59
+ # @example AllowedReceivers: ['ActionMailer::Preview', 'ActiveSupport::TimeZone'] (default)
60
+ # # good
61
+ # ActionMailer::Preview.all.first
62
+ # ActiveSupport::TimeZone.all.first
63
+ class RedundantActiveRecordAllMethod < Base
64
+ include ActiveRecordHelper
65
+ include AllowedReceivers
66
+ include RangeHelp
67
+ extend AutoCorrector
68
+
69
+ MSG = 'Redundant `all` detected.'
70
+
71
+ RESTRICT_ON_SEND = [:all].freeze
72
+
73
+ # Defined methods in `ActiveRecord::Querying::QUERYING_METHODS` on activerecord 7.1.0.
74
+ QUERYING_METHODS = %i[
75
+ and
76
+ annotate
77
+ any?
78
+ async_average
79
+ async_count
80
+ async_ids
81
+ async_maximum
82
+ async_minimum
83
+ async_pick
84
+ async_pluck
85
+ async_sum
86
+ average
87
+ calculate
88
+ count
89
+ create_or_find_by
90
+ create_or_find_by!
91
+ create_with
92
+ delete_all
93
+ delete_by
94
+ destroy_all
95
+ destroy_by
96
+ distinct
97
+ eager_load
98
+ except
99
+ excluding
100
+ exists?
101
+ extending
102
+ extract_associated
103
+ fifth
104
+ fifth!
105
+ find
106
+ find_by
107
+ find_by!
108
+ find_each
109
+ find_in_batches
110
+ find_or_create_by
111
+ find_or_create_by!
112
+ find_or_initialize_by
113
+ find_sole_by
114
+ first
115
+ first!
116
+ first_or_create
117
+ first_or_create!
118
+ first_or_initialize
119
+ forty_two
120
+ forty_two!
121
+ fourth
122
+ fourth!
123
+ from
124
+ group
125
+ having
126
+ ids
127
+ in_batches
128
+ in_order_of
129
+ includes
130
+ invert_where
131
+ joins
132
+ last
133
+ last!
134
+ left_joins
135
+ left_outer_joins
136
+ limit
137
+ lock
138
+ many?
139
+ maximum
140
+ merge
141
+ minimum
142
+ none
143
+ none?
144
+ offset
145
+ one?
146
+ only
147
+ optimizer_hints
148
+ or
149
+ order
150
+ pick
151
+ pluck
152
+ preload
153
+ readonly
154
+ references
155
+ regroup
156
+ reorder
157
+ reselect
158
+ rewhere
159
+ second
160
+ second!
161
+ second_to_last
162
+ second_to_last!
163
+ select
164
+ sole
165
+ strict_loading
166
+ sum
167
+ take
168
+ take!
169
+ third
170
+ third!
171
+ third_to_last
172
+ third_to_last!
173
+ touch_all
174
+ unscope
175
+ update_all
176
+ where
177
+ with
178
+ without
179
+ ].to_set.freeze
180
+
181
+ POSSIBLE_ENUMERABLE_BLOCK_METHODS = %i[any? count find none? one? select sum].freeze
182
+ SENSITIVE_METHODS_ON_ASSOCIATION = %i[delete_all destroy_all].freeze
183
+
184
+ def_node_matcher :followed_by_query_method?, <<~PATTERN
185
+ (send (send _ :all) QUERYING_METHODS ...)
186
+ PATTERN
187
+
188
+ def on_send(node)
189
+ return unless followed_by_query_method?(node.parent)
190
+ return if possible_enumerable_block_method?(node) || sensitive_association_method?(node)
191
+ return if node.receiver ? allowed_receiver?(node.receiver) : !inherit_active_record_base?(node)
192
+
193
+ range_of_all_method = offense_range(node)
194
+ add_offense(range_of_all_method) do |collector|
195
+ collector.remove(range_of_all_method)
196
+ collector.remove(node.parent.loc.dot)
197
+ end
198
+ end
199
+
200
+ private
201
+
202
+ def possible_enumerable_block_method?(node)
203
+ parent = node.parent
204
+ return false unless POSSIBLE_ENUMERABLE_BLOCK_METHODS.include?(parent.method_name)
205
+
206
+ parent.parent&.block_type? || parent.parent&.numblock_type? || parent.first_argument&.block_pass_type?
207
+ end
208
+
209
+ def sensitive_association_method?(node)
210
+ !node.receiver&.const_type? && SENSITIVE_METHODS_ON_ASSOCIATION.include?(node.parent.method_name)
211
+ end
212
+
213
+ def offense_range(node)
214
+ range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -53,6 +53,12 @@ module RuboCop
53
53
  # @example source that matches - by a foreign key
54
54
  # validates :user_id, presence: true
55
55
  #
56
+ # @example source that DOES NOT match - if condition
57
+ # validates :user_id, presence: true, if: condition
58
+ #
59
+ # @example source that DOES NOT match - unless condition
60
+ # validates :user_id, presence: true, unless: condition
61
+ #
56
62
  # @example source that DOES NOT match - strict validation
57
63
  # validates :user_id, presence: true, strict: true
58
64
  #
@@ -65,6 +71,7 @@ module RuboCop
65
71
  $[
66
72
  (hash <$(pair (sym :presence) true) ...>) # presence: true
67
73
  !(hash <$(pair (sym :strict) {true const}) ...>) # strict: true
74
+ !(hash <$(pair (sym {:if :unless}) _) ...>) # if: some_condition or unless: some_condition
68
75
  ]
69
76
  )
70
77
  PATTERN
@@ -100,7 +100,7 @@ module RuboCop
100
100
  else
101
101
  return false if node.arguments.empty?
102
102
 
103
- arg = node.arguments.first
103
+ arg = node.first_argument
104
104
  ->(n) { same_value?(arg, n.receiver) }
105
105
  end
106
106
 
@@ -3,25 +3,30 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Rails
6
- # Prefer `response.parsed_body` to `JSON.parse(response.body)`.
6
+ # Prefer `response.parsed_body` to custom parsing logic for `response.body`.
7
7
  #
8
8
  # @safety
9
- # This cop is unsafe because Content-Type may not be `application/json`. For example, the proprietary
10
- # Content-Type provided by corporate entities such as `application/vnd.github+json` is not supported at
11
- # `response.parsed_body` by default, so you still have to use `JSON.parse(response.body)` there.
9
+ # This cop is unsafe because Content-Type may not be `application/json` or `text/html`.
10
+ # For example, the proprietary Content-Type provided by corporate entities such as
11
+ # `application/vnd.github+json` is not supported at `response.parsed_body` by default,
12
+ # so you still have to use `JSON.parse(response.body)` there.
12
13
  #
13
14
  # @example
14
15
  # # bad
15
16
  # JSON.parse(response.body)
16
17
  #
18
+ # # bad
19
+ # Nokogiri::HTML.parse(response.body)
20
+ #
21
+ # # bad
22
+ # Nokogiri::HTML5.parse(response.body)
23
+ #
17
24
  # # good
18
25
  # response.parsed_body
19
26
  class ResponseParsedBody < Base
20
27
  extend AutoCorrector
21
28
  extend TargetRailsVersion
22
29
 
23
- MSG = 'Prefer `response.parsed_body` to `JSON.parse(response.body)`.'
24
-
25
30
  RESTRICT_ON_SEND = %i[parse].freeze
26
31
 
27
32
  minimum_target_rails_version 5.0
@@ -38,12 +43,27 @@ module RuboCop
38
43
  )
39
44
  PATTERN
40
45
 
46
+ # @!method nokogiri_html_parse_response_body(node)
47
+ def_node_matcher :nokogiri_html_parse_response_body, <<~PATTERN
48
+ (send
49
+ (const
50
+ (const {nil? cbase} :Nokogiri)
51
+ ${:HTML :HTML5}
52
+ )
53
+ :parse
54
+ (send
55
+ (send nil? :response)
56
+ :body
57
+ )
58
+ )
59
+ PATTERN
60
+
41
61
  def on_send(node)
42
- return unless json_parse_response_body?(node)
62
+ check_json_parse_response_body(node)
43
63
 
44
- add_offense(node) do |corrector|
45
- autocorrect(corrector, node)
46
- end
64
+ return unless target_rails_version >= 7.1
65
+
66
+ check_nokogiri_html_parse_response_body(node)
47
67
  end
48
68
 
49
69
  private
@@ -51,6 +71,28 @@ module RuboCop
51
71
  def autocorrect(corrector, node)
52
72
  corrector.replace(node, 'response.parsed_body')
53
73
  end
74
+
75
+ def check_json_parse_response_body(node)
76
+ return unless json_parse_response_body?(node)
77
+
78
+ add_offense(
79
+ node,
80
+ message: 'Prefer `response.parsed_body` to `JSON.parse(response.body)`.'
81
+ ) do |corrector|
82
+ autocorrect(corrector, node)
83
+ end
84
+ end
85
+
86
+ def check_nokogiri_html_parse_response_body(node)
87
+ return unless (const = nokogiri_html_parse_response_body(node))
88
+
89
+ add_offense(
90
+ node,
91
+ message: "Prefer `response.parsed_body` to `Nokogiri::#{const}.parse(response.body)`."
92
+ ) do |corrector|
93
+ autocorrect(corrector, node)
94
+ end
95
+ end
54
96
  end
55
97
  end
56
98
  end
@@ -17,7 +17,7 @@ module RuboCop
17
17
  # # good
18
18
  # def change
19
19
  # change_table :users do |t|
20
- # t.remove :name, :string
20
+ # t.remove :name, type: :string
21
21
  # end
22
22
  # end
23
23
  #
@@ -290,10 +290,10 @@ module RuboCop
290
290
  when :change
291
291
  false
292
292
  when :remove
293
- target_rails_version >= 6.1 && all_hash_key?(node.arguments.last, :type)
293
+ target_rails_version >= 6.1 && all_hash_key?(node.last_argument, :type)
294
294
  when :change_default, :change_column_default, :change_table_comment,
295
295
  :change_column_comment
296
- all_hash_key?(node.arguments.last, :from, :to)
296
+ all_hash_key?(node.last_argument, :from, :to)
297
297
  else
298
298
  true
299
299
  end
@@ -307,7 +307,7 @@ module RuboCop
307
307
 
308
308
  def within_reversible_or_up_only_block?(node)
309
309
  node.each_ancestor(:block).any? do |ancestor|
310
- (ancestor.block_type? && ancestor.send_node.method?(:reversible)) || ancestor.send_node.method?(:up_only)
310
+ (ancestor.block_type? && ancestor.method?(:reversible)) || ancestor.method?(:up_only)
311
311
  end
312
312
  end
313
313
 
@@ -12,7 +12,7 @@ module RuboCop
12
12
  # `Style/FileRead`, `Style/FileWrite` and `Rails/RootJoinChain`.
13
13
  #
14
14
  # @safety
15
- # This cop is unsafe for autocorrection because `Dir`'s `children`, `each_child`, `entries`, and `glob`
15
+ # This cop is unsafe for autocorrection because ``Dir``'s `children`, `each_child`, `entries`, and `glob`
16
16
  # methods return string element, but these methods of `Pathname` return `Pathname` element.
17
17
  #
18
18
  # @example
@@ -32,13 +32,28 @@ module RuboCop
32
32
  # Rails.root.join('db', 'schema.rb').write(content)
33
33
  # Rails.root.join('db', 'schema.rb').binwrite(content)
34
34
  #
35
- class RootPathnameMethods < Base
35
+ class RootPathnameMethods < Base # rubocop:disable Metrics/ClassLength
36
36
  extend AutoCorrector
37
37
  include RangeHelp
38
38
 
39
39
  MSG = '`%<rails_root>s` is a `Pathname` so you can just append `#%<method>s`.'
40
40
 
41
- DIR_METHODS = %i[children delete each_child empty? entries exist? glob mkdir open rmdir unlink].to_set.freeze
41
+ DIR_GLOB_METHODS = %i[glob].to_set.freeze
42
+
43
+ DIR_NON_GLOB_METHODS = %i[
44
+ children
45
+ delete
46
+ each_child
47
+ empty?
48
+ entries
49
+ exist?
50
+ mkdir
51
+ open
52
+ rmdir
53
+ unlink
54
+ ].to_set.freeze
55
+
56
+ DIR_METHODS = (DIR_GLOB_METHODS + DIR_NON_GLOB_METHODS).freeze
42
57
 
43
58
  FILE_METHODS = %i[
44
59
  atime
@@ -134,7 +149,8 @@ module RuboCop
134
149
 
135
150
  RESTRICT_ON_SEND = (DIR_METHODS + FILE_METHODS + FILE_TEST_METHODS + FILE_UTILS_METHODS).to_set.freeze
136
151
 
137
- def_node_matcher :pathname_method, <<~PATTERN
152
+ # @!method pathname_method_for_ruby_2_5_or_higher(node)
153
+ def_node_matcher :pathname_method_for_ruby_2_5_or_higher, <<~PATTERN
138
154
  {
139
155
  (send (const {nil? cbase} :Dir) $DIR_METHODS $_ $...)
140
156
  (send (const {nil? cbase} {:IO :File}) $FILE_METHODS $_ $...)
@@ -143,6 +159,16 @@ module RuboCop
143
159
  }
144
160
  PATTERN
145
161
 
162
+ # @!method pathname_method_for_ruby_2_4_or_lower(node)
163
+ def_node_matcher :pathname_method_for_ruby_2_4_or_lower, <<~PATTERN
164
+ {
165
+ (send (const {nil? cbase} :Dir) $DIR_NON_GLOB_METHODS $_ $...)
166
+ (send (const {nil? cbase} {:IO :File}) $FILE_METHODS $_ $...)
167
+ (send (const {nil? cbase} :FileTest) $FILE_TEST_METHODS $_ $...)
168
+ (send (const {nil? cbase} :FileUtils) $FILE_UTILS_METHODS $_ $...)
169
+ }
170
+ PATTERN
171
+
146
172
  def_node_matcher :dir_glob?, <<~PATTERN
147
173
  (send
148
174
  (const {cbase nil?} :Dir) :glob ...)
@@ -183,6 +209,14 @@ module RuboCop
183
209
  yield(method, path, args, rails_root)
184
210
  end
185
211
 
212
+ def pathname_method(node)
213
+ if target_ruby_version >= 2.5
214
+ pathname_method_for_ruby_2_5_or_higher(node)
215
+ else
216
+ pathname_method_for_ruby_2_4_or_lower(node)
217
+ end
218
+ end
219
+
186
220
  def build_path_glob_replacement(path, method)
187
221
  receiver = range_between(path.source_range.begin_pos, path.children.first.loc.selector.end_pos).source
188
222
 
@@ -188,7 +188,7 @@ module RuboCop
188
188
  end
189
189
 
190
190
  def persisted_referenced?(assignment)
191
- return unless assignment.referenced?
191
+ return false unless assignment.referenced?
192
192
 
193
193
  assignment.variable.references.any? do |reference|
194
194
  call_to_persisted?(reference.node.parent)
@@ -196,6 +196,8 @@ module RuboCop
196
196
  end
197
197
 
198
198
  def call_to_persisted?(node)
199
+ node = node.parent.condition if node.parenthesized_call? && node.parent.if_type?
200
+
199
201
  node.send_type? && node.method?(:persisted?)
200
202
  end
201
203
 
@@ -235,10 +237,10 @@ module RuboCop
235
237
 
236
238
  def in_condition_or_compound_boolean?(node)
237
239
  node = node.block_node || node
238
- parent = node.parent
240
+ parent = node.each_ancestor.find { |ancestor| !ancestor.begin_type? }
239
241
  return false unless parent
240
242
 
241
- operator_or_single_negative?(parent) || (conditional?(parent) && node == parent.condition)
243
+ operator_or_single_negative?(parent) || (conditional?(parent) && node == deparenthesize(parent.condition))
242
244
  end
243
245
 
244
246
  def operator_or_single_negative?(node)
@@ -249,6 +251,11 @@ module RuboCop
249
251
  parent.if_type? || parent.case_type?
250
252
  end
251
253
 
254
+ def deparenthesize(node)
255
+ node = node.children.last while node.begin_type?
256
+ node
257
+ end
258
+
252
259
  def checked_immediately?(node)
253
260
  node.parent && call_to_persisted?(node.parent)
254
261
  end
@@ -298,7 +305,7 @@ module RuboCop
298
305
 
299
306
  node = assignable_node(node)
300
307
  method, sibling_index = find_method_with_sibling_index(node.parent)
301
- return unless method && (method.def_type? || method.block_type?)
308
+ return false unless method && (method.def_type? || method.block_type?)
302
309
 
303
310
  method.children.size == node.sibling_index + sibling_index
304
311
  end
@@ -331,10 +338,10 @@ module RuboCop
331
338
 
332
339
  # Check argument signature as no arguments or one hash
333
340
  def expected_signature?(node)
334
- !node.arguments? ||
335
- (node.arguments.one? &&
336
- node.method_name != :destroy &&
337
- (node.first_argument.hash_type? || !node.first_argument.literal?))
341
+ return true unless node.arguments?
342
+ return false if !node.arguments.one? || node.method?(:destroy)
343
+
344
+ node.first_argument.hash_type? || !node.first_argument.literal?
338
345
  end
339
346
  end
340
347
  end
@@ -74,17 +74,25 @@ module RuboCop
74
74
  def on_send(node)
75
75
  if add_column_without_comment?(node)
76
76
  add_offense(node, message: COLUMN_MSG)
77
- elsif create_table?(node)
78
- if create_table_without_comment?(node)
79
- add_offense(node, message: TABLE_MSG)
80
- elsif create_table_column_call_without_comment?(node)
81
- add_offense(node.parent.body, message: COLUMN_MSG)
82
- end
77
+ elsif create_table_without_comment?(node)
78
+ add_offense(node, message: TABLE_MSG)
79
+ elsif create_table_with_block?(node.parent)
80
+ check_column_within_create_table_block(node.parent.body)
83
81
  end
84
82
  end
85
83
 
86
84
  private
87
85
 
86
+ def check_column_within_create_table_block(node)
87
+ if node.begin_type?
88
+ node.child_nodes.each do |child_node|
89
+ add_offense(child_node, message: COLUMN_MSG) if t_column_without_comment?(child_node)
90
+ end
91
+ elsif t_column_without_comment?(node)
92
+ add_offense(node, message: COLUMN_MSG)
93
+ end
94
+ end
95
+
88
96
  def add_column_without_comment?(node)
89
97
  add_column?(node) && !add_column_with_comment?(node)
90
98
  end
@@ -93,10 +101,8 @@ module RuboCop
93
101
  create_table?(node) && !create_table_with_comment?(node)
94
102
  end
95
103
 
96
- def create_table_column_call_without_comment?(node)
97
- create_table_with_block?(node.parent) &&
98
- t_column?(node.parent.body) &&
99
- !t_column_with_comment?(node.parent.body)
104
+ def t_column_without_comment?(node)
105
+ t_column?(node) && !t_column_with_comment?(node)
100
106
  end
101
107
  end
102
108
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Checks for uses of `select(:column_name)` with `map(&:column_name)`.
7
+ # These can be replaced with `pluck(:column_name)`.
8
+ #
9
+ # There also should be some performance improvement since it skips instantiating the model class for matches.
10
+ #
11
+ # @safety
12
+ # This cop is unsafe because the model might override the attribute getter.
13
+ # Additionally, the model's `after_initialize` hooks are skipped when using `pluck`.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # Model.select(:column_name).map(&:column_name)
18
+ #
19
+ # # good
20
+ # Model.pluck(:column_name)
21
+ #
22
+ class SelectMap < Base
23
+ extend AutoCorrector
24
+
25
+ MSG = 'Use `%<preferred_method>s` instead of `select` with `%<map_method>s`.'
26
+
27
+ RESTRICT_ON_SEND = %i[map collect].freeze
28
+
29
+ def on_send(node)
30
+ return unless node.first_argument
31
+
32
+ column_name = node.first_argument.source.delete_prefix('&:')
33
+ return unless (select_node = find_select_node(node, column_name))
34
+
35
+ offense_range = select_node.loc.selector.begin.join(node.source_range.end)
36
+ preferred_method = "pluck(:#{column_name})"
37
+ message = format(MSG, preferred_method: preferred_method, map_method: node.method_name)
38
+
39
+ add_offense(offense_range, message: message) do |corrector|
40
+ autocorrect(corrector, select_node, node, preferred_method)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def find_select_node(node, column_name)
47
+ node.descendants.detect do |select_candidate|
48
+ next if !select_candidate.send_type? || !select_candidate.method?(:select)
49
+
50
+ match_column_name?(select_candidate, column_name)
51
+ end
52
+ end
53
+
54
+ # rubocop:disable Metrics/AbcSize
55
+ def autocorrect(corrector, select_node, node, preferred_method)
56
+ corrector.remove(select_node.loc.dot || node.loc.dot)
57
+ corrector.remove(select_node.loc.selector.begin.join(select_node.source_range.end))
58
+ corrector.replace(node.loc.selector.begin.join(node.source_range.end), preferred_method)
59
+ end
60
+ # rubocop:enable Metrics/AbcSize
61
+
62
+ def match_column_name?(select_candidate, column_name)
63
+ return false unless select_candidate.arguments.one?
64
+ return false unless (first_argument = select_candidate.first_argument)
65
+
66
+ argument = case select_candidate.first_argument.type
67
+ when :sym
68
+ first_argument.source.delete_prefix(':')
69
+ when :str
70
+ first_argument.value if first_argument.respond_to?(:value)
71
+ end
72
+
73
+ argument == column_name
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -21,6 +21,7 @@ module RuboCop
21
21
  # # bad
22
22
  # Time.now
23
23
  # Time.parse('2015-03-02T19:05:37')
24
+ # '2015-03-02T19:05:37'.to_time
24
25
  #
25
26
  # # good
26
27
  # Time.current
@@ -44,19 +45,17 @@ module RuboCop
44
45
  extend AutoCorrector
45
46
 
46
47
  MSG = 'Do not use `%<current>s` without zone. Use `%<prefer>s` instead.'
47
-
48
48
  MSG_ACCEPTABLE = 'Do not use `%<current>s` without zone. Use one of %<prefer>s instead.'
49
-
50
49
  MSG_LOCALTIME = 'Do not use `Time.localtime` without offset or zone.'
50
+ MSG_STRING_TO_TIME = 'Do not use `String#to_time` without zone. Use `Time.zone.parse` instead.'
51
51
 
52
52
  GOOD_METHODS = %i[zone zone_default find_zone find_zone!].freeze
53
-
54
53
  DANGEROUS_METHODS = %i[now local new parse at].freeze
55
-
56
54
  ACCEPTED_METHODS = %i[in_time_zone utc getlocal xmlschema iso8601 jisx0301 rfc3339 httpdate to_i to_f].freeze
57
-
58
55
  TIMEZONE_SPECIFIER = /([A-Za-z]|[+-]\d{2}:?\d{2})\z/.freeze
59
56
 
57
+ RESTRICT_ON_SEND = %i[to_time].freeze
58
+
60
59
  def on_const(node)
61
60
  mod, klass = *node
62
61
  # we should only check core classes
@@ -66,6 +65,15 @@ module RuboCop
66
65
  check_time_node(klass, node.parent) if klass == :Time
67
66
  end
68
67
 
68
+ def on_send(node)
69
+ return if !node.receiver&.str_type? || !node.method?(:to_time)
70
+
71
+ add_offense(node.loc.selector, message: MSG_STRING_TO_TIME) do |corrector|
72
+ corrector.replace(node, "Time.zone.parse(#{node.receiver.source})") unless node.csend_type?
73
+ end
74
+ end
75
+ alias on_csend on_send
76
+
69
77
  private
70
78
 
71
79
  def autocorrect(corrector, node)