Class: RuboCop::Cop::Style::SafeNavigation
Relationships & Source Files | |
Super Chains via Extension / Inclusion / Inheritance | |
Class Chain:
self,
::RuboCop::Cop::TargetRubyVersion ,
::RuboCop::Cop::AutoCorrector ,
::RuboCop::Cop::Base ,
::RuboCop::ExcludeLimit ,
NodePattern::Macros,
RuboCop::AST::Sexp
|
|
Instance Chain:
|
|
Inherits: |
RuboCop::Cop::Base
|
Defined in: | lib/rubocop/cop/style/safe_navigation.rb |
Overview
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.
|
Constant Summary
-
LOGIC_JUMP_KEYWORDS =
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 97%i[break fail next raise return throw yield].freeze
-
MSG =
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 95'Use safe navigation (`&.`) instead of checking if an object ' \ 'exists before calling the method.'
::RuboCop::Cop::Base
- Inherited
EMPTY_OFFENSES, RESTRICT_ON_SEND
::RuboCop::Cop::RangeHelp
- Included
Class Attribute Summary
::RuboCop::Cop::AutoCorrector
- Extended
::RuboCop::Cop::Base
- Inherited
.gem_requirements, .lint?, | |
.support_autocorrect? | Returns if class supports autocorrect. |
.support_multiple_source? | Override if your cop should be called repeatedly for multiple investigations Between calls to |
Class Method Summary
::RuboCop::Cop::TargetRubyVersion
- Extended
maximum_target_ruby_version, minimum_target_ruby_version, required_maximum_ruby_version, required_minimum_ruby_version, support_target_ruby_version? |
::RuboCop::Cop::Base
- Inherited
.autocorrect_incompatible_with | List of cops that should not try to autocorrect at the same time as this cop. |
.badge | Naming. |
.callbacks_needed, .cop_name, .department, | |
.documentation_url | Returns a url to view this cops documentation online. |
.exclude_from_registry | Call for abstract Cop classes. |
.inherited, | |
.joining_forces | Override and return the Force class(es) you need to join. |
.match? | Returns true if the cop name or the cop namespace matches any of the given names. |
.new, | |
.requires_gem | Register a version requirement for the given gem name. |
.restrict_on_send |
::RuboCop::ExcludeLimit
- Extended
exclude_limit | Sets up a configuration option to have an exclude limit tracked. |
transform |
Instance Attribute Summary
::RuboCop::Cop::AllowedMethods
- Included
::RuboCop::Cop::Base
- Inherited
::RuboCop::Cop::AutocorrectLogic
- Included
Instance Method Summary
- #and_inside_begin?(node)
- #and_with_rhs_or?(node)
-
#modifier_if_safe_navigation_candidate(node)
if format: (if checked_variable body nil) unless format: (if checked_variable nil body).
- #not_nil_check?(node)
-
#on_and(node)
Metrics/CyclomaticComplexity, Metrics/MethodLength.
- #on_if(node)
- #report_offense(node, rhs, rhs_receiver, *removal_ranges, offense_range: node)
- #strip_begin(node)
- #ternary_safe_navigation_candidate(node)
- #add_safe_nav_to_all_methods_in_chain(corrector, start_method, method_chain) private
- #allowed_if_condition?(node) ⇒ Boolean private
- #and_parts(node) private
- #begin_range(node, method_call) private
- #chain_length(method_chain, method) private
- #collect_and_clauses(node) private
- #comments(node) private
- #concat_nodes(nodes, and_node) private
- #end_range(node, method_call) private
- #extract_common_parts(method_chain, checked_variable) private
- #extract_if_body(node) private
- #extract_parts_from_if(node) private
- #find_matching_receiver_invocation(method_chain, checked_variable) private
- #find_method_chain(node) private
- #handle_comments(corrector, node, method_call) private
- #max_chain_length private
- #method_called?(send_node) ⇒ Boolean private
- #negated?(send_node) ⇒ Boolean private
- #offending_node?(node, lhs_receiver, rhs, rhs_receiver) ⇒ Boolean private
- #relevant_comment_ranges(node) private
- #unsafe_method?(send_node) ⇒ Boolean private
- #unsafe_method_used?(method_chain, method) ⇒ Boolean private
- #use_var_only_in_unless_modifier?(node, variable) ⇒ Boolean private
::RuboCop::Cop::RangeHelp
- Included
#add_range, #column_offset_between, | |
#contents_range | A range containing only the contents of a literal with delimiters (e.g. |
#directions, | |
#effective_column | Returns the column attribute of the range, except if the range is on the first line and there’s a byte order mark at the beginning of that line, in which case 1 is subtracted from the column value. |
#final_pos, #move_pos, #move_pos_str, #range_between, #range_by_whole_lines, #range_with_comments, #range_with_comments_and_lines, #range_with_surrounding_comma, #range_with_surrounding_space, #source_range |
::RuboCop::Cop::NilMethods
- Included
::RuboCop::Cop::AllowedMethods
- Included
::RuboCop::Cop::Base
- Inherited
#add_global_offense | Adds an offense that has no particular location. |
#add_offense | Adds an offense on the specified range (or node with an expression) Unless that offense is disabled for this range, a corrector will be yielded to provide the cop the opportunity to autocorrect the offense. |
#begin_investigation | Called before any investigation. |
#callbacks_needed, | |
#cop_config | Configuration Helpers. |
#cop_name, #excluded_file?, | |
#external_dependency_checksum | This method should be overridden when a cop’s behavior depends on state that lives outside of these locations: |
#inspect, | |
#message | Gets called if no message is specified when calling |
#name | Alias for Base#cop_name. |
#offenses, | |
#on_investigation_end | Called after all on_… |
#on_new_investigation | Called before all on_… |
#on_other_file | Called instead of all on_… |
#parse | There should be very limited reasons for a Cop to do it’s own parsing. |
#parser_engine, | |
#ready | Called between investigations. |
#relevant_file?, #target_rails_version, #target_ruby_version, #annotate, #apply_correction, #attempt_correction, | |
#callback_argument | Reserved for Cop::Cop. |
#complete_investigation | Called to complete an investigation. |
#correct, #current_corrector, | |
#current_offense_locations | Reserved for Commissioner: |
#current_offenses, #currently_disabled_lines, #custom_severity, #default_severity, #disable_uncorrectable, #enabled_line?, #file_name_matches_any?, #find_message, #find_severity, #range_for_original, #range_from_node_or_range, | |
#reset_investigation | Actually private methods. |
#use_corrector |
::RuboCop::Cop::AutocorrectLogic
- Included
#disable_offense, #disable_offense_at_end_of_line, #disable_offense_before_and_after, #disable_offense_with_eol_or_surround_comment, #max_line_length, | |
#range_by_lines | Expand the given range to include all of any lines it covers. |
#range_of_first_line, #range_overlaps_offense?, #string_continuation, #string_continuation?, #surrounding_heredoc, #surrounding_percent_array |
::RuboCop::Cop::IgnoredNode
- Included
Constructor Details
This class inherits a constructor from RuboCop::Cop::Base
Instance Method Details
#allowed_if_condition?(node) ⇒ Boolean
(private)
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 277
def allowed_if_condition?(node) node.else? || node.elsif? end
#and_inside_begin?(node)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 136
def_node_matcher :and_inside_begin?, '`(begin and ...)'
#and_parts(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 222
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
#and_with_rhs_or?(node)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 130
def_node_matcher :and_with_rhs_or?, '(and _ {or (begin or)})'
#begin_range(node, method_call) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 352
def begin_range(node, method_call) range_between(node.source_range.begin_pos, method_call.source_range.begin_pos) end
#chain_length(method_chain, method) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 314
def chain_length(method_chain, method) method.each_ancestor(:send, :csend).inject(0) do |total, ancestor| break total + 1 if ancestor == method_chain total + 1 end end
#collect_and_clauses(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 204
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
#comments(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 258
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
#concat_nodes(nodes, and_node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 216
def concat_nodes(nodes, and_node) return nodes if and_node.each_ancestor(:block).any? nodes.concat(and_parts(and_node)) end
#end_range(node, method_call) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 356
def end_range(node, method_call) range_between(method_call.source_range.end_pos, node.source_range.end_pos) end
#extract_common_parts(method_chain, checked_variable) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 296
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
#extract_if_body(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 243
def extract_if_body(node) if node.ternary? node.branches.find { |branch| !branch.nil_type? } else node.node_parts[1] end end
#extract_parts_from_if(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 281
def extract_parts_from_if(node) variable, receiver = if node.ternary? (node) else (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
#find_matching_receiver_invocation(method_chain, checked_variable) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 304
def find_matching_receiver_invocation(method_chain, checked_variable) return nil unless method_chain.respond_to?(:receiver) receiver = method_chain.receiver return receiver if receiver == checked_variable find_matching_receiver_invocation(receiver, checked_variable) end
#find_method_chain(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 198
def find_method_chain(node) return node unless node&.parent&.call_type? find_method_chain(node.parent) end
#handle_comments(corrector, node, method_call) (private)
[ GitHub ]#max_chain_length (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 374
def max_chain_length cop_config.fetch('MaxChainLength', 2) end
#method_called?(send_node) ⇒ Boolean
(private)
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 348
def method_called?(send_node) send_node&.parent&.send_type? end
#negated?(send_node) ⇒ Boolean
(private)
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 340
def negated?(send_node) if method_called?(send_node) negated?(send_node.parent) else send_node.send_type? && send_node.method?(:!) end end
#not_nil_check?(node)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 133
def_node_matcher :not_nil_check?, '(send (send $_ :nil?) :!)'
#offending_node?(node, lhs_receiver, rhs, rhs_receiver) ⇒ Boolean
(private)
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 229
def offending_node?(node, lhs_receiver, rhs, rhs_receiver) # rubocop:disable Metrics/CyclomaticComplexity return false if 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?(rhs, rhs_receiver.parent) return false if rhs.send_type? && rhs.method?(:empty?) true end
#on_and(node)
Metrics/CyclomaticComplexity, Metrics/MethodLength
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 157
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) ) end end
#on_if(node)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 141
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 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. end end
#relevant_comment_ranges(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 264
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
#report_offense(node, rhs, rhs_receiver, *removal_ranges, offense_range: node)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 181
def report_offense(node, rhs, rhs_receiver, *removal_ranges, offense_range: node) add_offense(offense_range) do |corrector| # 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
#strip_begin(node)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 139
def_node_matcher :strip_begin, '{ (begin $!begin) $!(begin) }'
#unsafe_method?(send_node) ⇒ Boolean
(private)
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 334
def unsafe_method?(send_node) negated?(send_node) || send_node.assignment? || (!send_node.dot? && !send_node. ) end
#unsafe_method_used?(method_chain, method) ⇒ Boolean
(private)
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 322
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
#use_var_only_in_unless_modifier?(node, variable) ⇒ Boolean
(private)
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 239
def use_var_only_in_unless_modifier?(node, variable) node.if_type? && node.unless? && !method_called?(variable) end