Class: RuboCop::Cop::Style::CombinableLoops
| Relationships & Source Files | |
| Super Chains via Extension / Inclusion / Inheritance | |
|
Class Chain:
self,
::RuboCop::Cop::AutoCorrector,
::RuboCop::Cop::Base,
::RuboCop::ExcludeLimit,
NodePattern::Macros,
RuboCop::AST::Sexp
|
|
|
Instance Chain:
self,
::RuboCop::Cop::RangeHelp,
::RuboCop::Cop::Base,
::RuboCop::Cop::AutocorrectLogic,
::RuboCop::Cop::IgnoredNode,
::RuboCop::Util,
RuboCop::AST::Sexp
|
|
| Inherits: |
RuboCop::Cop::Base
|
| Defined in: | lib/rubocop/cop/style/combinable_loops.rb |
Overview
Checks for places where multiple consecutive loops over the same data can be combined into a single loop. It is very likely that combining them will make the code more efficient and more concise.
|
Note
|
Autocorrection is not applied when the block variable names differ in separate loops, as it is impossible to determine which variable name should be prioritized. |
Constant Summary
-
MSG =
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 66'Combine this loop with the previous loop.'
::RuboCop::Cop::Base - Inherited
DEPARTMENT_SEVERITIES, 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::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 | Reserved for Commissioner. |
::RuboCop::ExcludeLimit - Extended
| exclude_limit | Sets up a configuration option to have an exclude limit tracked. |
| transform | |
Instance Attribute Summary
::RuboCop::Cop::Base - Inherited
| #active_support_extensions_enabled?, #always_autocorrect?, #config, #config_to_allow_offenses, #config_to_allow_offenses=, #contextual_autocorrect?, #disabled_autocorrect?, | |
| #preview? | Whether the user opted in to unstable behavior, with |
| #processed_source, #project_index, #string_literals_frozen_by_default?, #target_satisfies_all_gem_version_requirements? | |
::RuboCop::Cop::AutocorrectLogic - Included
| #autocorrect?, #autocorrect_enabled?, #autocorrect_requested?, #autocorrect_with_disable_uncorrectable?, #correctable?, #disable_uncorrectable?, #safe_autocorrect?, | |
| #skipped_unsafe_correction_with_disable_uncorrectable? | Whether a correction exists but is skipped because it is unsafe in a safe autocorrect run, while |
| #contextual_autocorrect_held_back? | When LSP is enabled or the |
Instance Method Summary
- #on_block(node) (also: #on_numblock, #on_itblock)
- #on_for(node)
-
#on_itblock(node)
Alias for #on_block.
-
#on_numblock(node)
Alias for #on_block.
-
#closing_with_preceding_space(loop, next_loop)
private
Comments and heredoc bodies before the closing delimiter are kept.
- #collection_looping_method?(node) ⇒ Boolean private
- #combinable_blocks?(node, sibling) ⇒ Boolean private
-
#combinable_fors?(node, sibling) ⇒ Boolean
private
Combining loops with different iteration variables would leave the second body referencing an undefined variable, so only autocorrect when they match.
- #combinable_looping_blocks?(node, sibling) ⇒ Boolean private
- #combine_with_left_sibling(corrector, node) private
- #combined_with_right_sibling?(node) ⇒ Boolean private
- #correct_end_of_block(corrector, node) private
-
#directive_after_opening?(node) ⇒ Boolean
private
The opening is removed, so a directive after it would end up on a line of its own, where it covers the rest of the file instead of a single line.
- #first_combined_block(node) private
-
#loop_opening(node)
private
The block arguments or
{/doof a block, or the {do} or collection of a {for} loop. -
#opening_with_following_space(node)
private
Comments between the opening and the body are kept.
- #same_collection_looping_block?(node, sibling) ⇒ Boolean private
- #same_collection_looping_for?(node, sibling) ⇒ Boolean private
::RuboCop::Cop::RangeHelp - Included
| #add_range, | |
| #arguments_range | A range containing the first to the last argument of a method call or method definition. |
| #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 | rubocop:disable-next Metrics/ParameterLists. |
| #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::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 | — grouped under the Commissioner heading above. |
| #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_gem_version | Returns the version of a gem in the target, by default its locked version (i.e. from Gemfile.lock or gems.locked). |
| #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, #covering_disabled_range, #current_corrector, | |
| #current_offense_locations | Reserved for Commissioner: |
| #current_offenses, #currently_disabled_lines, #custom_severity, #default_severity, #disable_uncorrectable, #enabled_line?, | |
| #enabled_lines? | A multi-line offense is suppressed by a directive on any line of its range, not only its first line, matching the intuition that the directive is attached to the offending code. |
| #file_name_matches_any?, #find_message, #find_severity, #matches_absolute_include_pattern?, #new_offense, #range_for_original, #range_from_node_or_range, | |
| #reset_investigation | Actually private methods. |
| #suppression_reason | The |
| #use_corrector | |
::RuboCop::Cop::AutocorrectLogic - Included
::RuboCop::Cop::IgnoredNode - Included
Constructor Details
This class inherits a constructor from RuboCop::Cop::Base
Class Method Details
.autocorrect_incompatible_with
[ GitHub ]# File 'lib/rubocop/cop/style/combinable_loops.rb', line 68
def self.autocorrect_incompatible_with [Layout::BlockEndNewline, Layout::RedundantLineBreak] end
Instance Method Details
#closing_with_preceding_space(loop, next_loop) (private)
Comments and heredoc bodies before the closing delimiter are kept. When the next loop starts on the delimiter’s line, the line break before the delimiter is kept too, so that a comment doesn’t swallow the next loop.
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 166
def closing_with_preceding_space(loop, next_loop) separate_lines = !same_line?(loop.loc.end, next_loop) range_with_surrounding_space( loop.loc.end, side: :left, newlines: separate_lines, whitespace: separate_lines ) end
#collection_looping_method?(node) ⇒ Boolean (private)
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 101
def collection_looping_method?(node) method_name = node.method_name method_name.start_with?('each') || method_name.end_with?('_each') end
#combinable_blocks?(node, sibling) ⇒ Boolean (private)
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 122
def combinable_blocks?(node, sibling) return false unless node.arguments == sibling.arguments # Numbered and `it` parameters are implicit, so `arguments` is empty for both. return false unless node.argument_list == sibling.argument_list return false if directive_after_opening?(node) !node.body.type?(:rescue, :ensure) && !sibling.body.type?(:rescue, :ensure) end
#combinable_fors?(node, sibling) ⇒ Boolean (private)
Combining loops with different iteration variables would leave the second body referencing an undefined variable, so only autocorrect when they match.
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 133
def combinable_fors?(node, sibling) node.variable == sibling.variable && !directive_after_opening?(node) end
#combinable_looping_blocks?(node, sibling) ⇒ Boolean (private)
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 114
def combinable_looping_blocks?(node, sibling) same_collection_looping_block?(node, sibling) && node.body && sibling.body end
#combine_with_left_sibling(corrector, node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/combinable_loops.rb', line 156
def combine_with_left_sibling(corrector, node) corrector.remove(closing_with_preceding_space(node.left_sibling, node)) corrector.remove(opening_with_following_space(node)) correct_end_of_block(corrector, node) end
#combined_with_right_sibling?(node) ⇒ Boolean (private)
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 190
def combined_with_right_sibling?(node) sibling = node.right_sibling return false unless sibling&.any_block_type? # A disabled loop isn't corrected, so it doesn't get merged into this one. return false unless enabled_lines?(sibling.source_range) same_collection_looping_block?(sibling, node) && sibling.body && combinable_blocks?(sibling, node) end
#correct_end_of_block(corrector, node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/combinable_loops.rb', line 181
def correct_end_of_block(corrector, node) return unless node.left_sibling.respond_to?(:braces?) return if combined_with_right_sibling?(node) end_of_block = first_combined_block(node).braces? ? '}' : ' end' corrector.remove(node.loc.end) corrector.insert_before(node.source_range.end, end_of_block) end
#directive_after_opening?(node) ⇒ Boolean (private)
The opening is removed, so a directive after it would end up on a line of its own, where it covers the rest of the file instead of a single line.
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 139
def directive_after_opening?(node) opening_line = loop_opening(node).line return false if opening_line == node.body.first_line comment = processed_source.comment_at_line(opening_line) comment && DirectiveComment.new(comment).start_with_marker? end
#first_combined_block(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/style/combinable_loops.rb', line 200
def first_combined_block(node) first = node.left_sibling while (previous = first.left_sibling) && enabled_lines?(first.source_range) && combinable_looping_blocks?(first, previous) && combinable_blocks?(first, previous) first = previous end first end
#loop_opening(node) (private)
The block arguments or {/do of a block, or the {do} or collection of a {for} loop.
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 148
def loop_opening(node) if node.for_type? node.loc.begin || node.collection.source_range else (node.block_type? && node.arguments.source_range) || node.loc.begin end end
#on_block(node) Also known as: #on_numblock, #on_itblock
[ GitHub ]# File 'lib/rubocop/cop/style/combinable_loops.rb', line 72
def on_block(node) return unless node.parent&.begin_type? return unless collection_looping_method?(node) return unless combinable_looping_blocks?(node, node.left_sibling) add_offense(node) do |corrector| next unless combinable_blocks?(node, node.left_sibling) combine_with_left_sibling(corrector, node) end end
#on_for(node)
[ GitHub ]# File 'lib/rubocop/cop/style/combinable_loops.rb', line 87
def on_for(node) return unless node.parent&.begin_type? return unless same_collection_looping_for?(node, node.left_sibling) return unless node.body && node.left_sibling.body add_offense(node) do |corrector| next unless combinable_fors?(node, node.left_sibling) combine_with_left_sibling(corrector, node) end end
#on_itblock(node)
Alias for #on_block.
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 85
alias on_itblock on_block
#on_numblock(node)
Alias for #on_block.
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 84
alias on_numblock on_block
#opening_with_following_space(node) (private)
Comments between the opening and the body are kept.
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 175
def opening_with_following_space(node) opening = node.source_range.begin.join(loop_opening(node)) range_with_surrounding_space(opening, side: :right, whitespace: true) end
#same_collection_looping_block?(node, sibling) ⇒ Boolean (private)
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 106
def same_collection_looping_block?(node, sibling) return false if sibling.nil? || !sibling.any_block_type? sibling.method?(node.method_name) && sibling.receiver == node.receiver && sibling.send_node.arguments == node.send_node.arguments end
#same_collection_looping_for?(node, sibling) ⇒ Boolean (private)
# File 'lib/rubocop/cop/style/combinable_loops.rb', line 118
def same_collection_looping_for?(node, sibling) sibling&.for_type? && node.collection == sibling.collection end