rubocop 1.17.0 → 1.18.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +70 -29
  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_loader_resolver.rb +1 -1
  8. data/lib/rubocop/config_validator.rb +23 -10
  9. data/lib/rubocop/cop/base.rb +2 -2
  10. data/lib/rubocop/cop/bundler/duplicated_gem.rb +1 -1
  11. data/lib/rubocop/cop/bundler/gem_version.rb +38 -4
  12. data/lib/rubocop/cop/corrector.rb +4 -4
  13. data/lib/rubocop/cop/generator.rb +1 -1
  14. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +1 -1
  15. data/lib/rubocop/cop/layout/argument_alignment.rb +1 -1
  16. data/lib/rubocop/cop/layout/array_alignment.rb +2 -2
  17. data/lib/rubocop/cop/layout/block_alignment.rb +1 -1
  18. data/lib/rubocop/cop/layout/class_structure.rb +5 -1
  19. data/lib/rubocop/cop/layout/closing_parenthesis_indentation.rb +7 -1
  20. data/lib/rubocop/cop/layout/comment_indentation.rb +1 -1
  21. data/lib/rubocop/cop/layout/end_alignment.rb +8 -1
  22. data/lib/rubocop/cop/layout/first_argument_indentation.rb +1 -1
  23. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +2 -2
  24. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +2 -2
  25. data/lib/rubocop/cop/layout/first_parameter_indentation.rb +1 -1
  26. data/lib/rubocop/cop/layout/hash_alignment.rb +25 -24
  27. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +1 -1
  28. data/lib/rubocop/cop/layout/indentation_style.rb +2 -2
  29. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +122 -0
  30. data/lib/rubocop/cop/layout/multiline_array_brace_layout.rb +6 -6
  31. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +2 -2
  32. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +6 -6
  33. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +6 -6
  34. data/lib/rubocop/cop/layout/multiline_method_definition_brace_layout.rb +6 -6
  35. data/lib/rubocop/cop/layout/multiline_operation_indentation.rb +3 -3
  36. data/lib/rubocop/cop/layout/parameter_alignment.rb +2 -2
  37. data/lib/rubocop/cop/layout/space_around_operators.rb +5 -1
  38. data/lib/rubocop/cop/lint/duplicate_branch.rb +2 -1
  39. data/lib/rubocop/cop/lint/nested_percent_literal.rb +1 -1
  40. data/lib/rubocop/cop/lint/percent_string_array.rb +1 -1
  41. data/lib/rubocop/cop/lint/percent_symbol_array.rb +1 -1
  42. data/lib/rubocop/cop/lint/symbol_conversion.rb +1 -1
  43. data/lib/rubocop/cop/lint/unused_block_argument.rb +1 -1
  44. data/lib/rubocop/cop/lint/useless_assignment.rb +1 -1
  45. data/lib/rubocop/cop/lint/useless_times.rb +1 -1
  46. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  47. data/lib/rubocop/cop/mixin/check_line_breakable.rb +12 -3
  48. data/lib/rubocop/cop/mixin/hash_transform_method.rb +6 -1
  49. data/lib/rubocop/cop/naming/inclusive_language.rb +249 -0
  50. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +2 -2
  51. data/lib/rubocop/cop/style/block_delimiters.rb +15 -0
  52. data/lib/rubocop/cop/style/class_and_module_children.rb +14 -0
  53. data/lib/rubocop/cop/style/comment_annotation.rb +50 -6
  54. data/lib/rubocop/cop/style/double_cop_disable_directive.rb +1 -7
  55. data/lib/rubocop/cop/style/eval_with_location.rb +1 -1
  56. data/lib/rubocop/cop/style/frozen_string_literal_comment.rb +8 -2
  57. data/lib/rubocop/cop/style/hash_syntax.rb +1 -1
  58. data/lib/rubocop/cop/style/multiple_comparison.rb +1 -1
  59. data/lib/rubocop/cop/style/mutable_constant.rb +6 -8
  60. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +1 -1
  61. data/lib/rubocop/cop/style/quoted_symbols.rb +2 -2
  62. data/lib/rubocop/cop/style/redundant_regexp_character_class.rb +1 -1
  63. data/lib/rubocop/cop/style/regexp_literal.rb +3 -2
  64. data/lib/rubocop/cop/style/single_line_methods.rb +25 -15
  65. data/lib/rubocop/cop/style/special_global_vars.rb +3 -3
  66. data/lib/rubocop/cop/style/string_concatenation.rb +32 -5
  67. data/lib/rubocop/cop/style/string_literals.rb +2 -2
  68. data/lib/rubocop/cop/style/swap_values.rb +1 -1
  69. data/lib/rubocop/cop/style/unpack_first.rb +1 -1
  70. data/lib/rubocop/cop/variable_force/variable_table.rb +1 -1
  71. data/lib/rubocop/formatter/git_hub_actions_formatter.rb +1 -1
  72. data/lib/rubocop/options.rb +4 -4
  73. data/lib/rubocop/rspec/cop_helper.rb +1 -1
  74. data/lib/rubocop/rspec/expect_offense.rb +1 -1
  75. data/lib/rubocop/version.rb +1 -1
  76. metadata +11 -9
@@ -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)
@@ -135,6 +135,7 @@ module RuboCop
135
135
  class BlockDelimiters < Base
136
136
  include ConfigurableEnforcedStyle
137
137
  include IgnoredMethods
138
+ include RangeHelp
138
139
  extend AutoCorrector
139
140
 
140
141
  ALWAYS_BRACES_MESSAGE = 'Prefer `{...}` over `do...end` for blocks.'
@@ -231,6 +232,11 @@ module RuboCop
231
232
  corrector.insert_before(e, ' ') unless whitespace_before?(e)
232
233
  corrector.insert_after(b, ' ') unless whitespace_after?(b)
233
234
  corrector.replace(b, 'do')
235
+
236
+ if (comment = processed_source.comment_at_line(e.line))
237
+ move_comment_before_block(corrector, comment, loc.node, e)
238
+ end
239
+
234
240
  corrector.replace(e, 'end')
235
241
  end
236
242
 
@@ -252,6 +258,15 @@ module RuboCop
252
258
  /\s/.match?(range.source_buffer.source[range.begin_pos + length, 1])
253
259
  end
254
260
 
261
+ def move_comment_before_block(corrector, comment, block_node, closing_brace)
262
+ range = range_between(closing_brace.end_pos, comment.loc.expression.end_pos)
263
+
264
+ corrector.remove(range_with_surrounding_space(range: range, side: :right))
265
+ corrector.insert_after(closing_brace, "\n")
266
+
267
+ corrector.insert_before(block_node, "#{comment.text}\n")
268
+ end
269
+
255
270
  def get_blocks(node, &block)
256
271
  case node.type
257
272
  when :block
@@ -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
@@ -12,7 +12,7 @@ module RuboCop
12
12
  # incorrect registering of keywords (eg. `review`) inside a paragraph as an
13
13
  # annotation.
14
14
  #
15
- # @example
15
+ # @example RequireColon: true (default)
16
16
  # # bad
17
17
  # # TODO make better
18
18
  #
@@ -36,14 +36,36 @@ module RuboCop
36
36
  #
37
37
  # # good
38
38
  # # OPTIMIZE: does not work
39
+ #
40
+ # @example RequireColon: false
41
+ # # bad
42
+ # # TODO: make better
43
+ #
44
+ # # good
45
+ # # TODO make better
46
+ #
47
+ # # bad
48
+ # # fixme does not work
49
+ #
50
+ # # good
51
+ # # FIXME does not work
52
+ #
53
+ # # bad
54
+ # # Optimize does not work
55
+ #
56
+ # # good
57
+ # # OPTIMIZE does not work
39
58
  class CommentAnnotation < Base
40
59
  include AnnotationComment
41
60
  include RangeHelp
42
61
  extend AutoCorrector
43
62
 
44
- MSG = 'Annotation keywords like `%<keyword>s` should be all ' \
45
- 'upper case, followed by a colon, and a space, ' \
46
- 'then a note describing the problem.'
63
+ MSG_COLON_STYLE = 'Annotation keywords like `%<keyword>s` should be all ' \
64
+ 'upper case, followed by a colon, and a space, ' \
65
+ 'then a note describing the problem.'
66
+ MSG_SPACE_STYLE = 'Annotation keywords like `%<keyword>s` should be all ' \
67
+ 'upper case, followed by a space, ' \
68
+ 'then a note describing the problem.'
47
69
  MISSING_NOTE = 'Annotation comment, with keyword `%<keyword>s`, is missing a note.'
48
70
 
49
71
  def on_new_investigation
@@ -63,13 +85,15 @@ module RuboCop
63
85
  private
64
86
 
65
87
  def register_offense(range, note, first_word)
88
+ message = requires_colon? ? MSG_COLON_STYLE : MSG_SPACE_STYLE
89
+
66
90
  add_offense(
67
91
  range,
68
- message: format(note ? MSG : MISSING_NOTE, keyword: first_word)
92
+ message: format(note ? message : MISSING_NOTE, keyword: first_word)
69
93
  ) do |corrector|
70
94
  next if note.nil?
71
95
 
72
- corrector.replace(range, "#{first_word.upcase}: ")
96
+ correct_offense(corrector, range, first_word)
73
97
  end
74
98
  end
75
99
 
@@ -92,8 +116,28 @@ module RuboCop
92
116
  end
93
117
 
94
118
  def correct_annotation?(first_word, colon, space, note)
119
+ return correct_colon_annotation?(first_word, colon, space, note) if requires_colon?
120
+
121
+ correct_space_annotation?(first_word, colon, space, note)
122
+ end
123
+
124
+ def correct_colon_annotation?(first_word, colon, space, note)
95
125
  keyword?(first_word) && (colon && space && note || !colon && !note)
96
126
  end
127
+
128
+ def correct_space_annotation?(first_word, colon, space, note)
129
+ keyword?(first_word) && (!colon && space && note || !colon && !note)
130
+ end
131
+
132
+ def correct_offense(corrector, range, first_word)
133
+ return corrector.replace(range, "#{first_word.upcase}: ") if requires_colon?
134
+
135
+ corrector.replace(range, "#{first_word.upcase} ")
136
+ end
137
+
138
+ def requires_colon?
139
+ cop_config['RequireColon']
140
+ end
97
141
  end
98
142
  end
99
143
  end
@@ -36,13 +36,7 @@ module RuboCop
36
36
  next unless comment.text.scan(/# rubocop:(?:disable|todo)/).size > 1
37
37
 
38
38
  add_offense(comment) do |corrector|
39
- prefix = if comment.text.start_with?('# rubocop:disable')
40
- '# rubocop:disable'
41
- else
42
- '# rubocop:todo'
43
- end
44
-
45
- corrector.replace(comment, comment.text[/#{prefix} \S+/])
39
+ corrector.replace(comment, comment.text.gsub(%r{ # rubocop:(disable|todo)}, ','))
46
40
  end
47
41
  end
48
42
  end
@@ -43,7 +43,7 @@ module RuboCop
43
43
  # RUBY
44
44
  #
45
45
  # This cop works only when a string literal is given as a code string.
46
- # No offence is reported if a string variable is given as below:
46
+ # No offense is reported if a string variable is given as below:
47
47
  #
48
48
  # @example
49
49
  # # not checked
@@ -5,11 +5,17 @@ module RuboCop
5
5
  module Style
6
6
  # This cop is designed to help you transition from mutable string literals
7
7
  # to frozen string literals.
8
- # It will add the comment `# frozen_string_literal: true` to the top of
9
- # files to enable frozen string literals. Frozen string literals may be
8
+ # It will add the `# frozen_string_literal: true` magic comment to the top
9
+ # of files to enable frozen string literals. Frozen string literals may be
10
10
  # default in future Ruby. The comment will be added below a shebang and
11
11
  # encoding comment.
12
12
  #
13
+ # Note that the cop will ignore files where the comment exists but is set
14
+ # to `false` instead of `true`.
15
+ #
16
+ # To require a blank line after this comment, please see
17
+ # `Layout/EmptyLineAfterMagicComment` cop.
18
+ #
13
19
  # @example EnforcedStyle: always (default)
14
20
  # # The `always` style will always add the frozen string literal comment
15
21
  # # to a file, regardless of the Ruby version or if `freeze` or `<<` are
@@ -74,7 +74,7 @@ module RuboCop
74
74
  ruby19_no_mixed_keys_check(pairs)
75
75
  elsif style == :no_mixed_keys
76
76
  no_mixed_keys_check(pairs)
77
- elsif node.source.include?('=>')
77
+ else
78
78
  ruby19_check(pairs)
79
79
  end
80
80
  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
@@ -61,13 +61,12 @@ module RuboCop
61
61
 
62
62
  def on_casgn(node)
63
63
  _scope, _const_name, value = *node
64
- on_assignment(value)
65
- end
64
+ if value.nil? # This is only the case for `CONST += ...` or similarg66
65
+ parent = node.parent
66
+ return unless parent.or_asgn_type? # We only care about `CONST ||= ...`
66
67
 
67
- def on_or_asgn(node)
68
- lhs, value = *node
69
-
70
- return unless lhs&.casgn_type?
68
+ value = parent.children.last
69
+ end
71
70
 
72
71
  on_assignment(value)
73
72
  end
@@ -118,14 +117,13 @@ module RuboCop
118
117
  end
119
118
 
120
119
  def mutable_literal?(value)
121
- return false if value.nil?
122
120
  return false if frozen_regexp_or_range_literals?(value)
123
121
 
124
122
  value.mutable_literal?
125
123
  end
126
124
 
127
125
  def immutable_literal?(node)
128
- node.nil? || frozen_regexp_or_range_literals?(node) || node.immutable_literal?
126
+ frozen_regexp_or_range_literals?(node) || node.immutable_literal?
129
127
  end
130
128
 
131
129
  def frozen_string_literal?(node)
@@ -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
 
@@ -36,6 +36,7 @@ module RuboCop
36
36
  extend AutoCorrector
37
37
 
38
38
  MSG = 'Avoid single-line method definitions.'
39
+ NOT_SUPPORTED_ENDLESS_METHOD_BODY_TYPES = %i[return break next].freeze
39
40
 
40
41
  def on_def(node)
41
42
  return unless node.single_line?
@@ -62,29 +63,24 @@ module RuboCop
62
63
 
63
64
  def correct_to_endless?(body_node)
64
65
  return false if target_ruby_version < 3.0
65
-
66
- endless_method_config = config.for_cop('Style/EndlessMethod')
67
-
68
- return false unless endless_method_config['Enabled']
69
- return false if endless_method_config['EnforcedStyle'] == 'disallow'
66
+ return false if disallow_endless_method_style?
70
67
  return false unless body_node
71
- return false if body_node.parent.assignment_method?
68
+ return false if body_node.parent.assignment_method? ||
69
+ NOT_SUPPORTED_ENDLESS_METHOD_BODY_TYPES.include?(body_node.type)
72
70
 
73
71
  !(body_node.begin_type? || body_node.kwbegin_type?)
74
72
  end
75
73
 
76
74
  def correct_to_multiline(corrector, node)
77
- each_part(node.body) do |part|
78
- LineBreakCorrector.break_line_before(
79
- range: part, node: node, corrector: corrector,
80
- configured_width: configured_indentation_width
81
- )
75
+ if (body = node.body) && body.begin_type? && body.parenthesized_call?
76
+ break_line_before(corrector, node, body)
77
+ else
78
+ each_part(body) do |part|
79
+ break_line_before(corrector, node, part)
80
+ end
82
81
  end
83
82
 
84
- LineBreakCorrector.break_line_before(
85
- range: node.loc.end, node: node, corrector: corrector,
86
- indent_steps: 0, configured_width: configured_indentation_width
87
- )
83
+ break_line_before(corrector, node, node.loc.end, indent_steps: 0)
88
84
 
89
85
  move_comment(node, corrector)
90
86
  end
@@ -98,6 +94,13 @@ module RuboCop
98
94
  corrector.replace(node, replacement)
99
95
  end
100
96
 
97
+ def break_line_before(corrector, node, range, indent_steps: 1)
98
+ LineBreakCorrector.break_line_before(
99
+ range: range, node: node, corrector: corrector,
100
+ configured_width: configured_indentation_width, indent_steps: indent_steps
101
+ )
102
+ end
103
+
101
104
  def each_part(body)
102
105
  return unless body
103
106
 
@@ -129,6 +132,13 @@ module RuboCop
129
132
  def require_parentheses?(method_body)
130
133
  method_body.send_type? && !method_body.arguments.empty? && !method_body.comparison_method?
131
134
  end
135
+
136
+ def disallow_endless_method_style?
137
+ endless_method_config = config.for_cop('Style/EndlessMethod')
138
+ return false unless endless_method_config['Enabled']
139
+
140
+ endless_method_config['EnforcedStyle'] == 'disallow'
141
+ end
132
142
  end
133
143
  end
134
144
  end
@@ -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)