rubocop-rails 2.24.0 → 2.25.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24df033ffce85b621be414aaec3b62b7b5ac8613e5fc55ce54d58a4be9c22c8b
4
- data.tar.gz: 328d994492ff8e6b1f3634e62a1fefaa4e7f3e894492d9f65bc141c7f49e8dac
3
+ metadata.gz: cad5e6d0c6f188b6ff87adb04fbfb73c7c574b901de45256eb7fa6e50b7a136d
4
+ data.tar.gz: c8ab8f1d1c3284054af5847231dd996df24ee5441f638f95bc7e32420a3baf04
5
5
  SHA512:
6
- metadata.gz: 2be5897e47aa2e661a93ece9254578962af30ea0bad2f0303a92f63d846cc37bf7f85d9338e385b29a489202ce762ee5f077dd355443e24695f7b4869f4fd902
7
- data.tar.gz: 56db6c7a79ce813778ec0d6fcdbf57de7bdf7ea5b7463c78a58f1919c46b84c3edbbc8f8f02de123439a33df8bdf427b8e29a0d82d51ae624ace0890c8e8158f
6
+ metadata.gz: ea0ebe45e988d115aa45d7bdba2b728b0583e1e2d996d43af424eb8abc17ced35b528741fcc47b8b258de52edd2fb7bc1c92dd609b49ddeeaaaf26bce123e783
7
+ data.tar.gz: 7eb06cfce19f2a4ba78c6d571c65cf386e211b48ca2c9b5b9ca436ec9055e49f9737cbb3f9ce1b44ab432de87121295439effc59f093446cea88199bd08ba8a4
data/config/default.yml CHANGED
@@ -165,6 +165,7 @@ Rails/ActiveSupportOnLoad:
165
165
  - 'https://guides.rubyonrails.org/engines.html#available-load-hooks'
166
166
  SafeAutoCorrect: false
167
167
  VersionAdded: '2.16'
168
+ VersionChanged: '2.24'
168
169
 
169
170
  Rails/AddColumnIndex:
170
171
  Description: >-
@@ -692,7 +693,7 @@ Rails/NegateInclude:
692
693
  VersionChanged: '2.9'
693
694
 
694
695
  Rails/NotNullColumn:
695
- Description: 'Do not add a NOT NULL column without a default value.'
696
+ Description: 'Do not add a NOT NULL column without a default value to existing tables.'
696
697
  Enabled: true
697
698
  VersionAdded: '0.43'
698
699
  VersionChanged: '2.20'
@@ -1162,8 +1163,9 @@ Rails/UnknownEnv:
1162
1163
 
1163
1164
  Rails/UnusedIgnoredColumns:
1164
1165
  Description: 'Remove a column that does not exist from `ignored_columns`.'
1165
- Enabled: pending
1166
+ Enabled: false
1166
1167
  VersionAdded: '2.11'
1168
+ VersionChanged: '2.25'
1167
1169
  Include:
1168
1170
  - app/models/**/*.rb
1169
1171
 
@@ -1220,6 +1222,12 @@ Rails/WhereNotWithMultipleConditions:
1220
1222
  VersionAdded: '2.17'
1221
1223
  VersionChanged: '2.18'
1222
1224
 
1225
+ Rails/WhereRange:
1226
+ Description: 'Use ranges in `where` instead of manually constructing SQL.'
1227
+ StyleGuide: 'https://rails.rubystyle.guide/#where-ranges'
1228
+ Enabled: pending
1229
+ VersionAdded: '2.25'
1230
+
1223
1231
  # Accept `redirect_to(...) and return` and similar cases.
1224
1232
  Style/AndOr:
1225
1233
  EnforcedStyle: conditionals
@@ -4,13 +4,40 @@ module RuboCop
4
4
  module Cop
5
5
  # Common functionality for checking target rails version.
6
6
  module TargetRailsVersion
7
+ # Informs the base RuboCop gem that it the Rails version is checked via `requires_gem` API,
8
+ # without needing to call this `#support_target_rails_version` method.
9
+ USES_REQUIRES_GEM_API = true
10
+
7
11
  def minimum_target_rails_version(version)
8
- @minimum_target_rails_version = version
12
+ if respond_to?(:requires_gem)
13
+ case version
14
+ when Integer, Float then requires_gem(TARGET_GEM_NAME, ">= #{version}")
15
+ when String then requires_gem(TARGET_GEM_NAME, version)
16
+ end
17
+ else
18
+ # Fallback path for previous versions of RuboCop which don't support the `requires_gem` API yet.
19
+ @minimum_target_rails_version = version
20
+ end
9
21
  end
10
22
 
11
23
  def support_target_rails_version?(version)
12
- @minimum_target_rails_version <= version
24
+ if respond_to?(:requires_gem)
25
+ return false unless gem_requirements
26
+
27
+ gem_requirement = gem_requirements[TARGET_GEM_NAME]
28
+ return true unless gem_requirement # If we have no requirement, then we support all versions
29
+
30
+ gem_requirement.satisfied_by?(Gem::Version.new(version))
31
+ else
32
+ # Fallback path for previous versions of RuboCop which don't support the `requires_gem` API yet.
33
+ @minimum_target_rails_version <= version
34
+ end
13
35
  end
36
+
37
+ # Look for `railties` instead of `rails`, to support apps that only use a subset of `rails`
38
+ # See https://github.com/rubocop/rubocop/pull/11289
39
+ TARGET_GEM_NAME = 'railties'
40
+ private_constant :TARGET_GEM_NAME
14
41
  end
15
42
  end
16
43
  end
@@ -99,6 +99,8 @@ module RuboCop
99
99
 
100
100
  def use_redirect_to?(context)
101
101
  context.right_siblings.compact.any? do |sibling|
102
+ # Unwrap `return redirect_to :index`
103
+ sibling = sibling.children.first if sibling.return_type? && sibling.children.one?
102
104
  sibling.send_type? && sibling.method?(:redirect_to)
103
105
  end
104
106
  end
@@ -55,15 +55,35 @@ module RuboCop
55
55
  'ActiveSupport::TestCase' => 'active_support_test_case'
56
56
  }.freeze
57
57
 
58
+ RAILS_5_2_LOAD_HOOKS = {
59
+ 'ActiveRecord::ConnectionAdapters::SQLite3Adapter' => 'active_record_sqlite3adapter'
60
+ }.freeze
61
+
62
+ RAILS_7_1_LOAD_HOOKS = {
63
+ 'ActiveRecord::TestFixtures' => 'active_record_fixtures',
64
+ 'ActiveModel::Model' => 'active_model',
65
+ 'ActionText::EncryptedRichText' => 'action_text_encrypted_rich_text',
66
+ 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter' => 'active_record_postgresqladapter',
67
+ 'ActiveRecord::ConnectionAdapters::Mysql2Adapter' => 'active_record_mysql2adapter',
68
+ 'ActiveRecord::ConnectionAdapters::TrilogyAdapter' => 'active_record_trilogyadapter'
69
+ }.freeze
70
+
58
71
  def on_send(node)
59
72
  receiver, method, arguments = *node # rubocop:disable InternalAffairs/NodeDestructuring
60
- return unless receiver && (hook = LOAD_HOOKS[receiver.const_name])
73
+ return unless arguments && (hook = hook_for_const(receiver&.const_name))
61
74
 
62
75
  preferred = "ActiveSupport.on_load(:#{hook}) { #{method} #{arguments.source} }"
63
76
  add_offense(node, message: format(MSG, prefer: preferred, current: node.source)) do |corrector|
64
77
  corrector.replace(node, preferred)
65
78
  end
66
79
  end
80
+
81
+ def hook_for_const(const_name)
82
+ hook = LOAD_HOOKS[const_name]
83
+ hook ||= RAILS_5_2_LOAD_HOOKS[const_name] if target_rails_version >= 5.2
84
+ hook ||= RAILS_7_1_LOAD_HOOKS[const_name] if target_rails_version >= 7.1
85
+ hook
86
+ end
67
87
  end
68
88
  end
69
89
  end
@@ -51,7 +51,7 @@ module RuboCop
51
51
  return if allow?(begin_node, end_node)
52
52
 
53
53
  preferred_method = preferred_method(begin_node)
54
- if begin_node.method?(:beginning_of_week) && begin_node.arguments.one?
54
+ if begin_node.method?(:beginning_of_week) && begin_node.arguments.one? && end_node.arguments.one?
55
55
  return unless same_argument?(begin_node, end_node)
56
56
 
57
57
  preferred_method << "(#{begin_node.first_argument.source})"
@@ -13,6 +13,8 @@ module RuboCop
13
13
  # render plain: 'foo/bar', status: 304
14
14
  # redirect_to root_url, status: 301
15
15
  # head 200
16
+ # assert_response 200
17
+ # assert_redirected_to '/some/path', status: 301
16
18
  #
17
19
  # # good
18
20
  # render :foo, status: :ok
@@ -20,6 +22,8 @@ module RuboCop
20
22
  # render plain: 'foo/bar', status: :not_modified
21
23
  # redirect_to root_url, status: :moved_permanently
22
24
  # head :ok
25
+ # assert_response :ok
26
+ # assert_redirected_to '/some/path', status: :moved_permanently
23
27
  #
24
28
  # @example EnforcedStyle: numeric
25
29
  # # bad
@@ -28,6 +32,8 @@ module RuboCop
28
32
  # render plain: 'foo/bar', status: :not_modified
29
33
  # redirect_to root_url, status: :moved_permanently
30
34
  # head :ok
35
+ # assert_response :ok
36
+ # assert_redirected_to '/some/path', status: :moved_permanently
31
37
  #
32
38
  # # good
33
39
  # render :foo, status: 200
@@ -35,18 +41,22 @@ module RuboCop
35
41
  # render plain: 'foo/bar', status: 304
36
42
  # redirect_to root_url, status: 301
37
43
  # head 200
44
+ # assert_response 200
45
+ # assert_redirected_to '/some/path', status: 301
38
46
  #
39
47
  class HttpStatus < Base
40
48
  include ConfigurableEnforcedStyle
41
49
  extend AutoCorrector
42
50
 
43
- RESTRICT_ON_SEND = %i[render redirect_to head].freeze
51
+ RESTRICT_ON_SEND = %i[render redirect_to head assert_response assert_redirected_to].freeze
44
52
 
45
53
  def_node_matcher :http_status, <<~PATTERN
46
54
  {
47
55
  (send nil? {:render :redirect_to} _ $hash)
48
56
  (send nil? {:render :redirect_to} $hash)
49
- (send nil? :head ${int sym} ...)
57
+ (send nil? {:head :assert_response} ${int sym} ...)
58
+ (send nil? :assert_redirected_to _ $hash ...)
59
+ (send nil? :assert_redirected_to $hash ...)
50
60
  }
51
61
  PATTERN
52
62
 
@@ -3,24 +3,42 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Rails
6
- # Checks for add_column call with NOT NULL constraint in migration file.
6
+ # Checks for add_column calls with a NOT NULL constraint without a default
7
+ # value.
7
8
  #
8
- # `TEXT` can have default values in PostgreSQL, but not in MySQL.
9
- # It will automatically detect an adapter from `development` environment
10
- # in `config/database.yml` or the environment variable `DATABASE_URL`
11
- # when the `Database` option is not set. If the database is MySQL,
12
- # this cop ignores offenses for the `TEXT`.
9
+ # This cop only applies when adding a column to an existing table, since
10
+ # existing records will not have a value for the new column. New tables
11
+ # can freely use NOT NULL columns without defaults, since there are no
12
+ # records that could violate the constraint.
13
+ #
14
+ # If you need to add a NOT NULL column to an existing table, you must add
15
+ # it as nullable first, back-fill the data, and then use
16
+ # `change_column_null`. Alternatively, you could add the column with a
17
+ # default first to have the database automatically backfill existing rows,
18
+ # and then use `change_column_default` to remove the default.
19
+ #
20
+ # `TEXT` cannot have a default value in MySQL.
21
+ # The cop will automatically detect an adapter from `development`
22
+ # environment in `config/database.yml` or the environment variable
23
+ # `DATABASE_URL` when the `Database` option is not set. If the database
24
+ # is MySQL, this cop ignores offenses for `TEXT` columns.
13
25
  #
14
26
  # @example
15
27
  # # bad
16
28
  # add_column :users, :name, :string, null: false
17
29
  # add_reference :products, :category, null: false
30
+ # change_table :users do |t|
31
+ # t.string :name, null: false
32
+ # end
18
33
  #
19
34
  # # good
20
35
  # add_column :users, :name, :string, null: true
21
36
  # add_column :users, :name, :string, null: false, default: ''
37
+ # change_table :users do |t|
38
+ # t.string :name, null: false, default: ''
39
+ # end
22
40
  # add_reference :products, :category
23
- # add_reference :products, :category, null: false, default: 1
41
+ # change_column_null :products, :category_id, false
24
42
  class NotNullColumn < Base
25
43
  include DatabaseTypeResolvable
26
44
 
@@ -35,6 +53,22 @@ module RuboCop
35
53
  (send nil? :add_reference _ _ (hash $...))
36
54
  PATTERN
37
55
 
56
+ def_node_matcher :change_table?, <<~PATTERN
57
+ (block (send nil? :change_table ...) (args (arg $_)) _)
58
+ PATTERN
59
+
60
+ def_node_matcher :add_not_null_column_in_change_table?, <<~PATTERN
61
+ (send (lvar $_) :column _ $_ (hash $...))
62
+ PATTERN
63
+
64
+ def_node_matcher :add_not_null_column_via_shortcut_in_change_table?, <<~PATTERN
65
+ (send (lvar $_) $_ _ (hash $...))
66
+ PATTERN
67
+
68
+ def_node_matcher :add_not_null_reference_in_change_table?, <<~PATTERN
69
+ (send (lvar $_) :add_reference _ _ (hash $...))
70
+ PATTERN
71
+
38
72
  def_node_matcher :null_false?, <<~PATTERN
39
73
  (pair (sym :null) (false))
40
74
  PATTERN
@@ -48,16 +82,25 @@ module RuboCop
48
82
  check_add_reference(node)
49
83
  end
50
84
 
85
+ def on_block(node)
86
+ check_change_table(node)
87
+ end
88
+ alias on_numblock on_block
89
+
51
90
  private
52
91
 
92
+ def check_column(type, pairs)
93
+ if type.respond_to?(:value)
94
+ return if type.value == :virtual || type.value == 'virtual'
95
+ return if (type.value == :text || type.value == 'text') && database == MYSQL
96
+ end
97
+
98
+ check_pairs(pairs)
99
+ end
100
+
53
101
  def check_add_column(node)
54
102
  add_not_null_column?(node) do |type, pairs|
55
- if type.respond_to?(:value)
56
- return if type.value == :virtual || type.value == 'virtual'
57
- return if (type.value == :text || type.value == 'text') && database == MYSQL
58
- end
59
-
60
- check_pairs(pairs)
103
+ check_column(type, pairs)
61
104
  end
62
105
  end
63
106
 
@@ -67,6 +110,41 @@ module RuboCop
67
110
  end
68
111
  end
69
112
 
113
+ def check_add_column_in_change_table(node, table)
114
+ add_not_null_column_in_change_table?(node) do |receiver, type, pairs|
115
+ next unless receiver == table
116
+
117
+ check_column(type, pairs)
118
+ end
119
+ end
120
+
121
+ def check_add_column_via_shortcut_in_change_table(node, table)
122
+ add_not_null_column_via_shortcut_in_change_table?(node) do |receiver, type, pairs|
123
+ next unless receiver == table
124
+
125
+ check_column(type, pairs)
126
+ end
127
+ end
128
+
129
+ def check_add_reference_in_change_table(node, table)
130
+ add_not_null_reference_in_change_table?(node) do |receiver, pairs|
131
+ next unless receiver == table
132
+
133
+ check_pairs(pairs)
134
+ end
135
+ end
136
+
137
+ def check_change_table(node)
138
+ change_table?(node) do |table|
139
+ children = node.body.begin_type? ? node.body.children : [node.body]
140
+ children.each do |child|
141
+ check_add_column_in_change_table(child, table)
142
+ check_add_column_via_shortcut_in_change_table(child, table)
143
+ check_add_reference_in_change_table(child, table)
144
+ end
145
+ end
146
+ end
147
+
70
148
  def check_pairs(pairs)
71
149
  return if pairs.any? { |pair| default_option?(pair) }
72
150
 
@@ -9,6 +9,10 @@ module RuboCop
9
9
  # `pick` avoids. When called on an Active Record relation, `pick` adds a
10
10
  # limit to the query so that only one value is fetched from the database.
11
11
  #
12
+ # Note that when `pick` is added to a relation with an existing limit, it
13
+ # causes a subquery to be added. In most cases this is undesirable, and
14
+ # care should be taken while resolving this violation.
15
+ #
12
16
  # @safety
13
17
  # This cop is unsafe because `pluck` is defined on both `ActiveRecord::Relation` and `Enumerable`,
14
18
  # whereas `pick` is only defined on `ActiveRecord::Relation` in Rails 6.0. This was addressed
@@ -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
 
@@ -63,7 +63,7 @@ module RuboCop
63
63
  PATTERN
64
64
 
65
65
  def_node_matcher :good_insert?, <<~PATTERN
66
- (send _ {:insert :insert!} _ {
66
+ (call _ {:insert :insert!} _ {
67
67
  !(hash ...)
68
68
  (hash <(pair (sym !{:returning :unique_by}) _) ...>)
69
69
  } ...)
@@ -69,9 +69,10 @@ module RuboCop
69
69
  return if !node.receiver&.str_type? || !node.method?(:to_time)
70
70
 
71
71
  add_offense(node.loc.selector, message: MSG_STRING_TO_TIME) do |corrector|
72
- corrector.replace(node, "Time.zone.parse(#{node.receiver.source})")
72
+ corrector.replace(node, "Time.zone.parse(#{node.receiver.source})") unless node.csend_type?
73
73
  end
74
74
  end
75
+ alias on_csend on_send
75
76
 
76
77
  private
77
78
 
@@ -68,15 +68,23 @@ module RuboCop
68
68
  return unless uniq
69
69
 
70
70
  add_offense(node.loc.selector) do |corrector|
71
- method = node.method_name
72
-
73
- corrector.remove(dot_method_with_whitespace(method, node))
74
- corrector.insert_before(node.receiver.loc.dot.begin, '.distinct')
71
+ autocorrect(corrector, node)
75
72
  end
76
73
  end
77
74
 
78
75
  private
79
76
 
77
+ def autocorrect(corrector, node)
78
+ method = node.method_name
79
+
80
+ corrector.remove(dot_method_with_whitespace(method, node))
81
+ if (dot = node.receiver.loc.dot)
82
+ corrector.insert_before(dot.begin, '.distinct')
83
+ else
84
+ corrector.insert_before(node.receiver, 'distinct.')
85
+ end
86
+ end
87
+
80
88
  def dot_method_with_whitespace(method, node)
81
89
  range_between(dot_method_begin_pos(method, node), node.loc.selector.end_pos)
82
90
  end
@@ -87,7 +87,7 @@ module RuboCop
87
87
 
88
88
  def environments
89
89
  @environments ||= begin
90
- environments = cop_config['Environments'] || []
90
+ environments = cop_config['Environments'].dup || []
91
91
  environments << 'local' if target_rails_version >= 7.1
92
92
  environments
93
93
  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
@@ -29,7 +29,7 @@ module RuboCop
29
29
  # validates :foo, numericality: true
30
30
  # validates :foo, presence: true
31
31
  # validates :foo, absence: true
32
- # validates :foo, size: true
32
+ # validates :foo, length: true
33
33
  # validates :foo, uniqueness: true
34
34
  #
35
35
  class Validation < Base
@@ -120,7 +120,9 @@ module RuboCop
120
120
  end
121
121
 
122
122
  def validate_type(node)
123
- node.method_name.to_s.split('_')[1]
123
+ type = node.method_name.to_s.split('_')[1]
124
+
125
+ type == 'size' ? 'length' : type
124
126
  end
125
127
 
126
128
  def frozen_array_argument?(argument)
@@ -89,16 +89,20 @@ module RuboCop
89
89
  end
90
90
  end
91
91
 
92
+ # rubocop:disable Metrics/AbcSize
92
93
  def remove_where_method(corrector, node, where_node)
93
94
  range = range_between(where_node.loc.selector.begin_pos, where_node.loc.end.end_pos)
94
95
  if node.multiline? && !same_line?(node, where_node)
95
96
  range = range_by_whole_lines(range, include_final_newline: true)
96
- else
97
+ elsif where_node.receiver
97
98
  corrector.remove(where_node.loc.dot)
99
+ else
100
+ corrector.remove(node.loc.dot)
98
101
  end
99
102
 
100
103
  corrector.remove(range)
101
104
  end
105
+ # rubocop:enable Metrics/AbcSize
102
106
 
103
107
  def same_line?(left_joins_node, where_node)
104
108
  left_joins_node.loc.selector.line == where_node.loc.selector.line
@@ -0,0 +1,157 @@
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
+ # @example
10
+ # # bad
11
+ # User.where('age >= ?', 18)
12
+ # User.where.not('age >= ?', 18)
13
+ # User.where('age < ?', 18)
14
+ # User.where('age >= ? AND age < ?', 18, 21)
15
+ # User.where('age >= :start', start: 18)
16
+ # User.where('users.age >= ?', 18)
17
+ #
18
+ # # good
19
+ # User.where(age: 18..)
20
+ # User.where.not(age: 18..)
21
+ # User.where(age: ...18)
22
+ # User.where(age: 18...21)
23
+ # User.where(users: { age: 18.. })
24
+ #
25
+ # # good
26
+ # # There are no beginless ranges in ruby.
27
+ # User.where('age > ?', 18)
28
+ #
29
+ class WhereRange < Base
30
+ include RangeHelp
31
+ extend AutoCorrector
32
+ extend TargetRubyVersion
33
+ extend TargetRailsVersion
34
+
35
+ MSG = 'Use `%<good_method>s` instead of manually constructing SQL.'
36
+
37
+ RESTRICT_ON_SEND = %i[where not].freeze
38
+
39
+ # column >= ?
40
+ GTEQ_ANONYMOUS_RE = /\A([\w.]+)\s+>=\s+\?\z/.freeze
41
+ # column <[=] ?
42
+ LTEQ_ANONYMOUS_RE = /\A([\w.]+)\s+(<=?)\s+\?\z/.freeze
43
+ # column >= ? AND column <[=] ?
44
+ RANGE_ANONYMOUS_RE = /\A([\w.]+)\s+>=\s+\?\s+AND\s+\1\s+(<=?)\s+\?\z/i.freeze
45
+ # column >= :value
46
+ GTEQ_NAMED_RE = /\A([\w.]+)\s+>=\s+:(\w+)\z/.freeze
47
+ # column <[=] :value
48
+ LTEQ_NAMED_RE = /\A([\w.]+)\s+(<=?)\s+:(\w+)\z/.freeze
49
+ # column >= :value1 AND column <[=] :value2
50
+ RANGE_NAMED_RE = /\A([\w.]+)\s+>=\s+:(\w+)\s+AND\s+\1\s+(<=?)\s+:(\w+)\z/i.freeze
51
+
52
+ minimum_target_ruby_version 2.6
53
+ minimum_target_rails_version 6.0
54
+
55
+ def_node_matcher :where_range_call?, <<~PATTERN
56
+ {
57
+ (call _ {:where :not} (array $str_type? $_ +))
58
+ (call _ {:where :not} $str_type? $_ +)
59
+ }
60
+ PATTERN
61
+
62
+ def on_send(node)
63
+ return if node.method?(:not) && !where_not?(node)
64
+
65
+ where_range_call?(node) do |template_node, values_node|
66
+ column, value = extract_column_and_value(template_node, values_node)
67
+
68
+ return unless column
69
+
70
+ range = offense_range(node)
71
+ good_method = build_good_method(node.method_name, column, value)
72
+ message = format(MSG, good_method: good_method)
73
+
74
+ add_offense(range, message: message) do |corrector|
75
+ corrector.replace(range, good_method)
76
+ end
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def where_not?(node)
83
+ receiver = node.receiver
84
+ receiver&.send_type? && receiver&.method?(:where)
85
+ end
86
+
87
+ # rubocop:disable Metrics
88
+ def extract_column_and_value(template_node, values_node)
89
+ value =
90
+ case template_node.value
91
+ when GTEQ_ANONYMOUS_RE
92
+ "#{values_node[0].source}.."
93
+ when LTEQ_ANONYMOUS_RE
94
+ range_operator = range_operator(Regexp.last_match(2))
95
+ "#{range_operator}#{values_node[0].source}" if target_ruby_version >= 2.7
96
+ when RANGE_ANONYMOUS_RE
97
+ if values_node.size >= 2
98
+ range_operator = range_operator(Regexp.last_match(2))
99
+ "#{values_node[0].source}#{range_operator}#{values_node[1].source}"
100
+ end
101
+ when GTEQ_NAMED_RE
102
+ value_node = values_node[0]
103
+
104
+ if value_node.hash_type?
105
+ pair = find_pair(value_node, Regexp.last_match(2))
106
+ "#{pair.value.source}.." if pair
107
+ end
108
+ when LTEQ_NAMED_RE
109
+ value_node = values_node[0]
110
+
111
+ if value_node.hash_type?
112
+ pair = find_pair(value_node, Regexp.last_match(2))
113
+ if pair && target_ruby_version >= 2.7
114
+ range_operator = range_operator(Regexp.last_match(2))
115
+ "#{range_operator}#{pair.value.source}"
116
+ end
117
+ end
118
+ when RANGE_NAMED_RE
119
+ value_node = values_node[0]
120
+
121
+ if value_node.hash_type?
122
+ range_operator = range_operator(Regexp.last_match(3))
123
+ pair1 = find_pair(value_node, Regexp.last_match(2))
124
+ pair2 = find_pair(value_node, Regexp.last_match(4))
125
+ "#{pair1.value.source}#{range_operator}#{pair2.value.source}" if pair1 && pair2
126
+ end
127
+ end
128
+
129
+ [Regexp.last_match(1), value] if value
130
+ end
131
+ # rubocop:enable Metrics
132
+
133
+ def range_operator(comparison_operator)
134
+ comparison_operator == '<' ? '...' : '..'
135
+ end
136
+
137
+ def find_pair(hash_node, value)
138
+ hash_node.pairs.find { |pair| pair.key.value.to_sym == value.to_sym }
139
+ end
140
+
141
+ def offense_range(node)
142
+ range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
143
+ end
144
+
145
+ def build_good_method(method_name, column, value)
146
+ if column.include?('.')
147
+ table, column = column.split('.')
148
+
149
+ "#{method_name}(#{table}: { #{column}: #{value} })"
150
+ else
151
+ "#{method_name}(#{column}: #{value})"
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -138,3 +138,4 @@ require_relative 'rails/where_exists'
138
138
  require_relative 'rails/where_missing'
139
139
  require_relative 'rails/where_not'
140
140
  require_relative 'rails/where_not_with_multiple_conditions'
141
+ require_relative 'rails/where_range'
@@ -30,6 +30,7 @@ module RuboCop
30
30
 
31
31
  def build!(ast)
32
32
  raise "Unexpected type: #{ast.type}" unless ast.block_type?
33
+ return unless ast.body
33
34
 
34
35
  each_table(ast) do |table_def|
35
36
  next unless table_def.method?(:create_table)
@@ -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.0'
7
+ STRING = '2.25.0'
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.0
4
+ version: 2.25.0
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-03 00:00:00.000000000 Z
13
+ date: 2024-05-17 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -232,6 +232,7 @@ files:
232
232
  - lib/rubocop/cop/rails/where_missing.rb
233
233
  - lib/rubocop/cop/rails/where_not.rb
234
234
  - lib/rubocop/cop/rails/where_not_with_multiple_conditions.rb
235
+ - lib/rubocop/cop/rails/where_range.rb
235
236
  - lib/rubocop/cop/rails_cops.rb
236
237
  - lib/rubocop/rails.rb
237
238
  - lib/rubocop/rails/inject.rb
@@ -245,7 +246,7 @@ metadata:
245
246
  homepage_uri: https://docs.rubocop.org/rubocop-rails/
246
247
  changelog_uri: https://github.com/rubocop/rubocop-rails/blob/master/CHANGELOG.md
247
248
  source_code_uri: https://github.com/rubocop/rubocop-rails/
248
- documentation_uri: https://docs.rubocop.org/rubocop-rails/2.24/
249
+ documentation_uri: https://docs.rubocop.org/rubocop-rails/2.25/
249
250
  bug_tracker_uri: https://github.com/rubocop/rubocop-rails/issues
250
251
  rubygems_mfa_required: 'true'
251
252
  post_install_message:
@@ -263,7 +264,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
263
264
  - !ruby/object:Gem::Version
264
265
  version: '0'
265
266
  requirements: []
266
- rubygems_version: 3.3.26
267
+ rubygems_version: 3.5.3
267
268
  signing_key:
268
269
  specification_version: 4
269
270
  summary: Automatic Rails code style checking tool.