rubocop 1.17.0 → 1.18.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +73 -28
  4. data/lib/rubocop.rb +2 -0
  5. data/lib/rubocop/cli/command/suggest_extensions.rb +3 -3
  6. data/lib/rubocop/config_loader.rb +1 -1
  7. data/lib/rubocop/config_validator.rb +5 -5
  8. data/lib/rubocop/cop/base.rb +2 -2
  9. data/lib/rubocop/cop/bundler/duplicated_gem.rb +1 -1
  10. data/lib/rubocop/cop/bundler/gem_version.rb +38 -4
  11. data/lib/rubocop/cop/corrector.rb +4 -4
  12. data/lib/rubocop/cop/generator.rb +1 -1
  13. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +1 -1
  14. data/lib/rubocop/cop/layout/argument_alignment.rb +1 -1
  15. data/lib/rubocop/cop/layout/array_alignment.rb +2 -2
  16. data/lib/rubocop/cop/layout/block_alignment.rb +1 -1
  17. data/lib/rubocop/cop/layout/closing_parenthesis_indentation.rb +7 -1
  18. data/lib/rubocop/cop/layout/comment_indentation.rb +1 -1
  19. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +2 -2
  20. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +2 -2
  21. data/lib/rubocop/cop/layout/first_parameter_indentation.rb +1 -1
  22. data/lib/rubocop/cop/layout/hash_alignment.rb +9 -8
  23. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +1 -1
  24. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +122 -0
  25. data/lib/rubocop/cop/layout/multiline_array_brace_layout.rb +6 -6
  26. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +2 -2
  27. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +6 -6
  28. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +6 -6
  29. data/lib/rubocop/cop/layout/multiline_method_definition_brace_layout.rb +6 -6
  30. data/lib/rubocop/cop/layout/multiline_operation_indentation.rb +3 -3
  31. data/lib/rubocop/cop/layout/parameter_alignment.rb +2 -2
  32. data/lib/rubocop/cop/layout/space_around_operators.rb +1 -1
  33. data/lib/rubocop/cop/lint/nested_percent_literal.rb +1 -1
  34. data/lib/rubocop/cop/lint/percent_string_array.rb +1 -1
  35. data/lib/rubocop/cop/lint/percent_symbol_array.rb +1 -1
  36. data/lib/rubocop/cop/lint/symbol_conversion.rb +1 -1
  37. data/lib/rubocop/cop/lint/unused_block_argument.rb +1 -1
  38. data/lib/rubocop/cop/lint/useless_assignment.rb +1 -1
  39. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  40. data/lib/rubocop/cop/mixin/check_line_breakable.rb +10 -1
  41. data/lib/rubocop/cop/naming/inclusive_language.rb +249 -0
  42. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +2 -2
  43. data/lib/rubocop/cop/style/class_and_module_children.rb +14 -0
  44. data/lib/rubocop/cop/style/multiple_comparison.rb +1 -1
  45. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +1 -1
  46. data/lib/rubocop/cop/style/quoted_symbols.rb +2 -2
  47. data/lib/rubocop/cop/style/redundant_regexp_character_class.rb +1 -1
  48. data/lib/rubocop/cop/style/regexp_literal.rb +3 -2
  49. data/lib/rubocop/cop/style/special_global_vars.rb +3 -3
  50. data/lib/rubocop/cop/style/string_concatenation.rb +32 -5
  51. data/lib/rubocop/cop/style/string_literals.rb +2 -2
  52. data/lib/rubocop/cop/style/swap_values.rb +1 -1
  53. data/lib/rubocop/cop/style/unpack_first.rb +1 -1
  54. data/lib/rubocop/cop/variable_force/variable_table.rb +1 -1
  55. data/lib/rubocop/options.rb +4 -4
  56. data/lib/rubocop/rspec/cop_helper.rb +1 -1
  57. data/lib/rubocop/rspec/expect_offense.rb +1 -1
  58. data/lib/rubocop/version.rb +1 -1
  59. metadata +9 -7
@@ -70,7 +70,7 @@ module RuboCop
70
70
 
71
71
  MSG = 'Unnecessary symbol conversion; use `%<correction>s` instead.'
72
72
  MSG_CONSISTENCY = 'Symbol hash key should be quoted for consistency; ' \
73
- 'use `%<correction>s` instead.'
73
+ 'use `%<correction>s` instead.'
74
74
  RESTRICT_ON_SEND = %i[to_sym intern].freeze
75
75
 
76
76
  def on_send(node)
@@ -143,7 +143,7 @@ module RuboCop
143
143
 
144
144
  def message_for_underscore_prefix(variable)
145
145
  "If it's necessary, use `_` or `_#{variable.name}` " \
146
- "as an argument name to indicate that it won't be used."
146
+ "as an argument name to indicate that it won't be used."
147
147
  end
148
148
 
149
149
  def define_method_call?(variable)
@@ -85,7 +85,7 @@ module RuboCop
85
85
  return unless assignment.meta_assignment_node.equal?(return_value_node)
86
86
 
87
87
  " Use `#{assignment.operator.sub(/=$/, '')}` " \
88
- "instead of `#{assignment.operator}`."
88
+ "instead of `#{assignment.operator}`."
89
89
  end
90
90
 
91
91
  def similar_name_message(variable)
@@ -50,7 +50,7 @@ module RuboCop
50
50
  ->(node) { heredoc_node?(node) }
51
51
  else
52
52
  raise ArgumentError, "Unknown foldable type: #{type.inspect}. "\
53
- "Valid foldable types are: #{FOLDABLE_TYPES.join(', ')}."
53
+ "Valid foldable types are: #{FOLDABLE_TYPES.join(', ')}."
54
54
  end
55
55
  end
56
56
  end
@@ -72,7 +72,9 @@ module RuboCop
72
72
 
73
73
  # If a `send` node is not parenthesized, don't move the first element, because it
74
74
  # can result in changed behavior or a syntax error.
75
- elements = elements.drop(1) if node.send_type? && !node.parenthesized?
75
+ if node.send_type? && !node.parenthesized? && !first_argument_is_heredoc?(node)
76
+ elements = elements.drop(1)
77
+ end
76
78
 
77
79
  i = 0
78
80
  i += 1 while within_column_limit?(elements[i], max, line)
@@ -84,6 +86,13 @@ module RuboCop
84
86
  elements[i - 1]
85
87
  end
86
88
 
89
+ # @api private
90
+ def first_argument_is_heredoc?(node)
91
+ first_argument = node.first_argument
92
+
93
+ first_argument.respond_to?(:heredoc?) && first_argument.heredoc?
94
+ end
95
+
87
96
  # @api private
88
97
  # If a send node contains a heredoc argument, splitting cannot happen
89
98
  # after the heredoc or else it will cause a syntax error.
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Naming
6
+ # This cops recommends the use of inclusive language instead of problematic terms.
7
+ # The cop can check the following locations for offenses:
8
+ # - identifiers
9
+ # - constants
10
+ # - variables
11
+ # - strings
12
+ # - symbols
13
+ # - comments
14
+ # - file paths
15
+ # Each of these locations can be individually enabled/disabled via configuration,
16
+ # for example CheckIdentifiers = true/false.
17
+ #
18
+ # Flagged terms are configurable for the cop. For each flagged term an optional
19
+ # Regex can be specified to identify offenses. Suggestions for replacing a flagged term can
20
+ # be configured and will be displayed as part of the offense message.
21
+ # An AllowedRegex can be specified for a flagged term to exempt allowed uses of the term.
22
+ #
23
+ # @example FlaggedTerms: { whitelist: { Suggestions: ['allowlist'] } }
24
+ # # Suggest replacing identifier whitelist with allowlist
25
+ #
26
+ # # bad
27
+ # whitelist_users = %w(user1 user1)
28
+ #
29
+ # # good
30
+ # allowlist_users = %w(user1 user2)
31
+ #
32
+ # @example FlaggedTerms: { master: { Suggestions: ['main', 'primary', 'leader'] } }
33
+ # # Suggest replacing master in an instance variable name with main, primary, or leader
34
+ #
35
+ # # bad
36
+ # @master_node = 'node1.example.com'
37
+ #
38
+ # # good
39
+ # @primary_node = 'node1.example.com'
40
+ #
41
+ # @example FlaggedTerms: { whitelist: { Regex: !ruby/regexp '/white[-_\s]?list' } }
42
+ # # Identify problematic terms using a Regexp
43
+ #
44
+ # # bad
45
+ # white_list = %w(user1 user2)
46
+ #
47
+ # # good
48
+ # allow_list = %w(user1 user2)
49
+ #
50
+ # @example FlaggedTerms: { master: { AllowedRegex: 'master\'?s degree' } }
51
+ # # Specify allowed uses of the flagged term as a string or regexp.
52
+ #
53
+ # # bad
54
+ # # They had a masters
55
+ #
56
+ # # good
57
+ # # They had a master's degree
58
+ #
59
+ class InclusiveLanguage < Base
60
+ include RangeHelp
61
+
62
+ EMPTY_ARRAY = [].freeze
63
+
64
+ WordLocation = Struct.new(:word, :position)
65
+
66
+ def initialize(config = nil, options = nil)
67
+ super
68
+ @flagged_term_hash = {}
69
+ @flagged_terms_regex = nil
70
+ @allowed_regex = nil
71
+ @check_token = preprocess_check_config
72
+ preprocess_flagged_terms
73
+ end
74
+
75
+ def on_new_investigation
76
+ investigate_filepath if cop_config['CheckFilepaths']
77
+ investigate_tokens
78
+ end
79
+
80
+ private
81
+
82
+ def investigate_tokens
83
+ processed_source.each_token do |token|
84
+ next unless check_token?(token.type)
85
+
86
+ word_locations = scan_for_words(token.text)
87
+ next if word_locations.empty?
88
+
89
+ add_offenses_for_token(token, word_locations)
90
+ end
91
+ end
92
+
93
+ def add_offenses_for_token(token, word_locations)
94
+ word_locations.each do |word_location|
95
+ start_position = token.pos.begin_pos + token.pos.source.index(word_location.word)
96
+ range = range_between(start_position, start_position + word_location.word.length)
97
+ add_offense(range, message: create_message(word_location.word))
98
+ end
99
+ end
100
+
101
+ def check_token?(type)
102
+ !!@check_token[type]
103
+ end
104
+
105
+ def preprocess_check_config # rubocop:disable Metrics/AbcSize
106
+ {
107
+ tIDENTIFIER: cop_config['CheckIdentifiers'],
108
+ tCONSTANT: cop_config['CheckConstants'],
109
+ tIVAR: cop_config['CheckVariables'],
110
+ tCVAR: cop_config['CheckVariables'],
111
+ tGVAR: cop_config['CheckVariables'],
112
+ tSYMBOL: cop_config['CheckSymbols'],
113
+ tSTRING: cop_config['CheckStrings'],
114
+ tSTRING_CONTENT: cop_config['CheckStrings'],
115
+ tCOMMENT: cop_config['CheckComments']
116
+ }.freeze
117
+ end
118
+
119
+ def preprocess_flagged_terms
120
+ allowed_strings = []
121
+ flagged_term_strings = []
122
+ cop_config['FlaggedTerms'].each do |term, term_definition|
123
+ next if term_definition.nil?
124
+
125
+ allowed_strings.concat(process_allowed_regex(term_definition['AllowedRegex']))
126
+ regex_string = ensure_regex_string(term_definition['Regex'] || term)
127
+ flagged_term_strings << regex_string
128
+
129
+ add_to_flagged_term_hash(regex_string, term, term_definition)
130
+ end
131
+
132
+ set_regexes(flagged_term_strings, allowed_strings)
133
+ end
134
+
135
+ def add_to_flagged_term_hash(regex_string, term, term_definition)
136
+ @flagged_term_hash[Regexp.new(regex_string, Regexp::IGNORECASE)] =
137
+ term_definition.merge('Term' => term,
138
+ 'SuggestionString' =>
139
+ preprocess_suggestions(term_definition['Suggestions']))
140
+ end
141
+
142
+ def set_regexes(flagged_term_strings, allowed_strings)
143
+ @flagged_terms_regex = array_to_ignorecase_regex(flagged_term_strings)
144
+ @allowed_regex = array_to_ignorecase_regex(allowed_strings) unless allowed_strings.empty?
145
+ end
146
+
147
+ def process_allowed_regex(allowed)
148
+ return EMPTY_ARRAY if allowed.nil?
149
+
150
+ Array(allowed).map do |allowed_term|
151
+ next if allowed_term.is_a?(String) && allowed_term.strip.empty?
152
+
153
+ ensure_regex_string(allowed_term)
154
+ end
155
+ end
156
+
157
+ def ensure_regex_string(regex)
158
+ regex.is_a?(Regexp) ? regex.source : regex
159
+ end
160
+
161
+ def array_to_ignorecase_regex(strings)
162
+ Regexp.new(strings.join('|'), Regexp::IGNORECASE)
163
+ end
164
+
165
+ def investigate_filepath
166
+ word_locations = scan_for_words(processed_source.file_path)
167
+
168
+ case word_locations.length
169
+ when 0
170
+ return
171
+ when 1
172
+ message = create_single_word_message_for_file(word_locations.first.word)
173
+ else
174
+ words = word_locations.map(&:word)
175
+ message = create_multiple_word_message_for_file(words)
176
+ end
177
+
178
+ range = source_range(processed_source.buffer, 1, 0)
179
+ add_offense(range, message: message)
180
+ end
181
+
182
+ def create_single_word_message_for_file(word)
183
+ create_message(word).sub(/\.$/, ' in file path.')
184
+ end
185
+
186
+ def create_multiple_word_message_for_file(words)
187
+ quoted_words = words.map { |word| "'#{word}'" }
188
+ "Consider replacing problematic terms #{quoted_words.join(', ')} in file path."
189
+ end
190
+
191
+ def scan_for_words(input)
192
+ mask_input(input).enum_for(:scan, @flagged_terms_regex).map do
193
+ match = Regexp.last_match
194
+ WordLocation.new(match.to_s, match.offset(0).first)
195
+ end
196
+ end
197
+
198
+ def mask_input(str)
199
+ return str if @allowed_regex.nil?
200
+
201
+ safe_str = if str.valid_encoding?
202
+ str
203
+ else
204
+ str.encode('UTF-8', invalid: :replace, undef: :replace)
205
+ end
206
+ safe_str.gsub(@allowed_regex) { |match| '*' * match.size }
207
+ end
208
+
209
+ def create_message(word)
210
+ flagged_term = find_flagged_term(word)
211
+ "Consider replacing problematic term '#{word}'#{flagged_term['SuggestionString']}."
212
+ end
213
+
214
+ def find_flagged_term(word)
215
+ _regexp, flagged_term = @flagged_term_hash.find do |key, _term|
216
+ key.match?(word)
217
+ end
218
+ flagged_term
219
+ end
220
+
221
+ def create_message_for_file(word)
222
+ create_message(word).sub(/\.$/, ' in file path.')
223
+ end
224
+
225
+ def preprocess_suggestions(suggestions)
226
+ return '' if suggestions.nil? ||
227
+ (suggestions.is_a?(String) && suggestions.strip.empty?) || suggestions.empty?
228
+
229
+ format_suggestions(suggestions)
230
+ end
231
+
232
+ def format_suggestions(suggestions)
233
+ quoted_suggestions = Array(suggestions).map { |word| "'#{word}'" }
234
+ suggestion_str = case quoted_suggestions.size
235
+ when 1
236
+ quoted_suggestions.first
237
+ when 2
238
+ quoted_suggestions.join(' or ')
239
+ else
240
+ last_quoted = quoted_suggestions.pop
241
+ quoted_suggestions << "or #{last_quoted}"
242
+ quoted_suggestions.join(', ')
243
+ end
244
+ " with #{suggestion_str}"
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
@@ -147,9 +147,9 @@ module RuboCop
147
147
  include ConfigurableEnforcedStyle
148
148
 
149
149
  MSG = 'Memoized variable `%<var>s` does not match ' \
150
- 'method name `%<method>s`. Use `@%<suggested_var>s` instead.'
150
+ 'method name `%<method>s`. Use `@%<suggested_var>s` instead.'
151
151
  UNDERSCORE_REQUIRED = 'Memoized variable `%<var>s` does not start ' \
152
- 'with `_`. Use `@%<suggested_var>s` instead.'
152
+ 'with `_`. Use `@%<suggested_var>s` instead.'
153
153
  DYNAMIC_DEFINE_METHODS = %i[define_method define_singleton_method].to_set.freeze
154
154
 
155
155
  # @!method method_definition?(node)
@@ -84,6 +84,7 @@ module RuboCop
84
84
  def compact_definition(corrector, node)
85
85
  compact_node(corrector, node)
86
86
  remove_end(corrector, node.body)
87
+ unindent(corrector, node)
87
88
  end
88
89
 
89
90
  def compact_node(corrector, node)
@@ -114,6 +115,19 @@ module RuboCop
114
115
  corrector.remove(range)
115
116
  end
116
117
 
118
+ def configured_indentation_width
119
+ config.for_badge(Layout::IndentationWidth.badge).fetch('Width', 2)
120
+ end
121
+
122
+ def unindent(corrector, node)
123
+ return if node.body.children.last.nil?
124
+
125
+ column_delta = configured_indentation_width - leading_spaces(node.body.children.last).size
126
+ return if column_delta.zero?
127
+
128
+ AlignmentCorrector.correct(corrector, processed_source, node, column_delta)
129
+ end
130
+
117
131
  def leading_spaces(node)
118
132
  node.source_range.source_line[/\A\s*/]
119
133
  end
@@ -44,7 +44,7 @@ module RuboCop
44
44
  extend AutoCorrector
45
45
 
46
46
  MSG = 'Avoid comparing a variable with multiple items ' \
47
- 'in a conditional, use `Array#include?` instead.'
47
+ 'in a conditional, use `Array#include?` instead.'
48
48
 
49
49
  def on_new_investigation
50
50
  @last_comparison = nil
@@ -68,7 +68,7 @@ module RuboCop
68
68
  delimiters = preferred_delimiters_for(type)
69
69
 
70
70
  "`#{type}`-literals should be delimited by " \
71
- "`#{delimiters[0]}` and `#{delimiters[1]}`."
71
+ "`#{delimiters[0]}` and `#{delimiters[1]}`."
72
72
  end
73
73
 
74
74
  def preferred_delimiters_for(type)
@@ -36,9 +36,9 @@ module RuboCop
36
36
  extend AutoCorrector
37
37
 
38
38
  MSG_SINGLE = "Prefer single-quoted symbols when you don't need string interpolation " \
39
- 'or special symbols.'
39
+ 'or special symbols.'
40
40
  MSG_DOUBLE = 'Prefer double-quoted symbols unless you need single quotes to ' \
41
- 'avoid extra backslashes for escaping.'
41
+ 'avoid extra backslashes for escaping.'
42
42
 
43
43
  def on_sym(node)
44
44
  return unless quoted?(node)
@@ -32,7 +32,7 @@ module RuboCop
32
32
 
33
33
  REQUIRES_ESCAPE_OUTSIDE_CHAR_CLASS_CHARS = '.*+?{}()|$'.chars.freeze
34
34
  MSG_REDUNDANT_CHARACTER_CLASS = 'Redundant single-element character class, ' \
35
- '`%<char_class>s` can be replaced with `%<element>s`.'
35
+ '`%<char_class>s` can be replaced with `%<element>s`.'
36
36
 
37
37
  def on_regexp(node)
38
38
  each_redundant_character_class(node) do |loc|
@@ -117,7 +117,7 @@ module RuboCop
117
117
  def allowed_percent_r_literal?(node)
118
118
  style == :slashes && contains_disallowed_slash?(node) ||
119
119
  style == :percent_r ||
120
- allowed_mixed_percent_r?(node) || omit_parentheses_style?(node)
120
+ allowed_mixed_percent_r?(node) || allowed_omit_parentheses_with_percent_r_literal?(node)
121
121
  end
122
122
 
123
123
  def allowed_mixed_percent_r?(node)
@@ -149,8 +149,9 @@ module RuboCop
149
149
  config.for_cop('Style/PercentLiteralDelimiters') ['PreferredDelimiters']['%r'].chars
150
150
  end
151
151
 
152
- def omit_parentheses_style?(node)
152
+ def allowed_omit_parentheses_with_percent_r_literal?(node)
153
153
  return false unless node.parent&.call_type?
154
+ return true if node.content.start_with?(' ')
154
155
 
155
156
  enforced_style = config.for_cop('Style/MethodCallWithArgsParentheses')['EnforcedStyle']
156
157
 
@@ -53,10 +53,10 @@ module RuboCop
53
53
  extend AutoCorrector
54
54
 
55
55
  MSG_BOTH = 'Prefer `%<prefer>s` from the stdlib \'English\' ' \
56
- 'module (don\'t forget to require it) or `%<regular>s` over ' \
57
- '`%<global>s`.'
56
+ 'module (don\'t forget to require it) or `%<regular>s` over ' \
57
+ '`%<global>s`.'
58
58
  MSG_ENGLISH = 'Prefer `%<prefer>s` from the stdlib \'English\' ' \
59
- 'module (don\'t forget to require it) over `%<global>s`.'
59
+ 'module (don\'t forget to require it) over `%<global>s`.'
60
60
  MSG_REGULAR = 'Prefer `%<prefer>s` over `%<global>s`.'
61
61
 
62
62
  ENGLISH_VARS = { # rubocop:disable Style/MutableConstant
@@ -15,18 +15,37 @@ module RuboCop
15
15
  # lines, this cop does not register an offense; instead,
16
16
  # `Style/LineEndConcatenation` will pick up the offense if enabled.
17
17
  #
18
- # @example
18
+ # Two modes are supported:
19
+ # 1. `aggressive` style checks and corrects all occurrences of `+` where
20
+ # either the left or right side of `+` is a string literal.
21
+ # 2. `conservative` style on the other hand, checks and corrects only if
22
+ # left side (receiver of `+` method call) is a string literal.
23
+ # This is useful when the receiver is some expression that returns string like `Pathname`
24
+ # instead of a string literal.
25
+ #
26
+ # @example Mode: aggressive (default)
19
27
  # # bad
20
28
  # email_with_name = user.name + ' <' + user.email + '>'
29
+ # Pathname.new('/') + 'test'
21
30
  #
22
31
  # # good
23
32
  # email_with_name = "#{user.name} <#{user.email}>"
24
33
  # email_with_name = format('%s <%s>', user.name, user.email)
34
+ # "#{Pathname.new('/')}test"
25
35
  #
26
36
  # # accepted, line-end concatenation
27
37
  # name = 'First' +
28
38
  # 'Last'
29
39
  #
40
+ # @example Mode: conservative
41
+ # # bad
42
+ # 'Hello' + user.name
43
+ #
44
+ # # good
45
+ # "Hello #{user.name}"
46
+ # user.name + '!!'
47
+ # Pathname.new('/') + 'test'
48
+ #
30
49
  class StringConcatenation < Base
31
50
  include Util
32
51
  include RangeHelp
@@ -52,10 +71,15 @@ module RuboCop
52
71
  return if line_end_concatenation?(node)
53
72
 
54
73
  topmost_plus_node = find_topmost_plus_node(node)
74
+ parts = collect_parts(topmost_plus_node)
75
+ return unless parts[0..-2].any? { |receiver_node| offensive_for_mode?(receiver_node) }
55
76
 
56
- parts = []
57
- collect_parts(topmost_plus_node, parts)
77
+ register_offense(topmost_plus_node, parts)
78
+ end
79
+
80
+ private
58
81
 
82
+ def register_offense(topmost_plus_node, parts)
59
83
  add_offense(topmost_plus_node) do |corrector|
60
84
  correctable_parts = parts.none? { |part| uncorrectable?(part) }
61
85
  if correctable_parts && !corrected_ancestor?(topmost_plus_node)
@@ -67,7 +91,10 @@ module RuboCop
67
91
  end
68
92
  end
69
93
 
70
- private
94
+ def offensive_for_mode?(receiver_node)
95
+ mode = cop_config['Mode'].to_sym
96
+ mode == :aggressive || mode == :conservative && receiver_node.str_type?
97
+ end
71
98
 
72
99
  def line_end_concatenation?(node)
73
100
  # If the concatenation happens at the end of the line,
@@ -87,7 +114,7 @@ module RuboCop
87
114
  current
88
115
  end
89
116
 
90
- def collect_parts(node, parts)
117
+ def collect_parts(node, parts = [])
91
118
  return unless node
92
119
 
93
120
  if plus_node?(node)