Class: Reek::SmellDetectors::NestedIterators

Inherits:
BaseDetector show all
Defined in:
lib/reek/smell_detectors/nested_iterators.rb

Overview

A Nested Iterator occurs when a block contains another block.

NestedIterators reports failing methods only once.

See Nested-Iterators for details.

Defined Under Namespace

Classes: Iterator

Constant Summary collapse

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.

'max_allowed_nesting'
DEFAULT_MAX_ALLOWED_NESTING =
1
IGNORE_ITERATORS_KEY =

The name of the config field that sets the names of any methods for which nesting should not be considered

'ignore_iterators'
DEFAULT_IGNORE_ITERATORS =
['tap', 'Tempfile.create'].freeze

Constants inherited from BaseDetector

BaseDetector::DEFAULT_EXCLUDE_SET, BaseDetector::EXCLUDE_KEY

Instance Attribute Summary

Attributes inherited from BaseDetector

#config, #context

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from BaseDetector

#config_for, configuration_keys, contexts, descendants, #enabled?, #exception?, #expression, inherited, #initialize, #run, #smell_type, smell_type, #smell_warning, #source_line, to_detector, todo_configuration_for, #value

Constructor Details

This class inherits a constructor from Reek::SmellDetectors::BaseDetector

Class Method Details

.default_configObject



31
32
33
34
35
# 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

Instance Method Details

#find_candidatesArray<Iterator> (private)

Finds the set of independent most deeply nested iterators regardless of nesting depth.

Returns:



75
76
77
# File 'lib/reek/smell_detectors/nested_iterators.rb', line 75

def find_candidates
  scout(exp: expression, depth: 0)
end

#find_violationsArray<Iterator> (private)

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.

Returns:



66
67
68
# File 'lib/reek/smell_detectors/nested_iterators.rb', line 66

def find_violations
  find_candidates.select { |it| it.depth > max_allowed_nesting }
end

#ignore_iteratorsObject (private)



117
118
119
# File 'lib/reek/smell_detectors/nested_iterators.rb', line 117

def ignore_iterators
  @ignore_iterators ||= value(IGNORE_ITERATORS_KEY, context)
end

#ignored_iterator?(exp) ⇒ Boolean (private)

Returns:

  • (Boolean)


130
131
132
133
134
# 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

#increment_depth(iterator, depth) ⇒ Object (private)



121
122
123
# File 'lib/reek/smell_detectors/nested_iterators.rb', line 121

def increment_depth(iterator, depth)
  ignored_iterator?(iterator) ? depth : depth + 1
end

#max_allowed_nestingObject (private)



125
126
127
# File 'lib/reek/smell_detectors/nested_iterators.rb', line 125

def max_allowed_nesting
  @max_allowed_nesting ||= value(MAX_ALLOWED_NESTING_KEY, context)
end

#scout(exp:, depth:) ⇒ Array<Iterator> (private)

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”

Parameters:

  • exp (AST::Node)

    The given expression to analyze.

  • depth (Integer)

Returns:



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# 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

#sniffArray<SmellWarning>

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.

Returns:



44
45
46
47
48
49
50
51
52
# 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