rubocop 1.92.1 → 1.92.2

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rubocop/config.rb +0 -4
  3. data/lib/rubocop/config_obsoletion.rb +9 -0
  4. data/lib/rubocop/cop/correctors/empty_line_corrector.rb +1 -1
  5. data/lib/rubocop/cop/layout/empty_lines_after_module_inclusion.rb +1 -1
  6. data/lib/rubocop/cop/layout/end_alignment.rb +2 -8
  7. data/lib/rubocop/cop/layout/extra_spacing.rb +17 -6
  8. data/lib/rubocop/cop/layout/line_length.rb +23 -5
  9. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +1 -1
  10. data/lib/rubocop/cop/layout/single_line_block_chain.rb +1 -1
  11. data/lib/rubocop/cop/layout/space_around_operators.rb +31 -4
  12. data/lib/rubocop/cop/layout/space_before_block_braces.rb +12 -0
  13. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +1 -1
  14. data/lib/rubocop/cop/layout/space_inside_range_literal.rb +29 -11
  15. data/lib/rubocop/cop/lint/interpolation_check.rb +20 -17
  16. data/lib/rubocop/cop/lint/nested_percent_literal.rb +3 -0
  17. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +22 -1
  18. data/lib/rubocop/cop/lint/percent_string_array.rb +75 -9
  19. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +27 -8
  20. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +11 -4
  21. data/lib/rubocop/cop/mixin/comments_help.rb +1 -1
  22. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +17 -1
  23. data/lib/rubocop/cop/mixin/statement_modifier.rb +14 -1
  24. data/lib/rubocop/cop/mixin/trailing_comma.rb +1 -1
  25. data/lib/rubocop/cop/style/and_or.rb +33 -10
  26. data/lib/rubocop/cop/style/case_equality.rb +14 -3
  27. data/lib/rubocop/cop/style/class_and_module_children.rb +4 -3
  28. data/lib/rubocop/cop/style/endless_method.rb +34 -6
  29. data/lib/rubocop/cop/style/float_division.rb +16 -1
  30. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +1 -1
  31. data/lib/rubocop/cop/style/hash_lookup_method.rb +10 -0
  32. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +1 -0
  33. data/lib/rubocop/cop/style/method_def_parentheses.rb +9 -1
  34. data/lib/rubocop/cop/style/numeric_predicate.rb +7 -0
  35. data/lib/rubocop/cop/style/parentheses_around_condition.rb +18 -0
  36. data/lib/rubocop/cop/style/quoted_symbols.rb +5 -7
  37. data/lib/rubocop/cop/style/raise_args.rb +66 -7
  38. data/lib/rubocop/cop/style/reduce_to_hash.rb +32 -34
  39. data/lib/rubocop/cop/style/redundant_begin.rb +24 -2
  40. data/lib/rubocop/cop/style/redundant_exception.rb +13 -9
  41. data/lib/rubocop/cop/style/redundant_self.rb +9 -1
  42. data/lib/rubocop/cop/style/single_line_methods.rb +21 -0
  43. data/lib/rubocop/cop/style/string_concatenation.rb +12 -1
  44. data/lib/rubocop/plugin/configuration_integrator.rb +24 -3
  45. data/lib/rubocop/runner.rb +1 -1
  46. data/lib/rubocop/version.rb +1 -1
  47. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f289b93d86b59bdf80728f8130f7daeb1b6b6f940d6144cc425b627efe00061
4
- data.tar.gz: c87922f68ef6b6fb2d1cdf8ac29998908f6be8f7d39d8ed14b7a0772d33a6b69
3
+ metadata.gz: 2e582386de45680cecfa5173359f6ccb24894b2440ef9d6fec5ec5c0d28d0b41
4
+ data.tar.gz: ef3bdbb7e39a1960f5e1feb41df80f7afef2f35148a44693566f725b99715a3a
5
5
  SHA512:
6
- metadata.gz: cc6d21f74027a289aa14bf41cacbf840a0d156c93a33fa3526ee75607cb6d85ca3a6e219f4117c4d70462ef80fd8ae81d62ffd74cc8451c89c67754c4c719c87
7
- data.tar.gz: 8a0a4af9e4cbba6b0c242461cacba0ef1fe5ba6049e5d5814eeac5db118f73a18142927ea33ea43a9ffd7b751f340893abd861eeb7fcc502506897d09c74e860
6
+ metadata.gz: dff3c8ee84b5334d4aabb751f7de89b2cf2b87d65ad7970913132f558909d80da6306f60707f0ba1001d826a22c3491d9ab716c82defb20bb10216654cf75902
7
+ data.tar.gz: 6c6913ed8f1d4f30ee0044df54c2c370aa59b2f1f91de340f33295af795fcd56138b75583c37049c3fd117ffd51241600bd24e0c5e82d41b58304821a8170a25
@@ -56,10 +56,6 @@ module RuboCop
56
56
  deprecated_config = @for_cop[deprecated_cop_name]
57
57
  next if deprecated_config.empty?
58
58
 
59
- warn Rainbow(<<~WARNING).yellow
60
- Warning: Using `#{deprecated_cop_name}` configuration in #{loaded_path} for `#{cop}`.
61
- WARNING
62
-
63
59
  cop_options.merge!(@for_cop[deprecated_cop_name])
64
60
  end
65
61
  end
@@ -76,6 +76,15 @@ module RuboCop
76
76
  raise ValidationError, messages.join("\n")
77
77
  end
78
78
 
79
+ # Messages for every violated rule, regardless of severity, without raising.
80
+ # Used for configuration the user does not own (a plugin's defaults), where an
81
+ # obsolete key should be reported but must not stop the run. Extracted cops are
82
+ # left out: whether their gem is loaded is only known once every plugin has been
83
+ # integrated, and this is called while plugins are still loading.
84
+ def messages
85
+ rules.grep_v(ExtractedCop).select(&:violated?).map(&:message)
86
+ end
87
+
79
88
  def legacy_cop_names
80
89
  # Used by DepartmentName#qualified_legacy_cop_name
81
90
  cop_rules.map(&:old_name)
@@ -12,7 +12,7 @@ module RuboCop
12
12
  when :no_empty_lines
13
13
  corrector.remove(range)
14
14
  when :empty_lines
15
- corrector.insert_before(range.begin, "\n")
15
+ corrector.replace(range.begin.adjust(begin_pos: -1), "\n\n")
16
16
  end
17
17
  end
18
18
 
@@ -91,7 +91,7 @@ module RuboCop
91
91
  end
92
92
 
93
93
  def next_line_node(node)
94
- return if node.parent.type?(:if, :rescue, :ensure)
94
+ return if node.parent.type?(:if, :rescue, :ensure, :and, :or)
95
95
 
96
96
  node.right_sibling
97
97
  end
@@ -80,10 +80,6 @@ module RuboCop
80
80
  include RangeHelp
81
81
  extend AutoCorrector
82
82
 
83
- def on_class(node)
84
- check_other_alignment(node)
85
- end
86
-
87
83
  def on_sclass(node)
88
84
  if node.parent&.assignment?
89
85
  check_asgn_alignment(node.parent, node)
@@ -91,10 +87,8 @@ module RuboCop
91
87
  check_other_alignment(node)
92
88
  end
93
89
  end
94
-
95
- def on_module(node)
96
- check_other_alignment(node)
97
- end
90
+ alias on_class on_sclass
91
+ alias on_module on_sclass
98
92
 
99
93
  def on_if(node)
100
94
  check_other_alignment(node) unless node.ternary?
@@ -30,6 +30,7 @@ module RuboCop
30
30
  # some_object.method(arg) # this is some comment
31
31
  class ExtraSpacing < Base
32
32
  extend AutoCorrector
33
+ include LineLengthHelp
33
34
  include PrecedingFollowingAlignment
34
35
  include RangeHelp
35
36
 
@@ -77,9 +78,14 @@ module RuboCop
77
78
  def check_assignment(token)
78
79
  return unless aligned_with_preceding_equals_operator(token) == :no
79
80
 
81
+ tokens, align_to = equal_sign_alignment(token.line)
82
+ return if exceeds_max_line_length?(tokens, align_to)
83
+
80
84
  message = format(MSG_UNALIGNED_ASGN, location: 'preceding')
81
85
  add_offense(token.pos, message: message) do |corrector|
82
- align_equal_signs(token.pos, corrector)
86
+ tokens.each do |asgn_token|
87
+ align_equal_sign(corrector, asgn_token, align_to)
88
+ end
83
89
  end
84
90
  end
85
91
 
@@ -240,14 +246,19 @@ module RuboCop
240
246
  cop_config['ForceEqualSignAlignment']
241
247
  end
242
248
 
243
- def align_equal_signs(range, corrector)
244
- lines = all_relevant_assignment_lines(range.line)
249
+ def equal_sign_alignment(line_number)
250
+ lines = all_relevant_assignment_lines(line_number)
245
251
  tokens = assignment_tokens.select { |t| lines.include?(t.line) }
246
252
 
247
- columns = tokens.map { |t| align_column(t) }
248
- align_to = columns.max
253
+ [tokens, tokens.map { |t| align_column(t) }.max]
254
+ end
249
255
 
250
- tokens.each { |token| align_equal_sign(corrector, token, align_to) }
256
+ def exceeds_max_line_length?(tokens, align_to)
257
+ max_line_length && tokens.any? do |token|
258
+ padding = align_to - token.pos.last_column
259
+
260
+ padding.positive? && line_length(token.pos.source_line) + padding > max_line_length
261
+ end
251
262
  end
252
263
 
253
264
  def align_equal_sign(corrector, token, align_to)
@@ -125,6 +125,7 @@ module RuboCop
125
125
  @breakable_strings = {}
126
126
  @endless_methods_by_line = {}
127
127
  @heredocs = nil
128
+ @heredoc_openers = nil
128
129
 
129
130
  return unless processed_source.raw_source.include?(';')
130
131
 
@@ -214,11 +215,24 @@ module RuboCop
214
215
  range = breakable_node.source_range
215
216
 
216
217
  existing = breakable_range_by_line_index[line_index]
217
- return if existing
218
+ return if existing || after_heredoc_opener?(range)
218
219
 
219
220
  breakable_range_by_line_index[line_index] = range
220
221
  end
221
222
 
223
+ def after_heredoc_opener?(range)
224
+ heredoc_openers.any? do |opener|
225
+ opener.line == range.line && opener.begin_pos < range.begin_pos
226
+ end
227
+ end
228
+
229
+ def heredoc_openers
230
+ ast = processed_source.ast
231
+ return [] if ast.nil?
232
+
233
+ @heredoc_openers ||= ast.each_node(:any_str).select(&:heredoc?).map(&:source_range)
234
+ end
235
+
222
236
  def compact_nested_array_break?(node, breakable_node)
223
237
  return false if !node.array_type? || !node.square_brackets?
224
238
  return false unless breakable_node.equal?(node.children.first)
@@ -237,21 +251,24 @@ module RuboCop
237
251
  tokens = processed_source.tokens.select { |t| t.type == :tSEMI }
238
252
  tokens.reverse_each do |token|
239
253
  range = breakable_range_after_semicolon(token)
240
- breakable_range_by_line_index[range.line - 1] = range if range
254
+ next if range.nil? || after_heredoc_opener?(range)
255
+
256
+ breakable_range_by_line_index[range.line - 1] = range
241
257
  end
242
258
  end
243
259
 
244
260
  def check_for_breakable_block(block_node)
245
261
  return unless block_node.single_line?
246
- return if receiver_contains_heredoc?(block_node)
247
262
 
248
263
  line_index = block_node.loc.line - 1
249
264
  return if breakable_string_delimiters[line_index]
250
265
 
251
266
  range = breakable_block_range(block_node)
252
267
  pos = range.begin_pos + 1
268
+ range = range_between(pos, pos + 1)
269
+ return if after_heredoc_opener?(range)
253
270
 
254
- breakable_range_by_line_index[line_index] = range_between(pos, pos + 1)
271
+ breakable_range_by_line_index[line_index] = range
255
272
  end
256
273
 
257
274
  # rubocop:disable-next Metrics/AbcSize
@@ -289,7 +306,8 @@ module RuboCop
289
306
  !node.heredoc? &&
290
307
  # TODO: strings inside hashes, kwargs and arrays are currently ignored,
291
308
  # but could be considered in the future
292
- !node.parent&.type?(:pair, :kwoptarg, :array)
309
+ !node.parent&.type?(:pair, :kwoptarg, :array) &&
310
+ !after_heredoc_opener?(node.source_range)
293
311
  end
294
312
 
295
313
  def breakable_block_range(block_node)
@@ -115,7 +115,7 @@ module RuboCop
115
115
  node.loc.name
116
116
  when :sclass
117
117
  node.identifier.source_range
118
- when :masgn
118
+ when :masgn, :op_asgn, :or_asgn, :and_asgn
119
119
  node.lhs.source_range
120
120
  else
121
121
  # It is a wrapper with receiver of object attribute or access modifier.
@@ -28,7 +28,7 @@ module RuboCop
28
28
  # These cops rewrite a method chained to the block in the same pass,
29
29
  # which must not start a line of its own.
30
30
  def self.autocorrect_incompatible_with
31
- [Style::FloatDivision, Style::MapToHash, Style::NilComparison, Style::RedundantSort]
31
+ [Style::FloatDivision, Style::MapToHash, Style::MapToSet, Style::NilComparison, Style::RedundantSort]
32
32
  end
33
33
 
34
34
  def on_send(node)
@@ -245,20 +245,30 @@ module RuboCop
245
245
  return if with_space.is?(operator.source)
246
246
 
247
247
  "Space around operator `#{operator.source}` detected."
248
- elsif !/^\s.*\s$/.match?(with_space.source)
248
+ elsif missing_surrounding_space?(with_space)
249
249
  "Surrounding space missing for operator `#{operator.source}`."
250
- elsif excess_leading_space?(type, operator, with_space) ||
250
+ elsif excess_leading_space?(type, operator, with_space, right_operand) ||
251
251
  excess_trailing_space?(right_operand.source_range, with_space)
252
252
  "Operator `#{operator.source}` should be surrounded " \
253
253
  'by a single space.'
254
254
  end
255
255
  end
256
256
 
257
- def excess_leading_space?(type, operator, with_space)
257
+ def missing_surrounding_space?(with_space)
258
+ return false if /\A\s.*\s\z/m.match?(with_space.source)
259
+
260
+ !with_space.source.start_with?(/\s/) || !line_continuation_after?(with_space)
261
+ end
262
+
263
+ def line_continuation_after?(range)
264
+ processed_source.buffer.source[range.end_pos, 2] == "\\\n"
265
+ end
266
+
267
+ def excess_leading_space?(type, operator, with_space, right_operand)
258
268
  return false unless allow_for_alignment?
259
269
  return false unless with_space.source.start_with?(EXCESSIVE_SPACE)
260
270
 
261
- return !aligned_with_operator?(operator) unless grouped_alignment_type?(type)
271
+ return !aligned?(type, operator, right_operand) unless grouped_alignment_type?(type)
262
272
 
263
273
  token = Token.new(operator, nil, operator.source)
264
274
  align_preceding = aligned_with_preceding_equals_operator(token)
@@ -269,6 +279,23 @@ module RuboCop
269
279
  aligned_with_subsequent_equals_operator(token) != :yes
270
280
  end
271
281
 
282
+ def aligned?(type, operator, right_operand)
283
+ return true if aligned_with_operator?(operator)
284
+
285
+ type == :pair && aligned_with_another_hash_rocket?(right_operand)
286
+ end
287
+
288
+ def aligned_with_another_hash_rocket?(pair)
289
+ hash_rocket_columns(pair.parent)[pair.loc.operator.column] > 1
290
+ end
291
+
292
+ def hash_rocket_columns(hash_node)
293
+ @hash_rocket_columns ||= {}.compare_by_identity
294
+ @hash_rocket_columns[hash_node] ||= hash_node.pairs.select(&:hash_rocket?).map do |pair|
295
+ pair.loc.operator.column
296
+ end.tally
297
+ end
298
+
272
299
  def grouped_alignment_type?(type)
273
300
  type == :assignment || (type == :special_asgn && force_equal_sign_alignment?)
274
301
  end
@@ -136,11 +136,23 @@ module RuboCop
136
136
 
137
137
  def autocorrect(corrector, range)
138
138
  case range.source
139
+ when /\n/ then remove_line_continuation(corrector, range)
139
140
  when /\s/ then corrector.remove(range)
140
141
  else corrector.insert_before(range, ' ')
141
142
  end
142
143
  end
143
144
 
145
+ # When a heredoc body sits between the `\` and the brace, the newline before
146
+ # the brace ends the heredoc, and removing it would break the terminator.
147
+ def remove_line_continuation(corrector, range)
148
+ source = processed_source.buffer.source
149
+ return unless source[range.begin_pos - 1] == '\\'
150
+
151
+ begin_pos = range.begin_pos
152
+ begin_pos -= 1 while source[begin_pos - 1].match?(/[ \t\\]/)
153
+ corrector.remove(range_between(begin_pos, range.end_pos))
154
+ end
155
+
144
156
  def style_for_empty_braces
145
157
  case cop_config['EnforcedStyleForEmptyBraces']
146
158
  when 'space' then :space
@@ -83,7 +83,7 @@ module RuboCop
83
83
  extend AutoCorrector
84
84
 
85
85
  def self.autocorrect_incompatible_with
86
- [Style::BlockDelimiters]
86
+ [Layout::BlockAlignment, Style::BlockDelimiters]
87
87
  end
88
88
 
89
89
  def on_block(node)
@@ -18,6 +18,7 @@ module RuboCop
18
18
  # # good
19
19
  # 'a'..'z'
20
20
  class SpaceInsideRangeLiteral < Base
21
+ include RangeHelp
21
22
  extend AutoCorrector
22
23
 
23
24
  MSG = 'Space inside range literal.'
@@ -33,21 +34,38 @@ module RuboCop
33
34
  private
34
35
 
35
36
  def check(node)
36
- expression = node.source
37
- op = node.loc.operator.source
38
- escaped_op = op.gsub('.', '\.')
39
-
40
- # account for multiline range literals
41
- expression.sub!(/#{escaped_op}\n\s*/, op)
42
-
43
- return unless /(\s#{escaped_op})|(#{escaped_op}\s)/.match?(expression)
37
+ operator = node.loc.operator
38
+ spaces = [space_before(node, operator), space_after(node, operator)].compact
39
+ return if spaces.all? { |space| line_break_after_operator?(space, operator) }
44
40
 
45
41
  add_offense(node) do |corrector|
46
- corrector.replace(
47
- node, expression.sub(/\s+#{escaped_op}/, op).sub(/#{escaped_op}\s+/, op)
48
- )
42
+ spaces.each do |space|
43
+ corrector.remove(space)
44
+ end
49
45
  end
50
46
  end
47
+
48
+ def space_before(node, operator)
49
+ return unless node.begin
50
+
51
+ whitespace_range(node.begin.source_range.end_pos, operator.begin_pos)
52
+ end
53
+
54
+ def space_after(node, operator)
55
+ return unless node.end
56
+
57
+ whitespace_range(operator.end_pos, node.end.source_range.begin_pos)
58
+ end
59
+
60
+ def line_break_after_operator?(space, operator)
61
+ space.begin_pos == operator.end_pos && space.source.start_with?("\n")
62
+ end
63
+
64
+ def whitespace_range(begin_pos, end_pos)
65
+ range = range_between(begin_pos, end_pos)
66
+
67
+ range if range.source.match?(/\A\s+\z/)
68
+ end
51
69
  end
52
70
  end
53
71
  end
@@ -41,26 +41,22 @@ module RuboCop
41
41
 
42
42
  private
43
43
 
44
- # rubocop:disable-next Metrics/CyclomaticComplexity
45
44
  def check(node)
46
45
  return if node.parent&.regexp_type?
47
46
  return unless /(?<!\\)#\{.*\}/.match?(node.source)
48
47
  return if heredoc?(node)
49
- return unless node.loc.begin && node.loc.end
48
+ return unless single_quoted?(node)
50
49
  return unless valid_syntax?(node)
51
50
 
52
51
  add_offense(node) { |corrector| autocorrect(corrector, node) }
53
52
  end
54
53
 
55
- def autocorrect(corrector, node)
56
- starting_token, ending_token = if node.source.include?('"')
57
- ['%{', '}']
58
- else
59
- ['"', '"']
60
- end
54
+ def single_quoted?(node)
55
+ node.loc.begin&.is?("'") && node.loc.end
56
+ end
61
57
 
62
- corrector.replace(node.loc.begin, starting_token)
63
- corrector.replace(node.loc.end, ending_token)
58
+ def autocorrect(corrector, node)
59
+ corrector.replace(node, interpolated_source(node))
64
60
  end
65
61
 
66
62
  def heredoc?(node)
@@ -68,15 +64,22 @@ module RuboCop
68
64
  end
69
65
 
70
66
  def valid_syntax?(node)
71
- double_quoted_string = if node.source.include?('"')
72
- node.source.sub(/\A'/, '%{').sub(/'\z/, '}')
73
- else
74
- node.source.gsub(/\A'|'\z/, '"')
75
- end
76
-
77
- processed_source = parse(double_quoted_string)
67
+ processed_source = parse(interpolated_source(node))
78
68
  processed_source.valid_syntax? && processed_source.ast.dstr_type?
79
69
  end
70
+
71
+ def interpolated_source(node)
72
+ content = node.source[1...-1]
73
+ return %("#{content}") unless content.include?('"')
74
+ return "%{#{content}}" unless continued_string_part?(node)
75
+
76
+ %("#{content.gsub(/\\.|"/) { |m| m == '"' ? '\"' : m }}")
77
+ end
78
+
79
+ # A percent literal right after another string part is parsed as the `%` operator.
80
+ def continued_string_part?(node)
81
+ node.parent&.dstr_type? && node.left_sibling
82
+ end
80
83
  end
81
84
  end
82
85
  end
@@ -40,6 +40,7 @@ module RuboCop
40
40
  # NestedPercentLiteral violation to be emitted.
41
41
  PERCENT_LITERAL_TYPES = PreferredDelimiters::PERCENT_LITERAL_TYPES
42
42
  REGEXES = PERCENT_LITERAL_TYPES.map { |percent_literal| /\A#{percent_literal}\W/ }.freeze
43
+ FORMAT_SEQUENCE = /\A#{RuboCop::Cop::Utils::FormatString::SEQUENCE}\z/.freeze
43
44
 
44
45
  def on_array(node)
45
46
  process(node, *PERCENT_LITERAL_TYPES)
@@ -54,6 +55,8 @@ module RuboCop
54
55
  def contains_percent_literals?(node)
55
56
  node.each_child_node.any? do |child|
56
57
  literal = child.children.first.to_s.scrub
58
+ next false if literal.match?(FORMAT_SEQUENCE)
59
+
57
60
  REGEXES.any? { |regex| literal.match?(regex) }
58
61
  end
59
62
  end
@@ -57,7 +57,7 @@ module RuboCop
57
57
  node = node.children.first while node&.begin_type? && node.children.one?
58
58
  return false unless node
59
59
 
60
- keyword_operator?(node) || modifier_expression?(node)
60
+ keyword_operator?(node) || modifier_expression?(node) || command_call_operand?(node)
61
61
  end
62
62
 
63
63
  def keyword_operator?(node)
@@ -69,6 +69,27 @@ module RuboCop
69
69
  node.type?(:if, :while, :until) && node.modifier_form?
70
70
  end
71
71
 
72
+ def command_call_operand?(node)
73
+ if node.assignment? || (node.call_type? && node.assignment_method?)
74
+ command_call_value?(node.children.last)
75
+ elsif node.send_type? && node.prefix_bang?
76
+ command_call_value?(node.receiver)
77
+ else
78
+ false
79
+ end
80
+ end
81
+
82
+ def command_call_value?(node)
83
+ command_call?(node) || command_call_operand?(node)
84
+ end
85
+
86
+ def command_call?(node)
87
+ node = node.send_node if node.any_block_type?
88
+ return false unless node.type?(:call, :super, :yield)
89
+
90
+ node.arguments? && !node.parenthesized? && !node.operator_method?
91
+ end
92
+
72
93
  def chained_calls?(node)
73
94
  first_argument = node.first_argument
74
95
  first_argument.call_type? && (node.children.last&.children&.count || 0) > 1
@@ -9,6 +9,11 @@ module RuboCop
9
9
  # example, mistranslating an array of literals to percent string notation)
10
10
  # rather than meant to be part of the resulting strings.
11
11
  #
12
+ # When a quoted string contains whitespace, `%w` splits it into separate values,
13
+ # so removing the quotes would not restore the intended string.
14
+ # Such an array is corrected to an array literal instead, as long as its content
15
+ # reads as one with only string elements.
16
+ #
12
17
  # @safety
13
18
  # The cop is unsafe because the correction changes the values in the array
14
19
  # and that might have been done purposely.
@@ -26,6 +31,12 @@ module RuboCop
26
31
  #
27
32
  # # good
28
33
  # %w(foo bar)
34
+ #
35
+ # # bad - the quoted strings are split at whitespace
36
+ # %w('foo bar', 'baz')
37
+ #
38
+ # # good
39
+ # ['foo bar', 'baz']
29
40
  class PercentStringArray < Base
30
41
  include PercentLiteral
31
42
  extend AutoCorrector
@@ -36,27 +47,69 @@ module RuboCop
36
47
 
37
48
  MSG = "Within `%w`/`%W`, quotes and ',' are unnecessary and may be " \
38
49
  'unwanted in the resulting strings.'
50
+ MSG_SPLIT_STRING = 'Within `%w`/`%W`, quoted strings are split at whitespace; ' \
51
+ 'use an array literal instead.'
39
52
 
40
53
  def on_array(node)
41
54
  process(node, '%w', '%W')
42
55
  end
43
56
 
44
57
  def on_percent_literal(node)
45
- return unless contains_quotes_or_commas?(node)
58
+ if split_quoted_string?(node)
59
+ register_split_quoted_string(node)
60
+ elsif contains_quotes_or_commas?(node)
61
+ register_quotes_or_commas(node)
62
+ end
63
+ end
46
64
 
47
- add_offense(node) do |corrector|
48
- node.each_value do |value|
49
- range = value.source_range
65
+ private
50
66
 
51
- match = range.source.match(TRAILING_QUOTE)
52
- corrector.remove_trailing(range, match[0].length) if match
67
+ def split_quoted_string?(node)
68
+ sources = node.values.map { |value| value.source.delete_suffix(',') }
53
69
 
54
- corrector.remove_leading(range, 1) if LEADING_QUOTE.match?(range.source)
55
- end
70
+ sources.each_with_index.any? do |source, index|
71
+ next false unless (quote = unclosed_quote(source))
72
+
73
+ sources.drop(index + 1).any? { |rest| closing_quote?(rest, quote) }
56
74
  end
57
75
  end
58
76
 
59
- private
77
+ def unclosed_quote(source)
78
+ quote = source[0]
79
+ return unless LEADING_QUOTE.match?(quote)
80
+
81
+ quote if source.length > 1 && !source.end_with?(quote)
82
+ end
83
+
84
+ def closing_quote?(source, quote)
85
+ source.end_with?(quote) && (source.length == 1 || !source.start_with?(quote))
86
+ end
87
+
88
+ def register_split_quoted_string(node)
89
+ add_offense(node, message: MSG_SPLIT_STRING) do |corrector|
90
+ next unless (array_literal = array_literal_source(node))
91
+
92
+ corrector.replace(node, array_literal)
93
+ end
94
+ end
95
+
96
+ def array_literal_source(node)
97
+ content = range_between(node.loc.begin.end_pos, node.loc.end.begin_pos).source
98
+ array_literal = "[#{content}]"
99
+
100
+ array_literal if string_array?(parse(array_literal))
101
+ end
102
+
103
+ def string_array?(processed_source)
104
+ return false unless processed_source.valid_syntax? && processed_source.comments.empty?
105
+ return false unless (array = processed_source.ast).array_type?
106
+
107
+ array.children.all? { |child| quoted_string?(child) }
108
+ end
109
+
110
+ def quoted_string?(node)
111
+ node.type?(:str, :dstr) && node.loc?(:begin)
112
+ end
60
113
 
61
114
  def contains_quotes_or_commas?(node)
62
115
  node.values.any? do |value|
@@ -68,6 +121,19 @@ module RuboCop
68
121
  QUOTES_AND_COMMAS.any? { |pat| literal.match?(pat) }
69
122
  end
70
123
  end
124
+
125
+ def register_quotes_or_commas(node)
126
+ add_offense(node) do |corrector|
127
+ node.each_value do |value|
128
+ range = value.source_range
129
+
130
+ match = range.source.match(TRAILING_QUOTE)
131
+ corrector.remove_trailing(range, match[0].length) if match
132
+
133
+ corrector.remove_leading(range, 1) if LEADING_QUOTE.match?(range.source)
134
+ end
135
+ end
136
+ end
71
137
  end
72
138
  end
73
139
  end