rubocop 0.93.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -16
  3. data/config/default.yml +207 -77
  4. data/exe/rubocop +1 -1
  5. data/lib/rubocop.rb +16 -2
  6. data/lib/rubocop/cli/command/auto_genenerate_config.rb +1 -1
  7. data/lib/rubocop/cli/command/version.rb +1 -1
  8. data/lib/rubocop/comment_config.rb +1 -1
  9. data/lib/rubocop/config.rb +4 -0
  10. data/lib/rubocop/config_loader.rb +26 -8
  11. data/lib/rubocop/config_loader_resolver.rb +7 -5
  12. data/lib/rubocop/config_validator.rb +7 -6
  13. data/lib/rubocop/cop/badge.rb +9 -24
  14. data/lib/rubocop/cop/base.rb +16 -1
  15. data/lib/rubocop/cop/bundler/duplicated_gem.rb +26 -6
  16. data/lib/rubocop/cop/bundler/gem_comment.rb +1 -1
  17. data/lib/rubocop/cop/commissioner.rb +37 -23
  18. data/lib/rubocop/cop/corrector.rb +3 -1
  19. data/lib/rubocop/cop/correctors/percent_literal_corrector.rb +1 -1
  20. data/lib/rubocop/cop/force.rb +1 -1
  21. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +3 -3
  22. data/lib/rubocop/cop/gemspec/required_ruby_version.rb +4 -5
  23. data/lib/rubocop/cop/gemspec/ruby_version_globals_usage.rb +1 -1
  24. data/lib/rubocop/cop/generator.rb +1 -1
  25. data/lib/rubocop/cop/layout/block_alignment.rb +3 -4
  26. data/lib/rubocop/cop/layout/class_structure.rb +7 -0
  27. data/lib/rubocop/cop/layout/def_end_alignment.rb +1 -1
  28. data/lib/rubocop/cop/layout/else_alignment.rb +15 -2
  29. data/lib/rubocop/cop/layout/empty_lines_around_access_modifier.rb +1 -0
  30. data/lib/rubocop/cop/layout/end_alignment.rb +3 -3
  31. data/lib/rubocop/cop/layout/extra_spacing.rb +1 -2
  32. data/lib/rubocop/cop/layout/hash_alignment.rb +4 -4
  33. data/lib/rubocop/cop/layout/line_length.rb +8 -1
  34. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +24 -18
  35. data/lib/rubocop/cop/layout/space_around_operators.rb +4 -1
  36. data/lib/rubocop/cop/layout/space_inside_parens.rb +35 -13
  37. data/lib/rubocop/cop/layout/trailing_whitespace.rb +37 -13
  38. data/lib/rubocop/cop/lint/ambiguous_regexp_literal.rb +6 -3
  39. data/lib/rubocop/cop/lint/constant_definition_in_block.rb +23 -2
  40. data/lib/rubocop/cop/lint/debugger.rb +17 -28
  41. data/lib/rubocop/cop/lint/duplicate_branch.rb +93 -0
  42. data/lib/rubocop/cop/lint/duplicate_case_condition.rb +2 -12
  43. data/lib/rubocop/cop/lint/duplicate_regexp_character_class_element.rb +77 -0
  44. data/lib/rubocop/cop/lint/else_layout.rb +29 -3
  45. data/lib/rubocop/cop/lint/empty_block.rb +82 -0
  46. data/lib/rubocop/cop/lint/empty_class.rb +93 -0
  47. data/lib/rubocop/cop/lint/flip_flop.rb +8 -2
  48. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +38 -6
  49. data/lib/rubocop/cop/lint/loop.rb +4 -4
  50. data/lib/rubocop/cop/lint/nested_percent_literal.rb +14 -0
  51. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +58 -0
  52. data/lib/rubocop/cop/lint/number_conversion.rb +46 -13
  53. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +27 -8
  54. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +19 -16
  55. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +44 -11
  56. data/lib/rubocop/cop/lint/shadowed_exception.rb +4 -5
  57. data/lib/rubocop/cop/lint/to_enum_arguments.rb +95 -0
  58. data/lib/rubocop/cop/lint/to_json.rb +1 -1
  59. data/lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb +185 -0
  60. data/lib/rubocop/cop/lint/useless_access_modifier.rb +2 -2
  61. data/lib/rubocop/cop/lint/useless_method_definition.rb +2 -4
  62. data/lib/rubocop/cop/lint/useless_setter_call.rb +6 -1
  63. data/lib/rubocop/cop/metrics/class_length.rb +9 -3
  64. data/lib/rubocop/cop/metrics/parameter_lists.rb +4 -1
  65. data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
  66. data/lib/rubocop/cop/mixin/configurable_numbering.rb +3 -3
  67. data/lib/rubocop/cop/mixin/line_length_help.rb +1 -1
  68. data/lib/rubocop/cop/mixin/statement_modifier.rb +9 -4
  69. data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +12 -2
  70. data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +11 -5
  71. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +67 -18
  72. data/lib/rubocop/cop/naming/predicate_name.rb +2 -1
  73. data/lib/rubocop/cop/naming/variable_number.rb +98 -8
  74. data/lib/rubocop/cop/offense.rb +3 -3
  75. data/lib/rubocop/cop/security/open.rb +12 -10
  76. data/lib/rubocop/cop/style/accessor_grouping.rb +1 -1
  77. data/lib/rubocop/cop/style/and_or.rb +1 -3
  78. data/lib/rubocop/cop/style/arguments_forwarding.rb +142 -0
  79. data/lib/rubocop/cop/style/bisected_attr_accessor.rb +0 -4
  80. data/lib/rubocop/cop/style/case_like_if.rb +0 -4
  81. data/lib/rubocop/cop/style/class_equality_comparison.rb +19 -4
  82. data/lib/rubocop/cop/style/collection_compact.rb +91 -0
  83. data/lib/rubocop/cop/style/document_dynamic_eval_definition.rb +162 -0
  84. data/lib/rubocop/cop/style/double_negation.rb +6 -1
  85. data/lib/rubocop/cop/style/format_string_token.rb +47 -2
  86. data/lib/rubocop/cop/style/hash_syntax.rb +3 -3
  87. data/lib/rubocop/cop/style/identical_conditional_branches.rb +7 -2
  88. data/lib/rubocop/cop/style/if_inside_else.rb +37 -1
  89. data/lib/rubocop/cop/style/if_unless_modifier.rb +7 -3
  90. data/lib/rubocop/cop/style/infinite_loop.rb +4 -0
  91. data/lib/rubocop/cop/style/keyword_parameters_order.rb +12 -0
  92. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +10 -13
  93. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +6 -11
  94. data/lib/rubocop/cop/style/method_call_with_args_parentheses/require_parentheses.rb +7 -11
  95. data/lib/rubocop/cop/style/mixin_grouping.rb +0 -4
  96. data/lib/rubocop/cop/style/multiple_comparison.rb +55 -7
  97. data/lib/rubocop/cop/style/negated_if_else_condition.rb +104 -0
  98. data/lib/rubocop/cop/style/nil_lambda.rb +52 -0
  99. data/lib/rubocop/cop/style/raise_args.rb +21 -6
  100. data/lib/rubocop/cop/style/redundant_begin.rb +14 -4
  101. data/lib/rubocop/cop/style/redundant_parentheses.rb +4 -0
  102. data/lib/rubocop/cop/style/redundant_regexp_character_class.rb +7 -1
  103. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +1 -1
  104. data/lib/rubocop/cop/style/redundant_self.rb +3 -0
  105. data/lib/rubocop/cop/style/safe_navigation.rb +16 -4
  106. data/lib/rubocop/cop/style/semicolon.rb +3 -0
  107. data/lib/rubocop/cop/style/static_class.rb +97 -0
  108. data/lib/rubocop/cop/style/string_concatenation.rb +13 -1
  109. data/lib/rubocop/cop/style/swap_values.rb +108 -0
  110. data/lib/rubocop/cop/style/ternary_parentheses.rb +1 -1
  111. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +3 -1
  112. data/lib/rubocop/cop/style/while_until_modifier.rb +9 -0
  113. data/lib/rubocop/cop/team.rb +6 -1
  114. data/lib/rubocop/cop/util.rb +5 -1
  115. data/lib/rubocop/ext/regexp_node.rb +17 -9
  116. data/lib/rubocop/ext/regexp_parser.rb +84 -0
  117. data/lib/rubocop/formatter/formatter_set.rb +2 -1
  118. data/lib/rubocop/formatter/git_hub_actions_formatter.rb +47 -0
  119. data/lib/rubocop/formatter/offense_count_formatter.rb +1 -1
  120. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -1
  121. data/lib/rubocop/magic_comment.rb +2 -2
  122. data/lib/rubocop/options.rb +6 -1
  123. data/lib/rubocop/rspec/shared_contexts.rb +4 -0
  124. data/lib/rubocop/target_ruby.rb +57 -1
  125. data/lib/rubocop/version.rb +56 -6
  126. metadata +21 -5
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # This cop checks for uses of `if-else` and ternary operators with a negated condition
7
+ # which can be simplified by inverting condition and swapping branches.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # if !x
12
+ # do_something
13
+ # else
14
+ # do_something_else
15
+ # end
16
+ #
17
+ # # good
18
+ # if x
19
+ # do_something_else
20
+ # else
21
+ # do_something
22
+ # end
23
+ #
24
+ # # bad
25
+ # !x ? do_something : do_something_else
26
+ #
27
+ # # good
28
+ # x ? do_something_else : do_something
29
+ #
30
+ class NegatedIfElseCondition < Base
31
+ include RangeHelp
32
+ extend AutoCorrector
33
+
34
+ MSG = 'Invert the negated condition and swap the %<type>s branches.'
35
+
36
+ NEGATED_EQUALITY_METHODS = %i[!= !~].freeze
37
+
38
+ def self.autocorrect_incompatible_with
39
+ [Style::InverseMethods, Style::Not]
40
+ end
41
+
42
+ def on_new_investigation
43
+ @corrected_nodes = nil
44
+ end
45
+
46
+ def on_if(node)
47
+ return unless if_else?(node)
48
+
49
+ condition = node.condition
50
+ return unless negated_condition?(condition)
51
+
52
+ type = node.ternary? ? 'ternary' : 'if-else'
53
+ add_offense(node, message: format(MSG, type: type)) do |corrector|
54
+ unless corrected_ancestor?(node)
55
+ correct_negated_condition(corrector, condition)
56
+ swap_branches(corrector, node)
57
+
58
+ @corrected_nodes ||= Set.new.compare_by_identity
59
+ @corrected_nodes.add(node)
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def if_else?(node)
67
+ else_branch = node.else_branch
68
+ !node.elsif? && else_branch && (!else_branch.if_type? || !else_branch.elsif?)
69
+ end
70
+
71
+ def negated_condition?(node)
72
+ node.send_type? &&
73
+ (node.negation_method? || NEGATED_EQUALITY_METHODS.include?(node.method_name))
74
+ end
75
+
76
+ def corrected_ancestor?(node)
77
+ node.each_ancestor(:if).any? { |ancestor| @corrected_nodes&.include?(ancestor) }
78
+ end
79
+
80
+ def correct_negated_condition(corrector, node)
81
+ receiver, method_name, rhs = *node
82
+ replacement =
83
+ if node.negation_method?
84
+ receiver.source
85
+ else
86
+ inverted_method = method_name.to_s.sub('!', '=')
87
+ "#{receiver.source} #{inverted_method} #{rhs.source}"
88
+ end
89
+
90
+ corrector.replace(node, replacement)
91
+ end
92
+
93
+ def swap_branches(corrector, node)
94
+ if node.if_branch.nil?
95
+ corrector.remove(range_by_whole_lines(node.loc.else, include_final_newline: true))
96
+ else
97
+ corrector.replace(node.if_branch, node.else_branch.source)
98
+ corrector.replace(node.else_branch, node.if_branch.source)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # This cop checks for lambdas that always return nil, which can be replaced
7
+ # with an empty lambda instead.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # -> { nil }
12
+ #
13
+ # lambda do
14
+ # next nil
15
+ # end
16
+ #
17
+ # # good
18
+ # -> {}
19
+ #
20
+ # lambda do
21
+ # end
22
+ #
23
+ # -> (x) { nil if x }
24
+ #
25
+ class NilLambda < Base
26
+ extend AutoCorrector
27
+ include RangeHelp
28
+
29
+ MSG = 'Use an empty lambda instead of always returning nil.'
30
+
31
+ def_node_matcher :nil_return?, <<~PATTERN
32
+ { ({return next break} nil) (nil) }
33
+ PATTERN
34
+
35
+ def on_block(node)
36
+ return unless node.lambda?
37
+ return unless nil_return?(node.body)
38
+
39
+ add_offense(node) do |corrector|
40
+ range = if node.single_line?
41
+ range_with_surrounding_space(range: node.body.loc.expression)
42
+ else
43
+ range_by_whole_lines(node.body.loc.expression, include_final_newline: true)
44
+ end
45
+
46
+ corrector.remove(range)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -13,25 +13,32 @@ module RuboCop
13
13
  # will also suggest constructing error objects when the exception is
14
14
  # passed multiple arguments.
15
15
  #
16
+ # The exploded style has an `AllowedCompactTypes` configuration
17
+ # option that takes an Array of exception name Strings.
18
+ #
16
19
  # @example EnforcedStyle: exploded (default)
17
20
  # # bad
18
- # raise StandardError.new("message")
21
+ # raise StandardError.new('message')
19
22
  #
20
23
  # # good
21
- # raise StandardError, "message"
22
- # fail "message"
24
+ # raise StandardError, 'message'
25
+ # fail 'message'
23
26
  # raise MyCustomError.new(arg1, arg2, arg3)
24
27
  # raise MyKwArgError.new(key1: val1, key2: val2)
25
28
  #
29
+ # # With `AllowedCompactTypes` set to ['MyWrappedError']
30
+ # raise MyWrappedError.new(obj)
31
+ # raise MyWrappedError.new(obj), 'message'
32
+ #
26
33
  # @example EnforcedStyle: compact
27
34
  # # bad
28
- # raise StandardError, "message"
35
+ # raise StandardError, 'message'
29
36
  # raise RuntimeError, arg1, arg2, arg3
30
37
  #
31
38
  # # good
32
- # raise StandardError.new("message")
39
+ # raise StandardError.new('message')
33
40
  # raise MyCustomError.new(arg1, arg2, arg3)
34
- # fail "message"
41
+ # fail 'message'
35
42
  class RaiseArgs < Base
36
43
  include ConfigurableEnforcedStyle
37
44
  extend AutoCorrector
@@ -102,6 +109,8 @@ module RuboCop
102
109
  return unless first_arg.send_type? && first_arg.method?(:new)
103
110
  return if acceptable_exploded_args?(first_arg.arguments)
104
111
 
112
+ return if allowed_non_exploded_type?(first_arg)
113
+
105
114
  add_offense(node, message: format(EXPLODED_MSG, method: node.method_name)) do |corrector|
106
115
  replacement = correction_compact_to_exploded(node)
107
116
 
@@ -123,6 +132,12 @@ module RuboCop
123
132
  arg.hash_type? || arg.splat_type?
124
133
  end
125
134
 
135
+ def allowed_non_exploded_type?(arg)
136
+ type = arg.receiver.const_name
137
+
138
+ Array(cop_config['AllowedCompactTypes']).include?(type)
139
+ end
140
+
126
141
  def requires_parens?(parent)
127
142
  parent.and_type? || parent.or_type? ||
128
143
  parent.if_type? && parent.ternary?
@@ -85,10 +85,7 @@ module RuboCop
85
85
  end
86
86
 
87
87
  def on_kwbegin(node)
88
- return if node.parent&.assignment?
89
-
90
- first_child = node.children.first
91
- return if first_child.rescue_type? || first_child.ensure_type?
88
+ return if contain_rescue_or_ensure?(node) || valid_context_using_only_begin?(node)
92
89
 
93
90
  register_offense(node)
94
91
  end
@@ -101,6 +98,19 @@ module RuboCop
101
98
  corrector.remove(node.loc.end)
102
99
  end
103
100
  end
101
+
102
+ def contain_rescue_or_ensure?(node)
103
+ first_child = node.children.first
104
+
105
+ first_child.rescue_type? || first_child.ensure_type?
106
+ end
107
+
108
+ def valid_context_using_only_begin?(node)
109
+ parent = node.parent
110
+
111
+ node.each_ancestor.any?(&:assignment?) || parent&.post_condition_loop? ||
112
+ parent&.send_type? || parent&.operator_keyword?
113
+ end
104
114
  end
105
115
  end
106
116
  end
@@ -104,9 +104,13 @@ module RuboCop
104
104
  return offense(begin_node, 'a variable') if node.variable?
105
105
  return offense(begin_node, 'a constant') if node.const_type?
106
106
 
107
+ return offense(begin_node, 'an interpolated expression') if interpolation?(begin_node)
108
+
107
109
  check_send(begin_node, node) if node.call_type?
108
110
  end
109
111
 
112
+ def_node_matcher :interpolation?, '[^begin ^^dstr]'
113
+
110
114
  def check_send(begin_node, node)
111
115
  return check_unary(begin_node, node) if node.unary_operation?
112
116
 
@@ -19,6 +19,12 @@ module RuboCop
19
19
  # # good
20
20
  # r = /\s/
21
21
  #
22
+ # # bad
23
+ # r = %r{/[b]}
24
+ #
25
+ # # good
26
+ # r = %r{/b}
27
+ #
22
28
  # # good
23
29
  # r = /[ab]/
24
30
  class RedundantRegexpCharacterClass < Base
@@ -48,7 +54,7 @@ module RuboCop
48
54
  each_single_element_character_class(node) do |char_class|
49
55
  next unless redundant_single_element_character_class?(node, char_class)
50
56
 
51
- yield node.loc.begin.adjust(begin_pos: 1 + char_class.ts, end_pos: char_class.te)
57
+ yield char_class.loc.body
52
58
  end
53
59
  end
54
60
 
@@ -82,7 +82,7 @@ module RuboCop
82
82
 
83
83
  def each_escape(node)
84
84
  node.parsed_tree&.traverse&.reduce(0) do |char_class_depth, (event, expr)|
85
- yield(expr.text[1], expr.ts, !char_class_depth.zero?) if expr.type == :escape
85
+ yield(expr.text[1], expr.start_index, !char_class_depth.zero?) if expr.type == :escape
86
86
 
87
87
  if expr.type == :set
88
88
  char_class_depth + (event == :enter ? 1 : -1)
@@ -129,6 +129,9 @@ module RuboCop
129
129
  def allowed_send_node?(node)
130
130
  @allowed_send_nodes.include?(node) ||
131
131
  @local_variables_scopes[node].include?(node.method_name) ||
132
+ node.each_ancestor.any? do |ancestor|
133
+ @local_variables_scopes[ancestor].include?(node.method_name)
134
+ end ||
132
135
  KERNEL_METHODS.include?(node.method_name)
133
136
  end
134
137
 
@@ -142,10 +142,22 @@ module RuboCop
142
142
  end
143
143
 
144
144
  def comments(node)
145
- processed_source.each_comment_in_lines(
146
- node.loc.first_line...
147
- node.loc.last_line
148
- ).to_a
145
+ relevant_comment_ranges(node).each.with_object([]) do |range, comments|
146
+ comments.concat(processed_source.each_comment_in_lines(range).to_a)
147
+ end
148
+ end
149
+
150
+ def relevant_comment_ranges(node)
151
+ # Get source lines ranges inside the if node that aren't inside an inner node
152
+ # Comments inside an inner node should remain attached to that node, and not
153
+ # moved.
154
+ begin_pos = node.loc.first_line
155
+ end_pos = node.loc.last_line
156
+
157
+ node.child_nodes.each.with_object([]) do |child, ranges|
158
+ ranges << (begin_pos...child.loc.first_line)
159
+ begin_pos = child.loc.last_line
160
+ end << (begin_pos...end_pos)
149
161
  end
150
162
 
151
163
  def allowed_if_condition?(node)
@@ -70,6 +70,9 @@ module RuboCop
70
70
  private
71
71
 
72
72
  def check_for_line_terminator_or_opener
73
+ # Make the obvious check first
74
+ return unless @processed_source.raw_source.include?(';')
75
+
73
76
  each_semicolon { |line, column| convention_on(line, column, true) }
74
77
  end
75
78
 
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # This cop checks for places where classes with only class methods can be
7
+ # replaced with a module. Classes should be used only when it makes sense to create
8
+ # instances out of them.
9
+ #
10
+ # This cop is marked as unsafe, because it is possible that this class is a parent
11
+ # for some other subclass, monkey-patched with instance methods or
12
+ # a dummy instance is instantiated from it somewhere.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # class SomeClass
17
+ # def self.some_method
18
+ # # body omitted
19
+ # end
20
+ #
21
+ # def self.some_other_method
22
+ # # body omitted
23
+ # end
24
+ # end
25
+ #
26
+ # # good
27
+ # module SomeModule
28
+ # module_function
29
+ #
30
+ # def some_method
31
+ # # body omitted
32
+ # end
33
+ #
34
+ # def some_other_method
35
+ # # body omitted
36
+ # end
37
+ # end
38
+ #
39
+ # # good - has instance method
40
+ # class SomeClass
41
+ # def instance_method; end
42
+ # def self.class_method; end
43
+ # end
44
+ #
45
+ class StaticClass < Base
46
+ include VisibilityHelp
47
+
48
+ MSG = 'Prefer modules to classes with only class methods.'
49
+
50
+ def on_class(class_node)
51
+ return if class_node.parent_class
52
+
53
+ add_offense(class_node) if class_convertible_to_module?(class_node)
54
+ end
55
+
56
+ private
57
+
58
+ def class_convertible_to_module?(class_node)
59
+ nodes = class_elements(class_node)
60
+ return false if nodes.empty?
61
+
62
+ nodes.all? do |node|
63
+ node_visibility(node) == :public &&
64
+ node.defs_type? ||
65
+ sclass_convertible_to_module?(node) ||
66
+ node.equals_asgn? ||
67
+ extend_call?(node)
68
+ end
69
+ end
70
+
71
+ def extend_call?(node)
72
+ node.send_type? && node.method?(:extend)
73
+ end
74
+
75
+ def sclass_convertible_to_module?(node)
76
+ return false unless node.sclass_type?
77
+
78
+ class_elements(node).all? do |child|
79
+ node_visibility(child) == :public && (child.def_type? || child.equals_asgn?)
80
+ end
81
+ end
82
+
83
+ def class_elements(class_node)
84
+ class_def = class_node.body
85
+
86
+ if !class_def
87
+ []
88
+ elsif class_def.begin_type?
89
+ class_def.children
90
+ else
91
+ [class_def]
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -33,6 +33,10 @@ module RuboCop
33
33
  }
34
34
  PATTERN
35
35
 
36
+ def on_new_investigation
37
+ @corrected_nodes = nil
38
+ end
39
+
36
40
  def on_send(node)
37
41
  return unless string_concatenation?(node)
38
42
 
@@ -42,8 +46,12 @@ module RuboCop
42
46
  collect_parts(topmost_plus_node, parts)
43
47
 
44
48
  add_offense(topmost_plus_node) do |corrector|
45
- if parts.none? { |part| uncorrectable?(part) }
49
+ correctable_parts = parts.none? { |part| uncorrectable?(part) }
50
+ if correctable_parts && !corrected_ancestor?(topmost_plus_node)
46
51
  corrector.replace(topmost_plus_node, replacement(parts))
52
+
53
+ @corrected_nodes ||= Set.new.compare_by_identity
54
+ @corrected_nodes.add(topmost_plus_node)
47
55
  end
48
56
  end
49
57
  end
@@ -80,6 +88,10 @@ module RuboCop
80
88
  part.each_descendant(:block).any?
81
89
  end
82
90
 
91
+ def corrected_ancestor?(node)
92
+ node.each_ancestor(:send).any? { |ancestor| @corrected_nodes&.include?(ancestor) }
93
+ end
94
+
83
95
  def replacement(parts)
84
96
  interpolated_parts =
85
97
  parts.map do |part|