class Reek::SmellDetectors::NestedIterators
A Nested Iterator
occurs when a block contains another block.
NestedIterators
reports failing methods only once.
See {file:docs/Nested-Iterators.md} for details.
Constants
- DEFAULT_IGNORE_ITERATORS
- DEFAULT_MAX_ALLOWED_NESTING
- IGNORE_ITERATORS_KEY
-
The name of the config field that sets the names of any methods for which nesting should not be considered
- Iterator
-
Struct for conveniently associating iterators with their depth (that is, their nesting).
- MAX_ALLOWED_NESTING_KEY
-
The name of the config field that sets the maximum depth of nested iterators to be permitted within any single method.
Public Class Methods
Source
# File lib/reek/smell_detectors/nested_iterators.rb, line 31 def self.default_config super.merge( MAX_ALLOWED_NESTING_KEY => DEFAULT_MAX_ALLOWED_NESTING, IGNORE_ITERATORS_KEY => DEFAULT_IGNORE_ITERATORS) end
Reek::SmellDetectors::BaseDetector::default_config
Public Instance Methods
Source
# File lib/reek/smell_detectors/nested_iterators.rb, line 44 def sniff find_violations.group_by(&:depth).map do |depth, group| lines = group.map(&:line) smell_warning( lines: lines, message: "contains iterators nested #{depth} deep", parameters: { depth: depth }) end end
Generates a smell warning for each independent deepest nesting depth that is greater than our allowed maximum. This means if two iterators with the same depth were found, we combine them into one warning and merge the line information.
@return [Array<SmellWarning>]
Private Instance Methods
Source
# File lib/reek/smell_detectors/nested_iterators.rb, line 75 def find_candidates scout(exp: expression, depth: 0) end
Finds the set of independent most deeply nested iterators regardless of nesting depth.
@return [Array<Iterator>]
Source
# File lib/reek/smell_detectors/nested_iterators.rb, line 66 def find_violations find_candidates.select { |it| it.depth > max_allowed_nesting } end
Finds the set of independent most deeply nested iterators that are nested more deeply than allowed.
Here, independent means that if iterator A is contained within iterator B, we only include A. But if iterators A and B are both contained in iterator C, but A is not contained in B, nor B in A, both A and B are included.
@return [Array<Iterator>]
Source
# File lib/reek/smell_detectors/nested_iterators.rb, line 117 def ignore_iterators @ignore_iterators ||= value(IGNORE_ITERATORS_KEY, context) end
Source
# File lib/reek/smell_detectors/nested_iterators.rb, line 130 def ignored_iterator?(exp) ignore_iterators.any? do |pattern| /#{pattern}/ =~ exp.call.format_to_ruby end || exp.without_block_arguments? end
@quality :reek:FeatureEnvy
Source
# File lib/reek/smell_detectors/nested_iterators.rb, line 121 def increment_depth(iterator, depth) ignored_iterator?(iterator) ? depth : depth + 1 end
Source
# File lib/reek/smell_detectors/nested_iterators.rb, line 125 def max_allowed_nesting @max_allowed_nesting ||= value(MAX_ALLOWED_NESTING_KEY, context) end
Source
# File lib/reek/smell_detectors/nested_iterators.rb, line 97 def scout(exp:, depth:) return [] unless exp # Find all non-nested blocks in this expression exp.each_node([:block], [:block]).flat_map do |iterator| new_depth = increment_depth(iterator, depth) # 1st case: we recurse down the given block of the iterator. In this case # we need to check if we should increment the depth. # 2nd case: we recurse down the associated call of the iterator. In this case # the depth stays the same. nested_iterators = scout(exp: iterator.block, depth: new_depth) + scout(exp: iterator.call, depth: depth) if nested_iterators.empty? Iterator.new(iterator, new_depth) else nested_iterators end end end
A little digression into parser’s sexp is necessary here:
Given
foo.each() do ... end
this will end up as:
“foo.each() do … end” -> one of the :block nodes “each()” -> the node’s “call” “do … end” -> the node’s “block”
@param exp [AST::Node]
The given expression to analyze.
@param depth [Integer]
@return [Array<Iterator>]
@quality :reek:TooManyStatements { max_statements: 6 }