rubocop-rails 2.24.1 → 2.26.2

Sign up to get free protection for your applications and to get access to all the features.
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.