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 is set.
Constant Summary
-
LOGIC_JUMP_KEYWORDS =
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 92%i[break fail next raise return throw yield].freeze
-
MSG =
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 90'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 |
.builtin? |
Class Method Summary
::RuboCop::Cop::TargetRubyVersion
- Extended
::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 | Cops (other than builtin) are encouraged to implement this. |
.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::Base
- Inherited
::RuboCop::Cop::AutocorrectLogic
- Included
Instance Method Summary
-
#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)
- #on_if(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
- #autocorrect(corrector, node) private
- #begin_range(node, method_call) private
- #chain_length(method_chain, method) private
- #check_node(node) private
- #comments(node) private
- #end_range(node, method_call) private
- #extract_body(node) private
- #extract_common_parts(method_chain, checked_variable) private
- #extract_parts(node) private
- #extract_parts_from_and(node) private
- #extract_parts_from_if(node) private
- #find_matching_receiver_invocation(method_chain, checked_variable) private
- #handle_comments(corrector, node, method_call) private
- #max_chain_length private
- #method_call(node) private
- #method_called?(send_node) ⇒ Boolean private
- #negated?(send_node) ⇒ 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
#allowed_method?, #allowed_methods, #cop_config_allowed_methods, #cop_config_deprecated_values, | |
#ignored_method? | Alias for AllowedMethods#allowed_method?. |
::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, #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, #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 200
def allowed_if_condition?(node) node.else? || node.elsif? end
#autocorrect(corrector, node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 154
def autocorrect(corrector, node) body = extract_body(node) method_call = method_call(node) corrector.remove(begin_range(node, body)) corrector.remove(end_range(node, body)) corrector.insert_before(method_call.loc.dot, '&') unless method_call. handle_comments(corrector, node, method_call) add_safe_nav_to_all_methods_in_chain(corrector, method_call, body) end
#begin_range(node, method_call) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 299
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 261
def chain_length(method_chain, method) method.each_ancestor(:send).inject(1) do |total, ancestor| break total + 1 if ancestor == method_chain total + 1 end end
#check_node(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 139
def check_node(node) checked_variable, receiver, method_chain, method = extract_parts(node) return if receiver != checked_variable || receiver.nil? return if use_var_only_in_unless_modifier?(node, checked_variable) return if chain_length(method_chain, method) > max_chain_length return if unsafe_method_used?(method_chain, method) return if method_chain.method?(:empty?) add_offense(node) { |corrector| autocorrect(corrector, node) } end
#comments(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 181
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
#end_range(node, method_call) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 303
def end_range(node, method_call) range_between(method_call.source_range.end_pos, node.source_range.end_pos) end
#extract_body(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 166
def extract_body(node) if node.if_type? && node.ternary? node.branches.find { |branch| !branch.nil_type? } else node.node_parts[1] end end
#extract_common_parts(method_chain, checked_variable) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 243
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_parts(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 209
def extract_parts(node) case node.type when :if extract_parts_from_if(node) when :and extract_parts_from_and(node) end end
#extract_parts_from_and(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 233
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
#extract_parts_from_if(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 218
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 251
def find_matching_receiver_invocation(method_chain, checked_variable) return nil unless method_chain receiver = method_chain.receiver return receiver if receiver == checked_variable find_matching_receiver_invocation(receiver, checked_variable) end
#handle_comments(corrector, node, method_call) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 174
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
#max_chain_length (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 320
def max_chain_length cop_config.fetch('MaxChainLength', 2) end
#method_call(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 204
def method_call(node) _checked_variable, matching_receiver, = extract_parts(node) matching_receiver.parent end
#method_called?(send_node) ⇒ Boolean
(private)
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 295
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 287
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 125
def_node_matcher :not_nil_check?, '(send (send $_ :nil?) :!)'
#on_and(node)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 133
def on_and(node) check_node(node) end
#on_if(node)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 127
def on_if(node) return if allowed_if_condition?(node) check_node(node) end
#relevant_comment_ranges(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/safe_navigation.rb', line 187
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
#unsafe_method?(send_node) ⇒ Boolean
(private)
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 281
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 269
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 150
def use_var_only_in_unless_modifier?(node, variable) node.if_type? && node.unless? && !method_called?(variable) end