-
-
Notifications
You must be signed in to change notification settings - Fork 263
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
# frozen_string_literal: true | ||
|
||
module RuboCop | ||
module Cop | ||
module Rails | ||
# This cop identifies places where manually constructed SQL | ||
# in `where` can be replaced with `where.not(...)`. | ||
# | ||
# @example | ||
# # bad | ||
# User.where('name != ?', 'Gabe') | ||
# User.where('name != :name', name: 'Gabe') | ||
# User.where('name IS NOT NULL') | ||
# User.where('name NOT IN (?)', ['john', 'jane']) | ||
# User.where('name NOT IN (:names)', names: ['john', 'jane']) | ||
# | ||
# # good | ||
# User.where.not(name: 'Gabe') | ||
# User.where.not(name: nil) | ||
# User.where.not(name: ['john', 'jane']) | ||
# | ||
class WhereNot < Cop | ||
include RangeHelp | ||
|
||
MSG = 'Use `%<good_method>s` instead of manually constructing negated SQL in `where`.' | ||
|
||
def_node_matcher :where_method_call?, <<~PATTERN | ||
{ | ||
(send _ :where (array $str_type? $_ ?)) | ||
(send _ :where $str_type? $_ ?) | ||
} | ||
PATTERN | ||
|
||
def on_send(node) | ||
where_method_call?(node) do |template_node, value_node| | ||
value_node = value_node.first | ||
|
||
range = offense_range(node) | ||
|
||
column_and_value = extract_column_and_value(template_node, value_node) | ||
return unless column_and_value | ||
|
||
good_method = build_good_method(*column_and_value) | ||
message = format(MSG, good_method: good_method) | ||
|
||
add_offense(node, location: range, message: message) | ||
end | ||
end | ||
|
||
def autocorrect(node) | ||
where_method_call?(node) do |template_node, value_node| | ||
value_node = value_node.first | ||
|
||
lambda do |corrector| | ||
range = offense_range(node) | ||
|
||
column, value = *extract_column_and_value(template_node, value_node) | ||
replacement = build_good_method(column, value) | ||
|
||
corrector.replace(range, replacement) | ||
end | ||
end | ||
end | ||
|
||
NOT_EQ_ANONYMOUS_RE = /\A([\w.]+)\s+!=\s+\?\z/.freeze # column != ? | ||
NOT_IN_ANONYMOUS_RE = /\A([\w.]+)\s+NOT\s+IN\s+\(\?\)\z/i.freeze # column NOT IN (?) | ||
NOT_EQ_NAMED_RE = /\A([\w.]+)\s+!=\s+:(\w+)\z/.freeze # column != :column | ||
NOT_IN_NAMED_RE = /\A([\w.]+)\s+NOT\s+IN\s+\(:(\w+)\)\z/i.freeze # column NOT IN (:column) | ||
IS_NOT_NULL_RE = /\A([\w.]+)\s+IS\s+NOT\s+NULL\z/i.freeze # column IS NOT NULL | ||
|
||
private | ||
|
||
def offense_range(node) | ||
range_between(node.loc.selector.begin_pos, node.loc.expression.end_pos) | ||
end | ||
|
||
def extract_column_and_value(template_node, value_node) | ||
value = | ||
case template_node.value | ||
when NOT_EQ_ANONYMOUS_RE, NOT_IN_ANONYMOUS_RE | ||
value_node.source | ||
when NOT_EQ_NAMED_RE, NOT_IN_NAMED_RE | ||
pair = value_node.pairs.find { |p| p.key.value.to_sym == Regexp.last_match(2).to_sym } | ||
pair.value.source | ||
when IS_NOT_NULL_RE | ||
'nil' | ||
else | ||
return | ||
end | ||
|
||
[Regexp.last_match(1), value] | ||
end | ||
|
||
def build_good_method(column, value) | ||
if column.include?('.') | ||
"where.not('#{column}' => #{value})" | ||
else | ||
"where.not(#{column}: #{value})" | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe RuboCop::Cop::Rails::WhereNot do | ||
subject(:cop) { described_class.new } | ||
|
||
it 'registers an offense and corrects when using `!=` and anonymous placeholder' do | ||
expect_offense(<<~RUBY) | ||
User.where('name != ?', 'Gabe') | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where.not(name: 'Gabe')` instead of manually constructing negated SQL in `where`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where.not(name: 'Gabe') | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `!=` and named placeholder' do | ||
expect_offense(<<~RUBY) | ||
User.where('name != :name', name: 'Gabe') | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where.not(name: 'Gabe')` instead of manually constructing negated SQL in `where`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where.not(name: 'Gabe') | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `IS NOT NULL`' do | ||
expect_offense(<<~RUBY) | ||
User.where('name IS NOT NULL') | ||
^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where.not(name: nil)` instead of manually constructing negated SQL in `where`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where.not(name: nil) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `NOT IN` and anonymous placeholder' do | ||
expect_offense(<<~RUBY) | ||
User.where("name NOT IN (?)", ['john', 'jane']) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where.not(name: ['john', 'jane'])` instead of manually constructing negated SQL in `where`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where.not(name: ['john', 'jane']) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `NOT IN` and named placeholder' do | ||
expect_offense(<<~RUBY) | ||
User.where("name NOT IN (:names)", names: ['john', 'jane']) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where.not(name: ['john', 'jane'])` instead of manually constructing negated SQL in `where`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where.not(name: ['john', 'jane']) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using namespaced columns' do | ||
expect_offense(<<~RUBY) | ||
Course.where('enrollments.student_id != ?', student.id) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where.not('enrollments.student_id' => student.id)` instead of manually constructing negated SQL in `where`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
Course.where.not('enrollments.student_id' => student.id) | ||
RUBY | ||
end | ||
|
||
context 'with array arguments' do | ||
it 'registers an offense and corrects when using `!=` and anonymous placeholder' do | ||
expect_offense(<<~RUBY) | ||
User.where(['name != ?', 'Gabe']) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where.not(name: 'Gabe')` instead of manually constructing negated SQL in `where`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where.not(name: 'Gabe') | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `!=` and named placeholder' do | ||
expect_offense(<<~RUBY) | ||
User.where(['name != :name', { name: 'Gabe' }]) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where.not(name: 'Gabe')` instead of manually constructing negated SQL in `where`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where.not(name: 'Gabe') | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `IS NOT NULL`' do | ||
expect_offense(<<~RUBY) | ||
User.where(['name IS NOT NULL']) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where.not(name: nil)` instead of manually constructing negated SQL in `where`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where.not(name: nil) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `NOT IN` and anonymous placeholder' do | ||
expect_offense(<<~RUBY) | ||
User.where(["name NOT IN (?)", ['john', 'jane']]) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where.not(name: ['john', 'jane'])` instead of manually constructing negated SQL in `where`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where.not(name: ['john', 'jane']) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `NOT IN` and named placeholder' do | ||
expect_offense(<<~RUBY) | ||
User.where(["name NOT IN (:names)", names: ['john', 'jane']]) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where.not(name: ['john', 'jane'])` instead of manually constructing negated SQL in `where`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where.not(name: ['john', 'jane']) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using namespaced columns' do | ||
expect_offense(<<~RUBY) | ||
Course.where(['enrollments.student_id != ?', student.id]) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where.not('enrollments.student_id' => student.id)` instead of manually constructing negated SQL in `where`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
Course.where.not('enrollments.student_id' => student.id) | ||
RUBY | ||
end | ||
end | ||
|
||
it 'does not register an offense when using `where.not`' do | ||
expect_no_offenses(<<~RUBY) | ||
User.where.not(name: nil) | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense when not using template string' do | ||
expect_no_offenses(<<~RUBY) | ||
User.where(name: 'john') | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense when template string does not contain negation' do | ||
expect_no_offenses(<<~RUBY) | ||
User.where('name = ?', 'john') | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense when template string contains negation and additional boolean logic' do | ||
expect_no_offenses(<<~RUBY) | ||
User.where('name != ? AND age != ?', 'john', 19) | ||
RUBY | ||
end | ||
end |