Skip to content

Commit 1063b3e

Browse files
authored
Merge pull request rubocop#639 from niklas-hasselmeyer/where-not-with-multiple-arguments
[Fix rubocop#565] Add cop Rails/WhereNotWithMultipleConditions
2 parents 80c0902 + 7d03a57 commit 1063b3e

File tree

5 files changed

+109
-0
lines changed

5 files changed

+109
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#565](https://github.com/rubocop/rubocop-rails/issues/565): Add cop Rails/WhereNotWithMultipleConditions. ([@niklas-hasselmeyer][])

config/default.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,11 @@ Rails/WhereNot:
10901090
Enabled: 'pending'
10911091
VersionAdded: '2.8'
10921092

1093+
Rails/WhereNotWithMultipleConditions:
1094+
Description: 'Do not use `where.not(...)` with multiple conditions.'
1095+
Enabled: 'pending'
1096+
VersionAdded: '<<next>>'
1097+
10931098
# Accept `redirect_to(...) and return` and similar cases.
10941099
Style/AndOr:
10951100
EnforcedStyle: conditionals
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# Identifies calls to `where.not` with multiple hash arguments.
7+
#
8+
# The behavior of `where.not` changed in Rails 6.1. Prior to the change,
9+
# `.where.not(trashed: true, role: 'admin')` evaluated to
10+
# `WHERE trashed != TRUE AND role != 'admin'`.
11+
# From Rails 6.1 onwards, this executes the query
12+
# `WHERE NOT (trashed == TRUE AND roles == 'admin')`.
13+
#
14+
# @example
15+
# # bad
16+
# User.where.not(trashed: true, role: 'admin')
17+
# User.where.not(trashed: true, role: ['moderator', 'admin'])
18+
# User.joins(:posts).where.not(posts: { trashed: true, title: 'Rails' })
19+
#
20+
# # good
21+
# User.where.not(trashed: true)
22+
# User.where.not(role: ['moderator', 'admin'])
23+
# User.where.not(trashed: true).where.not(role: ['moderator', 'admin'])
24+
# User.where.not('trashed = ? OR role = ?', true, 'admin')
25+
class WhereNotWithMultipleConditions < Base
26+
MSG = 'Use a SQL statement instead of `where.not` with multiple conditions.'
27+
RESTRICT_ON_SEND = %i[not].freeze
28+
29+
def_node_matcher :where_not_call?, <<~PATTERN
30+
(send (send _ :where) :not $...)
31+
PATTERN
32+
33+
def on_send(node)
34+
where_not_call?(node) do |args|
35+
next unless args[0].hash_type?
36+
next unless multiple_arguments_hash? args[0]
37+
38+
range = node.receiver.loc.selector.with(end_pos: node.loc.expression.end_pos)
39+
40+
add_offense(range)
41+
end
42+
end
43+
44+
private
45+
46+
def multiple_arguments_hash?(hash)
47+
return true if hash.pairs.size >= 2
48+
return false unless hash.values[0].hash_type?
49+
50+
multiple_arguments_hash?(hash.values[0])
51+
end
52+
end
53+
end
54+
end
55+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,4 @@
127127
require_relative 'rails/where_exists'
128128
require_relative 'rails/where_missing'
129129
require_relative 'rails/where_not'
130+
require_relative 'rails/where_not_with_multiple_conditions'
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::WhereNotWithMultipleConditions, :config do
4+
it 'does not register an offense for where.not with one condition' do
5+
expect_no_offenses(<<~RUBY)
6+
User.where.not(trashed: true)
7+
RUBY
8+
end
9+
10+
it 'registers an offense for where.not with multiple conditions' do
11+
expect_offense(<<~RUBY)
12+
User.where.not(trashed: true, role: 'admin')
13+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use a SQL statement instead of `where.not` with multiple conditions.
14+
RUBY
15+
end
16+
17+
it 'registers an offense for where.not with nested multiple conditions' do
18+
expect_offense(<<~RUBY)
19+
User.joins(:posts).where.not({ posts: { trashed: true, title: 'Rails' } })
20+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use a SQL statement instead of `where.not` with multiple conditions.
21+
RUBY
22+
end
23+
24+
it 'does not register an offense for where with multiple conditions' do
25+
expect_no_offenses(<<~RUBY)
26+
User.where(trashed: false, role: 'admin')
27+
RUBY
28+
end
29+
30+
it 'does not register an offense for where.not with a SQL string' do
31+
expect_no_offenses(<<~RUBY)
32+
User.where.not('trashed = ? OR role = ?', true, 'admin')
33+
RUBY
34+
end
35+
36+
it 'does not register an offense for where.not with one array condition' do
37+
expect_no_offenses(<<~RUBY)
38+
User.where.not(role: ['moderator', 'admin'])
39+
RUBY
40+
end
41+
42+
it 'does not register an offense for chained where.not' do
43+
expect_no_offenses(<<~RUBY)
44+
User.where.not(trashed: true).where.not(role: 'admin')
45+
RUBY
46+
end
47+
end

0 commit comments

Comments
 (0)