class RuboCop::Cop::Style::SafeNavigation
foo.bar > 2 if foo
foo.baz + bar if foo
foo.baz = bar if foo
# comparison should not be converted to use safe navigation
# Methods that are used on assignment, arithmetic operation or
!foo || foo.bar
foo.nil? || foo.bar
# This could start returning ‘nil` as well as the return of the method
foo && foo.empty?
# do the opposite of what the author intends.
# When checking `foo&.empty?` in a conditional, `foo` being `nil` will actually
foo < bar if foo
foo && foo < bar
# Method calls that do not use `.`
foo && foo.nil? # method that `nil` responds to
foo && foo.bar.baz.qux # method chain with more than 2 methods
foo&.bar(param) { |e| e.something }
foo&.bar { |e| e.something }
foo&.bar(param1, param2)
foo&.bar&.baz
foo&.bar
# good
foo && foo.bar(param) { |e| e.something }
foo && foo.bar { |e| e.something }
foo && foo.bar(param1, param2)
foo && foo.bar.baz
foo && foo.bar
foo.bar unless foo.nil?
foo.bar unless !foo
foo.bar if !foo.nil?
foo.bar(param) { |e| e.something } if foo
foo.bar { |e| e.something } if foo
foo.bar(param1, param2) if foo
foo.bar.baz if foo
foo.bar if foo
# bad
@example
returns.
`foo&.bar` can start returning `nil` as well as what the method
of the method is. If this is converted to safe navigation,
the return of this code is limited to `false` and whatever the return
check for code in the format `!foo.nil? && foo.bar`. As it is written,
The default for this is `false`. When configured to `true`, this will
Configuration option: ConvertCodeThatCanStartToReturnNil
not register an offense for method chains that exceed 2 methods.
need to be changed to use safe navigation. We have limited the cop to
in the chain need to be checked for safety, and all of the methods will
safe navigation (`&.`). If there is a method chain, all of the methods
check for the variable whose method is being called to
This cop transforms usages of a method call safeguarded by a non `nil`
def add_safe_nav_to_all_methods_in_chain(corrector,
def add_safe_nav_to_all_methods_in_chain(corrector, start_method, method_chain) start_method.each_ancestor do |ancestor| break unless %i[send block].include?(ancestor.type) next unless ancestor.send_type? corrector.insert_before(ancestor.loc.dot, '&') break if ancestor == method_chain end end
def allowed_if_condition?(node)
def allowed_if_condition?(node) node.else? || node.elsif? || node.ternary? end
def autocorrect(corrector, node)
def autocorrect(corrector, node) body = node.node_parts[1] method_call = method_call(node) corrector.remove(begin_range(node, body)) corrector.remove(end_range(node, body)) corrector.insert_before(method_call.loc.dot, '&') handle_comments(corrector, node, method_call) add_safe_nav_to_all_methods_in_chain(corrector, method_call, body) end
def begin_range(node, method_call)
def begin_range(node, method_call) range_between(node.loc.expression.begin_pos, method_call.loc.expression.begin_pos) end
def chain_size(method_chain, method)
def chain_size(method_chain, method) method.each_ancestor(:send).inject(0) do |total, ancestor| break total + 1 if ancestor == method_chain total + 1 end end
def check_node(node)
def check_node(node) checked_variable, receiver, method_chain, method = extract_parts(node) return unless receiver == checked_variable return if use_var_only_in_unless_modifier?(node, checked_variable) # method is already a method call so this is actually checking for a # chain greater than 2 return if chain_size(method_chain, method) > 1 return if unsafe_method_used?(method_chain, method) return if method_chain.method?(:empty?) add_offense(node) do |corrector| autocorrect(corrector, node) end end
def comments(node)
def comments(node) relevant_comment_ranges(node).each.with_object([]) do |range, comments| comments.concat(processed_source.each_comment_in_lines(range).to_a) end end
def end_range(node, method_call)
def end_range(node, method_call) range_between(method_call.loc.expression.end_pos, node.loc.expression.end_pos) end
def extract_common_parts(method_chain, checked_variable)
def extract_common_parts(method_chain, checked_variable) matching_receiver = find_matching_receiver_invocation(method_chain, checked_variable) method = matching_receiver.parent if matching_receiver [checked_variable, matching_receiver, method] end
def extract_parts(node)
def extract_parts(node) case node.type when :if extract_parts_from_if(node) when :and extract_parts_from_and(node) end end
def extract_parts_from_and(node)
def extract_parts_from_and(node) checked_variable, rhs = *node if cop_config['ConvertCodeThatCanStartToReturnNil'] checked_variable = not_nil_check?(checked_variable) || checked_variable end checked_variable, matching_receiver, method = extract_common_parts(rhs, checked_variable) [checked_variable, matching_receiver, rhs, method] end
def extract_parts_from_if(node)
def extract_parts_from_if(node) variable, receiver = modifier_if_safe_navigation_candidate(node) checked_variable, matching_receiver, method = extract_common_parts(receiver, variable) matching_receiver = nil if receiver && LOGIC_JUMP_KEYWORDS.include?(receiver.type) [checked_variable, matching_receiver, receiver, method] end
def find_matching_receiver_invocation(method_chain, checked_variable)
def find_matching_receiver_invocation(method_chain, checked_variable) return nil unless method_chain receiver = if method_chain.block_type? method_chain.send_node.receiver else method_chain.receiver end return receiver if receiver == checked_variable find_matching_receiver_invocation(receiver, checked_variable) end
def handle_comments(corrector, node, method_call)
def handle_comments(corrector, node, method_call) comments = comments(node) return if comments.empty? corrector.insert_before(method_call, "#{comments.map(&:text).join("\n")}\n") end
def method_call(node)
def method_call(node) _checked_variable, matching_receiver, = extract_parts(node) matching_receiver.parent end
def method_called?(send_node)
def method_called?(send_node) send_node&.parent&.send_type? end
def negated?(send_node)
def negated?(send_node) if method_called?(send_node) negated?(send_node.parent) else send_node.send_type? && send_node.method?(:!) end end
def on_and(node)
def on_and(node) check_node(node) end
def on_if(node)
def on_if(node) return if allowed_if_condition?(node) check_node(node) end
def relevant_comment_ranges(node)
def relevant_comment_ranges(node) # Get source lines ranges inside the if node that aren't inside an inner node # Comments inside an inner node should remain attached to that node, and not # moved. begin_pos = node.loc.first_line end_pos = node.loc.last_line node.child_nodes.each.with_object([]) do |child, ranges| ranges << (begin_pos...child.loc.first_line) begin_pos = child.loc.last_line end << (begin_pos...end_pos) end
def unsafe_method?(send_node)
def unsafe_method?(send_node) negated?(send_node) || send_node.assignment? || !send_node.dot? end
def unsafe_method_used?(method_chain, method)
def unsafe_method_used?(method_chain, method) return true if unsafe_method?(method) method.each_ancestor(:send).any? do |ancestor| break true unless config.for_cop('Lint/SafeNavigationChain')['Enabled'] break true if unsafe_method?(ancestor) break true if nil_methods.include?(ancestor.method_name) break false if ancestor == method_chain end end
def use_var_only_in_unless_modifier?(node, variable)
def use_var_only_in_unless_modifier?(node, variable) node.if_type? && node.unless? && !method_called?(variable) end