class RuboCop::Cop::Style::SafeNavigation
This cop transforms usages of a method call safeguarded by a non `nil` check for the variable whose method is being called to safe navigation (`&.`). If there is a method chain, all of the methods in the chain need to be checked for safety, and all of the methods will need to be changed to use safe navigation. We have limited the cop to not register an offense for method chains that exceed 2 methods.
Configuration option: ConvertCodeThatCanStartToReturnNil The default for this is `false`. When configured to `true`, this will check for code in the format `!foo.nil? && foo.bar`. As it is written, the return of this code is limited to `false` and whatever the return of the method is. If this is converted to safe navigation, `foo&.bar` can start returning `nil` as well as what the method returns.
@example
# bad foo.bar if foo foo.bar.baz if foo foo.bar(param1, param2) if foo foo.bar { |e| e.something } if foo foo.bar(param) { |e| e.something } if foo foo.bar if !foo.nil? foo.bar unless !foo foo.bar unless foo.nil? foo && foo.bar foo && foo.bar.baz foo && foo.bar(param1, param2) foo && foo.bar { |e| e.something } foo && foo.bar(param) { |e| e.something } # good foo&.bar foo&.bar&.baz foo&.bar(param1, param2) foo&.bar { |e| e.something } foo&.bar(param) { |e| e.something } foo && foo.bar.baz.qux # method chain with more than 2 methods foo && foo.nil? # method that `nil` responds to # Method calls that do not use `.` foo && foo < bar foo < bar if foo # This could start returning `nil` as well as the return of the method foo.nil? || foo.bar !foo || foo.bar # Methods that are used on assignment, arithmetic operation or # comparison should not be converted to use safe navigation foo.baz = bar if foo foo.baz + bar if foo foo.bar > 2 if foo
Constants
- LOGIC_JUMP_KEYWORDS
- MSG
Public Instance Methods
# File lib/rubocop/cop/style/safe_navigation.rb, line 114 def autocorrect(node) _check, body, = node.node_parts _checked_variable, matching_receiver, = extract_parts(node) method_call, = matching_receiver.parent lambda do |corrector| corrector.remove(begin_range(node, body)) corrector.remove(end_range(node, body)) corrector.insert_before(method_call.loc.dot, '&') add_safe_nav_to_all_methods_in_chain(corrector, method_call, body) end end
# File lib/rubocop/cop/style/safe_navigation.rb, line 98 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) add_offense(node) end
# File lib/rubocop/cop/style/safe_navigation.rb, line 94 def on_and(node) check_node(node) end
# File lib/rubocop/cop/style/safe_navigation.rb, line 88 def on_if(node) return if allowed_if_condition?(node) check_node(node) end
# File lib/rubocop/cop/style/safe_navigation.rb, line 110 def use_var_only_in_unless_modifier?(node, variable) node.if_type? && node.unless? && !method_called?(variable) end
Private Instance Methods
# File lib/rubocop/cop/style/safe_navigation.rb, line 130 def allowed_if_condition?(node) node.else? || node.elsif? || node.ternary? end
# File lib/rubocop/cop/style/safe_navigation.rb, line 230 def begin_range(node, method_call) range_between(node.loc.expression.begin_pos, method_call.loc.expression.begin_pos) end
# File lib/rubocop/cop/style/safe_navigation.rb, line 192 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
# File lib/rubocop/cop/style/safe_navigation.rb, line 235 def end_range(node, method_call) range_between(method_call.loc.expression.end_pos, node.loc.expression.end_pos) end
# File lib/rubocop/cop/style/safe_navigation.rb, line 169 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
# File lib/rubocop/cop/style/safe_navigation.rb, line 134 def extract_parts(node) case node.type when :if extract_parts_from_if(node) when :and extract_parts_from_and(node) end end
# File lib/rubocop/cop/style/safe_navigation.rb, line 157 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
# File lib/rubocop/cop/style/safe_navigation.rb, line 143 def extract_parts_from_if(node) variable, receiver = modifier_if_safe_navigation_candidate(node) checked_variable, matching_receiver, method = extract_common_parts(receiver, variable) if receiver && LOGIC_JUMP_KEYWORDS.include?(receiver.type) matching_receiver = nil end [checked_variable, matching_receiver, receiver, method] end
# File lib/rubocop/cop/style/safe_navigation.rb, line 178 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
# File lib/rubocop/cop/style/safe_navigation.rb, line 226 def method_called?(send_node) send_node&.parent&.send_type? end
# File lib/rubocop/cop/style/safe_navigation.rb, line 218 def negated?(send_node) if method_called?(send_node) negated?(send_node.parent) else send_node.send_type? && send_node.method?(:!) end end
# File lib/rubocop/cop/style/safe_navigation.rb, line 214 def unsafe_method?(send_node) negated?(send_node) || send_node.assignment? || !send_node.dot? end
# File lib/rubocop/cop/style/safe_navigation.rb, line 200 def unsafe_method_used?(method_chain, method) return true if unsafe_method?(method) method.each_ancestor(:send).any? do |ancestor| unless config.for_cop('Lint/SafeNavigationChain')['Enabled'] break true end break true if unsafe_method?(ancestor) break true if nil_methods.include?(ancestor.method_name) break false if ancestor == method_chain end end