rubocop 1.18.3 → 1.20.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +46 -7
  4. data/lib/rubocop/cli.rb +18 -0
  5. data/lib/rubocop/config_loader.rb +2 -2
  6. data/lib/rubocop/config_loader_resolver.rb +21 -6
  7. data/lib/rubocop/config_validator.rb +18 -5
  8. data/lib/rubocop/cop/bundler/gem_filename.rb +103 -0
  9. data/lib/rubocop/cop/bundler/ordered_gems.rb +1 -1
  10. data/lib/rubocop/cop/correctors/require_library_corrector.rb +23 -0
  11. data/lib/rubocop/cop/documentation.rb +1 -1
  12. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +1 -1
  13. data/lib/rubocop/cop/internal_affairs/inherit_deprecated_cop_class.rb +34 -0
  14. data/lib/rubocop/cop/internal_affairs/undefined_config.rb +71 -0
  15. data/lib/rubocop/cop/internal_affairs.rb +2 -0
  16. data/lib/rubocop/cop/layout/class_structure.rb +5 -1
  17. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +9 -0
  18. data/lib/rubocop/cop/layout/end_alignment.rb +10 -2
  19. data/lib/rubocop/cop/layout/first_argument_indentation.rb +1 -1
  20. data/lib/rubocop/cop/layout/hash_alignment.rb +22 -18
  21. data/lib/rubocop/cop/layout/heredoc_indentation.rb +0 -7
  22. data/lib/rubocop/cop/layout/indentation_style.rb +2 -2
  23. data/lib/rubocop/cop/layout/leading_comment_space.rb +1 -1
  24. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +33 -14
  25. data/lib/rubocop/cop/layout/multiline_method_argument_line_breaks.rb +3 -0
  26. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +22 -9
  27. data/lib/rubocop/cop/layout/space_around_operators.rb +8 -1
  28. data/lib/rubocop/cop/layout/space_before_comment.rb +1 -1
  29. data/lib/rubocop/cop/layout/space_inside_parens.rb +5 -5
  30. data/lib/rubocop/cop/layout/trailing_whitespace.rb +24 -1
  31. data/lib/rubocop/cop/lint/ambiguous_range.rb +105 -0
  32. data/lib/rubocop/cop/lint/ambiguous_regexp_literal.rb +5 -2
  33. data/lib/rubocop/cop/lint/debugger.rb +2 -2
  34. data/lib/rubocop/cop/lint/duplicate_branch.rb +2 -1
  35. data/lib/rubocop/cop/lint/duplicate_methods.rb +8 -5
  36. data/lib/rubocop/cop/lint/shadowed_argument.rb +1 -1
  37. data/lib/rubocop/cop/mixin/annotation_comment.rb +57 -34
  38. data/lib/rubocop/cop/mixin/check_line_breakable.rb +2 -2
  39. data/lib/rubocop/cop/mixin/documentation_comment.rb +5 -2
  40. data/lib/rubocop/cop/mixin/frozen_string_literal.rb +14 -1
  41. data/lib/rubocop/cop/mixin/hash_transform_method.rb +6 -1
  42. data/lib/rubocop/cop/mixin/heredoc.rb +7 -0
  43. data/lib/rubocop/cop/mixin/percent_array.rb +13 -7
  44. data/lib/rubocop/cop/mixin/require_library.rb +59 -0
  45. data/lib/rubocop/cop/mixin/space_before_punctuation.rb +1 -1
  46. data/lib/rubocop/cop/naming/inclusive_language.rb +18 -1
  47. data/lib/rubocop/cop/style/block_delimiters.rb +39 -6
  48. data/lib/rubocop/cop/style/comment_annotation.rb +25 -39
  49. data/lib/rubocop/cop/style/commented_keyword.rb +2 -1
  50. data/lib/rubocop/cop/style/conditional_assignment.rb +19 -5
  51. data/lib/rubocop/cop/style/double_cop_disable_directive.rb +1 -7
  52. data/lib/rubocop/cop/style/double_negation.rb +12 -1
  53. data/lib/rubocop/cop/style/encoding.rb +26 -15
  54. data/lib/rubocop/cop/style/eval_with_location.rb +1 -1
  55. data/lib/rubocop/cop/style/explicit_block_argument.rb +32 -7
  56. data/lib/rubocop/cop/style/frozen_string_literal_comment.rb +1 -1
  57. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +11 -0
  58. data/lib/rubocop/cop/style/hash_except.rb +4 -3
  59. data/lib/rubocop/cop/style/hash_transform_keys.rb +0 -3
  60. data/lib/rubocop/cop/style/identical_conditional_branches.rb +30 -5
  61. data/lib/rubocop/cop/style/method_def_parentheses.rb +10 -1
  62. data/lib/rubocop/cop/style/missing_else.rb +7 -0
  63. data/lib/rubocop/cop/style/mutable_constant.rb +73 -13
  64. data/lib/rubocop/cop/style/redundant_begin.rb +25 -0
  65. data/lib/rubocop/cop/style/redundant_freeze.rb +4 -3
  66. data/lib/rubocop/cop/style/redundant_self_assignment_branch.rb +83 -0
  67. data/lib/rubocop/cop/style/redundant_sort.rb +2 -2
  68. data/lib/rubocop/cop/style/semicolon.rb +32 -24
  69. data/lib/rubocop/cop/style/single_line_block_params.rb +3 -1
  70. data/lib/rubocop/cop/style/single_line_methods.rb +14 -9
  71. data/lib/rubocop/cop/style/sole_nested_conditional.rb +4 -0
  72. data/lib/rubocop/cop/style/special_global_vars.rb +21 -0
  73. data/lib/rubocop/cop/style/struct_inheritance.rb +3 -0
  74. data/lib/rubocop/cop/style/symbol_array.rb +3 -3
  75. data/lib/rubocop/cop/style/word_array.rb +23 -5
  76. data/lib/rubocop/cop/util.rb +7 -2
  77. data/lib/rubocop/formatter/git_hub_actions_formatter.rb +1 -1
  78. data/lib/rubocop/magic_comment.rb +44 -15
  79. data/lib/rubocop/options.rb +1 -1
  80. data/lib/rubocop/version.rb +1 -1
  81. data/lib/rubocop.rb +6 -1
  82. metadata +12 -5
@@ -12,6 +12,7 @@ module RuboCop
12
12
  #
13
13
  # Auto-correction removes comments from `end` keyword and keeps comments
14
14
  # for `class`, `module`, `def` and `begin` above the keyword.
15
+ # It is marked as unsafe auto-correction as it may remove meaningful comments.
15
16
  #
16
17
  # @example
17
18
  # # bad
@@ -50,7 +51,7 @@ module RuboCop
50
51
 
51
52
  def on_new_investigation
52
53
  processed_source.comments.each do |comment|
53
- next unless (match = line(comment).match(/(?<keyword>\S+).*#/)) && offensive?(comment)
54
+ next unless offensive?(comment) && (match = line(comment).match(/(?<keyword>\S+).*#/))
54
55
 
55
56
  register_offense(comment, match[:keyword])
56
57
  end
@@ -26,7 +26,7 @@ module RuboCop
26
26
  # `when` nodes contain the entire branch including the condition.
27
27
  # We only need the contents of the branch, not the condition.
28
28
  def expand_when_branches(when_branches)
29
- when_branches.map { |branch| branch.children[1] }
29
+ when_branches.map(&:body)
30
30
  end
31
31
 
32
32
  def tail(branch)
@@ -272,6 +272,16 @@ module RuboCop
272
272
  check_node(node, branches)
273
273
  end
274
274
 
275
+ def on_case_match(node)
276
+ return unless style == :assign_to_condition
277
+ return unless node.else_branch
278
+
279
+ in_pattern_branches = expand_when_branches(node.in_pattern_branches)
280
+ branches = [*in_pattern_branches, node.else_branch]
281
+
282
+ check_node(node, branches)
283
+ end
284
+
275
285
  private
276
286
 
277
287
  def check_assignment_to_condition(node)
@@ -297,7 +307,7 @@ module RuboCop
297
307
  end
298
308
 
299
309
  # @!method candidate_condition?(node)
300
- def_node_matcher :candidate_condition?, '[{if case} !#allowed_ternary?]'
310
+ def_node_matcher :candidate_condition?, '[{if case case_match} !#allowed_ternary?]'
301
311
 
302
312
  def allowed_ternary?(assignment)
303
313
  assignment.if_type? && assignment.ternary? && !include_ternary?
@@ -319,7 +329,7 @@ module RuboCop
319
329
  end
320
330
 
321
331
  def move_assignment_outside_condition(corrector, node)
322
- if node.case_type?
332
+ if node.case_type? || node.case_match_type?
323
333
  CaseCorrector.correct(corrector, self, node)
324
334
  elsif node.ternary?
325
335
  TernaryCorrector.correct(corrector, node)
@@ -333,7 +343,7 @@ module RuboCop
333
343
 
334
344
  if ternary_condition?(condition)
335
345
  TernaryCorrector.move_assignment_inside_condition(corrector, node)
336
- elsif condition.case_type?
346
+ elsif condition.case_type? || condition.case_match_type?
337
347
  CaseCorrector.move_assignment_inside_condition(corrector, node)
338
348
  elsif condition.if_type?
339
349
  IfCorrector.move_assignment_inside_condition(corrector, node)
@@ -631,7 +641,11 @@ module RuboCop
631
641
  end
632
642
 
633
643
  def extract_branches(case_node)
634
- when_branches = expand_when_branches(case_node.when_branches)
644
+ when_branches = if case_node.case_type?
645
+ expand_when_branches(case_node.when_branches)
646
+ else
647
+ expand_when_branches(case_node.in_pattern_branches)
648
+ end
635
649
 
636
650
  [when_branches, case_node.else_branch]
637
651
  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
@@ -62,7 +62,7 @@ module RuboCop
62
62
  def end_of_method_definition?(node)
63
63
  return false unless (def_node = find_def_node_from_ascendant(node))
64
64
 
65
- last_child = def_node.child_nodes.last
65
+ last_child = find_last_child(def_node.body)
66
66
 
67
67
  last_child.last_line == node.last_line
68
68
  end
@@ -73,6 +73,17 @@ module RuboCop
73
73
 
74
74
  find_def_node_from_ascendant(node.parent)
75
75
  end
76
+
77
+ def find_last_child(node)
78
+ case node.type
79
+ when :rescue
80
+ find_last_child(node.body)
81
+ when :ensure
82
+ find_last_child(node.child_nodes.first)
83
+ else
84
+ node.child_nodes.last
85
+ end
86
+ end
76
87
  end
77
88
  end
78
89
  end
@@ -13,38 +13,49 @@ module RuboCop
13
13
  include RangeHelp
14
14
  extend AutoCorrector
15
15
 
16
- MSG_UNNECESSARY = 'Unnecessary utf-8 encoding comment.'
16
+ MSG = 'Unnecessary utf-8 encoding comment.'
17
17
  ENCODING_PATTERN = /#.*coding\s?[:=]\s?(?:UTF|utf)-8/.freeze
18
18
  SHEBANG = '#!'
19
19
 
20
20
  def on_new_investigation
21
21
  return if processed_source.buffer.source.empty?
22
22
 
23
- line_number = encoding_line_number(processed_source)
24
- return unless (@message = offense(processed_source, line_number))
23
+ comments.each do |line_number, comment|
24
+ next unless offense?(comment)
25
25
 
26
- range = processed_source.buffer.line_range(line_number + 1)
27
- add_offense(range, message: @message) do |corrector|
28
- corrector.remove(range_with_surrounding_space(range: range, side: :right))
26
+ register_offense(line_number, comment)
29
27
  end
30
28
  end
31
29
 
32
30
  private
33
31
 
34
- def offense(processed_source, line_number)
35
- line = processed_source[line_number]
32
+ def comments
33
+ processed_source.lines.each.with_index.with_object({}) do |(line, line_number), comments|
34
+ next if line.start_with?(SHEBANG)
36
35
 
37
- MSG_UNNECESSARY if encoding_omitable?(line)
36
+ comment = MagicComment.parse(line)
37
+ return comments unless comment.valid?
38
+
39
+ comments[line_number + 1] = comment
40
+ end
38
41
  end
39
42
 
40
- def encoding_omitable?(line)
41
- ENCODING_PATTERN.match?(line)
43
+ def offense?(comment)
44
+ comment.encoding_specified? && comment.encoding.casecmp('utf-8').zero?
42
45
  end
43
46
 
44
- def encoding_line_number(processed_source)
45
- line_number = 0
46
- line_number += 1 if processed_source[line_number].start_with?(SHEBANG)
47
- line_number
47
+ def register_offense(line_number, comment)
48
+ range = processed_source.buffer.line_range(line_number)
49
+
50
+ add_offense(range) do |corrector|
51
+ text = comment.without(:encoding)
52
+
53
+ if text.blank?
54
+ corrector.remove(range_with_surrounding_space(range: range, side: :right))
55
+ else
56
+ corrector.replace(range, text)
57
+ end
58
+ end
48
59
  end
49
60
  end
50
61
  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
@@ -93,18 +93,43 @@ module RuboCop
93
93
 
94
94
  def add_block_argument(node, corrector)
95
95
  if node.arguments?
96
- last_arg = node.arguments.last
97
- arg_range = range_with_surrounding_comma(last_arg.source_range, :right)
98
- replacement = ' &block'
99
- replacement = ",#{replacement}" unless arg_range.source.end_with?(',')
100
- corrector.insert_after(arg_range, replacement) unless last_arg.blockarg_type?
101
- elsif node.call_type? || node.zsuper_type?
102
- corrector.insert_after(node, '(&block)')
96
+ insert_argument(node, corrector)
97
+ elsif empty_arguments?(node)
98
+ corrector.replace(node.arguments, '(&block)')
99
+ elsif call_like?(node)
100
+ correct_call_node(node, corrector)
103
101
  else
104
102
  corrector.insert_after(node.loc.name, '(&block)')
105
103
  end
106
104
  end
107
105
 
106
+ def empty_arguments?(node)
107
+ # Is there an arguments node with only parentheses?
108
+ node.arguments.is_a?(RuboCop::AST::Node) && node.arguments.loc.begin
109
+ end
110
+
111
+ def call_like?(node)
112
+ node.call_type? || node.zsuper_type? || node.super_type?
113
+ end
114
+
115
+ def insert_argument(node, corrector)
116
+ last_arg = node.arguments.last
117
+ arg_range = range_with_surrounding_comma(last_arg.source_range, :right)
118
+ replacement = ' &block'
119
+ replacement = ",#{replacement}" unless arg_range.source.end_with?(',')
120
+ corrector.insert_after(arg_range, replacement) unless last_arg.blockarg_type?
121
+ end
122
+
123
+ def correct_call_node(node, corrector)
124
+ corrector.insert_after(node, '(&block)')
125
+ return unless node.parenthesized?
126
+
127
+ args_begin = Util.args_begin(node)
128
+ args_end = Util.args_end(node)
129
+ range = range_between(args_begin.begin_pos, args_end.end_pos)
130
+ corrector.remove(range)
131
+ end
132
+
108
133
  def block_body_range(block_node, send_node)
109
134
  range_between(send_node.loc.expression.end_pos, block_node.loc.end.end_pos)
110
135
  end
@@ -141,7 +141,7 @@ module RuboCop
141
141
 
142
142
  def frozen_string_literal_comment(processed_source)
143
143
  processed_source.find_token do |token|
144
- token.text.start_with?(FrozenStringLiteral::FROZEN_STRING_LITERAL)
144
+ token.text.start_with?(FROZEN_STRING_LITERAL)
145
145
  end
146
146
  end
147
147
 
@@ -29,6 +29,7 @@ module RuboCop
29
29
  # # good
30
30
  # [{ one: 1 }, { two: 2 }]
31
31
  class HashAsLastArrayItem < Base
32
+ include RangeHelp
32
33
  include ConfigurableEnforcedStyle
33
34
  extend AutoCorrector
34
35
 
@@ -74,6 +75,7 @@ module RuboCop
74
75
  return if node.children.empty? # Empty hash cannot be "unbraced"
75
76
 
76
77
  add_offense(node, message: 'Omit the braces around the hash.') do |corrector|
78
+ remove_last_element_trailing_comma(corrector, node.parent)
77
79
  corrector.remove(node.loc.begin)
78
80
  corrector.remove(node.loc.end)
79
81
  end
@@ -82,6 +84,15 @@ module RuboCop
82
84
  def braces_style?
83
85
  style == :braces
84
86
  end
87
+
88
+ def remove_last_element_trailing_comma(corrector, node)
89
+ range = range_with_surrounding_space(
90
+ range: node.children.last.source_range,
91
+ side: :right
92
+ ).end.resize(1)
93
+
94
+ corrector.remove(range) if range.source == ','
95
+ end
85
96
  end
86
97
  end
87
98
  end
@@ -49,7 +49,7 @@ module RuboCop
49
49
  return unless bad_method?(block) && semantically_except_method?(node, block)
50
50
 
51
51
  except_key = except_key(block)
52
- return unless safe_to_register_offense?(block, except_key)
52
+ return if except_key.nil? || !safe_to_register_offense?(block, except_key)
53
53
 
54
54
  range = offense_range(node)
55
55
  preferred_method = "except(#{except_key.source})"
@@ -81,10 +81,11 @@ module RuboCop
81
81
  end
82
82
 
83
83
  def except_key(node)
84
- key_argument = node.argument_list.first
84
+ key_argument = node.argument_list.first.source
85
85
  lhs, _method_name, rhs = *node.body
86
+ return if [lhs, rhs].map(&:source).none?(key_argument)
86
87
 
87
- [lhs, rhs].find { |operand| operand.source != key_argument.source }
88
+ [lhs, rhs].find { |operand| operand.source != key_argument }
88
89
  end
89
90
 
90
91
  def offense_range(node)
@@ -27,11 +27,8 @@ module RuboCop
27
27
  # {a: 1, b: 2}.transform_keys { |k| k.to_s }
28
28
  class HashTransformKeys < Base
29
29
  include HashTransformMethod
30
- extend TargetRubyVersion
31
30
  extend AutoCorrector
32
31
 
33
- minimum_target_ruby_version 2.5
34
-
35
32
  # @!method on_bad_each_with_object(node)
36
33
  def_node_matcher :on_bad_each_with_object, <<~PATTERN
37
34
  (block
@@ -7,6 +7,22 @@ module RuboCop
7
7
  # each branch of a conditional expression. Such expressions should normally
8
8
  # be placed outside the conditional expression - before or after it.
9
9
  #
10
+ # This cop is marked unsafe auto-correction as the order of method invocations
11
+ # must be guaranteed in the following case:
12
+ #
13
+ # [source,ruby]
14
+ # ----
15
+ # if method_that_modifies_global_state # 1
16
+ # method_that_relies_on_global_state # 2
17
+ # foo # 3
18
+ # else
19
+ # method_that_relies_on_global_state # 2
20
+ # bar # 3
21
+ # end
22
+ # ----
23
+ #
24
+ # In such a case, auto-correction may change the invocation order.
25
+ #
10
26
  # NOTE: The cop is poorly named and some people might think that it actually
11
27
  # checks for duplicated conditional branches. The name will probably be changed
12
28
  # in a future major RuboCop release.
@@ -124,21 +140,30 @@ module RuboCop
124
140
  return if branches.any?(&:nil?)
125
141
 
126
142
  tails = branches.map { |branch| tail(branch) }
127
- check_expressions(node, tails, :after_condition) if duplicated_expressions?(tails)
143
+ check_expressions(node, tails, :after_condition) if duplicated_expressions?(node, tails)
128
144
 
129
145
  heads = branches.map { |branch| head(branch) }
130
- check_expressions(node, heads, :before_condition) if duplicated_expressions?(heads)
146
+ check_expressions(node, heads, :before_condition) if duplicated_expressions?(node, heads)
131
147
  end
132
148
 
133
- def duplicated_expressions?(expressions)
134
- expressions.size > 1 && expressions.uniq.one?
149
+ def duplicated_expressions?(node, expressions)
150
+ unique_expressions = expressions.uniq
151
+ return false unless expressions.size >= 1 && unique_expressions.one?
152
+
153
+ unique_expression = unique_expressions.first
154
+ return true unless unique_expression.assignment?
155
+
156
+ lhs = unique_expression.child_nodes.first
157
+ node.condition.child_nodes.none? { |n| n.source == lhs.source if n.variable? }
135
158
  end
136
159
 
137
- def check_expressions(node, expressions, insert_position)
160
+ def check_expressions(node, expressions, insert_position) # rubocop:disable Metrics/MethodLength
138
161
  inserted_expression = false
139
162
 
140
163
  expressions.each do |expression|
141
164
  add_offense(expression) do |corrector|
165
+ next if node.if_type? && node.ternary?
166
+
142
167
  range = range_by_whole_lines(expression.source_range, include_final_newline: true)
143
168
  corrector.remove(range)
144
169
  next if inserted_expression
@@ -96,7 +96,7 @@ module RuboCop
96
96
  MSG_MISSING = 'Use def with parentheses when there are parameters.'
97
97
 
98
98
  def on_def(node)
99
- return if node.endless?
99
+ return if forced_parentheses?(node)
100
100
 
101
101
  args = node.arguments
102
102
 
@@ -129,6 +129,15 @@ module RuboCop
129
129
  corrector.insert_after(arguments_range, ')')
130
130
  end
131
131
 
132
+ def forced_parentheses?(node)
133
+ # Regardless of style, parentheses are necessary for:
134
+ # 1. Endless methods
135
+ # 2. Argument lists containing a `forward-arg` (`...`)
136
+ # Removing the parens would be a syntax error here.
137
+
138
+ node.endless? || node.arguments.any?(&:forward_arg_type?)
139
+ end
140
+
132
141
  def require_parentheses?(args)
133
142
  style == :require_parentheses ||
134
143
  (style == :require_no_parentheses_except_multiline && args.multiline?)
@@ -5,6 +5,9 @@ module RuboCop
5
5
  module Style
6
6
  # Checks for `if` expressions that do not have an `else` branch.
7
7
  #
8
+ # NOTE: Pattern matching is allowed to have no `else` branch because unlike `if` and `case`,
9
+ # it raises `NoMatchingPatternError` if the pattern doesn't match and without having `else`.
10
+ #
8
11
  # Supported styles are: if, case, both.
9
12
  #
10
13
  # @example EnforcedStyle: if
@@ -114,6 +117,10 @@ module RuboCop
114
117
  check(node)
115
118
  end
116
119
 
120
+ def on_case_match(node)
121
+ # do nothing.
122
+ end
123
+
117
124
  private
118
125
 
119
126
  def check(node)
@@ -14,8 +14,16 @@ module RuboCop
14
14
  # positives. Luckily, there is no harm in freezing an already
15
15
  # frozen object.
16
16
  #
17
+ # From Ruby 3.0, this cop honours the magic comment
18
+ # 'shareable_constant_value'. When this magic comment is set to any
19
+ # acceptable value other than none, it will suppress the offenses
20
+ # raised by this cop. It enforces frozen state.
21
+ #
17
22
  # NOTE: Regexp and Range literals are frozen objects since Ruby 3.0.
18
23
  #
24
+ # NOTE: From Ruby 3.0, this cop allows explicit freezing of interpolated
25
+ # string literals when `# frozen-string-literal: true` is used.
26
+ #
19
27
  # @example EnforcedStyle: literals (default)
20
28
  # # bad
21
29
  # CONST = [1, 2, 3]
@@ -52,7 +60,59 @@ module RuboCop
52
60
  # puts 1
53
61
  # end
54
62
  # end.freeze
63
+ #
64
+ # @example
65
+ # # Magic comment - shareable_constant_value: literal
66
+ #
67
+ # # bad
68
+ # CONST = [1, 2, 3]
69
+ #
70
+ # # good
71
+ # # shareable_constant_value: literal
72
+ # CONST = [1, 2, 3]
73
+ #
74
+ # NOTE: This special directive helps to create constants
75
+ # that hold only immutable objects, or Ractor-shareable
76
+ # constants. - ruby docs
77
+ #
55
78
  class MutableConstant < Base
79
+ # Handles magic comment shareable_constant_value with O(n ^ 2) complexity
80
+ # n - number of lines in the source
81
+ # Iterates over all lines before a CONSTANT
82
+ # until it reaches shareable_constant_value
83
+ module ShareableConstantValue
84
+ module_function
85
+
86
+ def recent_shareable_value?(node)
87
+ shareable_constant_comment = magic_comment_in_scope node
88
+ return false if shareable_constant_comment.nil?
89
+
90
+ shareable_constant_value = MagicComment.parse(shareable_constant_comment)
91
+ .shareable_constant_value
92
+ shareable_constant_value_enabled? shareable_constant_value
93
+ end
94
+
95
+ # Identifies the most recent magic comment with valid shareable constant values
96
+ # thats in scope for this node
97
+ def magic_comment_in_scope(node)
98
+ processed_source_till_node(node).reverse_each.find do |line|
99
+ MagicComment.parse(line).valid_shareable_constant_value?
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def processed_source_till_node(node)
106
+ processed_source.lines[0..(node.last_line - 1)]
107
+ end
108
+
109
+ def shareable_constant_value_enabled?(value)
110
+ %w[literal experimental_everything experimental_copy].include? value
111
+ end
112
+ end
113
+ private_constant :ShareableConstantValue
114
+
115
+ include ShareableConstantValue
56
116
  include FrozenStringLiteral
57
117
  include ConfigurableEnforcedStyle
58
118
  extend AutoCorrector
@@ -61,13 +121,12 @@ module RuboCop
61
121
 
62
122
  def on_casgn(node)
63
123
  _scope, _const_name, value = *node
64
- on_assignment(value)
65
- end
124
+ if value.nil? # This is only the case for `CONST += ...` or similarg66
125
+ parent = node.parent
126
+ return unless parent.or_asgn_type? # We only care about `CONST ||= ...`
66
127
 
67
- def on_or_asgn(node)
68
- lhs, value = *node
69
-
70
- return unless lhs&.casgn_type?
128
+ value = parent.children.last
129
+ end
71
130
 
72
131
  on_assignment(value)
73
132
  end
@@ -86,18 +145,18 @@ module RuboCop
86
145
  return if immutable_literal?(value)
87
146
  return if operation_produces_immutable_object?(value)
88
147
  return if frozen_string_literal?(value)
148
+ return if shareable_constant_value?(value)
89
149
 
90
150
  add_offense(value) { |corrector| autocorrect(corrector, value) }
91
151
  end
92
152
 
93
153
  def check(value)
94
154
  range_enclosed_in_parentheses = range_enclosed_in_parentheses?(value)
95
-
96
155
  return unless mutable_literal?(value) ||
97
156
  target_ruby_version <= 2.7 && range_enclosed_in_parentheses
98
157
 
99
- return if FROZEN_STRING_LITERAL_TYPES.include?(value.type) &&
100
- frozen_string_literals_enabled?
158
+ return if frozen_string_literal?(value)
159
+ return if shareable_constant_value?(value)
101
160
 
102
161
  add_offense(value) { |corrector| autocorrect(corrector, value) }
103
162
  end
@@ -118,18 +177,19 @@ module RuboCop
118
177
  end
119
178
 
120
179
  def mutable_literal?(value)
121
- return false if value.nil?
122
180
  return false if frozen_regexp_or_range_literals?(value)
123
181
 
124
182
  value.mutable_literal?
125
183
  end
126
184
 
127
185
  def immutable_literal?(node)
128
- node.nil? || frozen_regexp_or_range_literals?(node) || node.immutable_literal?
186
+ frozen_regexp_or_range_literals?(node) || node.immutable_literal?
129
187
  end
130
188
 
131
- def frozen_string_literal?(node)
132
- FROZEN_STRING_LITERAL_TYPES.include?(node.type) && frozen_string_literals_enabled?
189
+ def shareable_constant_value?(node)
190
+ return false if target_ruby_version < 3.0
191
+
192
+ recent_shareable_value? node
133
193
  end
134
194
 
135
195
  def frozen_regexp_or_range_literals?(node)