Skip to content

Commit 661d89b

Browse files
committed
Add new Rails/HashLiteralKeysConversion cop
1 parent 7616bde commit 661d89b

File tree

5 files changed

+355
-0
lines changed

5 files changed

+355
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#1332](https://github.com/rubocop/rubocop-rails/issues/1332): Add new `Rails/HashLiteralKeysConversion` cop. ([@fatkodima][])

config/default.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,11 @@ Rails/HasManyOrHasOneDependent:
540540
Include:
541541
- app/models/**/*.rb
542542

543+
Rails/HashLiteralKeysConversion:
544+
Description: 'Convert hash literal keys manually instead of using keys conversion methods.'
545+
Enabled: pending
546+
VersionAdded: '<<next>>'
547+
543548
Rails/HelperInstanceVariable:
544549
Description: 'Do not use instance variables in helpers.'
545550
Enabled: true
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# Detects when keys conversion methods are called on literal hashes, where it is redundant
7+
# or keys can be manually converted to the required type.
8+
#
9+
# @example
10+
# # bad
11+
# { a: 1, b: 2 }.symbolize_keys
12+
#
13+
# # bad
14+
# { a: 1, b: 2 }.stringify_keys
15+
#
16+
# # good
17+
# { 'a' => 1, 'b' => 2 }
18+
#
19+
# # good
20+
# { a: 1, var => 3 }.symbolize_keys
21+
#
22+
class HashLiteralKeysConversion < Base
23+
extend AutoCorrector
24+
25+
REDUNDANT_CONVERSION_MSG = 'Redundant hash keys conversion, all the keys have the required type.'
26+
MSG = 'Convert hash keys explicitly to the required type.'
27+
28+
CONVERSION_METHODS = {
29+
symbolize_keys: :sym,
30+
symbolize_keys!: :sym,
31+
stringify_keys: :str,
32+
stringify_keys!: :str,
33+
deep_symbolize_keys: :sym,
34+
deep_symbolize_keys!: :sym,
35+
deep_stringify_keys: :str,
36+
deep_stringify_keys!: :str
37+
}.freeze
38+
39+
RESTRICT_ON_SEND = CONVERSION_METHODS.keys
40+
41+
def on_send(node)
42+
return unless (receiver = node.receiver)&.hash_type?
43+
return unless convertible_hash?(receiver)
44+
45+
type = CONVERSION_METHODS[node.method_name]
46+
deep = node.method_name.start_with?('deep_')
47+
48+
check(node, receiver, type: type, deep: deep)
49+
end
50+
51+
# rubocop:disable Metrics/AbcSize
52+
def check(node, hash_node, type: :sym, deep: false)
53+
pair_nodes = pair_nodes(hash_node, deep: deep)
54+
55+
type_pairs, other_pairs = pair_nodes.partition { |pair_node| pair_node.key.type == type }
56+
57+
if type_pairs == pair_nodes
58+
add_offense(node.loc.selector, message: REDUNDANT_CONVERSION_MSG) do |corrector|
59+
corrector.remove(node.loc.dot)
60+
corrector.remove(node.loc.selector)
61+
end
62+
else
63+
add_offense(node.loc.selector) do |corrector|
64+
corrector.remove(node.loc.dot)
65+
corrector.remove(node.loc.selector)
66+
autocorrect_hash_keys(other_pairs, type, corrector)
67+
end
68+
end
69+
end
70+
# rubocop:enable Metrics/AbcSize
71+
72+
private
73+
74+
def convertible_hash?(node)
75+
node.pairs.each do |pair|
76+
key, value = *pair
77+
return false if pair.value_omission?
78+
return false unless key.str_type? || key.sym_type?
79+
return false if key.value.match?(/\W/)
80+
return convertible_hash?(value) if value.hash_type?
81+
end
82+
83+
true
84+
end
85+
86+
def pair_nodes(hash_node, deep: false)
87+
if deep
88+
pair_nodes = []
89+
do_pair_nodes(hash_node, pair_nodes)
90+
pair_nodes
91+
else
92+
hash_node.pairs
93+
end
94+
end
95+
96+
def do_pair_nodes(hash_node, pair_nodes)
97+
hash_node.pairs.each do |pair_node|
98+
pair_nodes << pair_node
99+
do_pair_nodes(pair_node.value, pair_nodes) if pair_node.value.hash_type?
100+
end
101+
end
102+
103+
def autocorrect_hash_keys(pair_nodes, type, corrector)
104+
pair_nodes.each do |pair_node|
105+
if type == :sym
106+
corrector.replace(pair_node.key, ":#{pair_node.key.value}")
107+
else
108+
corrector.replace(pair_node.key, "'#{pair_node.key.source}'")
109+
end
110+
111+
corrector.replace(pair_node.loc.operator, '=>')
112+
end
113+
end
114+
end
115+
end
116+
end
117+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
require_relative 'rails/freeze_time'
6060
require_relative 'rails/has_and_belongs_to_many'
6161
require_relative 'rails/has_many_or_has_one_dependent'
62+
require_relative 'rails/hash_literal_keys_conversion'
6263
require_relative 'rails/helper_instance_variable'
6364
require_relative 'rails/http_positional_arguments'
6465
require_relative 'rails/http_status'
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::HashLiteralKeysConversion, :config do
4+
it 'registers an offense and corrects when using `symbolize_keys` with only symbol keys' do
5+
expect_offense(<<~RUBY)
6+
{ a: 1, b: 2 }.symbolize_keys
7+
^^^^^^^^^^^^^^ Redundant hash keys conversion, all the keys have the required type.
8+
RUBY
9+
10+
expect_correction(<<~RUBY)
11+
{ a: 1, b: 2 }
12+
RUBY
13+
end
14+
15+
it 'registers an offense and corrects when using `symbolize_keys` with only symbol and string keys' do
16+
expect_offense(<<~RUBY)
17+
{ a: 1, 'b' => 2 }.symbolize_keys
18+
^^^^^^^^^^^^^^ Convert hash keys explicitly to the required type.
19+
RUBY
20+
21+
expect_correction(<<~RUBY)
22+
{ a: 1, :b => 2 }
23+
RUBY
24+
end
25+
26+
it 'does not register an offense when using `symbolize_keys` with integer keys' do
27+
expect_no_offenses(<<~RUBY)
28+
{ a: 1, 2 => 3 }.symbolize_keys
29+
RUBY
30+
end
31+
32+
it 'does not register an offense when using `symbolize_keys` with non hash literal receiver' do
33+
expect_no_offenses(<<~RUBY)
34+
options.symbolize_keys
35+
RUBY
36+
end
37+
38+
it 'registers an offense and corrects when using `stringify_keys` with only string keys' do
39+
expect_offense(<<~RUBY)
40+
{ 'a' => 1, 'b' => 2 }.stringify_keys
41+
^^^^^^^^^^^^^^ Redundant hash keys conversion, all the keys have the required type.
42+
RUBY
43+
44+
expect_correction(<<~RUBY)
45+
{ 'a' => 1, 'b' => 2 }
46+
RUBY
47+
end
48+
49+
it 'registers an offense and corrects when using `stringify_keys` with only symbol and string keys' do
50+
expect_offense(<<~RUBY)
51+
{ a: 1, 'b' => 2 }.stringify_keys
52+
^^^^^^^^^^^^^^ Convert hash keys explicitly to the required type.
53+
RUBY
54+
55+
expect_correction(<<~RUBY)
56+
{ 'a'=> 1, 'b' => 2 }
57+
RUBY
58+
end
59+
60+
it 'does not register an offense when using `stringify_keys` with integer keys' do
61+
expect_no_offenses(<<~RUBY)
62+
{ 'a' => 1, 2 => 3 }.stringify_keys
63+
RUBY
64+
end
65+
66+
it 'does not register an offense when using `stringify_keys` with non hash literal receiver' do
67+
expect_no_offenses(<<~RUBY)
68+
options.stringify_keys
69+
RUBY
70+
end
71+
72+
it 'registers an offense and corrects when using `deep_symbolize_keys` with symbol keys' do
73+
expect_offense(<<~RUBY)
74+
{
75+
a: 1,
76+
b: {
77+
c: 1
78+
}
79+
}.deep_symbolize_keys
80+
^^^^^^^^^^^^^^^^^^^ Redundant hash keys conversion, all the keys have the required type.
81+
RUBY
82+
83+
expect_correction(<<~RUBY)
84+
{
85+
a: 1,
86+
b: {
87+
c: 1
88+
}
89+
}
90+
RUBY
91+
end
92+
93+
it 'registers an offense and corrects when using `deep_symbolize_keys` with symbol and string keys' do
94+
expect_offense(<<~RUBY)
95+
{
96+
'a' => 1,
97+
b: {
98+
c: 1
99+
}
100+
}.deep_symbolize_keys
101+
^^^^^^^^^^^^^^^^^^^ Convert hash keys explicitly to the required type.
102+
RUBY
103+
104+
expect_correction(<<~RUBY)
105+
{
106+
:a => 1,
107+
b: {
108+
c: 1
109+
}
110+
}
111+
RUBY
112+
end
113+
114+
it 'registers an offense and corrects when using `deep_symbolize_keys` with flat and only symbol and string keys' do
115+
expect_offense(<<~RUBY)
116+
{
117+
'a' => 1,
118+
b: 2
119+
}.deep_symbolize_keys
120+
^^^^^^^^^^^^^^^^^^^ Convert hash keys explicitly to the required type.
121+
RUBY
122+
123+
expect_correction(<<~RUBY)
124+
{
125+
:a => 1,
126+
b: 2
127+
}
128+
RUBY
129+
end
130+
131+
it 'does not register an offense when using `deep_symbolize_keys` with integer keys' do
132+
expect_no_offenses(<<~RUBY)
133+
{
134+
'a' => 1,
135+
b: {
136+
2 => 3
137+
}
138+
}.deep_symbolize_keys
139+
RUBY
140+
end
141+
142+
it 'does not register an offense when using `deep_symbolize_keys` with non hash literal receiver' do
143+
expect_no_offenses(<<~RUBY)
144+
options.deep_symbolize_keys
145+
RUBY
146+
end
147+
148+
it 'registers an offense and corrects when using `deep_stringify_keys` with only string keys' do
149+
expect_offense(<<~RUBY)
150+
{
151+
'a' => 1,
152+
'b' => {
153+
'c' => 1
154+
}
155+
}.deep_stringify_keys
156+
^^^^^^^^^^^^^^^^^^^ Redundant hash keys conversion, all the keys have the required type.
157+
RUBY
158+
159+
expect_correction(<<~RUBY)
160+
{
161+
'a' => 1,
162+
'b' => {
163+
'c' => 1
164+
}
165+
}
166+
RUBY
167+
end
168+
169+
it 'registers an offense and corrects when using `deep_stringify_keys` with only symbol and string keys' do
170+
expect_offense(<<~RUBY)
171+
{
172+
'a' => 1,
173+
b: {
174+
c: 1
175+
}
176+
}.deep_stringify_keys
177+
^^^^^^^^^^^^^^^^^^^ Convert hash keys explicitly to the required type.
178+
RUBY
179+
180+
expect_correction(<<~RUBY)
181+
{
182+
'a' => 1,
183+
'b'=> {
184+
'c'=> 1
185+
}
186+
}
187+
RUBY
188+
end
189+
190+
it 'does not register an offense when using `deep_stringify_keys` with integer keys' do
191+
expect_no_offenses(<<~RUBY)
192+
{
193+
'a' => 1,
194+
b: {
195+
2 => 3
196+
}
197+
}.deep_stringify_keys
198+
RUBY
199+
end
200+
201+
it 'does not register an offense when using `deep_stringify_keys` with non hash literal receiver' do
202+
expect_no_offenses(<<~RUBY)
203+
options.deep_stringify_keys
204+
RUBY
205+
end
206+
207+
it 'registers an offense and autocorrects when using `symbolize_keys` with empty hash literal' do
208+
expect_offense(<<~RUBY)
209+
{}.symbolize_keys
210+
^^^^^^^^^^^^^^ Redundant hash keys conversion, all the keys have the required type.
211+
RUBY
212+
213+
expect_correction(<<~RUBY)
214+
{}
215+
RUBY
216+
end
217+
218+
it 'does not register an offense when using `symbolize_keys` with non alphanumeric keys' do
219+
expect_no_offenses(<<~RUBY)
220+
{ 'hello world' => 1 }.symbolize_keys
221+
RUBY
222+
end
223+
224+
context 'Ruby >= 3.1', :ruby31 do
225+
it 'does not register an offense when using hash value omission' do
226+
expect_no_offenses(<<~RUBY)
227+
{ a:, b: 2 }.stringify_keys
228+
RUBY
229+
end
230+
end
231+
end

0 commit comments

Comments
 (0)