Skip to content

Commit

Permalink
Add new Rails/HashLiteralKeysConversion cop
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima committed Aug 19, 2024
1 parent 7616bde commit df568b2
Show file tree
Hide file tree
Showing 5 changed files with 422 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog/new_hash_literal_keys_conversion_cop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#1332](https://github.com/rubocop/rubocop-rails/issues/1332): Add new `Rails/HashLiteralKeysConversion` cop. ([@fatkodima][])
5 changes: 5 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,11 @@ Rails/HasManyOrHasOneDependent:
Include:
- app/models/**/*.rb

Rails/HashLiteralKeysConversion:
Description: 'Convert hash literal keys manually instead of using keys conversion methods.'
Enabled: pending
VersionAdded: '<<next>>'

Rails/HelperInstanceVariable:
Description: 'Do not use instance variables in helpers.'
Enabled: true
Expand Down
146 changes: 146 additions & 0 deletions lib/rubocop/cop/rails/hash_literal_keys_conversion.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Rails
# Detects when keys conversion methods are called on literal hashes, where it is redundant
# or keys can be manually converted to the required type.
#
# @example
# # bad
# { a: 1, b: 2 }.symbolize_keys
#
# # bad
# { a: 1, b: 2 }.stringify_keys
#
# # good
# { 'a' => 1, 'b' => 2 }
#
# # good
# { a: 1, var => 3 }.symbolize_keys
#
# # good
# { a:, b: 2 }.stringify_keys
# { a: 1, b: foo }.deep_stringify_keys
#
class HashLiteralKeysConversion < Base
extend AutoCorrector

REDUNDANT_CONVERSION_MSG = 'Redundant hash keys conversion, all the keys have the required type.'
MSG = 'Convert hash keys explicitly to the required type.'

CONVERSION_METHODS = {
symbolize_keys: :sym,
symbolize_keys!: :sym,
stringify_keys: :str,
stringify_keys!: :str,
deep_symbolize_keys: :sym,
deep_symbolize_keys!: :sym,
deep_stringify_keys: :str,
deep_stringify_keys!: :str
}.freeze

RESTRICT_ON_SEND = CONVERSION_METHODS.keys

def on_send(node)
return unless (receiver = node.receiver)&.hash_type?

type = CONVERSION_METHODS[node.method_name]
deep = node.method_name.start_with?('deep_')
return unless convertible_hash?(receiver, deep: deep)

check(node, receiver, type: type, deep: deep)
end

# rubocop:disable Metrics/AbcSize
def check(node, hash_node, type: :sym, deep: false)
pair_nodes = pair_nodes(hash_node, deep: deep)

type_pairs, other_pairs = pair_nodes.partition { |pair_node| pair_node.key.type == type }

if type_pairs == pair_nodes
add_offense(node.loc.selector, message: REDUNDANT_CONVERSION_MSG) do |corrector|
corrector.remove(node.loc.dot)
corrector.remove(node.loc.selector)
end
else
add_offense(node.loc.selector) do |corrector|
corrector.remove(node.loc.dot)
corrector.remove(node.loc.selector)
autocorrect_hash_keys(other_pairs, type, corrector)
end
end
end
# rubocop:enable Metrics/AbcSize

private

def convertible_hash?(node, deep: false)
node.pairs.each do |pair|
return false unless convertible_key?(pair)
return false if deep && !convertible_node?(pair.value)
end

true
end

def convertible_key?(pair)
key, _value = *pair

(key.str_type? || key.sym_type?) && !pair.value_omission? && !key.value.match?(/\W/)
end

def convertible_array?(node)
node.values.all? do |value|
convertible_node?(value)
end
end

def convertible_node?(node)
if node.hash_type?
convertible_hash?(node)
elsif node.array_type?
convertible_array?(node)
else
node.literal?
end
end

def pair_nodes(hash_node, deep: false)
if deep
pair_nodes = []
do_pair_nodes(hash_node, pair_nodes)
pair_nodes
else
hash_node.pairs
end
end

def do_pair_nodes(node, pair_nodes)
if node.hash_type?
node.pairs.each do |pair_node|
pair_nodes << pair_node
do_pair_nodes(pair_node.value, pair_nodes)
end
elsif node.array_type?
node.each_value do |value|
do_pair_nodes(value, pair_nodes)
end
end
end

def autocorrect_hash_keys(pair_nodes, type, corrector)
pair_nodes.each do |pair_node|
if type == :sym
corrector.replace(pair_node.key, ":#{pair_node.key.value}")
else
corrector.replace(pair_node.key, "'#{pair_node.key.source}'")
end

corrector.replace(pair_node.loc.operator, '=>')
end
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/rails_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
require_relative 'rails/freeze_time'
require_relative 'rails/has_and_belongs_to_many'
require_relative 'rails/has_many_or_has_one_dependent'
require_relative 'rails/hash_literal_keys_conversion'
require_relative 'rails/helper_instance_variable'
require_relative 'rails/http_positional_arguments'
require_relative 'rails/http_status'
Expand Down
Loading

0 comments on commit df568b2

Please sign in to comment.