rubocop 1.88.0 → 1.88.1

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +3 -1
  3. data/lib/rubocop/cop/bundler/gem_comment.rb +3 -1
  4. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  5. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
  6. data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
  7. data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
  8. data/lib/rubocop/cop/gemspec/require_mfa.rb +4 -1
  9. data/lib/rubocop/cop/layout/block_alignment.rb +17 -0
  10. data/lib/rubocop/cop/layout/class_structure.rb +7 -3
  11. data/lib/rubocop/cop/layout/condition_position.rb +13 -3
  12. data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
  13. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +14 -1
  14. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +13 -14
  15. data/lib/rubocop/cop/layout/indentation_width.rb +28 -0
  16. data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
  17. data/lib/rubocop/cop/lint/assignment_in_condition.rb +13 -1
  18. data/lib/rubocop/cop/lint/to_enum_arguments.rb +7 -1
  19. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +32 -8
  20. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  21. data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
  22. data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
  23. data/lib/rubocop/cop/mixin/hash_transform_method.rb +4 -0
  24. data/lib/rubocop/cop/naming/file_name.rb +4 -3
  25. data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
  26. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +9 -0
  27. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +9 -3
  28. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  29. data/lib/rubocop/cop/security/marshal_load.rb +1 -1
  30. data/lib/rubocop/cop/style/accessor_grouping.rb +11 -1
  31. data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
  32. data/lib/rubocop/cop/style/dir_empty.rb +4 -0
  33. data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
  34. data/lib/rubocop/cop/style/empty_class_definition.rb +8 -1
  35. data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
  36. data/lib/rubocop/cop/style/empty_literal.rb +7 -2
  37. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
  38. data/lib/rubocop/cop/style/env_home.rb +4 -0
  39. data/lib/rubocop/cop/style/even_odd.rb +11 -1
  40. data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
  41. data/lib/rubocop/cop/style/file_null.rb +4 -2
  42. data/lib/rubocop/cop/style/format_string.rb +13 -1
  43. data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
  44. data/lib/rubocop/cop/style/if_with_semicolon.rb +9 -1
  45. data/lib/rubocop/cop/style/inline_comment.rb +1 -1
  46. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
  47. data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
  48. data/lib/rubocop/cop/style/lambda.rb +7 -1
  49. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
  50. data/lib/rubocop/cop/style/map_into_array.rb +1 -1
  51. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +6 -2
  52. data/lib/rubocop/cop/style/method_def_parentheses.rb +1 -1
  53. data/lib/rubocop/cop/style/min_max_comparison.rb +3 -0
  54. data/lib/rubocop/cop/style/multiline_if_then.rb +1 -1
  55. data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
  56. data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
  57. data/lib/rubocop/cop/style/nil_lambda.rb +8 -0
  58. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  59. data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
  60. data/lib/rubocop/cop/style/option_hash.rb +1 -1
  61. data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
  62. data/lib/rubocop/cop/style/parallel_assignment.rb +11 -2
  63. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  64. data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
  65. data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
  66. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  67. data/lib/rubocop/cop/style/redundant_format.rb +28 -0
  68. data/lib/rubocop/cop/style/redundant_line_continuation.rb +11 -3
  69. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
  70. data/lib/rubocop/cop/style/redundant_self.rb +9 -0
  71. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +23 -4
  72. data/lib/rubocop/cop/style/semicolon.rb +4 -4
  73. data/lib/rubocop/cop/style/single_line_do_end_block.rb +17 -4
  74. data/lib/rubocop/cop/style/string_hash_keys.rb +1 -0
  75. data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
  76. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
  77. data/lib/rubocop/runner.rb +5 -3
  78. data/lib/rubocop/version.rb +1 -1
  79. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 672884fb2ea66b13be308094ffcec44f915943867fac5542b82ceaba3ecf32e9
4
- data.tar.gz: f489cbc9ae6385c18d4d97140e4589503ac28a82a4288c5bac915679471ea683
3
+ metadata.gz: 04f5790c0be94705978732f52d462bd3afe919498e1e6be410fcfece0faaf4cd
4
+ data.tar.gz: 87c79acdf7db812d889bc2464a61d9921db70daf4e6c11117be879d7d9b64991
5
5
  SHA512:
6
- metadata.gz: 8d2cb39bc17b788f176096a12ddf766dbdcfafe8a6fcd09e830544053665b3b42f204b0b7fe01a61d2be94d64fe859f6783710f01df71f136e28d9e46ec1ebf6
7
- data.tar.gz: 18067fc7914382cf8c13a07a34b9774647bd85ea34e85eac8a705da85af7039f31befae4e850b23989ddb91918f4cf10bccd4d578dc79a65de5422db86b51ad2
6
+ metadata.gz: c8491483d605d97e0c746f171fdebf6b5b009a0e1e01392764a9fbf12208dad6a7fe8e8529f32d3a2b1ae37e39f150b689638cd7dbc901040f27b6cdf1981ab5
7
+ data.tar.gz: 7aa878cbe87d967607a65f37a1b3eafd47507d7a9c54c3bede0398f689527254fa4d65460d3af11d233244eb53eb7058bd5b69326588d7b3d3ed0fbb864afef8
data/config/default.yml CHANGED
@@ -2844,7 +2844,7 @@ Metrics/PerceivedComplexity:
2844
2844
  Description: 'Checks that the perceived complexity of methods is not higher than the configured maximum.'
2845
2845
  Enabled: true
2846
2846
  VersionAdded: '0.25'
2847
- VersionChanged: '0.81'
2847
+ VersionChanged: '1.88'
2848
2848
  AllowedMethods: []
2849
2849
  AllowedPatterns: []
2850
2850
  Max: 8
@@ -4658,7 +4658,9 @@ Style/MagicCommentFormat:
4658
4658
  Style/MapCompactWithConditionalBlock:
4659
4659
  Description: 'Prefer `select` or `reject` over `map { ... }.compact`.'
4660
4660
  Enabled: pending
4661
+ SafeAutoCorrect: false
4661
4662
  VersionAdded: '1.30'
4663
+ VersionChanged: '1.88'
4662
4664
 
4663
4665
  Style/MapIntoArray:
4664
4666
  Description: 'Checks for usages of `each` with `<<`, `push`, or `append` which can be replaced by `map`.'
@@ -163,7 +163,9 @@ module RuboCop
163
163
  def gem_options(node)
164
164
  return [] unless node.last_argument&.hash_type?
165
165
 
166
- node.last_argument.keys.map(&:value)
166
+ # Only literal keys carry an option name to check; a non-literal key
167
+ # (e.g. a variable or method call) has no `value` and must be skipped.
168
+ node.last_argument.keys.filter_map { |key| key.value if key.type?(:sym, :str) }
167
169
  end
168
170
  end
169
171
  end
@@ -27,7 +27,7 @@ module RuboCop
27
27
  if block_node.arguments?
28
28
  format(CORRECTION_WITH_ARGUMENTS,
29
29
  collection: collection_node.source,
30
- variables: argument_node.children.first.source)
30
+ variables: argument_node.children.map(&:source).join(', '))
31
31
  else
32
32
  format(CORRECTION_WITHOUT_ARGUMENTS, enumerable: collection_node.source)
33
33
  end
@@ -86,7 +86,13 @@ module RuboCop
86
86
  end
87
87
 
88
88
  def lambda_arg_string
89
- arguments.children.map(&:source).join(', ')
89
+ # Block-local (shadow) arguments are separated from regular arguments by a
90
+ # `;`; joining everything with `,` would turn them into extra parameters
91
+ # and change the lambda's arity.
92
+ regular, shadow = arguments.children.partition { |arg| !arg.shadowarg_type? }
93
+ arg_string = regular.map(&:source).join(', ')
94
+ arg_string += "; #{shadow.map(&:source).join(', ')}" unless shadow.empty?
95
+ arg_string
90
96
  end
91
97
 
92
98
  def needs_separating_space?
@@ -18,7 +18,14 @@ module RuboCop
18
18
  current_range = declaration_with_comment(node)
19
19
  previous_range = declaration_with_comment(previous_declaration)
20
20
 
21
- ->(corrector) { corrector.swap(current_range, previous_range) }
21
+ lambda do |corrector|
22
+ if current_range.source.end_with?("\n")
23
+ corrector.swap(current_range, previous_range)
24
+ else
25
+ corrector.insert_before(previous_range, "#{current_range.source}\n")
26
+ corrector.remove(current_range)
27
+ end
28
+ end
22
29
  end
23
30
 
24
31
  private
@@ -95,7 +95,7 @@ module RuboCop
95
95
  private
96
96
 
97
97
  def forbidden_gem?(gem_name)
98
- !cop_config['AllowedGems'].include?(gem_name)
98
+ !Array(cop_config['AllowedGems']).include?(gem_name)
99
99
  end
100
100
 
101
101
  def message(_range)
@@ -143,7 +143,10 @@ module RuboCop
143
143
  #{block_var}.metadata['rubygems_mfa_required'] = 'true'
144
144
  RUBY
145
145
 
146
- if (last_assignment = metadata_assignment(processed_source.ast).to_a.last)
146
+ # Scope the search to the current spec block. Searching the whole file
147
+ # would, for a second `Gem::Specification.new` block, insert the directive
148
+ # into the first block, leaving this block uncorrected and looping forever.
149
+ if (last_assignment = metadata_assignment(node).to_a.last)
147
150
  corrector.insert_after(last_assignment, "\n#{require_mfa_directive}")
148
151
  else
149
152
  corrector.insert_before(node.loc.end, "#{require_mfa_directive}\n")
@@ -281,15 +281,32 @@ module RuboCop
281
281
  end
282
282
  end
283
283
 
284
+ # rubocop:disable Metrics/AbcSize
284
285
  def do_line_begins_inside_argument?(node, do_loc)
285
286
  line_begin_pos = do_loc.begin_pos - do_loc.column
286
287
  first_char_pos = line_begin_pos + (do_loc.source_line =~ /\S/)
288
+ return false unless inside_parentheses?(node, first_char_pos)
287
289
 
288
290
  (node.send_node.arguments + node.arguments).any? do |argument|
289
291
  argument.source_range.begin_pos <= first_char_pos &&
290
292
  first_char_pos < argument.source_range.end_pos
291
293
  end
292
294
  end
295
+ # rubocop:enable Metrics/AbcSize
296
+
297
+ # The continuation line indentation is only an unmeaningful alignment target when
298
+ # it is dictated by an opening delimiter, i.e. the line begins inside `(` or `[`.
299
+ # For a bare argument list without parentheses the indentation is the author's
300
+ # deliberate alignment, so the anchor must not move to the method dispatch line.
301
+ def inside_parentheses?(node, pos)
302
+ preceding_tokens = processed_source.tokens.select do |token|
303
+ token.begin_pos >= node.source_range.begin_pos && token.begin_pos < pos
304
+ end
305
+ opens = preceding_tokens.count { |token| token.left_parens? || token.left_bracket? }
306
+ closes = preceding_tokens.count { |token| token.right_parens? || token.right_bracket? }
307
+
308
+ opens > closes
309
+ end
293
310
  end
294
311
  end
295
312
  end
@@ -278,10 +278,14 @@ module RuboCop
278
278
 
279
279
  return [] unless class_def
280
280
 
281
- if class_def.type?(:def, :send)
282
- [class_def]
283
- else
281
+ # Only a multi-statement body (`begin`/`kwbegin`) wraps several elements; any
282
+ # single statement (`def`, `send`, `csend`, `if`, ...) is itself the sole element.
283
+ # Exploding such a node into its children would yield non-node values (e.g. a
284
+ # method-name `Symbol` from a `csend`) and crash later checks.
285
+ if class_def.type?(:begin, :kwbegin)
284
286
  class_def.children.compact
287
+ else
288
+ [class_def]
285
289
  end
286
290
  end
287
291
 
@@ -44,16 +44,26 @@ module RuboCop
44
44
  message = message(condition)
45
45
 
46
46
  add_offense(condition, message: message) do |corrector|
47
- range = range_by_whole_lines(condition.source_range, include_final_newline: true)
48
-
49
47
  corrector.insert_after(condition.parent.loc.keyword, " #{condition.source}")
50
- corrector.remove(range)
48
+ corrector.remove(removal_range(node, condition))
51
49
  end
52
50
  end
53
51
 
54
52
  def message(condition)
55
53
  format(MSG, keyword: condition.parent.keyword)
56
54
  end
55
+
56
+ # When a body statement shares the condition's line (e.g. `while\n cond; body\nend`),
57
+ # removing the whole line would delete the body too. In that case only remove the
58
+ # condition and its trailing separator, preserving the body statement.
59
+ def removal_range(node, condition)
60
+ body = node.body
61
+ if body && body.source_range.line == condition.source_range.last_line
62
+ range_between(condition.source_range.begin_pos, body.source_range.begin_pos)
63
+ else
64
+ range_by_whole_lines(condition.source_range, include_final_newline: true)
65
+ end
66
+ end
57
67
  end
58
68
  end
59
69
  end
@@ -95,8 +95,7 @@ module RuboCop
95
95
  end
96
96
 
97
97
  def autocorrect(corrector, node)
98
- previous_token = previous_token(node)
99
- range = if previous_token && same_line?(node, previous_token)
98
+ range = if inline_comment?(node)
100
99
  range_with_surrounding_space(node.source_range, newlines: false)
101
100
  else
102
101
  range_by_whole_lines(node.source_range, include_final_newline: true)
@@ -138,14 +137,13 @@ module RuboCop
138
137
  cop_config['AllowMarginComment']
139
138
  end
140
139
 
141
- def current_token(comment)
142
- processed_source.tokens.find { |token| token.pos == comment.source_range }
143
- end
144
-
145
- def previous_token(node)
146
- current_token = current_token(node)
147
- index = processed_source.tokens.index(current_token)
148
- index.zero? ? nil : processed_source.tokens[index - 1]
140
+ # A comment is inline when code precedes it on the same line. Detecting this
141
+ # from the source (rather than the token stream) is required because, for a
142
+ # comment trailing a heredoc opener, the preceding token is the heredoc end on
143
+ # a later line, which would wrongly trigger whole-line removal of the opener.
144
+ def inline_comment?(comment)
145
+ preceding_source = processed_source.lines[comment.loc.line - 1][0...comment.loc.column]
146
+ !preceding_source.strip.empty?
149
147
  end
150
148
  end
151
149
  end
@@ -274,7 +274,20 @@ module RuboCop
274
274
  end
275
275
 
276
276
  def end_loc(node)
277
- node.source_range.end
277
+ end_location = node.source_range.end
278
+ trailing_heredoc_end(node, end_location) || end_location
279
+ end
280
+
281
+ # For an endless method whose body is a heredoc (e.g. `def a = <<~TEXT`), the
282
+ # node's source range ends at the heredoc opening line, before the heredoc body.
283
+ # Use the heredoc's closing delimiter so the def's real end is located after the
284
+ # heredoc and blank-line insertion does not land inside the heredoc body.
285
+ def trailing_heredoc_end(node, end_location)
286
+ heredocs = node.each_descendant(:any_str).select(&:heredoc?)
287
+ return if heredocs.empty?
288
+
289
+ heredoc_end = heredocs.map { |heredoc| heredoc.loc.heredoc_end }.max_by(&:end_pos)
290
+ heredoc_end if heredoc_end.end_pos > end_location.end_pos
278
291
  end
279
292
 
280
293
  def autocorrect_remove_lines(corrector, newline_pos, count)
@@ -28,10 +28,10 @@ module RuboCop
28
28
  # # bad
29
29
  # hash = {
30
30
  # key: :value
31
- # }
32
- # and_in_a_method_call({
33
- # no: :difference
34
- # })
31
+ # }
32
+ # in_a_method_call({
33
+ # foo: :bar
34
+ # })
35
35
  # takes_multi_pairs_hash(x: {
36
36
  # a: 1,
37
37
  # b: 2
@@ -42,13 +42,12 @@ module RuboCop
42
42
  # })
43
43
  #
44
44
  # # good
45
- # special_inside_parentheses
46
45
  # hash = {
47
46
  # key: :value
48
47
  # }
49
- # but_in_a_method_call({
50
- # its_like: :this
51
- # })
48
+ # in_a_method_call({
49
+ # foo: :bar
50
+ # })
52
51
  # takes_multi_pairs_hash(x: {
53
52
  # a: 1,
54
53
  # b: 2
@@ -67,17 +66,17 @@ module RuboCop
67
66
  # # bad
68
67
  # hash = {
69
68
  # key: :value
70
- # }
71
- # but_in_a_method_call({
72
- # its_like: :this
73
- # })
69
+ # }
70
+ # in_a_method_call({
71
+ # foo: :bar
72
+ # })
74
73
  #
75
74
  # # good
76
75
  # hash = {
77
76
  # key: :value
78
77
  # }
79
- # and_in_a_method_call({
80
- # no: :difference
78
+ # in_a_method_call({
79
+ # foo: :bar
81
80
  # })
82
81
  #
83
82
  #
@@ -25,6 +25,17 @@ module RuboCop
25
25
  # end
26
26
  # end
27
27
  #
28
+ # @example
29
+ # # bad
30
+ # value = (
31
+ # foo - bar
32
+ # )
33
+ #
34
+ # # good
35
+ # value = (
36
+ # foo - bar
37
+ # )
38
+ #
28
39
  # @example AllowedPatterns: ['^\s*module']
29
40
  # # bad
30
41
  # module A
@@ -95,6 +106,16 @@ module RuboCop
95
106
  check_indentation(node.loc.end, node.children.first)
96
107
  end
97
108
 
109
+ def on_begin(node)
110
+ # Only a parenthesized grouping expression (e.g. `(\n foo\n)`) has
111
+ # explicit delimiters. Indent the body one step from the line
112
+ # the opening parenthesis is on, but only when the closing parenthesis is
113
+ # first on its line (a body on the opening line is skipped downstream).
114
+ return unless parentheses?(node) && begins_its_line?(node.loc.end)
115
+
116
+ check_indentation(opening_line_start(node.loc.begin), node.children.first)
117
+ end
118
+
98
119
  def on_block(node)
99
120
  end_loc = node.loc.end
100
121
 
@@ -188,6 +209,13 @@ module RuboCop
188
209
  AlignmentCorrector.correct(corrector, processed_source, node, @column_delta)
189
210
  end
190
211
 
212
+ # Returns a range at the first non-space column of the line the opening parenthesis is on,
213
+ # used as the base to indent the body from.
214
+ def opening_line_start(begin_loc)
215
+ column = begin_loc.source_line =~ /\S/
216
+ source_range(processed_source.buffer, begin_loc.line, column)
217
+ end
218
+
191
219
  def check_members(base, members)
192
220
  check_indentation(base, select_check_member(members.first))
193
221
 
@@ -204,10 +204,14 @@ module RuboCop
204
204
 
205
205
  def autocorrect(corrector, range, right_operand)
206
206
  range_source = range.source
207
+ # Match the operator exactly, not by substring, so compound assignments
208
+ # like `**=` and `/=` are not mistaken for `**` and `/` (which would drop
209
+ # the `=` and silently change the program's behavior).
210
+ operator = range_source.strip
207
211
 
208
- if range_source.include?('**') && !space_around_exponent_operator?
212
+ if operator == '**' && !space_around_exponent_operator?
209
213
  corrector.replace(range, '**')
210
- elsif range_source.include?('/') && !space_around_slash_operator?(right_operand)
214
+ elsif operator == '/' && !space_around_slash_operator?(right_operand)
211
215
  corrector.replace(range, '/')
212
216
  elsif range_source.end_with?("\n")
213
217
  corrector.replace(range, " #{range_source.strip}\n")
@@ -78,13 +78,25 @@ module RuboCop
78
78
  end
79
79
 
80
80
  def allowed_construct?(asgn_node)
81
- asgn_node.begin_type? || conditional_assignment?(asgn_node)
81
+ return true if asgn_node.begin_type?
82
+
83
+ conditional_assignment?(asgn_node) || discarded_assignment?(asgn_node)
82
84
  end
83
85
 
84
86
  def conditional_assignment?(asgn_node)
85
87
  !asgn_node.loc.operator
86
88
  end
87
89
 
90
+ # An assignment that is a statement of a multi-statement `begin`
91
+ # (e.g. `(foo = bar; baz)`) has its value discarded, so it is not used
92
+ # as the condition. Wrapping it in parentheses would only conflict with
93
+ # `Style/RedundantParentheses`, so it is left alone.
94
+ def discarded_assignment?(asgn_node)
95
+ parent = asgn_node.parent
96
+
97
+ parent&.begin_type? && parent.children.size > 1
98
+ end
99
+
88
100
  def skip_children?(asgn_node)
89
101
  (asgn_node.call_type? && !asgn_node.assignment_method?) ||
90
102
  empty_condition?(asgn_node) ||
@@ -109,9 +109,11 @@ module RuboCop
109
109
  when :optarg
110
110
  send_arg.source == def_arg_name.to_s
111
111
  when :kwoptarg, :kwarg
112
- send_arg.hash_type? &&
112
+ keyword_hash_argument?(send_arg) &&
113
113
  send_arg.pairs.any? { |pair| passing_keyword_arg?(pair, def_arg_name) }
114
114
  when :kwrestarg
115
+ return false unless keyword_hash_argument?(send_arg)
116
+
115
117
  send_arg.each_child_node(:kwsplat, :forwarded_kwrestarg).any? do |child|
116
118
  child.source == def_arg.source
117
119
  end
@@ -120,6 +122,10 @@ module RuboCop
120
122
  end
121
123
  end
122
124
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
125
+
126
+ def keyword_hash_argument?(send_arg)
127
+ send_arg.hash_type? && !send_arg.braces?
128
+ end
123
129
  end
124
130
  end
125
131
  end
@@ -44,9 +44,7 @@ module RuboCop
44
44
 
45
45
  def on_regexp(node)
46
46
  RuboCop::Util.silence_warnings do
47
- node.parsed_tree&.each_expression do |expr|
48
- detect_offenses(node, expr)
49
- end
47
+ detect_offenses_in_tree(node, node.parsed_tree)
50
48
  end
51
49
  end
52
50
 
@@ -55,19 +53,43 @@ module RuboCop
55
53
  return if node.each_descendant(:dstr).any?
56
54
 
57
55
  regexp_constructor(node) do |text|
58
- parse_regexp(text.value)&.each_expression do |expr|
59
- detect_offenses(text, expr)
60
- end
56
+ detect_offenses_in_tree(text, parse_regexp(text.value))
61
57
  end
62
58
  end
63
59
 
64
60
  private
65
61
 
66
- def detect_offenses(node, expr)
67
- return unless expr.type?(:literal)
62
+ # When a character class opens with a bare `]` (e.g. `[^]]`), `regexp_parser` parses
63
+ # `[^]` / `[]` as an empty set and reports the closing `]` as a separate literal.
64
+ # Ruby treats that `]` as the end of the class, not as an unescaped bracket,
65
+ # so the first `]` following an empty set must be skipped.
66
+ def detect_offenses_in_tree(node, tree)
67
+ return unless tree
68
+
69
+ skip_class_closer = false
70
+ tree.each_expression do |expr|
71
+ if empty_character_set?(expr)
72
+ skip_class_closer = true
73
+ elsif expr.type?(:literal)
74
+ skip_class_closer = detect_offenses(node, expr, skip_class_closer)
75
+ end
76
+ end
77
+ end
78
+
79
+ def empty_character_set?(expr)
80
+ expr.type?(:set) && expr.expressions.empty?
81
+ end
68
82
 
83
+ def detect_offenses(node, expr, skip_class_closer)
69
84
  expr.text.scan(/(?<!\\)\]/) do
70
85
  pos = Regexp.last_match.begin(0)
86
+
87
+ # The first `]` following an empty `[^]` / `[]` set closes the character class.
88
+ if skip_class_closer
89
+ skip_class_closer = false
90
+ next
91
+ end
92
+
71
93
  # If the unescaped bracket is the first character of the regexp, Ruby does not warn.
72
94
  # `pos` is relative to the sub-expression, so add its start offset (`expr.ts`).
73
95
  next if (expr.ts + pos).zero?
@@ -78,6 +100,8 @@ module RuboCop
78
100
  corrector.replace(location, '\]')
79
101
  end
80
102
  end
103
+
104
+ skip_class_closer
81
105
  end
82
106
 
83
107
  def range_at_index(node, index, offset)
@@ -58,7 +58,7 @@ module RuboCop
58
58
  return unless node.method?(:define_method)
59
59
 
60
60
  method_name = node.send_node.first_argument
61
- return if method_name.basic_literal? && allowed?(method_name.value)
61
+ return if method_name&.basic_literal? && allowed?(method_name.value)
62
62
 
63
63
  check_code_length(node)
64
64
  end
@@ -12,9 +12,15 @@ module RuboCop
12
12
  # nodes count. In contrast to the CyclomaticComplexity cop, this cop
13
13
  # considers `else` nodes as adding complexity.
14
14
  #
15
+ # A `case`/`in` branch whose pattern is a simple literal (e.g. `in 1`, `in "red"`, `in 1..10`)
16
+ # or a constant/type (e.g. `in Integer`) and has no guard is just as easy to read as a `when`
17
+ # branch, so it is discounted the same way. Branches with structural patterns (e.g. array,
18
+ # hash, or find patterns), bindings, alternatives, or a guard add the full complexity of
19
+ # a decision point.
20
+ #
15
21
  # @example
16
22
  #
17
- # def my_method # 1
23
+ # def example_1 # 1
18
24
  # if cond # 1
19
25
  # case var # 2 (0.8 + 4 * 0.2, rounded)
20
26
  # when 1 then func_one
@@ -26,33 +32,58 @@ module RuboCop
26
32
  # do_something until a && b # 2
27
33
  # end # ===
28
34
  # end # 7 complexity points
35
+ #
36
+ # def example_2 # 1
37
+ # case color # 1 (3 * 0.2, rounded)
38
+ # in "red" then func_red
39
+ # in "blue" then func_blue
40
+ # in "green" then func_green
41
+ # end # ===
42
+ # end # 2 complexity points
29
43
  class PerceivedComplexity < CyclomaticComplexity
30
44
  MSG = 'Perceived complexity for `%<method>s` is too high. [%<complexity>d/%<max>d]'
31
45
 
32
- COUNTED_NODES = (CyclomaticComplexity::COUNTED_NODES - [:when] + [:case]).freeze
46
+ COUNTED_NODES = (
47
+ CyclomaticComplexity::COUNTED_NODES - %i[when in_pattern] + %i[case case_match]
48
+ ).freeze
33
49
 
34
50
  private
35
51
 
52
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
36
53
  def complexity_score_for(node)
37
54
  case node.type
38
55
  when :case
39
- # If cond is nil, that means each when has an expression that
40
- # evaluates to true or false. It's just an alternative to
41
- # if/elsif/elsif... so the when nodes count.
56
+ # If cond is nil, that means each when has an expression that evaluates to true or
57
+ # false. It's just an alternative to if/elsif/elsif... so the when nodes count.
42
58
  nb_branches = node.when_branches.length + (node.else_branch ? 1 : 0)
43
59
  if node.condition.nil?
44
60
  nb_branches
45
61
  else
46
- # Otherwise, the case node gets 0.8 complexity points and each
47
- # when gets 0.2.
62
+ # Otherwise, the case node gets 0.8 complexity points and each when gets 0.2.
48
63
  ((nb_branches * 0.2) + 0.8).round
49
64
  end
65
+ when :case_match
66
+ # Simple `in` branches are discounted like `when`, while structural patterns keep
67
+ # the full complexity of a decision point.
68
+ score = node.in_pattern_branches.sum { |branch| simple_in_pattern?(branch) ? 0.2 : 1 }
69
+ score += 0.2 if node.else_branch
70
+ score.round
50
71
  when :if
51
72
  node.else? && !node.elsif? ? 2 : 1
52
73
  else
53
74
  super
54
75
  end
55
76
  end
77
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
78
+
79
+ def simple_in_pattern?(in_pattern_node)
80
+ # `in_pattern_node.children[1]` is the guard (`if`/`unless`), or `nil`.
81
+ return false unless in_pattern_node.children[1].nil?
82
+
83
+ # A scalar literal, a literal range, or a constant/type is as easy to read as a `when`.
84
+ pattern = in_pattern_node.pattern
85
+ pattern.literal? || pattern.const_type?
86
+ end
56
87
  end
57
88
  end
58
89
  end
@@ -182,10 +182,18 @@ module RuboCop
182
182
  return ":\"#{value.source}\"" if value.dsym_type?
183
183
  return "\"#{value.source}\"" if value.dstr_type?
184
184
  return ":#{value.source}" if value.sym_type?
185
+ # The element of a `%w` array can contain characters that are special
186
+ # inside a single-quoted string (e.g. a `'`), so escape them rather than
187
+ # wrapping the raw source.
188
+ return to_single_quoted(value.value) if value.str_type?
185
189
 
186
190
  "'#{value.source}'"
187
191
  end
188
192
 
193
+ def to_single_quoted(string)
194
+ "'#{string.gsub(/['\\]/) { |character| "\\#{character}" }}'"
195
+ end
196
+
189
197
  def except_key(node)
190
198
  key_arg = node.argument_list.first.source
191
199
  body, = extract_body_if_negated(node.body)
@@ -94,6 +94,10 @@ module RuboCop
94
94
 
95
95
  return unless captures.use_transformed_argname?
96
96
 
97
+ # A splat transforming expression (e.g. `[k, *v]`) can't be used as a
98
+ # standalone block return value, so the rewrite would produce invalid Ruby.
99
+ return if captures.transforming_body_expr.splat_type?
100
+
97
101
  message = "Prefer `#{new_method_name}` over `#{match_desc}`."
98
102
  add_offense(node, message: message) do |corrector|
99
103
  correction = prepare_correction(node)
@@ -203,9 +203,10 @@ module RuboCop
203
203
 
204
204
  def match_acronym?(expected, name)
205
205
  expected = expected.to_s
206
- name = name.to_s
207
-
208
- allowed_acronyms.any? { |acronym| expected.gsub(acronym.capitalize, acronym) == name }
206
+ name = allowed_acronyms.reduce(name.to_s) do |result, acronym|
207
+ result.gsub(acronym, acronym.capitalize)
208
+ end
209
+ expected == name
209
210
  end
210
211
 
211
212
  def to_namespace(path) # rubocop:disable Metrics/AbcSize