rubocop-rails 2.24.1 → 2.26.2

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -2
  3. data/config/default.yml +29 -6
  4. data/lib/rubocop/cop/mixin/target_rails_version.rb +29 -2
  5. data/lib/rubocop/cop/rails/action_order.rb +1 -5
  6. data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +1 -5
  7. data/lib/rubocop/cop/rails/application_record.rb +4 -0
  8. data/lib/rubocop/cop/rails/bulk_change_table.rb +10 -4
  9. data/lib/rubocop/cop/rails/compact_blank.rb +29 -8
  10. data/lib/rubocop/cop/rails/date.rb +2 -2
  11. data/lib/rubocop/cop/rails/enum_hash.rb +31 -8
  12. data/lib/rubocop/cop/rails/enum_syntax.rb +128 -0
  13. data/lib/rubocop/cop/rails/enum_uniqueness.rb +29 -7
  14. data/lib/rubocop/cop/rails/file_path.rb +1 -1
  15. data/lib/rubocop/cop/rails/http_status.rb +12 -2
  16. data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +1 -1
  17. data/lib/rubocop/cop/rails/link_to_blank.rb +2 -2
  18. data/lib/rubocop/cop/rails/not_null_column.rb +93 -13
  19. data/lib/rubocop/cop/rails/pick.rb +4 -0
  20. data/lib/rubocop/cop/rails/pluck_in_where.rb +17 -8
  21. data/lib/rubocop/cop/rails/pluralization_grammar.rb +29 -15
  22. data/lib/rubocop/cop/rails/present.rb +0 -2
  23. data/lib/rubocop/cop/rails/redundant_active_record_all_method.rb +0 -29
  24. data/lib/rubocop/cop/rails/redundant_foreign_key.rb +1 -1
  25. data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +9 -0
  26. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +1 -1
  27. data/lib/rubocop/cop/rails/reflection_class_name.rb +1 -1
  28. data/lib/rubocop/cop/rails/render_plain_text.rb +6 -3
  29. data/lib/rubocop/cop/rails/request_referer.rb +1 -1
  30. data/lib/rubocop/cop/rails/root_pathname_methods.rb +15 -11
  31. data/lib/rubocop/cop/rails/skips_model_validations.rb +8 -3
  32. data/lib/rubocop/cop/rails/unknown_env.rb +1 -1
  33. data/lib/rubocop/cop/rails/unused_ignored_columns.rb +6 -0
  34. data/lib/rubocop/cop/rails/validation.rb +8 -3
  35. data/lib/rubocop/cop/rails/where_equals.rb +28 -12
  36. data/lib/rubocop/cop/rails/where_not.rb +11 -6
  37. data/lib/rubocop/cop/rails/where_range.rb +203 -0
  38. data/lib/rubocop/cop/rails_cops.rb +2 -0
  39. data/lib/rubocop/rails/schema_loader/schema.rb +1 -1
  40. data/lib/rubocop/rails/version.rb +1 -1
  41. metadata +8 -6
@@ -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
- (call _ :where (array $str_type? $_ ?))
37
- (call _ :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|
@@ -69,11 +74,12 @@ module RuboCop
69
74
  range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
70
75
  end
71
76
 
77
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
72
78
  def extract_column_and_value(template_node, value_node)
73
79
  value =
74
80
  case template_node.value
75
81
  when EQ_ANONYMOUS_RE, IN_ANONYMOUS_RE
76
- value_node.source
82
+ value_node&.source
77
83
  when EQ_NAMED_RE, IN_NAMED_RE
78
84
  return unless value_node&.hash_type?
79
85
 
@@ -85,18 +91,28 @@ module RuboCop
85
91
  return
86
92
  end
87
93
 
88
- [Regexp.last_match(1), value]
94
+ column_qualifier = Regexp.last_match(1)
95
+ return if column_qualifier.count('.') > 1
96
+
97
+ [column_qualifier, value]
89
98
  end
99
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
90
100
 
91
- def build_good_method(column, value)
101
+ def build_good_method(method_name, column, value)
92
102
  if column.include?('.')
93
103
  table, column = column.split('.')
94
104
 
95
- "where(#{table}: { #{column}: #{value} })"
105
+ "#{method_name}(#{table}: { #{column}: #{value} })"
96
106
  else
97
- "where(#{column}: #{value})"
107
+ "#{method_name}(#{column}: #{value})"
98
108
  end
99
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
100
116
  end
101
117
  end
102
118
  end
@@ -43,10 +43,10 @@ module RuboCop
43
43
 
44
44
  range = offense_range(node)
45
45
 
46
- column_and_value = extract_column_and_value(template_node, value_node)
47
- return unless column_and_value
46
+ column, value = extract_column_and_value(template_node, value_node)
47
+ return unless value
48
48
 
49
- good_method = build_good_method(node.loc.dot&.source, *column_and_value)
49
+ good_method = build_good_method(node.loc.dot&.source, column, value)
50
50
  message = format(MSG, good_method: good_method)
51
51
 
52
52
  add_offense(range, message: message) do |corrector|
@@ -68,13 +68,14 @@ module RuboCop
68
68
  range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
69
69
  end
70
70
 
71
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
71
72
  def extract_column_and_value(template_node, value_node)
72
73
  value =
73
74
  case template_node.value
74
75
  when NOT_EQ_ANONYMOUS_RE, NOT_IN_ANONYMOUS_RE
75
- value_node.source
76
+ value_node&.source
76
77
  when NOT_EQ_NAMED_RE, NOT_IN_NAMED_RE
77
- return unless value_node.hash_type?
78
+ return unless value_node&.hash_type?
78
79
 
79
80
  pair = value_node.pairs.find { |p| p.key.value.to_sym == Regexp.last_match(2).to_sym }
80
81
  pair.value.source
@@ -84,8 +85,12 @@ module RuboCop
84
85
  return
85
86
  end
86
87
 
87
- [Regexp.last_match(1), value]
88
+ column_qualifier = Regexp.last_match(1)
89
+ return if column_qualifier.count('.') > 1
90
+
91
+ [column_qualifier, value]
88
92
  end
93
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
89
94
 
90
95
  def build_good_method(dot, column, value)
91
96
  dot ||= '.'
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Identifies places where manually constructed SQL
7
+ # in `where` can be replaced with ranges.
8
+ #
9
+ # @safety
10
+ # This cop's autocorrection is unsafe because it can change the query
11
+ # by explicitly attaching the column to the wrong table.
12
+ # For example, `Booking.joins(:events).where('end_at < ?', Time.current)` will correctly
13
+ # implicitly attach the `end_at` column to the `events` table. But when autocorrected to
14
+ # `Booking.joins(:events).where(end_at: ...Time.current)`, it will now be incorrectly
15
+ # explicitly attached to the `bookings` table.
16
+ #
17
+ # @example
18
+ # # bad
19
+ # User.where('age >= ?', 18)
20
+ # User.where.not('age >= ?', 18)
21
+ # User.where('age < ?', 18)
22
+ # User.where('age >= ? AND age < ?', 18, 21)
23
+ # User.where('age >= :start', start: 18)
24
+ # User.where('users.age >= ?', 18)
25
+ #
26
+ # # good
27
+ # User.where(age: 18..)
28
+ # User.where.not(age: 18..)
29
+ # User.where(age: ...18)
30
+ # User.where(age: 18...21)
31
+ # User.where(users: { age: 18.. })
32
+ #
33
+ # # good
34
+ # # There are no beginless ranges in ruby.
35
+ # User.where('age > ?', 18)
36
+ #
37
+ class WhereRange < Base
38
+ include RangeHelp
39
+ extend AutoCorrector
40
+ extend TargetRubyVersion
41
+ extend TargetRailsVersion
42
+
43
+ MSG = 'Use `%<good_method>s` instead of manually constructing SQL.'
44
+
45
+ RESTRICT_ON_SEND = %i[where not].freeze
46
+
47
+ # column >= ?
48
+ GTEQ_ANONYMOUS_RE = /\A\s*([\w.]+)\s+>=\s+\?\s*\z/.freeze
49
+ # column <[=] ?
50
+ LTEQ_ANONYMOUS_RE = /\A\s*([\w.]+)\s+(<=?)\s+\?\s*\z/.freeze
51
+ # column >= ? AND column <[=] ?
52
+ RANGE_ANONYMOUS_RE = /\A\s*([\w.]+)\s+>=\s+\?\s+AND\s+\1\s+(<=?)\s+\?\s*\z/i.freeze
53
+ # column >= :value
54
+ GTEQ_NAMED_RE = /\A\s*([\w.]+)\s+>=\s+:(\w+)\s*\z/.freeze
55
+ # column <[=] :value
56
+ LTEQ_NAMED_RE = /\A\s*([\w.]+)\s+(<=?)\s+:(\w+)\s*\z/.freeze
57
+ # column >= :value1 AND column <[=] :value2
58
+ RANGE_NAMED_RE = /\A\s*([\w.]+)\s+>=\s+:(\w+)\s+AND\s+\1\s+(<=?)\s+:(\w+)\s*\z/i.freeze
59
+
60
+ minimum_target_ruby_version 2.6
61
+ minimum_target_rails_version 6.0
62
+
63
+ def_node_matcher :where_range_call?, <<~PATTERN
64
+ {
65
+ (call _ {:where :not} (array $str_type? $_ +))
66
+ (call _ {:where :not} $str_type? $_ +)
67
+ }
68
+ PATTERN
69
+
70
+ def on_send(node)
71
+ return if node.method?(:not) && !where_not?(node)
72
+
73
+ where_range_call?(node) do |template_node, values_node|
74
+ column, value = extract_column_and_value(template_node, values_node)
75
+
76
+ return unless column
77
+
78
+ range = offense_range(node)
79
+ good_method = build_good_method(node.method_name, column, value)
80
+ message = format(MSG, good_method: good_method)
81
+
82
+ add_offense(range, message: message) do |corrector|
83
+ corrector.replace(range, good_method)
84
+ end
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def where_not?(node)
91
+ receiver = node.receiver
92
+ receiver&.send_type? && receiver&.method?(:where)
93
+ end
94
+
95
+ # rubocop:disable Metrics
96
+ def extract_column_and_value(template_node, values_node)
97
+ case template_node.value
98
+ when GTEQ_ANONYMOUS_RE
99
+ lhs = values_node[0]
100
+ operator = '..'
101
+ when LTEQ_ANONYMOUS_RE
102
+ if target_ruby_version >= 2.7
103
+ operator = range_operator(Regexp.last_match(2))
104
+ rhs = values_node[0]
105
+ end
106
+ when RANGE_ANONYMOUS_RE
107
+ if values_node.size >= 2
108
+ lhs = values_node[0]
109
+ operator = range_operator(Regexp.last_match(2))
110
+ rhs = values_node[1]
111
+ end
112
+ when GTEQ_NAMED_RE
113
+ value_node = values_node[0]
114
+
115
+ if value_node.hash_type?
116
+ pair = find_pair(value_node, Regexp.last_match(2))
117
+ lhs = pair.value
118
+ operator = '..'
119
+ end
120
+ when LTEQ_NAMED_RE
121
+ value_node = values_node[0]
122
+
123
+ if value_node.hash_type?
124
+ pair = find_pair(value_node, Regexp.last_match(2))
125
+ if pair && target_ruby_version >= 2.7
126
+ operator = range_operator(Regexp.last_match(2))
127
+ rhs = pair.value
128
+ end
129
+ end
130
+ when RANGE_NAMED_RE
131
+ value_node = values_node[0]
132
+
133
+ if value_node.hash_type?
134
+ pair1 = find_pair(value_node, Regexp.last_match(2))
135
+ pair2 = find_pair(value_node, Regexp.last_match(4))
136
+
137
+ if pair1 && pair2
138
+ lhs = pair1.value
139
+ operator = range_operator(Regexp.last_match(3))
140
+ rhs = pair2.value
141
+ end
142
+ end
143
+ else
144
+ return
145
+ end
146
+
147
+ if lhs
148
+ lhs_source = parentheses_needed?(lhs) ? "(#{lhs.source})" : lhs.source
149
+ end
150
+
151
+ if rhs
152
+ rhs_source = parentheses_needed?(rhs) ? "(#{rhs.source})" : rhs.source
153
+ end
154
+
155
+ column_qualifier = Regexp.last_match(1)
156
+ return if column_qualifier.count('.') > 1
157
+
158
+ [column_qualifier, "#{lhs_source}#{operator}#{rhs_source}"] if operator
159
+ end
160
+ # rubocop:enable Metrics
161
+
162
+ def range_operator(comparison_operator)
163
+ comparison_operator == '<' ? '...' : '..'
164
+ end
165
+
166
+ def find_pair(hash_node, value)
167
+ hash_node.pairs.find { |pair| pair.key.value.to_sym == value.to_sym }
168
+ end
169
+
170
+ def offense_range(node)
171
+ range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
172
+ end
173
+
174
+ def build_good_method(method_name, column, value)
175
+ if column.include?('.')
176
+ table, column = column.split('.')
177
+
178
+ "#{method_name}(#{table}: { #{column}: #{value} })"
179
+ else
180
+ "#{method_name}(#{column}: #{value})"
181
+ end
182
+ end
183
+
184
+ def parentheses_needed?(node)
185
+ !parentheses_not_needed?(node)
186
+ end
187
+
188
+ def parentheses_not_needed?(node)
189
+ node.variable? ||
190
+ node.literal? ||
191
+ node.reference? ||
192
+ node.const_type? ||
193
+ node.begin_type? ||
194
+ parenthesized_call_node?(node)
195
+ end
196
+
197
+ def parenthesized_call_node?(node)
198
+ node.call_type? && (node.arguments.empty? || node.parenthesized_call?)
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -46,6 +46,7 @@ require_relative 'rails/duration_arithmetic'
46
46
  require_relative 'rails/dynamic_find_by'
47
47
  require_relative 'rails/eager_evaluation_log_message'
48
48
  require_relative 'rails/enum_hash'
49
+ require_relative 'rails/enum_syntax'
49
50
  require_relative 'rails/enum_uniqueness'
50
51
  require_relative 'rails/env_local'
51
52
  require_relative 'rails/environment_comparison'
@@ -138,3 +139,4 @@ require_relative 'rails/where_exists'
138
139
  require_relative 'rails/where_missing'
139
140
  require_relative 'rails/where_not'
140
141
  require_relative 'rails/where_not_with_multiple_conditions'
142
+ require_relative 'rails/where_range'
@@ -178,7 +178,7 @@ module RuboCop
178
178
  attr_reader :table_name
179
179
 
180
180
  def initialize(node)
181
- super(node)
181
+ super
182
182
 
183
183
  @table_name = node.first_argument.value
184
184
  @columns, @expression = build_columns_or_expr(node.arguments[1])
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Rails
5
5
  # This module holds the RuboCop Rails version information.
6
6
  module Version
7
- STRING = '2.24.1'
7
+ STRING = '2.26.2'
8
8
 
9
9
  def self.document_version
10
10
  STRING.match('\d+\.\d+').to_s
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.24.1
4
+ version: 2.26.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bozhidar Batsov
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2024-03-25 00:00:00.000000000 Z
13
+ date: 2024-09-21 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -46,7 +46,7 @@ dependencies:
46
46
  requirements:
47
47
  - - ">="
48
48
  - !ruby/object:Gem::Version
49
- version: 1.33.0
49
+ version: 1.52.0
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
52
  version: '2.0'
@@ -56,7 +56,7 @@ dependencies:
56
56
  requirements:
57
57
  - - ">="
58
58
  - !ruby/object:Gem::Version
59
- version: 1.33.0
59
+ version: 1.52.0
60
60
  - - "<"
61
61
  - !ruby/object:Gem::Version
62
62
  version: '2.0'
@@ -140,6 +140,7 @@ files:
140
140
  - lib/rubocop/cop/rails/dynamic_find_by.rb
141
141
  - lib/rubocop/cop/rails/eager_evaluation_log_message.rb
142
142
  - lib/rubocop/cop/rails/enum_hash.rb
143
+ - lib/rubocop/cop/rails/enum_syntax.rb
143
144
  - lib/rubocop/cop/rails/enum_uniqueness.rb
144
145
  - lib/rubocop/cop/rails/env_local.rb
145
146
  - lib/rubocop/cop/rails/environment_comparison.rb
@@ -232,6 +233,7 @@ files:
232
233
  - lib/rubocop/cop/rails/where_missing.rb
233
234
  - lib/rubocop/cop/rails/where_not.rb
234
235
  - lib/rubocop/cop/rails/where_not_with_multiple_conditions.rb
236
+ - lib/rubocop/cop/rails/where_range.rb
235
237
  - lib/rubocop/cop/rails_cops.rb
236
238
  - lib/rubocop/rails.rb
237
239
  - lib/rubocop/rails/inject.rb
@@ -245,7 +247,7 @@ metadata:
245
247
  homepage_uri: https://docs.rubocop.org/rubocop-rails/
246
248
  changelog_uri: https://github.com/rubocop/rubocop-rails/blob/master/CHANGELOG.md
247
249
  source_code_uri: https://github.com/rubocop/rubocop-rails/
248
- documentation_uri: https://docs.rubocop.org/rubocop-rails/2.24/
250
+ documentation_uri: https://docs.rubocop.org/rubocop-rails/2.26/
249
251
  bug_tracker_uri: https://github.com/rubocop/rubocop-rails/issues
250
252
  rubygems_mfa_required: 'true'
251
253
  post_install_message:
@@ -263,7 +265,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
263
265
  - !ruby/object:Gem::Version
264
266
  version: '0'
265
267
  requirements: []
266
- rubygems_version: 3.3.26
268
+ rubygems_version: 3.5.16
267
269
  signing_key:
268
270
  specification_version: 4
269
271
  summary: Automatic Rails code style checking tool.