Skip to content

Commit df568b2

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

File tree

5 files changed

+422
-0
lines changed

5 files changed

+422
-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: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
# # good
23+
# { a:, b: 2 }.stringify_keys
24+
# { a: 1, b: foo }.deep_stringify_keys
25+
#
26+
class HashLiteralKeysConversion < Base
27+
extend AutoCorrector
28+
29+
REDUNDANT_CONVERSION_MSG = 'Redundant hash keys conversion, all the keys have the required type.'
30+
MSG = 'Convert hash keys explicitly to the required type.'
31+
32+
CONVERSION_METHODS = {
33+
symbolize_keys: :sym,
34+
symbolize_keys!: :sym,
35+
stringify_keys: :str,
36+
stringify_keys!: :str,
37+
deep_symbolize_keys: :sym,
38+
deep_symbolize_keys!: :sym,
39+
deep_stringify_keys: :str,
40+
deep_stringify_keys!: :str
41+
}.freeze
42+
43+
RESTRICT_ON_SEND = CONVERSION_METHODS.keys
44+
45+
def on_send(node)
46+
return unless (receiver = node.receiver)&.hash_type?
47+
48+
type = CONVERSION_METHODS[node.method_name]
49+
deep = node.method_name.start_with?('deep_')
50+
return unless convertible_hash?(receiver, deep: deep)
51+
52+
check(node, receiver, type: type, deep: deep)
53+
end
54+
55+
# rubocop:disable Metrics/AbcSize
56+
def check(node, hash_node, type: :sym, deep: false)
57+
pair_nodes = pair_nodes(hash_node, deep: deep)
58+
59+
type_pairs, other_pairs = pair_nodes.partition { |pair_node| pair_node.key.type == type }
60+
61+
if type_pairs == pair_nodes
62+
add_offense(node.loc.selector, message: REDUNDANT_CONVERSION_MSG) do |corrector|
63+
corrector.remove(node.loc.dot)
64+
corrector.remove(node.loc.selector)
65+
end
66+
else
67+
add_offense(node.loc.selector) do |corrector|
68+
corrector.remove(node.loc.dot)
69+
corrector.remove(node.loc.selector)
70+
autocorrect_hash_keys(other_pairs, type, corrector)
71+
end
72+
end
73+
end
74+
# rubocop:enable Metrics/AbcSize
75+
76+
private
77+
78+
def convertible_hash?(node, deep: false)
79+
node.pairs.each do |pair|
80+
return false unless convertible_key?(pair)
81+
return false if deep && !convertible_node?(pair.value)
82+
end
83+
84+
true
85+
end
86+
87+
def convertible_key?(pair)
88+
key, _value = *pair
89+
90+
(key.str_type? || key.sym_type?) && !pair.value_omission? && !key.value.match?(/\W/)
91+
end
92+
93+
def convertible_array?(node)
94+
node.values.all? do |value|
95+
convertible_node?(value)
96+
end
97+
end
98+
99+
def convertible_node?(node)
100+
if node.hash_type?
101+
convertible_hash?(node)
102+
elsif node.array_type?
103+
convertible_array?(node)
104+
else
105+
node.literal?
106+
end
107+
end
108+
109+
def pair_nodes(hash_node, deep: false)
110+
if deep
111+
pair_nodes = []
112+
do_pair_nodes(hash_node, pair_nodes)
113+
pair_nodes
114+
else
115+
hash_node.pairs
116+
end
117+
end
118+
119+
def do_pair_nodes(node, pair_nodes)
120+
if node.hash_type?
121+
node.pairs.each do |pair_node|
122+
pair_nodes << pair_node
123+
do_pair_nodes(pair_node.value, pair_nodes)
124+
end
125+
elsif node.array_type?
126+
node.each_value do |value|
127+
do_pair_nodes(value, pair_nodes)
128+
end
129+
end
130+
end
131+
132+
def autocorrect_hash_keys(pair_nodes, type, corrector)
133+
pair_nodes.each do |pair_node|
134+
if type == :sym
135+
corrector.replace(pair_node.key, ":#{pair_node.key.value}")
136+
else
137+
corrector.replace(pair_node.key, "'#{pair_node.key.source}'")
138+
end
139+
140+
corrector.replace(pair_node.loc.operator, '=>')
141+
end
142+
end
143+
end
144+
end
145+
end
146+
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'

0 commit comments

Comments
 (0)