class RuboCop::Cop::Style::SafeNavigation
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.
The default for ‘ConvertCodeThatCanStartToReturnNil` 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.
The default for ‘MaxChainLength` is `2`. We have limited the cop to not register an offense for method chains that exceed this option’s value.
NOTE: This cop will recognize offenses but not autocorrect code when the right hand side (RHS) of the ‘&&` statement is an `||` statement (eg. `foo && (foo.bar? || foo.baz?)`). It can be corrected manually by removing the `foo &&` and adding `&.` to each `foo` on the RHS.
@safety
Autocorrection is unsafe because if a value is `false`, the resulting code will have different behavior or raise an error. [source,ruby] ---- x = false x && x.foo # return false x&.foo # raises NoMethodError ----
@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 } foo ? foo.bar : nil foo.nil? ? nil : foo.bar !foo.nil? ? foo.bar : nil !foo ? nil : foo.bar # 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 # When checking `foo&.empty?` in a conditional, `foo` being `nil` will actually # do the opposite of what the author intends. foo && foo.empty? # 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 foo ? foo[index] : nil # Ignored `foo&.[](index)` due to unclear readability benefit. foo ? foo[idx] = v : nil # Ignored `foo&.[]=(idx, v)` due to unclear readability benefit. foo ? foo * 42 : nil # Ignored `foo&.*(42)` due to unclear readability benefit.
Constants
- LOGIC_JUMP_KEYWORDS
- MSG
Public Instance Methods
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 162 def on_and(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength collect_and_clauses(node).each do |(lhs, lhs_operator_range), (rhs, _rhs_operator_range)| lhs_not_nil_check = not_nil_check?(lhs) lhs_receiver = lhs_not_nil_check || lhs rhs_receiver = find_matching_receiver_invocation(strip_begin(rhs), lhs_receiver) next if !cop_config['ConvertCodeThatCanStartToReturnNil'] && lhs_not_nil_check next unless offending_node?(node, lhs_receiver, rhs, rhs_receiver) # Since we are evaluating every clause in potentially a complex chain of `and` nodes, # we need to ensure that there isn't an object check happening lhs_method_chain = find_method_chain(lhs_receiver) next unless lhs_method_chain == lhs_receiver || lhs_not_nil_check report_offense( node, rhs, rhs_receiver, range_with_surrounding_space(range: lhs.source_range, side: :right), range_with_surrounding_space(range: lhs_operator_range, side: :right), offense_range: range_between(lhs.source_range.begin_pos, rhs.source_range.end_pos) ) do |corrector| corrector.replace(rhs_receiver, lhs_receiver.source) end ignore_node(node) end end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 145 def on_if(node) return if allowed_if_condition?(node) checked_variable, receiver, method_chain, _method = extract_parts_from_if(node) return unless offending_node?(node, checked_variable, method_chain, receiver) body = extract_if_body(node) method_call = receiver.parent return if dotless_operator_call?(method_call) || method_call.double_colon? removal_ranges = [begin_range(node, body), end_range(node, body)] report_offense(node, method_chain, method_call, *removal_ranges) do |corrector| corrector.insert_before(method_call.loc.dot, '&') unless method_call.safe_navigation? end end
Private Instance Methods
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 293 def allowed_if_condition?(node) node.else? || node.elsif? end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 232 def and_parts(node) parts = [node.loc.operator] parts << node.rhs unless and_inside_begin?(node.rhs) parts << node.lhs unless node.lhs.and_type? || and_inside_begin?(node.lhs) parts end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 381 def begin_range(node, method_call) range_between(node.source_range.begin_pos, method_call.source_range.begin_pos) end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 340 def chain_length(method_chain, method) method.each_ancestor(:call).inject(0) do |total, ancestor| break total + 1 if ancestor == method_chain total + 1 end end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 214 def collect_and_clauses(node) # Collect the lhs, operator and rhs of all `and` nodes # `and` nodes can be nested and can contain `begin` nodes # This gives us a source-ordered list of clauses that is then used to look # for matching receivers as well as operator locations for offense and corrections node.each_descendant(:and) .inject(and_parts(node)) { |nodes, and_node| concat_nodes(nodes, and_node) } .sort_by { |a| a.is_a?(RuboCop::AST::Node) ? a.source_range.begin_pos : a.begin_pos } .each_slice(2) .each_cons(2) end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 274 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
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 226 def concat_nodes(nodes, and_node) return nodes if and_node.each_ancestor(:block).any? nodes.concat(and_parts(and_node)) end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 261 def dotless_operator_call?(method_call) return false if method_call.loc.dot method_call.method?(:[]) || method_call.method?(:[]=) || method_call.operator_method? end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 385 def end_range(node, method_call) range_between(method_call.source_range.end_pos, node.source_range.end_pos) end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 312 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
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 253 def extract_if_body(node) if node.ternary? node.branches.find { |branch| !branch.nil_type? } else node.node_parts[1] end end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 297 def extract_parts_from_if(node) variable, receiver = if node.ternary? ternary_safe_navigation_candidate(node) else modifier_if_safe_navigation_candidate(node) end 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
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 320 def find_matching_receiver_invocation(method_chain, checked_variable) return nil unless method_chain.respond_to?(:receiver) receiver = method_chain.receiver return receiver if matching_nodes?(receiver, checked_variable) find_matching_receiver_invocation(receiver, checked_variable) end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 208 def find_method_chain(node) return node unless node&.parent&.call_type? find_method_chain(node.parent) end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 267 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
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 334 def matching_call_nodes?(left, right) return false unless left && right.respond_to?(:call_type?) left.call_type? && right.call_type? && left.children == right.children end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 330 def matching_nodes?(left, right) left == right || matching_call_nodes?(left, right) end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 402 def max_chain_length cop_config.fetch('MaxChainLength', 2) end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 377 def method_called?(send_node) send_node&.parent&.send_type? end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 369 def negated?(send_node) if method_called?(send_node) negated?(send_node.parent) else send_node.send_type? && send_node.method?(:!) end end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 239 def offending_node?(node, lhs_receiver, rhs, rhs_receiver) # rubocop:disable Metrics/CyclomaticComplexity return false if !matching_nodes?(lhs_receiver, rhs_receiver) || rhs_receiver.nil? return false if use_var_only_in_unless_modifier?(node, lhs_receiver) return false if chain_length(rhs, rhs_receiver) > max_chain_length return false if unsafe_method_used?(node, rhs, rhs_receiver.parent) return false if rhs.send_type? && rhs.method?(:empty?) true end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 280 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
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 191 def report_offense(node, rhs, rhs_receiver, *removal_ranges, offense_range: node) add_offense(offense_range) do |corrector| next if ignored_node?(node) # If the RHS is an `or` we cannot safely autocorrect because in order to remove # the non-nil check we need to add safe-navs to all clauses where the receiver is used next if and_with_rhs_or?(node) removal_ranges.each { |range| corrector.remove(range) } yield corrector if block_given? handle_comments(corrector, node, rhs) add_safe_nav_to_all_methods_in_chain(corrector, rhs_receiver, rhs) end end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 360 def unsafe_method?(node, send_node) return true if negated?(send_node) return false if node.respond_to?(:ternary?) && node.ternary? send_node.assignment? || (!send_node.dot? && !send_node.safe_navigation?) end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 348 def unsafe_method_used?(node, method_chain, method) return true if unsafe_method?(node, method) method.each_ancestor(:send).any? do |ancestor| break true unless config.cop_enabled?('Lint/SafeNavigationChain') break true if unsafe_method?(node, ancestor) break true if nil_methods.include?(ancestor.method_name) break false if ancestor == method_chain end end
Source
# File lib/rubocop/cop/style/safe_navigation.rb, line 249 def use_var_only_in_unless_modifier?(node, variable) node.if_type? && node.unless? && !method_called?(variable) end