rubocop-rails 2.24.1 → 2.25.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee7d9bdd0fa7838d8bfecece133ccd92cc308603bcdf120c14224c5f2e312344
4
- data.tar.gz: 7d740b495ffa368d26a73808af9adff990bb9c1d317d905f877b4f3e2025487c
3
+ metadata.gz: b55703b258e4df9bae3a9a8c1b77a4fa143af6af3ed9b1b22118fd839d1ce06c
4
+ data.tar.gz: 24568d7d8d22d69469ae9a4aaf426ea45bf848989604a8744ab6493bc0b222a3
5
5
  SHA512:
6
- metadata.gz: 98172f41e7b1aab55931f351757425815850809edd6fc4f5bc55c59271d9f12459ac51c2002e409534b623f321ab27798c32aaccb2d15d60c7bf309a9c044d58
7
- data.tar.gz: c78e9a0d9e53e5203a2dd1846a79c3e9c7a16af69e98d94e6b7ae6358ae6a29b71c2ac3f09ddb2a9be06dc64097a736c3379730f50b9309c0e2ae084267aee70
6
+ metadata.gz: 47d5668b744967fc740b47552e9d05068fc5ce5c209a4a994a429262a0628b13e0f1e48d6a39018430fb7e874f88e4badbd9427219b9a77220e522d6706b1c3c
7
+ data.tar.gz: 242030d6fe9063d51f18e98869e2a048a4b7b847f0f6a25937fc3d9781dbb7749de8b015b3fa0b4933511b83487ad060fbdbf9adb7942a20bfc035d5fc9e2f04
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # RuboCop Rails
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/rubocop-rails.svg)](https://badge.fury.io/rb/rubocop-rails)
4
- [![CircleCI](https://circleci.com/gh/rubocop/rubocop-rails.svg?style=svg)](https://circleci.com/gh/rubocop/rubocop-rails)
4
+ [![CI](https://github.com/rubocop/rubocop-rails/actions/workflows/test.yml/badge.svg)](https://github.com/rubocop/rubocop-rails/actions/workflows/test.yml)
5
5
 
6
6
  A [RuboCop](https://github.com/rubocop/rubocop) extension focused on enforcing Rails best practices and coding conventions.
7
7
 
data/config/default.yml CHANGED
@@ -693,7 +693,7 @@ Rails/NegateInclude:
693
693
  VersionChanged: '2.9'
694
694
 
695
695
  Rails/NotNullColumn:
696
- 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.'
697
697
  Enabled: true
698
698
  VersionAdded: '0.43'
699
699
  VersionChanged: '2.20'
@@ -1018,8 +1018,9 @@ Rails/SkipsModelValidations:
1018
1018
  See reference for more information.
1019
1019
  Reference: 'https://guides.rubyonrails.org/active_record_validations.html#skipping-validations'
1020
1020
  Enabled: true
1021
+ Safe: false
1021
1022
  VersionAdded: '0.47'
1022
- VersionChanged: '2.7'
1023
+ VersionChanged: '2.25'
1023
1024
  ForbiddenMethods:
1024
1025
  - decrement!
1025
1026
  - decrement_counter
@@ -1163,8 +1164,9 @@ Rails/UnknownEnv:
1163
1164
 
1164
1165
  Rails/UnusedIgnoredColumns:
1165
1166
  Description: 'Remove a column that does not exist from `ignored_columns`.'
1166
- Enabled: pending
1167
+ Enabled: false
1167
1168
  VersionAdded: '2.11'
1169
+ VersionChanged: '2.25'
1168
1170
  Include:
1169
1171
  - app/models/**/*.rb
1170
1172
 
@@ -1221,6 +1223,13 @@ Rails/WhereNotWithMultipleConditions:
1221
1223
  VersionAdded: '2.17'
1222
1224
  VersionChanged: '2.18'
1223
1225
 
1226
+ Rails/WhereRange:
1227
+ Description: 'Use ranges in `where` instead of manually constructing SQL.'
1228
+ StyleGuide: 'https://rails.rubystyle.guide/#where-ranges'
1229
+ Enabled: pending
1230
+ SafeAutoCorrect: false
1231
+ VersionAdded: '2.25'
1232
+
1224
1233
  # Accept `redirect_to(...) and return` and similar cases.
1225
1234
  Style/AndOr:
1226
1235
  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
@@ -113,8 +113,10 @@ module RuboCop
113
113
  MYSQL_COMBINABLE_ALTER_METHODS = %i[rename_column add_index remove_index].freeze
114
114
 
115
115
  POSTGRESQL_COMBINABLE_TRANSFORMATIONS = %i[change_default].freeze
116
+ POSTGRESQL_COMBINABLE_TRANSFORMATIONS_SINCE_6_1 = %i[change_null].freeze
116
117
 
117
118
  POSTGRESQL_COMBINABLE_ALTER_METHODS = %i[change_column_default].freeze
119
+ POSTGRESQL_COMBINABLE_ALTER_METHODS_SINCE_6_1 = %i[change_column_null].freeze
118
120
 
119
121
  def on_def(node)
120
122
  return unless support_bulk_alter?
@@ -196,7 +198,9 @@ module RuboCop
196
198
  when MYSQL
197
199
  COMBINABLE_ALTER_METHODS + MYSQL_COMBINABLE_ALTER_METHODS
198
200
  when POSTGRESQL
199
- COMBINABLE_ALTER_METHODS + POSTGRESQL_COMBINABLE_ALTER_METHODS
201
+ result = COMBINABLE_ALTER_METHODS + POSTGRESQL_COMBINABLE_ALTER_METHODS
202
+ result += POSTGRESQL_COMBINABLE_ALTER_METHODS_SINCE_6_1 if target_rails_version >= 6.1
203
+ result
200
204
  end
201
205
  end
202
206
 
@@ -205,7 +209,9 @@ module RuboCop
205
209
  when MYSQL
206
210
  COMBINABLE_TRANSFORMATIONS + MYSQL_COMBINABLE_TRANSFORMATIONS
207
211
  when POSTGRESQL
208
- COMBINABLE_TRANSFORMATIONS + POSTGRESQL_COMBINABLE_TRANSFORMATIONS
212
+ result = COMBINABLE_TRANSFORMATIONS + POSTGRESQL_COMBINABLE_TRANSFORMATIONS
213
+ result += POSTGRESQL_COMBINABLE_TRANSFORMATIONS_SINCE_6_1 if target_rails_version >= 6.1
214
+ result
209
215
  end
210
216
  end
211
217
 
@@ -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,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Rails
6
- # Checks for calls to `link_to` that contain a
6
+ # Checks for calls to `link_to`, `link_to_if`, and `link_to_unless` methods that contain a
7
7
  # `target: '_blank'` but no `rel: 'noopener'`. This can be a security
8
8
  # risk as the loaded page will have control over the previous page
9
9
  # and could change its location for phishing purposes.
@@ -24,7 +24,7 @@ module RuboCop
24
24
  extend AutoCorrector
25
25
 
26
26
  MSG = 'Specify a `:rel` option containing noopener.'
27
- RESTRICT_ON_SEND = %i[link_to].freeze
27
+ RESTRICT_ON_SEND = %i[link_to link_to_if link_to_unless].freeze
28
28
 
29
29
  def_node_matcher :blank_target?, <<~PATTERN
30
30
  (pair {(sym :target) (str "target")} {(str "_blank") (sym :_blank)})
@@ -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,43 @@ 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
+ next unless node.body
140
+
141
+ children = node.body.begin_type? ? node.body.children : [node.body]
142
+ children.each do |child|
143
+ check_add_column_in_change_table(child, table)
144
+ check_add_column_via_shortcut_in_change_table(child, table)
145
+ check_add_reference_in_change_table(child, table)
146
+ end
147
+ end
148
+ end
149
+
70
150
  def check_pairs(pairs)
71
151
  return if pairs.any? { |pair| default_option?(pair) }
72
152
 
@@ -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
@@ -9,6 +9,9 @@ module RuboCop
9
9
  #
10
10
  # Methods may be ignored from this rule by configuring a `AllowedMethods`.
11
11
  #
12
+ # @safety
13
+ # This cop is unsafe if the receiver object is not an Active Record object.
14
+ #
12
15
  # @example
13
16
  # # bad
14
17
  # Article.first.decrement!(:view_count)
@@ -63,7 +66,7 @@ module RuboCop
63
66
  PATTERN
64
67
 
65
68
  def_node_matcher :good_insert?, <<~PATTERN
66
- (send _ {:insert :insert!} _ {
69
+ (call _ {:insert :insert!} _ {
67
70
  !(hash ...)
68
71
  (hash <(pair (sym !{:returning :unique_by}) _) ...>)
69
72
  } ...)
@@ -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
@@ -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
@@ -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)
@@ -0,0 +1,198 @@
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
+ end
144
+
145
+ if lhs
146
+ lhs_source = parentheses_needed?(lhs) ? "(#{lhs.source})" : lhs.source
147
+ end
148
+
149
+ if rhs
150
+ rhs_source = parentheses_needed?(rhs) ? "(#{rhs.source})" : rhs.source
151
+ end
152
+
153
+ [Regexp.last_match(1), "#{lhs_source}#{operator}#{rhs_source}"] if operator
154
+ end
155
+ # rubocop:enable Metrics
156
+
157
+ def range_operator(comparison_operator)
158
+ comparison_operator == '<' ? '...' : '..'
159
+ end
160
+
161
+ def find_pair(hash_node, value)
162
+ hash_node.pairs.find { |pair| pair.key.value.to_sym == value.to_sym }
163
+ end
164
+
165
+ def offense_range(node)
166
+ range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
167
+ end
168
+
169
+ def build_good_method(method_name, column, value)
170
+ if column.include?('.')
171
+ table, column = column.split('.')
172
+
173
+ "#{method_name}(#{table}: { #{column}: #{value} })"
174
+ else
175
+ "#{method_name}(#{column}: #{value})"
176
+ end
177
+ end
178
+
179
+ def parentheses_needed?(node)
180
+ !parentheses_not_needed?(node)
181
+ end
182
+
183
+ def parentheses_not_needed?(node)
184
+ node.variable? ||
185
+ node.literal? ||
186
+ node.reference? ||
187
+ node.const_type? ||
188
+ node.begin_type? ||
189
+ parenthesized_call_node?(node)
190
+ end
191
+
192
+ def parenthesized_call_node?(node)
193
+ node.call_type? && (node.arguments.empty? || node.parenthesized_call?)
194
+ end
195
+ end
196
+ end
197
+ end
198
+ 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'
@@ -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.25.1'
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.25.1
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-06-29 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.11
267
268
  signing_key:
268
269
  specification_version: 4
269
270
  summary: Automatic Rails code style checking tool.