rubocop-rails 2.19.1 → 2.30.3

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.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +70 -16
  4. data/config/default.yml +173 -28
  5. data/lib/rubocop/cop/mixin/active_record_helper.rb +16 -4
  6. data/lib/rubocop/cop/mixin/active_record_migrations_helper.rb +2 -2
  7. data/lib/rubocop/cop/mixin/database_type_resolvable.rb +66 -0
  8. data/lib/rubocop/cop/mixin/index_method.rb +68 -61
  9. data/lib/rubocop/cop/mixin/routes_helper.rb +20 -0
  10. data/lib/rubocop/cop/mixin/target_rails_version.rb +27 -2
  11. data/lib/rubocop/cop/rails/action_controller_flash_before_render.rb +3 -1
  12. data/lib/rubocop/cop/rails/action_controller_test_case.rb +2 -2
  13. data/lib/rubocop/cop/rails/action_filter.rb +3 -0
  14. data/lib/rubocop/cop/rails/action_order.rb +1 -5
  15. data/lib/rubocop/cop/rails/active_record_aliases.rb +2 -2
  16. data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +1 -5
  17. data/lib/rubocop/cop/rails/active_support_aliases.rb +6 -5
  18. data/lib/rubocop/cop/rails/active_support_on_load.rb +21 -1
  19. data/lib/rubocop/cop/rails/add_column_index.rb +1 -0
  20. data/lib/rubocop/cop/rails/after_commit_override.rb +1 -1
  21. data/lib/rubocop/cop/rails/application_record.rb +4 -0
  22. data/lib/rubocop/cop/rails/assert_not.rb +0 -1
  23. data/lib/rubocop/cop/rails/belongs_to.rb +1 -1
  24. data/lib/rubocop/cop/rails/blank.rb +1 -1
  25. data/lib/rubocop/cop/rails/bulk_change_table.rb +19 -45
  26. data/lib/rubocop/cop/rails/compact_blank.rb +29 -8
  27. data/lib/rubocop/cop/rails/content_tag.rb +2 -2
  28. data/lib/rubocop/cop/rails/dangerous_column_names.rb +448 -0
  29. data/lib/rubocop/cop/rails/date.rb +14 -5
  30. data/lib/rubocop/cop/rails/delegate.rb +53 -7
  31. data/lib/rubocop/cop/rails/duplicate_association.rb +71 -10
  32. data/lib/rubocop/cop/rails/dynamic_find_by.rb +3 -3
  33. data/lib/rubocop/cop/rails/eager_evaluation_log_message.rb +2 -2
  34. data/lib/rubocop/cop/rails/enum_hash.rb +31 -8
  35. data/lib/rubocop/cop/rails/enum_syntax.rb +130 -0
  36. data/lib/rubocop/cop/rails/enum_uniqueness.rb +29 -7
  37. data/lib/rubocop/cop/rails/env_local.rb +69 -0
  38. data/lib/rubocop/cop/rails/expanded_date_range.rb +1 -1
  39. data/lib/rubocop/cop/rails/file_path.rb +186 -18
  40. data/lib/rubocop/cop/rails/find_by.rb +3 -3
  41. data/lib/rubocop/cop/rails/find_by_id.rb +9 -23
  42. data/lib/rubocop/cop/rails/find_each.rb +1 -1
  43. data/lib/rubocop/cop/rails/freeze_time.rb +1 -1
  44. data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +1 -1
  45. data/lib/rubocop/cop/rails/helper_instance_variable.rb +1 -1
  46. data/lib/rubocop/cop/rails/http_positional_arguments.rb +7 -0
  47. data/lib/rubocop/cop/rails/http_status.rb +16 -5
  48. data/lib/rubocop/cop/rails/i18n_lazy_lookup.rb +63 -13
  49. data/lib/rubocop/cop/rails/i18n_locale_texts.rb +5 -1
  50. data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +23 -3
  51. data/lib/rubocop/cop/rails/index_by.rb +28 -12
  52. data/lib/rubocop/cop/rails/index_with.rb +28 -12
  53. data/lib/rubocop/cop/rails/inquiry.rb +2 -1
  54. data/lib/rubocop/cop/rails/inverse_of.rb +1 -1
  55. data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +19 -10
  56. data/lib/rubocop/cop/rails/link_to_blank.rb +2 -2
  57. data/lib/rubocop/cop/rails/match_route.rb +1 -9
  58. data/lib/rubocop/cop/rails/multiple_route_paths.rb +50 -0
  59. data/lib/rubocop/cop/rails/not_null_column.rb +100 -6
  60. data/lib/rubocop/cop/rails/output.rb +3 -2
  61. data/lib/rubocop/cop/rails/pick.rb +10 -5
  62. data/lib/rubocop/cop/rails/pluck.rb +21 -1
  63. data/lib/rubocop/cop/rails/pluck_id.rb +2 -1
  64. data/lib/rubocop/cop/rails/pluck_in_where.rb +35 -13
  65. data/lib/rubocop/cop/rails/pluralization_grammar.rb +30 -16
  66. data/lib/rubocop/cop/rails/presence.rb +1 -1
  67. data/lib/rubocop/cop/rails/present.rb +1 -3
  68. data/lib/rubocop/cop/rails/rake_environment.rb +22 -6
  69. data/lib/rubocop/cop/rails/redundant_active_record_all_method.rb +190 -0
  70. data/lib/rubocop/cop/rails/redundant_foreign_key.rb +1 -1
  71. data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +16 -0
  72. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +2 -2
  73. data/lib/rubocop/cop/rails/reflection_class_name.rb +2 -2
  74. data/lib/rubocop/cop/rails/refute_methods.rb +0 -1
  75. data/lib/rubocop/cop/rails/relative_date_constant.rb +1 -1
  76. data/lib/rubocop/cop/rails/render_plain_text.rb +6 -3
  77. data/lib/rubocop/cop/rails/request_referer.rb +1 -1
  78. data/lib/rubocop/cop/rails/response_parsed_body.rb +52 -10
  79. data/lib/rubocop/cop/rails/reversible_migration.rb +7 -5
  80. data/lib/rubocop/cop/rails/root_pathname_methods.rb +58 -15
  81. data/lib/rubocop/cop/rails/save_bang.rb +22 -14
  82. data/lib/rubocop/cop/rails/schema_comment.rb +17 -10
  83. data/lib/rubocop/cop/rails/select_map.rb +79 -0
  84. data/lib/rubocop/cop/rails/skips_model_validations.rb +9 -4
  85. data/lib/rubocop/cop/rails/squished_sql_heredocs.rb +1 -2
  86. data/lib/rubocop/cop/rails/strip_heredoc.rb +1 -1
  87. data/lib/rubocop/cop/rails/strong_parameters_expect.rb +104 -0
  88. data/lib/rubocop/cop/rails/three_state_boolean_column.rb +4 -5
  89. data/lib/rubocop/cop/rails/time_zone.rb +26 -11
  90. data/lib/rubocop/cop/rails/transaction_exit_statement.rb +40 -9
  91. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +11 -26
  92. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +17 -21
  93. data/lib/rubocop/cop/rails/unknown_env.rb +5 -1
  94. data/lib/rubocop/cop/rails/unused_ignored_columns.rb +6 -0
  95. data/lib/rubocop/cop/rails/unused_render_content.rb +67 -0
  96. data/lib/rubocop/cop/rails/validation.rb +9 -4
  97. data/lib/rubocop/cop/rails/where_equals.rb +29 -12
  98. data/lib/rubocop/cop/rails/where_exists.rb +9 -9
  99. data/lib/rubocop/cop/rails/where_missing.rb +6 -2
  100. data/lib/rubocop/cop/rails/where_not.rb +18 -11
  101. data/lib/rubocop/cop/rails/where_range.rb +203 -0
  102. data/lib/rubocop/cop/rails_cops.rb +11 -0
  103. data/lib/rubocop/rails/migration_file_skippable.rb +54 -0
  104. data/lib/rubocop/rails/plugin.rb +48 -0
  105. data/lib/rubocop/rails/schema_loader/schema.rb +8 -7
  106. data/lib/rubocop/rails/schema_loader.rb +5 -15
  107. data/lib/rubocop/rails/version.rb +1 -1
  108. data/lib/rubocop/rails.rb +1 -8
  109. data/lib/rubocop-rails.rb +12 -4
  110. metadata +55 -11
  111. data/lib/rubocop/rails/inject.rb +0 -18
@@ -21,12 +21,15 @@ 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
27
28
  # Time.zone.now
28
29
  # Time.zone.parse('2015-03-02T19:05:37')
29
30
  # Time.zone.parse('2015-03-02T19:05:37Z') # Respect ISO 8601 format with timezone specifier.
31
+ # Time.parse('2015-03-02T19:05:37Z') # Also respects ISO 8601
32
+ # '2015-03-02T19:05:37Z'.to_time # Also respects ISO 8601
30
33
  #
31
34
  # @example EnforcedStyle: flexible (default)
32
35
  # # `flexible` allows usage of `in_time_zone` instead of `zone`.
@@ -44,18 +47,16 @@ module RuboCop
44
47
  extend AutoCorrector
45
48
 
46
49
  MSG = 'Do not use `%<current>s` without zone. Use `%<prefer>s` instead.'
47
-
48
50
  MSG_ACCEPTABLE = 'Do not use `%<current>s` without zone. Use one of %<prefer>s instead.'
49
-
50
51
  MSG_LOCALTIME = 'Do not use `Time.localtime` without offset or zone.'
52
+ MSG_STRING_TO_TIME = 'Do not use `String#to_time` without zone. Use `Time.zone.parse` instead.'
51
53
 
52
54
  GOOD_METHODS = %i[zone zone_default find_zone find_zone!].freeze
53
-
54
55
  DANGEROUS_METHODS = %i[now local new parse at].freeze
55
-
56
56
  ACCEPTED_METHODS = %i[in_time_zone utc getlocal xmlschema iso8601 jisx0301 rfc3339 httpdate to_i to_f].freeze
57
+ TIMEZONE_SPECIFIER = /([A-Za-z]|[+-]\d{2}:?\d{2})\z/.freeze
57
58
 
58
- TIMEZONE_SPECIFIER = /([A-z]|[+-]\d{2}:?\d{2})\z/.freeze
59
+ RESTRICT_ON_SEND = %i[to_time].freeze
59
60
 
60
61
  def on_const(node)
61
62
  mod, klass = *node
@@ -66,6 +67,16 @@ module RuboCop
66
67
  check_time_node(klass, node.parent) if klass == :Time
67
68
  end
68
69
 
70
+ def on_send(node)
71
+ return if !node.receiver&.str_type? || !node.method?(:to_time)
72
+ return if attach_timezone_specifier?(node.receiver)
73
+
74
+ add_offense(node.loc.selector, message: MSG_STRING_TO_TIME) do |corrector|
75
+ corrector.replace(node, "Time.zone.parse(#{node.receiver.source})") unless node.csend_type?
76
+ end
77
+ end
78
+ alias on_csend on_send
79
+
69
80
  private
70
81
 
71
82
  def autocorrect(corrector, node)
@@ -86,11 +97,9 @@ module RuboCop
86
97
  end
87
98
 
88
99
  def autocorrect_time_new(node, corrector)
89
- if node.arguments?
90
- corrector.replace(node.loc.selector, 'local')
91
- else
92
- corrector.replace(node.loc.selector, 'now')
93
- end
100
+ replacement = replacement(node)
101
+
102
+ corrector.replace(node.loc.selector, replacement)
94
103
  end
95
104
 
96
105
  # remove redundant `.in_time_zone` from `Time.zone.now.in_time_zone`
@@ -172,7 +181,7 @@ module RuboCop
172
181
 
173
182
  def safe_method(method_name, node)
174
183
  if %w[new current].include?(method_name)
175
- node.arguments? ? 'local' : 'now'
184
+ replacement(node)
176
185
  else
177
186
  method_name
178
187
  end
@@ -248,6 +257,12 @@ module RuboCop
248
257
  pair.key.sym_type? && pair.key.value == :in && !pair.value.nil_type?
249
258
  end
250
259
  end
260
+
261
+ def replacement(node)
262
+ return 'now' unless node.arguments?
263
+
264
+ node.first_argument.str_type? ? 'parse' : 'local'
265
+ end
251
266
  end
252
267
  end
253
268
  end
@@ -13,6 +13,12 @@ module RuboCop
13
13
  # error when rollback is desired, and to use `next` when commit is
14
14
  # desired.
15
15
  #
16
+ # If you are defining custom transaction methods, you can configure it with `TransactionMethods`.
17
+ #
18
+ # NOTE: This cop is disabled on Rails >= 7.2 because transactions were restored
19
+ # to their historical behavior. In Rails 7.1, the behavior is controlled with
20
+ # the config `active_record.commit_transaction_on_non_local_return`.
21
+ #
16
22
  # @example
17
23
  # # bad
18
24
  # ApplicationRecord.transaction do
@@ -34,6 +40,11 @@ module RuboCop
34
40
  # throw if user.active?
35
41
  # end
36
42
  #
43
+ # # bad, as `with_lock` implicitly opens a transaction too
44
+ # ApplicationRecord.with_lock do
45
+ # break if user.active?
46
+ # end
47
+ #
37
48
  # # good
38
49
  # ApplicationRecord.transaction do
39
50
  # # Rollback
@@ -45,12 +56,16 @@ module RuboCop
45
56
  # # Commit
46
57
  # next if user.active?
47
58
  # end
59
+ #
60
+ # @example TransactionMethods: ["custom_transaction"]
61
+ # # bad
62
+ # CustomModel.custom_transaction do
63
+ # return if user.active?
64
+ # end
65
+ #
48
66
  class TransactionExitStatement < Base
49
- MSG = <<~MSG.chomp
50
- Exit statement `%<statement>s` is not allowed. Use `raise` (rollback) or `next` (commit).
51
- MSG
52
-
53
- RESTRICT_ON_SEND = %i[transaction with_lock].freeze
67
+ MSG = 'Exit statement `%<statement>s` is not allowed. Use `raise` (rollback) or `next` (commit).'
68
+ BUILT_IN_TRANSACTION_METHODS = %i[transaction with_lock].freeze
54
69
 
55
70
  def_node_search :exit_statements, <<~PATTERN
56
71
  ({return | break | send nil? :throw} ...)
@@ -65,10 +80,10 @@ module RuboCop
65
80
  PATTERN
66
81
 
67
82
  def on_send(node)
68
- return unless (parent = node.parent)
69
- return unless parent.block_type? && parent.body
83
+ return if target_rails_version >= 7.2
84
+ return unless in_transaction_block?(node)
70
85
 
71
- exit_statements(parent.body).each do |statement_node|
86
+ exit_statements(node.parent.body).each do |statement_node|
72
87
  next if statement_node.break_type? && nested_block?(statement_node)
73
88
 
74
89
  statement = statement(statement_node)
@@ -80,6 +95,13 @@ module RuboCop
80
95
 
81
96
  private
82
97
 
98
+ def in_transaction_block?(node)
99
+ return false unless transaction_method_name?(node.method_name)
100
+ return false unless (parent = node.parent)
101
+
102
+ parent.any_block_type? && parent.body
103
+ end
104
+
83
105
  def statement(statement_node)
84
106
  if statement_node.return_type?
85
107
  'return'
@@ -91,7 +113,16 @@ module RuboCop
91
113
  end
92
114
 
93
115
  def nested_block?(statement_node)
94
- !statement_node.ancestors.find(&:block_type?).method?(:transaction)
116
+ name = statement_node.ancestors.find(&:any_block_type?).children.first.method_name
117
+ !transaction_method_name?(name)
118
+ end
119
+
120
+ def transaction_method_name?(method_name)
121
+ BUILT_IN_TRANSACTION_METHODS.include?(method_name) || transaction_method?(method_name)
122
+ end
123
+
124
+ def transaction_method?(method_name)
125
+ cop_config.fetch('TransactionMethods', []).include?(method_name.to_s)
95
126
  end
96
127
  end
97
128
  end
@@ -51,43 +51,28 @@ module RuboCop
51
51
 
52
52
  MSG = 'Use `distinct` before `pluck`.'
53
53
  RESTRICT_ON_SEND = %i[uniq].freeze
54
- NEWLINE = "\n"
55
- PATTERN = '[!^block (send (send %<type>s :pluck ...) :uniq ...)]'
56
54
 
57
- def_node_matcher :conservative_node_match, format(PATTERN, type: 'const')
58
-
59
- def_node_matcher :aggressive_node_match, format(PATTERN, type: '_')
55
+ def_node_matcher :uniq_before_pluck, '[!^any_block $(send $(send _ :pluck ...) :uniq ...)]'
60
56
 
61
57
  def on_send(node)
62
- uniq = if style == :conservative
63
- conservative_node_match(node)
64
- else
65
- aggressive_node_match(node)
66
- end
67
-
68
- return unless uniq
69
-
70
- add_offense(node.loc.selector) do |corrector|
71
- method = node.method_name
58
+ uniq_before_pluck(node) do |uniq_node, pluck_node|
59
+ next if style == :conservative && !pluck_node.receiver&.const_type?
72
60
 
73
- corrector.remove(dot_method_with_whitespace(method, node))
74
- corrector.insert_before(node.receiver.loc.dot.begin, '.distinct')
61
+ add_offense(uniq_node.loc.selector) do |corrector|
62
+ autocorrect(corrector, uniq_node, pluck_node)
63
+ end
75
64
  end
76
65
  end
77
66
 
78
67
  private
79
68
 
80
- def dot_method_with_whitespace(method, node)
81
- range_between(dot_method_begin_pos(method, node), node.loc.selector.end_pos)
82
- end
83
-
84
- def dot_method_begin_pos(method, node)
85
- lines = node.source.split(NEWLINE)
69
+ def autocorrect(corrector, uniq_node, pluck_node)
70
+ corrector.remove(range_between(pluck_node.loc.end.end_pos, uniq_node.loc.selector.end_pos))
86
71
 
87
- if lines.last.strip == ".#{method}"
88
- node.source.rindex(NEWLINE)
72
+ if (dot = pluck_node.loc.dot)
73
+ corrector.insert_before(dot.begin, '.distinct')
89
74
  else
90
- node.loc.dot.begin_pos
75
+ corrector.insert_before(pluck_node, 'distinct.')
91
76
  end
92
77
  end
93
78
  end
@@ -31,11 +31,11 @@ module RuboCop
31
31
  RESTRICT_ON_SEND = %i[validates].freeze
32
32
 
33
33
  def on_send(node)
34
- return unless uniqueness_part(node)
35
- return if condition_part?(node)
36
34
  return unless schema
35
+ return unless (uniqueness_part = uniqueness_part(node))
36
+ return if uniqueness_part.falsey_literal? || condition_part?(node, uniqueness_part)
37
37
 
38
- klass, table, names = find_schema_information(node)
38
+ klass, table, names = find_schema_information(node, uniqueness_part)
39
39
  return unless names
40
40
  return if with_index?(klass, table, names)
41
41
 
@@ -44,12 +44,12 @@ module RuboCop
44
44
 
45
45
  private
46
46
 
47
- def find_schema_information(node)
47
+ def find_schema_information(node, uniqueness_part)
48
48
  klass = class_node(node)
49
49
  return unless klass
50
50
 
51
51
  table = schema.table_by(name: table_name(klass))
52
- names = column_names(node)
52
+ names = column_names(node, uniqueness_part)
53
53
 
54
54
  [klass, table, names]
55
55
  end
@@ -71,12 +71,12 @@ module RuboCop
71
71
  end
72
72
  end
73
73
 
74
- def column_names(node)
74
+ def column_names(node, uniqueness_part)
75
75
  arg = node.first_argument
76
- return unless arg.str_type? || arg.sym_type?
76
+ return unless arg.type?(:str, :sym)
77
77
 
78
78
  ret = [arg.value]
79
- names_from_scope = column_names_from_scope(node)
79
+ names_from_scope = column_names_from_scope(uniqueness_part)
80
80
  ret.concat(names_from_scope) if names_from_scope
81
81
 
82
82
  ret = ret.flat_map do |name|
@@ -90,11 +90,10 @@ module RuboCop
90
90
  ret.include?(nil) ? nil : ret.to_set
91
91
  end
92
92
 
93
- def column_names_from_scope(node)
94
- uniq = uniqueness_part(node)
95
- return unless uniq.hash_type?
93
+ def column_names_from_scope(uniqueness_part)
94
+ return unless uniqueness_part.hash_type?
96
95
 
97
- scope = find_scope(uniq)
96
+ scope = find_scope(uniqueness_part)
98
97
  return unless scope
99
98
 
100
99
  scope = unfreeze_scope(scope)
@@ -125,8 +124,8 @@ module RuboCop
125
124
  end
126
125
 
127
126
  def uniqueness_part(node)
128
- pairs = node.arguments.last
129
- return unless pairs.hash_type?
127
+ pairs = node.last_argument
128
+ return unless pairs&.hash_type?
130
129
 
131
130
  pairs.each_pair.find do |pair|
132
131
  next unless pair.key.sym_type? && pair.key.value == :uniqueness
@@ -135,14 +134,11 @@ module RuboCop
135
134
  end
136
135
  end
137
136
 
138
- def condition_part?(node)
139
- pairs = node.arguments.last
140
- return unless pairs.hash_type?
141
-
137
+ def condition_part?(node, uniqueness_node)
138
+ pairs = node.last_argument
139
+ return false unless pairs.hash_type?
142
140
  return true if condition_hash_part?(pairs, keys: %i[if unless])
143
-
144
- uniqueness_node = uniqueness_part(node)
145
- return unless uniqueness_node&.hash_type?
141
+ return false unless uniqueness_node.hash_type?
146
142
 
147
143
  condition_hash_part?(uniqueness_node, keys: %i[if unless conditions])
148
144
  end
@@ -86,7 +86,11 @@ module RuboCop
86
86
  end
87
87
 
88
88
  def environments
89
- cop_config['Environments']
89
+ @environments ||= begin
90
+ environments = cop_config['Environments'].dup || []
91
+ environments << 'local' if target_rails_version >= 7.1
92
+ environments
93
+ end
90
94
  end
91
95
  end
92
96
  end
@@ -7,6 +7,12 @@ module RuboCop
7
7
  # `ignored_columns` is necessary to drop a column from RDBMS, but you don't need it after the migration
8
8
  # to drop the column. You avoid forgetting to remove `ignored_columns` by this cop.
9
9
  #
10
+ # IMPORTANT: This cop can't be used to effectively check for unused columns because the development
11
+ # and production schema can be out of sync until the migration has been run on production. As such,
12
+ # this cop can cause `ignored_columns` to be removed even though the production schema still contains
13
+ # the column, which can lead to downtime when the migration is actually executed. Only enable this cop
14
+ # if you know your migrations will be run before any of your Rails applications boot with the modified code.
15
+ #
10
16
  # @example
11
17
  # # bad
12
18
  # class User < ApplicationRecord
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # If you try to render content along with a non-content status code (100-199, 204, 205, or 304),
7
+ # it will be dropped from the response.
8
+ #
9
+ # This cop checks for uses of `render` which specify both body content and a non-content status.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # render 'foo', status: :continue
14
+ # render status: 100, plain: 'Ruby!'
15
+ #
16
+ # # good
17
+ # head :continue
18
+ # head 100
19
+ class UnusedRenderContent < Base
20
+ include RangeHelp
21
+
22
+ MSG = 'Do not specify body content for a response with a non-content status code'
23
+ RESTRICT_ON_SEND = %i[render].freeze
24
+ NON_CONTENT_STATUS_CODES = Set[*100..199, 204, 205, 304] & ::Rack::Utils::SYMBOL_TO_STATUS_CODE.values
25
+ NON_CONTENT_STATUSES = Set[
26
+ *::Rack::Utils::SYMBOL_TO_STATUS_CODE.invert.fetch_values(*NON_CONTENT_STATUS_CODES)
27
+ ]
28
+ BODY_OPTIONS = Set[
29
+ :action,
30
+ :body,
31
+ :content_type,
32
+ :file,
33
+ :html,
34
+ :inline,
35
+ :json,
36
+ :js,
37
+ :layout,
38
+ :plain,
39
+ :raw,
40
+ :template,
41
+ :text,
42
+ :xml
43
+ ]
44
+
45
+ def_node_matcher :non_content_status?, <<~PATTERN
46
+ (pair
47
+ (sym :status)
48
+ {(sym NON_CONTENT_STATUSES) (int NON_CONTENT_STATUS_CODES)}
49
+ )
50
+ PATTERN
51
+
52
+ def_node_matcher :unused_render_content?, <<~PATTERN
53
+ (send nil? :render {
54
+ (hash <#non_content_status? $(pair (sym BODY_OPTIONS) _) ...>) |
55
+ $({str sym} _) (hash <#non_content_status? ...>)
56
+ })
57
+ PATTERN
58
+
59
+ def on_send(node)
60
+ unused_render_content?(node) do |unused_content_node|
61
+ add_offense(unused_content_node)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -8,6 +8,7 @@ module RuboCop
8
8
  # @example
9
9
  # # bad
10
10
  # validates_acceptance_of :foo
11
+ # validates_comparison_of :foo
11
12
  # validates_confirmation_of :foo
12
13
  # validates_exclusion_of :foo
13
14
  # validates_format_of :foo
@@ -22,6 +23,7 @@ module RuboCop
22
23
  # # good
23
24
  # validates :foo, acceptance: true
24
25
  # validates :foo, confirmation: true
26
+ # validates :foo, comparison: true
25
27
  # validates :foo, exclusion: true
26
28
  # validates :foo, format: true
27
29
  # validates :foo, inclusion: true
@@ -29,7 +31,7 @@ module RuboCop
29
31
  # validates :foo, numericality: true
30
32
  # validates :foo, presence: true
31
33
  # validates :foo, absence: true
32
- # validates :foo, size: true
34
+ # validates :foo, length: true
33
35
  # validates :foo, uniqueness: true
34
36
  #
35
37
  class Validation < Base
@@ -39,6 +41,7 @@ module RuboCop
39
41
 
40
42
  TYPES = %w[
41
43
  acceptance
44
+ comparison
42
45
  confirmation
43
46
  exclusion
44
47
  format
@@ -51,16 +54,16 @@ module RuboCop
51
54
  uniqueness
52
55
  ].freeze
53
56
 
54
- RESTRICT_ON_SEND = TYPES.map { |p| "validates_#{p}_of".to_sym }.freeze
57
+ RESTRICT_ON_SEND = TYPES.map { |p| :"validates_#{p}_of" }.freeze
55
58
  ALLOWLIST = TYPES.map { |p| "validates :column, #{p}: value" }.freeze
56
59
 
57
60
  def on_send(node)
58
61
  return if node.receiver
62
+ return unless (last_argument = node.last_argument)
59
63
 
60
64
  range = node.loc.selector
61
65
 
62
66
  add_offense(range, message: message(node)) do |corrector|
63
- last_argument = node.arguments.last
64
67
  return if !last_argument.literal? && !last_argument.splat_type? && !frozen_array_argument?(last_argument)
65
68
 
66
69
  corrector.replace(range, 'validates')
@@ -120,7 +123,9 @@ module RuboCop
120
123
  end
121
124
 
122
125
  def validate_type(node)
123
- node.method_name.to_s.split('_')[1]
126
+ type = node.method_name.to_s.split('_')[1]
127
+
128
+ type == 'size' ? 'length' : type
124
129
  end
125
130
 
126
131
  def frozen_array_argument?(argument)
@@ -4,7 +4,8 @@ module RuboCop
4
4
  module Cop
5
5
  module Rails
6
6
  # Identifies places where manually constructed SQL
7
- # in `where` can be replaced with `where(attribute: value)`.
7
+ # in `where` and `where.not` can be replaced with
8
+ # `where(attribute: value)` and `where.not(attribute: value)`.
8
9
  #
9
10
  # @safety
10
11
  # This cop's autocorrection is unsafe because is may change SQL.
@@ -13,6 +14,7 @@ module RuboCop
13
14
  # @example
14
15
  # # bad
15
16
  # User.where('name = ?', 'Gabe')
17
+ # User.where.not('name = ?', 'Gabe')
16
18
  # User.where('name = :name', name: 'Gabe')
17
19
  # User.where('name IS NULL')
18
20
  # User.where('name IN (?)', ['john', 'jane'])
@@ -21,6 +23,7 @@ module RuboCop
21
23
  #
22
24
  # # good
23
25
  # User.where(name: 'Gabe')
26
+ # User.where.not(name: 'Gabe')
24
27
  # User.where(name: nil)
25
28
  # User.where(name: ['john', 'jane'])
26
29
  # User.where(users: { name: 'Gabe' })
@@ -29,25 +32,27 @@ module RuboCop
29
32
  extend AutoCorrector
30
33
 
31
34
  MSG = 'Use `%<good_method>s` instead of manually constructing SQL.'
32
- RESTRICT_ON_SEND = %i[where].freeze
35
+ RESTRICT_ON_SEND = %i[where not].freeze
33
36
 
34
37
  def_node_matcher :where_method_call?, <<~PATTERN
35
38
  {
36
- (send _ :where (array $str_type? $_ ?))
37
- (send _ :where $str_type? $_ ?)
39
+ (call _ {:where :not} (array $str_type? $_ ?))
40
+ (call _ {:where :not} $str_type? $_ ?)
38
41
  }
39
42
  PATTERN
40
43
 
41
44
  def on_send(node)
45
+ return if node.method?(:not) && !where_not?(node)
46
+
42
47
  where_method_call?(node) do |template_node, value_node|
43
48
  value_node = value_node.first
44
49
 
45
50
  range = offense_range(node)
46
51
 
47
- column_and_value = extract_column_and_value(template_node, value_node)
48
- return unless column_and_value
52
+ column, value = extract_column_and_value(template_node, value_node)
53
+ return unless value
49
54
 
50
- good_method = build_good_method(*column_and_value)
55
+ good_method = build_good_method(node.method_name, column, value)
51
56
  message = format(MSG, good_method: good_method)
52
57
 
53
58
  add_offense(range, message: message) do |corrector|
@@ -55,6 +60,7 @@ module RuboCop
55
60
  end
56
61
  end
57
62
  end
63
+ alias on_csend on_send
58
64
 
59
65
  EQ_ANONYMOUS_RE = /\A([\w.]+)\s+=\s+\?\z/.freeze # column = ?
60
66
  IN_ANONYMOUS_RE = /\A([\w.]+)\s+IN\s+\(\?\)\z/i.freeze # column IN (?)
@@ -68,11 +74,12 @@ module RuboCop
68
74
  range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
69
75
  end
70
76
 
77
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
71
78
  def extract_column_and_value(template_node, value_node)
72
79
  value =
73
80
  case template_node.value
74
81
  when EQ_ANONYMOUS_RE, IN_ANONYMOUS_RE
75
- value_node.source
82
+ value_node&.source
76
83
  when EQ_NAMED_RE, IN_NAMED_RE
77
84
  return unless value_node&.hash_type?
78
85
 
@@ -84,18 +91,28 @@ module RuboCop
84
91
  return
85
92
  end
86
93
 
87
- [Regexp.last_match(1), value]
94
+ column_qualifier = Regexp.last_match(1)
95
+ return if column_qualifier.count('.') > 1
96
+
97
+ [column_qualifier, value]
88
98
  end
99
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
89
100
 
90
- def build_good_method(column, value)
101
+ def build_good_method(method_name, column, value)
91
102
  if column.include?('.')
92
103
  table, column = column.split('.')
93
104
 
94
- "where(#{table}: { #{column}: #{value} })"
105
+ "#{method_name}(#{table}: { #{column}: #{value} })"
95
106
  else
96
- "where(#{column}: #{value})"
107
+ "#{method_name}(#{column}: #{value})"
97
108
  end
98
109
  end
110
+
111
+ def where_not?(node)
112
+ return false unless (receiver = node.receiver)
113
+
114
+ receiver.send_type? && receiver.method?(:where)
115
+ end
99
116
  end
100
117
  end
101
118
  end
@@ -39,7 +39,6 @@ module RuboCop
39
39
  # # bad
40
40
  # User.exists?(name: 'john')
41
41
  # User.exists?(['name = ?', 'john'])
42
- # User.exists?('name = ?', 'john')
43
42
  # user.posts.exists?(published: true)
44
43
  #
45
44
  # # good
@@ -56,11 +55,11 @@ module RuboCop
56
55
  RESTRICT_ON_SEND = %i[exists?].freeze
57
56
 
58
57
  def_node_matcher :where_exists_call?, <<~PATTERN
59
- (send (send _ :where $...) :exists?)
58
+ (call (call _ :where $...) :exists?)
60
59
  PATTERN
61
60
 
62
61
  def_node_matcher :exists_with_args?, <<~PATTERN
63
- (send _ :exists? $...)
62
+ (call _ :exists? $...)
64
63
  PATTERN
65
64
 
66
65
  def on_send(node)
@@ -68,7 +67,7 @@ module RuboCop
68
67
  return unless convertable_args?(args)
69
68
 
70
69
  range = correction_range(node)
71
- good_method = build_good_method(args)
70
+ good_method = build_good_method(args, dot: node.loc.dot)
72
71
  message = format(MSG, good_method: good_method, bad_method: range.source)
73
72
 
74
73
  add_offense(range, message: message) do |corrector|
@@ -76,6 +75,7 @@ module RuboCop
76
75
  end
77
76
  end
78
77
  end
78
+ alias on_csend on_send
79
79
 
80
80
  private
81
81
 
@@ -109,11 +109,11 @@ module RuboCop
109
109
  end
110
110
  end
111
111
 
112
- def build_good_method(args)
112
+ def build_good_method(args, dot:)
113
113
  if exists_style?
114
114
  build_good_method_exists(args)
115
115
  elsif where_style?
116
- build_good_method_where(args)
116
+ build_good_method_where(args, dot&.source || '.')
117
117
  end
118
118
  end
119
119
 
@@ -125,11 +125,11 @@ module RuboCop
125
125
  end
126
126
  end
127
127
 
128
- def build_good_method_where(args)
128
+ def build_good_method_where(args, dot_source)
129
129
  if args.size > 1
130
- "where(#{args.map(&:source).join(', ')}).exists?"
130
+ "where(#{args.map(&:source).join(', ')})#{dot_source}exists?"
131
131
  else
132
- "where(#{args[0].source}).exists?"
132
+ "where(#{args[0].source})#{dot_source}exists?"
133
133
  end
134
134
  end
135
135
  end