Skip to content

Commit 64626d2

Browse files
authored
Merge pull request #1501 from lovro-bikic/rails-order-arguments
Add new cop `Rails/OrderArguments`
2 parents cf75383 + 249c743 commit 64626d2

5 files changed

Lines changed: 219 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#1501](https://github.com/rubocop/rubocop-rails/pull/1501): Add new cop `Rails/OrderArguments`. ([@lovro-bikic][])

config/default.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,13 @@ Rails/NotNullColumn:
748748
Include:
749749
- db/**/*.rb
750750

751+
Rails/OrderArguments:
752+
Description: 'Prefer symbol arguments over strings in `order` method.'
753+
StyleGuide: 'https://rails.rubystyle.guide/#order-arguments'
754+
Enabled: pending
755+
VersionAdded: '<<next>>'
756+
Safe: false
757+
751758
Rails/OrderById:
752759
Description: >-
753760
Do not use the `id` column for ordering.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# Prefer symbol arguments over strings in `order` method.
7+
#
8+
# @safety
9+
# Cop is unsafe because the receiver might not be an Active Record query.
10+
#
11+
# @example
12+
# # bad
13+
# User.order('name')
14+
# User.order('name DESC')
15+
#
16+
# # good
17+
# User.order(:name)
18+
# User.order(name: :desc)
19+
#
20+
class OrderArguments < Base
21+
extend AutoCorrector
22+
23+
MSG = 'Prefer `%<prefer>s` instead.'
24+
25+
RESTRICT_ON_SEND = %i[order].freeze
26+
27+
def_node_matcher :string_order, <<~PATTERN
28+
(call _ :order (str $_value)+)
29+
PATTERN
30+
31+
ORDER_EXPRESSION_REGEX = /\A(\w+) ?(asc|desc)?\z/i.freeze
32+
33+
def on_send(node)
34+
return unless (current_expressions = string_order(node))
35+
return unless (preferred_expressions = replacement(current_expressions))
36+
37+
offense_range = find_offense_range(node)
38+
add_offense(offense_range, message: format(MSG, prefer: preferred_expressions)) do |corrector|
39+
corrector.replace(offense_range, preferred_expressions)
40+
end
41+
end
42+
alias on_csend on_send
43+
44+
private
45+
46+
def find_offense_range(node)
47+
node.first_argument.source_range.join(node.last_argument.source_range)
48+
end
49+
50+
def replacement(order_expressions)
51+
order_arguments = order_expressions.flat_map { |expr| expr.split(',') }
52+
order_arguments.map! { |arg| extract_column_and_direction(arg.strip) }
53+
54+
return if order_arguments.any?(&:nil?)
55+
56+
convert_to_preferred_arguments(order_arguments).join(', ')
57+
end
58+
59+
def convert_to_preferred_arguments(order_expressions)
60+
use_hash = false
61+
order_expressions.map do |column, direction|
62+
if direction == :asc && !use_hash
63+
":#{column}"
64+
else
65+
use_hash = true
66+
"#{column}: :#{direction}"
67+
end
68+
end
69+
end
70+
71+
def extract_column_and_direction(order_expression)
72+
return unless (column, direction = ORDER_EXPRESSION_REGEX.match(order_expression)&.captures)
73+
74+
[column.downcase, direction&.downcase&.to_sym || :asc]
75+
end
76+
end
77+
end
78+
end
79+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
require_relative 'rails/multiple_route_paths'
8383
require_relative 'rails/negate_include'
8484
require_relative 'rails/not_null_column'
85+
require_relative 'rails/order_arguments'
8586
require_relative 'rails/order_by_id'
8687
require_relative 'rails/output'
8788
require_relative 'rails/output_safety'
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::OrderArguments, :config do
4+
it 'registers an offense for `order` with a string argument' do
5+
expect_offense(<<~RUBY)
6+
User.order('first_name')
7+
^^^^^^^^^^^^ Prefer `:first_name` instead.
8+
RUBY
9+
10+
expect_correction(<<~RUBY)
11+
User.order(:first_name)
12+
RUBY
13+
end
14+
15+
it 'registers an offense for `order` with multiple string arguments' do
16+
expect_offense(<<~RUBY)
17+
User.order('first_name', 'last_name')
18+
^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `:first_name, :last_name` instead.
19+
RUBY
20+
21+
expect_correction(<<~RUBY)
22+
User.order(:first_name, :last_name)
23+
RUBY
24+
end
25+
26+
it 'registers an offense for `order` with a string argument with DESC direction' do
27+
expect_offense(<<~RUBY)
28+
User.order('name DESC')
29+
^^^^^^^^^^^ Prefer `name: :desc` instead.
30+
RUBY
31+
32+
expect_correction(<<~RUBY)
33+
User.order(name: :desc)
34+
RUBY
35+
end
36+
37+
it 'registers an offense for `order` with a string argument with ASC order' do
38+
expect_offense(<<~RUBY)
39+
User.order('name ASC')
40+
^^^^^^^^^^ Prefer `:name` instead.
41+
RUBY
42+
43+
expect_correction(<<~RUBY)
44+
User.order(:name)
45+
RUBY
46+
end
47+
48+
it 'registers an offense for `order` with DESC column followed by implicit ASC column' do
49+
expect_offense(<<~RUBY)
50+
User.order('first_name DESC, last_name')
51+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `first_name: :desc, last_name: :asc` instead.
52+
RUBY
53+
54+
expect_correction(<<~RUBY)
55+
User.order(first_name: :desc, last_name: :asc)
56+
RUBY
57+
end
58+
59+
it 'registers an offense for `order` with explicit ASC column followed by implicit ASC column' do
60+
expect_offense(<<~RUBY)
61+
User.order('first_name ASC, last_name')
62+
^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `:first_name, :last_name` instead.
63+
RUBY
64+
65+
expect_correction(<<~RUBY)
66+
User.order(:first_name, :last_name)
67+
RUBY
68+
end
69+
70+
it 'registers an offense for safe navigation `order` with a string argument' do
71+
expect_offense(<<~RUBY)
72+
User&.order('first_name')
73+
^^^^^^^^^^^^ Prefer `:first_name` instead.
74+
RUBY
75+
76+
expect_correction(<<~RUBY)
77+
User&.order(:first_name)
78+
RUBY
79+
end
80+
81+
it 'registers an offense for `order` with multiple string arguments, one of which has multiple sorts' do
82+
expect_offense(<<~RUBY)
83+
User.order('first_name, middle_name', 'last_name')
84+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `:first_name, :middle_name, :last_name` instead.
85+
RUBY
86+
87+
expect_correction(<<~RUBY)
88+
User.order(:first_name, :middle_name, :last_name)
89+
RUBY
90+
end
91+
92+
it 'registers an offense for `order` with lowercase direction' do
93+
expect_offense(<<~RUBY)
94+
User.order('name desc')
95+
^^^^^^^^^^^ Prefer `name: :desc` instead.
96+
RUBY
97+
98+
expect_correction(<<~RUBY)
99+
User.order(name: :desc)
100+
RUBY
101+
end
102+
103+
it 'registers an offense for `order` with uppercase column' do
104+
expect_offense(<<~RUBY)
105+
User.order('NAME DESC')
106+
^^^^^^^^^^^ Prefer `name: :desc` instead.
107+
RUBY
108+
109+
expect_correction(<<~RUBY)
110+
User.order(name: :desc)
111+
RUBY
112+
end
113+
114+
it 'does not register an offense for `order` with symbol argument' do
115+
expect_no_offenses(<<~RUBY)
116+
User.order(:first_name)
117+
RUBY
118+
end
119+
120+
it 'does not register an offense for `order` with a string and a symbol argument' do
121+
expect_no_offenses(<<~RUBY)
122+
User.order(:first_name, 'last_name')
123+
RUBY
124+
end
125+
126+
it 'does not register an offense for `order` with string argument expression' do
127+
expect_no_offenses(<<~RUBY)
128+
User.order('LEFT(first_name, 1)')
129+
RUBY
130+
end
131+
end

0 commit comments

Comments
 (0)