rubocop 1.0.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -16
  3. data/config/default.yml +141 -14
  4. data/exe/rubocop +1 -1
  5. data/lib/rubocop.rb +16 -0
  6. data/lib/rubocop/cli/command/auto_genenerate_config.rb +1 -1
  7. data/lib/rubocop/comment_config.rb +1 -1
  8. data/lib/rubocop/config_loader.rb +7 -6
  9. data/lib/rubocop/cop/bundler/duplicated_gem.rb +26 -6
  10. data/lib/rubocop/cop/bundler/gem_comment.rb +1 -1
  11. data/lib/rubocop/cop/commissioner.rb +10 -10
  12. data/lib/rubocop/cop/corrector.rb +3 -1
  13. data/lib/rubocop/cop/force.rb +1 -1
  14. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +3 -3
  15. data/lib/rubocop/cop/gemspec/required_ruby_version.rb +4 -5
  16. data/lib/rubocop/cop/gemspec/ruby_version_globals_usage.rb +1 -1
  17. data/lib/rubocop/cop/generator.rb +1 -1
  18. data/lib/rubocop/cop/layout/block_alignment.rb +3 -4
  19. data/lib/rubocop/cop/layout/def_end_alignment.rb +1 -1
  20. data/lib/rubocop/cop/layout/else_alignment.rb +15 -2
  21. data/lib/rubocop/cop/layout/empty_lines_around_access_modifier.rb +1 -0
  22. data/lib/rubocop/cop/layout/end_alignment.rb +3 -3
  23. data/lib/rubocop/cop/layout/extra_spacing.rb +1 -2
  24. data/lib/rubocop/cop/layout/hash_alignment.rb +4 -4
  25. data/lib/rubocop/cop/layout/line_length.rb +8 -1
  26. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +24 -18
  27. data/lib/rubocop/cop/layout/space_inside_parens.rb +35 -13
  28. data/lib/rubocop/cop/layout/trailing_whitespace.rb +1 -1
  29. data/lib/rubocop/cop/lint/constant_definition_in_block.rb +23 -2
  30. data/lib/rubocop/cop/lint/debugger.rb +17 -28
  31. data/lib/rubocop/cop/lint/duplicate_branch.rb +93 -0
  32. data/lib/rubocop/cop/lint/duplicate_case_condition.rb +2 -12
  33. data/lib/rubocop/cop/lint/duplicate_regexp_character_class_element.rb +77 -0
  34. data/lib/rubocop/cop/lint/else_layout.rb +29 -3
  35. data/lib/rubocop/cop/lint/empty_block.rb +82 -0
  36. data/lib/rubocop/cop/lint/empty_class.rb +93 -0
  37. data/lib/rubocop/cop/lint/flip_flop.rb +8 -2
  38. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +38 -6
  39. data/lib/rubocop/cop/lint/loop.rb +4 -4
  40. data/lib/rubocop/cop/lint/nested_percent_literal.rb +14 -0
  41. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +58 -0
  42. data/lib/rubocop/cop/lint/number_conversion.rb +46 -13
  43. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +27 -8
  44. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +19 -16
  45. data/lib/rubocop/cop/lint/shadowed_exception.rb +4 -5
  46. data/lib/rubocop/cop/lint/to_enum_arguments.rb +95 -0
  47. data/lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb +185 -0
  48. data/lib/rubocop/cop/lint/useless_access_modifier.rb +2 -2
  49. data/lib/rubocop/cop/lint/useless_method_definition.rb +2 -4
  50. data/lib/rubocop/cop/lint/useless_setter_call.rb +6 -1
  51. data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
  52. data/lib/rubocop/cop/mixin/configurable_numbering.rb +3 -3
  53. data/lib/rubocop/cop/mixin/line_length_help.rb +1 -1
  54. data/lib/rubocop/cop/mixin/statement_modifier.rb +9 -4
  55. data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +11 -1
  56. data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +11 -5
  57. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +67 -18
  58. data/lib/rubocop/cop/naming/predicate_name.rb +2 -1
  59. data/lib/rubocop/cop/naming/variable_number.rb +98 -8
  60. data/lib/rubocop/cop/offense.rb +3 -3
  61. data/lib/rubocop/cop/style/and_or.rb +1 -3
  62. data/lib/rubocop/cop/style/arguments_forwarding.rb +142 -0
  63. data/lib/rubocop/cop/style/bisected_attr_accessor.rb +0 -4
  64. data/lib/rubocop/cop/style/case_like_if.rb +0 -4
  65. data/lib/rubocop/cop/style/collection_compact.rb +91 -0
  66. data/lib/rubocop/cop/style/document_dynamic_eval_definition.rb +162 -0
  67. data/lib/rubocop/cop/style/double_negation.rb +6 -1
  68. data/lib/rubocop/cop/style/hash_syntax.rb +3 -3
  69. data/lib/rubocop/cop/style/identical_conditional_branches.rb +7 -2
  70. data/lib/rubocop/cop/style/if_inside_else.rb +37 -1
  71. data/lib/rubocop/cop/style/if_unless_modifier.rb +7 -3
  72. data/lib/rubocop/cop/style/infinite_loop.rb +4 -0
  73. data/lib/rubocop/cop/style/keyword_parameters_order.rb +12 -0
  74. data/lib/rubocop/cop/style/mixin_grouping.rb +0 -4
  75. data/lib/rubocop/cop/style/multiple_comparison.rb +55 -7
  76. data/lib/rubocop/cop/style/negated_if_else_condition.rb +104 -0
  77. data/lib/rubocop/cop/style/nil_lambda.rb +52 -0
  78. data/lib/rubocop/cop/style/raise_args.rb +21 -6
  79. data/lib/rubocop/cop/style/redundant_regexp_character_class.rb +7 -1
  80. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +1 -1
  81. data/lib/rubocop/cop/style/semicolon.rb +3 -0
  82. data/lib/rubocop/cop/style/static_class.rb +97 -0
  83. data/lib/rubocop/cop/style/swap_values.rb +108 -0
  84. data/lib/rubocop/cop/style/while_until_modifier.rb +9 -0
  85. data/lib/rubocop/cop/team.rb +6 -1
  86. data/lib/rubocop/cop/util.rb +5 -1
  87. data/lib/rubocop/ext/regexp_node.rb +17 -9
  88. data/lib/rubocop/ext/regexp_parser.rb +84 -0
  89. data/lib/rubocop/formatter/formatter_set.rb +2 -1
  90. data/lib/rubocop/formatter/git_hub_actions_formatter.rb +47 -0
  91. data/lib/rubocop/magic_comment.rb +2 -2
  92. data/lib/rubocop/options.rb +2 -0
  93. data/lib/rubocop/rspec/shared_contexts.rb +4 -0
  94. data/lib/rubocop/target_ruby.rb +57 -1
  95. data/lib/rubocop/version.rb +1 -1
  96. metadata +21 -5
@@ -15,6 +15,20 @@ module RuboCop
15
15
  # valid_attributes: %i[name content],
16
16
  # nested_attributes: %i[name content %i[incorrectly nested]]
17
17
  # }
18
+ #
19
+ # # good
20
+ #
21
+ # # Neither is incompatible with the bad case, but probably the intended code.
22
+ # attributes = {
23
+ # valid_attributes: %i[name content],
24
+ # nested_attributes: [:name, :content, %i[incorrectly nested]]
25
+ # }
26
+ #
27
+ # attributes = {
28
+ # valid_attributes: %i[name content],
29
+ # nested_attributes: [:name, :content, [:incorrectly, :nested]]
30
+ # }
31
+ #
18
32
  class NestedPercentLiteral < Base
19
33
  include PercentLiteral
20
34
 
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Lint
6
+ # Checks for the presence of a `return` inside a `begin..end` block
7
+ # in assignment contexts.
8
+ # In this situation, the `return` will result in an exit from the current
9
+ # method, possibly leading to unexpected behavior.
10
+ #
11
+ # @example
12
+ #
13
+ # # bad
14
+ #
15
+ # @some_variable ||= begin
16
+ # return some_value if some_condition_is_met
17
+ #
18
+ # do_something
19
+ # end
20
+ #
21
+ # @example
22
+ #
23
+ # # good
24
+ #
25
+ # @some_variable ||= begin
26
+ # if some_condition_is_met
27
+ # some_value
28
+ # else
29
+ # do_something
30
+ # end
31
+ # end
32
+ #
33
+ # # good
34
+ #
35
+ # some_variable = if some_condition_is_met
36
+ # return if another_condition_is_met
37
+ #
38
+ # some_value
39
+ # else
40
+ # do_something
41
+ # end
42
+ #
43
+ class NoReturnInBeginEndBlocks < Cop
44
+ MSG = 'Do not `return` in `begin..end` blocks in assignment contexts.'
45
+
46
+ def on_lvasgn(node)
47
+ node.each_node(:kwbegin) do |kwbegin_node|
48
+ kwbegin_node.each_node(:return) do |return_node|
49
+ add_offense(return_node)
50
+ end
51
+ end
52
+ end
53
+ alias on_or_asgn on_lvasgn
54
+ alias on_op_asgn on_lvasgn
55
+ end
56
+ end
57
+ end
58
+ end
@@ -7,6 +7,17 @@ module RuboCop
7
7
  # number conversion can cause unexpected error if auto type conversion
8
8
  # fails. Cop prefer parsing with number class instead.
9
9
  #
10
+ # Conversion with `Integer`, `Float`, etc. will raise an `ArgumentError`
11
+ # if given input that is not numeric (eg. an empty string), whereas
12
+ # `to_i`, etc. will try to convert regardless of input (`''.to_i => 0`).
13
+ # As such, this cop is disabled by default because it's not necessarily
14
+ # always correct to raise if a value is not numeric.
15
+ #
16
+ # NOTE: Some values cannot be converted properly using one of the `Kernel`
17
+ # method (for instance, `Time` and `DateTime` values are allowed by this
18
+ # cop by default). Similarly, Rails' duration methods do not work well
19
+ # with `Integer()` and can be ignored with `IgnoredMethods`.
20
+ #
10
21
  # @example
11
22
  #
12
23
  # # bad
@@ -20,8 +31,19 @@ module RuboCop
20
31
  # Integer('10', 10)
21
32
  # Float('10.2')
22
33
  # Complex('10')
34
+ #
35
+ # @example IgnoredMethods: [minutes]
36
+ #
37
+ # # good
38
+ # 10.minutes.to_i
39
+ #
40
+ # @example IgnoredClasses: [Time, DateTime] (default)
41
+ #
42
+ # # good
43
+ # Time.now.to_datetime.to_i
23
44
  class NumberConversion < Base
24
45
  extend AutoCorrector
46
+ include IgnoredMethods
25
47
 
26
48
  CONVERSION_METHOD_CLASS_MAPPING = {
27
49
  to_i: "#{Integer.name}(%<number_object>s, 10)",
@@ -38,13 +60,9 @@ module RuboCop
38
60
  (send $_ ${:to_i :to_f :to_c})
39
61
  PATTERN
40
62
 
41
- def_node_matcher :datetime?, <<~PATTERN
42
- (send (const {nil? (cbase)} {:Time :DateTime}) ...)
43
- PATTERN
44
-
45
63
  def on_send(node)
46
64
  to_method(node) do |receiver, to_method|
47
- next if receiver.nil? || date_time_object?(receiver)
65
+ next if receiver.nil? || ignore_receiver?(receiver)
48
66
 
49
67
  message = format(
50
68
  MSG,
@@ -60,18 +78,33 @@ module RuboCop
60
78
 
61
79
  private
62
80
 
63
- def date_time_object?(node)
64
- child = node
65
- while child&.send_type?
66
- return true if datetime? child
81
+ def correct_method(node, receiver)
82
+ format(CONVERSION_METHOD_CLASS_MAPPING[node.method_name],
83
+ number_object: receiver.source)
84
+ end
67
85
 
68
- child = child.children[0]
86
+ def ignore_receiver?(receiver)
87
+ if receiver.send_type? && ignored_method?(receiver.method_name)
88
+ true
89
+ elsif (receiver = top_receiver(receiver))
90
+ receiver.const_type? && ignored_class?(receiver.const_name)
91
+ else
92
+ false
69
93
  end
70
94
  end
71
95
 
72
- def correct_method(node, receiver)
73
- format(CONVERSION_METHOD_CLASS_MAPPING[node.method_name],
74
- number_object: receiver.source)
96
+ def top_receiver(node)
97
+ receiver = node
98
+ receiver = receiver.receiver until receiver.receiver.nil?
99
+ receiver
100
+ end
101
+
102
+ def ignored_classes
103
+ cop_config.fetch('IgnoredClasses', [])
104
+ end
105
+
106
+ def ignored_class?(name)
107
+ ignored_classes.include?(name.to_s)
75
108
  end
76
109
  end
77
110
  end
@@ -19,7 +19,7 @@ module RuboCop
19
19
  # puts $1 # => foo
20
20
  #
21
21
  class OutOfRangeRegexpRef < Base
22
- MSG = 'Do not use out of range reference for the Regexp.'
22
+ MSG = '$%<backref>s is out of range (%<count>s regexp capture %<group>s detected).'
23
23
 
24
24
  REGEXP_RECEIVER_METHODS = %i[=~ === match].to_set.freeze
25
25
  REGEXP_ARGUMENT_METHODS = %i[=~ match grep gsub gsub! sub sub! [] slice slice! index rindex
@@ -35,14 +35,13 @@ module RuboCop
35
35
  check_regexp(node.children.first)
36
36
  end
37
37
 
38
- def on_send(node)
38
+ def after_send(node)
39
39
  @valid_ref = nil
40
40
 
41
- if node.receiver&.regexp_type?
42
- check_regexp(node.receiver)
43
- elsif node.first_argument&.regexp_type? \
44
- && REGEXP_ARGUMENT_METHODS.include?(node.method_name)
41
+ if regexp_first_argument?(node)
45
42
  check_regexp(node.first_argument)
43
+ elsif regexp_receiver?(node)
44
+ check_regexp(node.receiver)
46
45
  end
47
46
  end
48
47
 
@@ -56,9 +55,16 @@ module RuboCop
56
55
 
57
56
  def on_nth_ref(node)
58
57
  backref, = *node
59
- return if @valid_ref.nil?
58
+ return if @valid_ref.nil? || backref <= @valid_ref
59
+
60
+ message = format(
61
+ MSG,
62
+ backref: backref,
63
+ count: @valid_ref.zero? ? 'no' : @valid_ref,
64
+ group: @valid_ref == 1 ? 'group' : 'groups'
65
+ )
60
66
 
61
- add_offense(node) if backref > @valid_ref
67
+ add_offense(node, message: message)
62
68
  end
63
69
 
64
70
  private
@@ -73,6 +79,19 @@ module RuboCop
73
79
  node.each_capture(named: false).count
74
80
  end
75
81
  end
82
+
83
+ def regexp_first_argument?(send_node)
84
+ send_node.first_argument&.regexp_type? \
85
+ && REGEXP_ARGUMENT_METHODS.include?(send_node.method_name)
86
+ end
87
+
88
+ def regexp_receiver?(send_node)
89
+ send_node.receiver&.regexp_type?
90
+ end
91
+
92
+ def nth_ref_receiver?(send_node)
93
+ send_node.receiver&.nth_ref_type?
94
+ end
76
95
  end
77
96
  end
78
97
  end
@@ -88,31 +88,34 @@ module RuboCop
88
88
  begin_pos = reposition(source, begin_pos, -1)
89
89
  end_pos = reposition(source, end_pos, 1)
90
90
 
91
- comma_pos =
92
- if source[begin_pos - 1] == ','
93
- :before
94
- elsif source[end_pos] == ','
95
- :after
96
- else
97
- :none
98
- end
99
-
100
- range_to_remove(begin_pos, end_pos, comma_pos, comment)
91
+ range_to_remove(begin_pos, end_pos, comment)
101
92
  end
102
93
 
103
- def range_to_remove(begin_pos, end_pos, comma_pos, comment)
94
+ def range_to_remove(begin_pos, end_pos, comment)
104
95
  start = comment_start(comment)
96
+ source = comment.loc.expression.source
105
97
 
106
- case comma_pos
107
- when :before
108
- range_between(start + begin_pos - 1, start + end_pos)
109
- when :after
110
- range_between(start + begin_pos, start + end_pos + 1)
98
+ if source[begin_pos - 1] == ','
99
+ range_with_comma_before(start, begin_pos, end_pos)
100
+ elsif source[end_pos] == ','
101
+ range_with_comma_after(comment, start, begin_pos, end_pos)
111
102
  else
112
103
  range_between(start, comment.loc.expression.end_pos)
113
104
  end
114
105
  end
115
106
 
107
+ def range_with_comma_before(start, begin_pos, end_pos)
108
+ range_between(start + begin_pos - 1, start + end_pos)
109
+ end
110
+
111
+ # If the list of cops is comma-separated, but without a empty space after the comma,
112
+ # we should **not** remove the prepending empty space, thus begin_pos += 1
113
+ def range_with_comma_after(comment, start, begin_pos, end_pos)
114
+ begin_pos += 1 if comment.loc.expression.source[end_pos + 1] != ' '
115
+
116
+ range_between(start + begin_pos, start + end_pos + 1)
117
+ end
118
+
116
119
  def all_or_name(name)
117
120
  name == 'all' ? 'all cops' : name
118
121
  end
@@ -140,11 +140,10 @@ module RuboCop
140
140
  rescued_groups.each_cons(2).all? do |x, y|
141
141
  if x.include?(Exception)
142
142
  false
143
- elsif y.include?(Exception)
144
- true
145
- elsif x.none? || y.none?
146
- # consider sorted if a group is empty or only contains
147
- # `nil`s
143
+ elsif y.include?(Exception) ||
144
+ # consider sorted if a group is empty or only contains
145
+ # `nil`s
146
+ x.none? || y.none?
148
147
  true
149
148
  else
150
149
  (x <=> y || 0) <= 0
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Lint
6
+ # This cop ensures that `to_enum`/`enum_for`, called for the current method,
7
+ # has correct arguments.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # def method(x, y = 1)
12
+ # return to_enum(__method__, x) # `y` is missing
13
+ # end
14
+ #
15
+ # # good
16
+ # def method(x, y = 1)
17
+ # return to_enum(__method__, x, y)
18
+ # end
19
+ #
20
+ # # bad
21
+ # def method(required:)
22
+ # return to_enum(:method, required: something) # `required` has incorrect value
23
+ # end
24
+ #
25
+ # # good
26
+ # def method(required:)
27
+ # return to_enum(:method, required: required)
28
+ # end
29
+ #
30
+ class ToEnumArguments < Base
31
+ MSG = 'Ensure you correctly provided all the arguments.'
32
+
33
+ RESTRICT_ON_SEND = %i[to_enum enum_for].freeze
34
+
35
+ def_node_matcher :enum_conversion_call?, <<~PATTERN
36
+ (send {nil? self} {:to_enum :enum_for} $_ $...)
37
+ PATTERN
38
+
39
+ def_node_matcher :method_name?, <<~PATTERN
40
+ {(send nil? :__method__) (sym %1)}
41
+ PATTERN
42
+
43
+ def_node_matcher :passing_keyword_arg?, <<~PATTERN
44
+ (pair (sym %1) (lvar %1))
45
+ PATTERN
46
+
47
+ def on_send(node)
48
+ def_node = node.each_ancestor(:def, :defs).first
49
+ return unless def_node
50
+
51
+ enum_conversion_call?(node) do |method_node, arguments|
52
+ add_offense(node) unless method_name?(method_node, def_node.method_name) &&
53
+ arguments_match?(arguments, def_node)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def arguments_match?(arguments, def_node)
60
+ index = 0
61
+
62
+ def_node.arguments.reject(&:blockarg_type?).all? do |def_arg|
63
+ send_arg = arguments[index]
64
+ case def_arg.type
65
+ when :arg, :restarg, :optarg
66
+ index += 1
67
+ end
68
+
69
+ send_arg && argument_match?(send_arg, def_arg)
70
+ end
71
+ end
72
+
73
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
74
+ def argument_match?(send_arg, def_arg)
75
+ def_arg_name = def_arg.children[0]
76
+
77
+ case def_arg.type
78
+ when :arg, :restarg
79
+ send_arg.source == def_arg.source
80
+ when :optarg
81
+ send_arg.source == def_arg_name.to_s
82
+ when :kwoptarg, :kwarg
83
+ send_arg.hash_type? &&
84
+ send_arg.pairs.any? { |pair| passing_keyword_arg?(pair, def_arg_name) }
85
+ when :kwrestarg
86
+ send_arg.each_child_node(:kwsplat).any? { |child| child.source == def_arg.source }
87
+ when :forward_arg
88
+ send_arg.forwarded_args_type?
89
+ end
90
+ end
91
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Lint
6
+ # Looks for `reduce` or `inject` blocks where the value returned (implicitly or
7
+ # explicitly) does not include the accumulator. A block is considered valid as
8
+ # long as at least one return value includes the accumulator.
9
+ #
10
+ # If the accumulator is not included in the return value, then the entire
11
+ # block will just return a transformation of the last element value, and
12
+ # could be rewritten as such without a loop.
13
+ #
14
+ # Also catches instances where an index of the accumulator is returned, as
15
+ # this may change the type of object being retained.
16
+ #
17
+ # NOTE: For the purpose of reducing false positives, this cop only flags
18
+ # returns in `reduce` blocks where the element is the only variable in
19
+ # the expression (since we will not be able to tell what other variables
20
+ # relate to via static analysis).
21
+ #
22
+ # @example
23
+ #
24
+ # # bad
25
+ # (1..4).reduce(0) do |acc, el|
26
+ # el * 2
27
+ # end
28
+ #
29
+ # # bad, may raise a NoMethodError after the first iteration
30
+ # %w(a b c).reduce({}) do |acc, letter|
31
+ # acc[letter] = true
32
+ # end
33
+ #
34
+ # # good
35
+ # (1..4).reduce(0) do |acc, el|
36
+ # acc + el * 2
37
+ # end
38
+ #
39
+ # # good, element is returned but modified using the accumulator
40
+ # values.reduce do |acc, el|
41
+ # el << acc
42
+ # el
43
+ # end
44
+ #
45
+ # # good, returns the accumulator instead of the index
46
+ # %w(a b c).reduce({}) do |acc, letter|
47
+ # acc[letter] = true
48
+ # acc
49
+ # end
50
+ #
51
+ # # good, at least one branch returns the accumulator
52
+ # values.reduce(nil) do |result, value|
53
+ # break result if something?
54
+ # value
55
+ # end
56
+ #
57
+ # # ignored as the return value cannot be determined
58
+ # enum.reduce do |acc, el|
59
+ # x = foo(acc, el)
60
+ # bar(x)
61
+ # end
62
+ class UnmodifiedReduceAccumulator < Base
63
+ MSG = 'Ensure the accumulator `%<accum>s` will be modified by `%<method>s`.'
64
+ MSG_INDEX = 'Do not return an element of the accumulator in `%<method>s`.'
65
+
66
+ def_node_matcher :reduce_with_block?, <<~PATTERN
67
+ (block (send _recv {:reduce :inject} ...) (args arg+) ...)
68
+ PATTERN
69
+
70
+ def_node_matcher :accumulator_index?, <<~PATTERN
71
+ (send (lvar %1) {:[] :[]=} ...)
72
+ PATTERN
73
+
74
+ def_node_search :element_modified?, <<~PATTERN
75
+ {
76
+ (send _receiver !{:[] :[]=} <`(lvar %1) `_ ...>) # method(el, ...)
77
+ (send (lvar %1) _message <{ivar gvar cvar lvar send} ...>) # el.method(...)
78
+ (lvasgn %1 _) # el = ...
79
+ (%RuboCop::AST::Node::SHORTHAND_ASSIGNMENTS (lvasgn %1) ... _) # el += ...
80
+ }
81
+ PATTERN
82
+
83
+ def_node_matcher :lvar_used?, <<~PATTERN
84
+ {
85
+ (lvar %1)
86
+ (lvasgn %1 ...)
87
+ (send (lvar %1) :<< ...)
88
+ (dstr (begin (lvar %1)))
89
+ (%RuboCop::AST::Node::SHORTHAND_ASSIGNMENTS (lvasgn %1))
90
+ }
91
+ PATTERN
92
+
93
+ def_node_search :expression_values, <<~PATTERN
94
+ {
95
+ (%RuboCop::AST::Node::VARIABLES $_)
96
+ (%RuboCop::AST::Node::EQUALS_ASSIGNMENTS $_ ...)
97
+ (send (%RuboCop::AST::Node::VARIABLES $_) :<< ...)
98
+ $(send _ _)
99
+ (dstr (begin {(%RuboCop::AST::Node::VARIABLES $_)}))
100
+ (%RuboCop::AST::Node::SHORTHAND_ASSIGNMENTS (%RuboCop::AST::Node::EQUALS_ASSIGNMENTS $_) ...)
101
+ }
102
+ PATTERN
103
+
104
+ def on_block(node)
105
+ return unless reduce_with_block?(node)
106
+
107
+ check_return_values(node)
108
+ end
109
+
110
+ private
111
+
112
+ # Return values in a block are either the value given to next,
113
+ # the last line of a multiline block, or the only line of the block
114
+ def return_values(block_body_node)
115
+ nodes = [block_body_node.begin_type? ? block_body_node.child_nodes.last : block_body_node]
116
+
117
+ block_body_node.each_descendant(:next, :break) do |n|
118
+ # Ignore `next`/`break` inside an inner block
119
+ next if n.each_ancestor(:block).first != block_body_node.parent
120
+ next unless n.first_argument
121
+
122
+ nodes << n.first_argument
123
+ end
124
+
125
+ nodes
126
+ end
127
+
128
+ def check_return_values(block_node)
129
+ return_values = return_values(block_node.body)
130
+ accumulator_name = block_arg_name(block_node, 0)
131
+ element_name = block_arg_name(block_node, 1)
132
+ message_opts = { method: block_node.method_name, accum: accumulator_name }
133
+
134
+ if (node = returned_accumulator_index(return_values, accumulator_name))
135
+ add_offense(node, message: format(MSG_INDEX, message_opts))
136
+ elsif potential_offense?(return_values, block_node.body, element_name, accumulator_name)
137
+ return_values.each do |return_val|
138
+ unless acceptable_return?(return_val, element_name)
139
+ add_offense(return_val, message: format(MSG, message_opts))
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ def block_arg_name(node, index)
146
+ node.arguments[index].node_parts[0]
147
+ end
148
+
149
+ # Look for an index of the accumulator being returned
150
+ # This is always an offense, in order to try to catch potential exceptions
151
+ # due to type mismatches
152
+ def returned_accumulator_index(return_values, accumulator_name)
153
+ return_values.detect { |val| accumulator_index?(val, accumulator_name) }
154
+ end
155
+
156
+ def potential_offense?(return_values, block_body, element_name, accumulator_name)
157
+ !(element_modified?(block_body, element_name) ||
158
+ returns_accumulator_anywhere?(return_values, accumulator_name))
159
+ end
160
+
161
+ # If the accumulator is used in any return value, the node is acceptable since
162
+ # the accumulator has a chance to change each iteration
163
+ def returns_accumulator_anywhere?(return_values, accumulator_name)
164
+ return_values.any? { |node| lvar_used?(node, accumulator_name) }
165
+ end
166
+
167
+ # Determine if a return value is acceptable for the purposes of this cop
168
+ # If it is an expression containing the accumulator, it is acceptable
169
+ # Otherwise, it is only unacceptable if it contains the iterated element, since we
170
+ # otherwise do not have enough information to prevent false positives.
171
+ def acceptable_return?(return_val, element_name)
172
+ vars = expression_values(return_val).uniq
173
+ return true if vars.none? || (vars - [element_name]).any?
174
+
175
+ false
176
+ end
177
+
178
+ # Exclude `begin` nodes inside a `dstr` from being collected by `return_values`
179
+ def allowed_type?(parent_node)
180
+ !parent_node.dstr_type?
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end