-
-
Notifications
You must be signed in to change notification settings - Fork 266
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #362 from eugeneius/where_equals
Add new Rails/WhereEquals cop
- Loading branch information
Showing
7 changed files
with
318 additions
and
2 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,105 @@ | ||
# 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(attribute: value)`. | ||
# | ||
# @example | ||
# # bad | ||
# User.where('name = ?', 'Gabe') | ||
# User.where('name = :name', name: 'Gabe') | ||
# User.where('name IS NULL') | ||
# User.where('name IN (?)', ['john', 'jane']) | ||
# User.where('name IN (:names)', names: ['john', 'jane']) | ||
# | ||
# # good | ||
# User.where(name: 'Gabe') | ||
# User.where(name: nil) | ||
# User.where(name: ['john', 'jane']) | ||
class WhereEquals < Cop | ||
include RangeHelp | ||
|
||
MSG = 'Use `%<good_method>s` instead of manually constructing SQL.' | ||
|
||
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 | ||
|
||
EQ_ANONYMOUS_RE = /\A([\w.]+)\s+=\s+\?\z/.freeze # column = ? | ||
IN_ANONYMOUS_RE = /\A([\w.]+)\s+IN\s+\(\?\)\z/i.freeze # column IN (?) | ||
EQ_NAMED_RE = /\A([\w.]+)\s+=\s+:(\w+)\z/.freeze # column = :column | ||
IN_NAMED_RE = /\A([\w.]+)\s+IN\s+\(:(\w+)\)\z/i.freeze # column IN (:column) | ||
IS_NULL_RE = /\A([\w.]+)\s+IS\s+NULL\z/i.freeze # column IS 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 EQ_ANONYMOUS_RE, IN_ANONYMOUS_RE | ||
value_node.source | ||
when EQ_NAMED_RE, IN_NAMED_RE | ||
return unless value_node.hash_type? | ||
|
||
pair = value_node.pairs.find { |p| p.key.value.to_sym == Regexp.last_match(2).to_sym } | ||
pair.value.source | ||
when IS_NULL_RE | ||
'nil' | ||
else | ||
return | ||
end | ||
|
||
[Regexp.last_match(1), value] | ||
end | ||
|
||
def build_good_method(column, value) | ||
if column.include?('.') | ||
"where('#{column}' => #{value})" | ||
else | ||
"where(#{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::WhereEquals 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(name: 'Gabe')` instead of manually constructing SQL. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where(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(name: 'Gabe')` instead of manually constructing SQL. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where(name: 'Gabe') | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `IS NULL`' do | ||
expect_offense(<<~RUBY) | ||
User.where('name IS NULL') | ||
^^^^^^^^^^^^^^^^^^^^^ Use `where(name: nil)` instead of manually constructing SQL. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where(name: nil) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `IN` and anonymous placeholder' do | ||
expect_offense(<<~RUBY) | ||
User.where("name IN (?)", ['john', 'jane']) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where(name: ['john', 'jane'])` instead of manually constructing SQL. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where(name: ['john', 'jane']) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `IN` and named placeholder' do | ||
expect_offense(<<~RUBY) | ||
User.where("name IN (:names)", names: ['john', 'jane']) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where(name: ['john', 'jane'])` instead of manually constructing SQL. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where(name: ['john', 'jane']) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `=` and namespaced columns' do | ||
expect_offense(<<~RUBY) | ||
Course.where('enrollments.student_id = ?', student.id) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where('enrollments.student_id' => student.id)` instead of manually constructing SQL. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
Course.where('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(name: 'Gabe')` instead of manually constructing SQL. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where(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(name: 'Gabe')` instead of manually constructing SQL. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where(name: 'Gabe') | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `IS NULL`' do | ||
expect_offense(<<~RUBY) | ||
User.where(['name IS NULL']) | ||
^^^^^^^^^^^^^^^^^^^^^^^ Use `where(name: nil)` instead of manually constructing SQL. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where(name: nil) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `IN` and anonymous placeholder' do | ||
expect_offense(<<~RUBY) | ||
User.where(["name IN (?)", ['john', 'jane']]) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where(name: ['john', 'jane'])` instead of manually constructing SQL. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where(name: ['john', 'jane']) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `IN` and named placeholder' do | ||
expect_offense(<<~RUBY) | ||
User.where(["name IN (:names)", names: ['john', 'jane']]) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where(name: ['john', 'jane'])` instead of manually constructing SQL. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
User.where(name: ['john', 'jane']) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense and corrects when using `=` and namespaced columns' do | ||
expect_offense(<<~RUBY) | ||
Course.where(['enrollments.student_id = ?', student.id]) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `where('enrollments.student_id' => student.id)` instead of manually constructing SQL. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
Course.where('enrollments.student_id' => student.id) | ||
RUBY | ||
end | ||
end | ||
|
||
it 'does not register an offense when using `where`' do | ||
expect_no_offenses(<<~RUBY) | ||
User.where(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 contains negation' do | ||
expect_no_offenses(<<~RUBY) | ||
User.where('name != ?', 'john') | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense when template string contains `=` and additional boolean logic' do | ||
expect_no_offenses(<<~RUBY) | ||
User.where('name = ? AND age = ?', 'john', 19) | ||
RUBY | ||
end | ||
end |