class RuboCop::Cop::Style::HashFetchChain
hash.fetch(‘foo’, {}).fetch(‘bar’, {})
# ok - not handled by the cop since the final ‘fetch` value is non-nil
hash.dig(’foo’, ‘bar’)
# good
hash.fetch(‘foo’, {}).fetch(‘bar’, nil)
# has ‘nil` as a default value
# earlier members of the chain can return `{}` as long as the final `fetch`
# bad
hash.fetch(’foo’, nil)&.fetch(‘bar’, nil)
# bad
@example
is a ‘Hash` or that `fetch` or `dig` have the expected standard implementation.
This cop is unsafe because it cannot be guaranteed that the receiver
@safety
a single method call.
NOTE: See `Style/DigChain` for replacing chains of `dig` calls with
given with `dig`.
the chain will not be registered as an offense, as the default value cannot be safely
as long as the final call in the chain is a nil value. If a non-nil value is given,
If the 2nd parameter is `{}` or `Hash.new`, an offense will also be registered,
multiple arguments.
and in both cases, these can be simplified with a single call to `dig` with
is that each step in the chain returns either `nil` or another hash,
When `fetch(identifier, nil)` calls are chained on a hash, the expectation
Use `Hash#dig` instead of chaining potentially null `fetch` calls.
def inspect_chain(node)
def inspect_chain(node) arguments = [] while (arg = diggable?(node)) arguments.unshift(arg) ignore_node(node) last_replaceable_node = node node = node.receiver end [last_replaceable_node, arguments] end
def last_fetch_non_nil?(node)
def last_fetch_non_nil?(node) # When chaining `fetch` methods, `fetch(x, {})` is acceptable within # the chain, as long as the last method in the chain has a `nil` # default value. return false unless node.method?(:fetch) !node.last_argument&.nil_type? end
def on_send(node)
def on_send(node) return if ignored_node?(node) return if last_fetch_non_nil?(node) last_replaceable_node, arguments = inspect_chain(node) return unless last_replaceable_node return unless arguments.size > 1 range = last_replaceable_node.selector.join(node.loc.end) replacement = replacement(arguments) message = format(MSG, replacement: replacement) add_offense(range, message: message) do |corrector| corrector.replace(range, replacement) end end
def replacement(arguments)
def replacement(arguments) values = arguments.map(&:source).join(', ') "dig(#{values})" end