rubocop 1.88.1 → 1.88.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +2 -0
  3. data/lib/rubocop/cop/base.rb +13 -8
  4. data/lib/rubocop/cop/commissioner.rb +13 -11
  5. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +12 -2
  6. data/lib/rubocop/cop/layout/comment_indentation.rb +9 -2
  7. data/lib/rubocop/cop/layout/else_alignment.rb +1 -14
  8. data/lib/rubocop/cop/layout/line_length.rb +10 -5
  9. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +6 -1
  10. data/lib/rubocop/cop/lint/debugger.rb +13 -1
  11. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +7 -0
  12. data/lib/rubocop/cop/lint/to_json.rb +8 -1
  13. data/lib/rubocop/cop/lint/unreachable_code.rb +9 -4
  14. data/lib/rubocop/cop/lint/useless_numeric_operation.rb +10 -10
  15. data/lib/rubocop/cop/lint/void.rb +2 -2
  16. data/lib/rubocop/cop/mixin/allowed_methods.rb +5 -5
  17. data/lib/rubocop/cop/mixin/allowed_pattern.rb +5 -1
  18. data/lib/rubocop/cop/mixin/forbidden_pattern.rb +5 -1
  19. data/lib/rubocop/cop/mixin/statement_modifier.rb +1 -1
  20. data/lib/rubocop/cop/style/arguments_forwarding.rb +1 -1
  21. data/lib/rubocop/cop/style/array_intersect.rb +4 -4
  22. data/lib/rubocop/cop/style/collection_compact.rb +1 -1
  23. data/lib/rubocop/cop/style/def_with_parentheses.rb +6 -2
  24. data/lib/rubocop/cop/style/documentation_method.rb +5 -1
  25. data/lib/rubocop/cop/style/hash_conversion.rb +14 -6
  26. data/lib/rubocop/cop/style/hash_lookup_method.rb +2 -6
  27. data/lib/rubocop/cop/style/identical_conditional_branches.rb +15 -4
  28. data/lib/rubocop/cop/style/if_unless_modifier.rb +4 -4
  29. data/lib/rubocop/cop/style/invertible_unless_condition.rb +25 -5
  30. data/lib/rubocop/cop/style/lambda_call.rb +11 -0
  31. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +5 -3
  32. data/lib/rubocop/cop/style/method_def_parentheses.rb +4 -0
  33. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +10 -7
  34. data/lib/rubocop/cop/style/mixin_usage.rb +1 -1
  35. data/lib/rubocop/cop/style/module_function.rb +4 -2
  36. data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +13 -1
  37. data/lib/rubocop/cop/style/redundant_heredoc_delimiter_quotes.rb +6 -1
  38. data/lib/rubocop/cop/style/struct_inheritance.rb +3 -1
  39. data/lib/rubocop/cop/style/trivial_accessors.rb +1 -1
  40. data/lib/rubocop/cop/team.rb +18 -4
  41. data/lib/rubocop/runner.rb +50 -10
  42. data/lib/rubocop/version.rb +1 -1
  43. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04f5790c0be94705978732f52d462bd3afe919498e1e6be410fcfece0faaf4cd
4
- data.tar.gz: 87c79acdf7db812d889bc2464a61d9921db70daf4e6c11117be879d7d9b64991
3
+ metadata.gz: 8642dc64b958118e0563ed93c4837ac13aca29cb8f8a6267155ef450380ea1b3
4
+ data.tar.gz: 569ff18924510b28b60ff03563053d0c53983829d7f4755c0ad39cc213b41f64
5
5
  SHA512:
6
- metadata.gz: c8491483d605d97e0c746f171fdebf6b5b009a0e1e01392764a9fbf12208dad6a7fe8e8529f32d3a2b1ae37e39f150b689638cd7dbc901040f27b6cdf1981ab5
7
- data.tar.gz: 7aa878cbe87d967607a65f37a1b3eafd47507d7a9c54c3bede0398f689527254fa4d65460d3af11d233244eb53eb7058bd5b69326588d7b3d3ed0fbb864afef8
6
+ metadata.gz: 1cc966f73a8b890c335fedbc55fa0ec0f8a73099703cf46111e8619933f2d4b9436cf1c7d597f7fef0282e1e1e2f6b6adc4b4e38c7195eabb600fe97d9931526
7
+ data.tar.gz: 94cc97c23eeaaff0ec7ab9046a2275547fe2363449d0582733e30be26868e098d12c609703fdc2318b1e4d96b8205abf6fe537d655950cf558debfcb6224366c
data/config/default.yml CHANGED
@@ -2213,7 +2213,9 @@ Lint/NumberedParameterAssignment:
2213
2213
  Lint/NumericOperationWithConstantResult:
2214
2214
  Description: 'Checks for numeric operations with constant results.'
2215
2215
  Enabled: pending
2216
+ SafeAutoCorrect: false
2216
2217
  VersionAdded: '1.69'
2218
+ VersionChanged: '1.88'
2217
2219
 
2218
2220
  Lint/OrAssignmentToConstant:
2219
2221
  Description: 'Checks unintended or-assignment to constant.'
@@ -331,12 +331,11 @@ module RuboCop
331
331
  # @api private
332
332
  def self.callbacks_needed
333
333
  @callbacks_needed ||= public_instance_methods.select do |m|
334
- # OPTIMIZE: Check method existence first to make fewer `start_with?` calls.
335
- # At the time of writing this comment, this excludes 98 of ~104 methods.
336
- # `start_with?` with two string arguments instead of a regex is faster
337
- # in this specific case as well.
338
- !Base.method_defined?(m) && # exclude standard "callbacks" like 'on_begin_investigation'
339
- m.start_with?('on_', 'after_')
334
+ # OPTIMIZE: `start_with?` with two string arguments instead of a regex
335
+ # is faster in this specific case.
336
+ m.start_with?('on_', 'after_') &&
337
+ # exclude standard "callbacks" like 'on_new_investigation' unless refined
338
+ instance_method(m).owner != Base
340
339
  end
341
340
  end
342
341
  # rubocop:enable Layout/ClassStructure
@@ -545,15 +544,21 @@ module RuboCop
545
544
  end
546
545
 
547
546
  def range_for_original(range)
547
+ buffer = @current_original.buffer
548
+ return range if @current_offset.zero? && range.source_buffer.equal?(buffer)
549
+
548
550
  ::Parser::Source::Range.new(
549
- @current_original.buffer,
551
+ buffer,
550
552
  range.begin_pos + @current_offset,
551
553
  range.end_pos + @current_offset
552
554
  )
553
555
  end
554
556
 
555
557
  def target_satisfies_all_gem_version_requirements?
556
- self.class.gem_requirements.all? do |gem_name, version_req|
558
+ gem_requirements = self.class.gem_requirements
559
+ return true if gem_requirements.empty?
560
+
561
+ gem_requirements.all? do |gem_name, version_req|
557
562
  all_gem_versions_in_target = @config.gem_versions_in_target
558
563
  next false unless all_gem_versions_in_target
559
564
 
@@ -64,14 +64,16 @@ module RuboCop
64
64
  r = '#' unless RESTRICTED_CALLBACKS.include?(method_name) # has Restricted?
65
65
  c = '#' if NO_CHILD_NODES.include?(node_type) # has Children?
66
66
 
67
+ # The `after_` calls are guarded because barely any cop defines an `after_`
68
+ # callback, and the guard is cheaper than the method call it avoids.
67
69
  class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
68
- def on_#{node_type}(node) # def on_send(node)
69
- trigger_responding_cops(:on_#{node_type}, node) # trigger_responding_cops(:on_send, node)
70
- #{r} trigger_restricted_cops(:on_#{node_type}, node) # trigger_restricted_cops(:on_send, node)
71
- #{c} super(node) # super(node)
72
- #{c} trigger_responding_cops(:after_#{node_type}, node) # trigger_responding_cops(:after_send, node)
73
- #{c}#{r} trigger_restricted_cops(:after_#{node_type}, node) # trigger_restricted_cops(:after_send, node)
74
- end # end
70
+ def on_#{node_type}(node) # def on_send(node)
71
+ trigger_responding_cops(:on_#{node_type}, node) # trigger_responding_cops(:on_send, node)
72
+ #{r} trigger_restricted_cops(:on_#{node_type}, node) # trigger_restricted_cops(:on_send, node)
73
+ #{c} super(node) # super(node)
74
+ #{c} trigger_responding_cops(:after_#{node_type}, node) if @callbacks[:after_#{node_type}] # trigger_responding_cops(:after_send, node) if @callbacks[:after_send]
75
+ #{c}#{r} trigger_restricted_cops(:after_#{node_type}, node) unless @restricted_map[:after_#{node_type}].empty? # trigger_restricted_cops(:after_send, node) unless @restricted_map[:after_send].empty?
76
+ end # end
75
77
  RUBY
76
78
  end
77
79
 
@@ -81,13 +83,13 @@ module RuboCop
81
83
 
82
84
  begin_investigation(processed_source, offset: offset, original: original)
83
85
  if processed_source.valid_syntax?
84
- invoke(:on_new_investigation, @cops)
86
+ invoke(:on_new_investigation, @callbacks[:on_new_investigation])
85
87
  invoke_with_argument(:investigate, @forces, processed_source)
86
88
 
87
89
  walk(processed_source.ast) unless @cops.empty?
88
- invoke(:on_investigation_end, @cops)
90
+ invoke(:on_investigation_end, @callbacks[:on_investigation_end])
89
91
  else
90
- invoke(:on_other_file, @cops)
92
+ invoke(:on_other_file, @callbacks[:on_other_file])
91
93
  end
92
94
  reports = @cops.map { |cop| cop.send(:complete_investigation) }
93
95
  InvestigationReport.new(processed_source, reports, @errors)
@@ -157,7 +159,7 @@ module RuboCop
157
159
  end
158
160
 
159
161
  def invoke(callback, cops)
160
- cops.each { |cop| with_cop_error_handling(cop) { cop.send(callback) } }
162
+ cops&.each { |cop| with_cop_error_handling(cop) { cop.send(callback) } }
161
163
  end
162
164
 
163
165
  def invoke_with_argument(callback, cops, arg)
@@ -83,18 +83,28 @@ module RuboCop
83
83
  def duplicated_assignment_method_nodes
84
84
  assignment_method_declarations(processed_source.ast)
85
85
  .select(&:assignment_method?)
86
- .group_by(&:method_name)
86
+ .group_by { |node| [enclosing_specification(node), node.method_name] }
87
87
  .values
88
88
  .select { |nodes| nodes.size > 1 }
89
89
  end
90
90
 
91
91
  def duplicated_indexed_assignment_method_nodes
92
92
  indexed_assignment_method_declarations(processed_source.ast)
93
- .group_by { |node| [node.children.first.method_name, node.first_argument] }
93
+ .group_by { |node| indexed_assignment_key(node) }
94
94
  .values
95
95
  .select { |nodes| nodes.size > 1 }
96
96
  end
97
97
 
98
+ def indexed_assignment_key(node)
99
+ [enclosing_specification(node), node.children.first.method_name, node.first_argument]
100
+ end
101
+
102
+ # Assignments in separate `Gem::Specification.new` blocks are not duplicates of
103
+ # one another, so the enclosing specification is part of the grouping key.
104
+ def enclosing_specification(node)
105
+ node.each_ancestor(:block).find { |block| gem_specification?(block) }
106
+ end
107
+
98
108
  def register_offense(node, assignment, line_of_first_occurrence)
99
109
  line_range = node.loc.column...node.loc.last_column
100
110
  offense_location = source_range(processed_source.buffer, node.first_line, line_range)
@@ -155,8 +155,15 @@ module RuboCop
155
155
 
156
156
  def less_indented?(line)
157
157
  rule = config.for_cop('Layout/AccessModifierIndentation')['EnforcedStyle'] == 'outdent'
158
- access_modifier = 'private|protected|public'
159
- /\A\s*(end\b|[)}\]])/.match?(line) || (rule && /\A\s*(#{access_modifier})\b/.match?(line))
158
+ /\A\s*(end\b|[)}\]])/.match?(line) || (rule && bare_access_modifier?(line))
159
+ end
160
+
161
+ # Only a bare access modifier (the keyword alone on its line) is outdented by
162
+ # `Layout/AccessModifierIndentation`. An inline modifier such as `private def foo`
163
+ # keeps the regular method indentation, so a comment above it must not be pushed
164
+ # one level deeper.
165
+ def bare_access_modifier?(line)
166
+ /\A\s*(private|protected|public)\s*(#.*)?\z/.match?(line)
160
167
  end
161
168
 
162
169
  def two_alternatives?(line)
@@ -92,13 +92,7 @@ module RuboCop
92
92
  case parent.type
93
93
  when :def, :defs then base_for_method_definition(parent)
94
94
  when :kwbegin then parent.loc.begin
95
- when :block, :numblock, :itblock
96
- assignment_node = assignment_node(parent)
97
- if same_line?(parent, assignment_node)
98
- assignment_node.source_range
99
- else
100
- parent.send_node.source_range
101
- end
95
+ when :block, :numblock, :itblock then start_line_range(parent)
102
96
  else node.loc.keyword
103
97
  end
104
98
  end
@@ -143,13 +137,6 @@ module RuboCop
143
137
  autocorrect(corrector, else_range)
144
138
  end
145
139
  end
146
-
147
- def assignment_node(node)
148
- assignment_node = node.ancestors.first
149
- return unless assignment_node&.assignment?
150
-
151
- assignment_node
152
- end
153
140
  end
154
141
  end
155
142
  end
@@ -420,11 +420,16 @@ module RuboCop
420
420
  # The maximum allowed length of a string value is:
421
421
  # `Max` - end delimiter (quote) - continuation characters (space and slash)
422
422
  max_length = max - 3
423
- # If the string is on the same line as its parent, offset by the column difference
424
- # (Only apply when on same line to avoid negative offsets for multi-line dstr)
425
- if same_line?(node, node.parent)
426
- max_length -= column_offset_between(node.loc, node.parent.loc)
427
- end
423
+ # Offset by the string's starting column so the broken line actually fits
424
+ # within `Max`. When on the same line as its parent, use the column difference;
425
+ # otherwise the string is indented on its own line, so subtract that indentation.
426
+ # (Without this, an indented string under a multi-line parent never shortens
427
+ # below `Max` and the autocorrect loops, inserting empty `"" \` fragments.)
428
+ max_length -= if same_line?(node, node.parent)
429
+ column_offset_between(node.loc, node.parent.loc)
430
+ else
431
+ node.loc.column
432
+ end
428
433
  node.source[0...(max_length)]
429
434
  end
430
435
  end
@@ -73,7 +73,12 @@ module RuboCop
73
73
  end
74
74
 
75
75
  def find_pair_ancestor(node)
76
- node.each_ancestor.find(&:pair_type?)
76
+ node.each_ancestor do |ancestor|
77
+ return ancestor if ancestor.pair_type?
78
+ break if grouped_expression?(ancestor) || inside_arg_list_parentheses?(node, ancestor)
79
+ end
80
+
81
+ nil
77
82
  end
78
83
 
79
84
  def unwrap_block_node(node)
@@ -75,9 +75,10 @@ module RuboCop
75
75
  MSG = 'Remove debugger entry point `%<source>s`.'
76
76
 
77
77
  def on_send(node)
78
+ return unless debugger_method?(node) || debugger_require?(node)
78
79
  return if assumed_usage_context?(node)
79
80
 
80
- add_offense(node) if debugger_method?(node) || debugger_require?(node)
81
+ add_offense(node)
81
82
  end
82
83
 
83
84
  private
@@ -101,9 +102,20 @@ module RuboCop
101
102
  end
102
103
 
103
104
  def debugger_method?(send_node)
105
+ return false unless debugger_method_names.include?(send_node.method_name)
106
+
104
107
  debugger_methods.include?(chained_method_name(send_node))
105
108
  end
106
109
 
110
+ # The last segment of each configured debugger method, used to cheaply
111
+ # rule out the vast majority of `send` nodes before building the
112
+ # chained method name.
113
+ def debugger_method_names
114
+ @debugger_method_names ||= debugger_methods.to_set do |method|
115
+ method.to_s.split('.').last&.to_sym
116
+ end
117
+ end
118
+
107
119
  def debugger_require?(send_node)
108
120
  return false unless send_node.method?(:require) && send_node.arguments.one?
109
121
  return false unless (argument = send_node.first_argument).str_type?
@@ -15,6 +15,13 @@ module RuboCop
15
15
  # can't determine the type of `x`. If `x` is an `Array` or `String`, it doesn't perform
16
16
  # a numeric operation.
17
17
  #
18
+ # @safety
19
+ # This cop is unsafe because the autocorrection drops the operands, which
20
+ # discards any side effects of evaluating them and can change behavior when
21
+ # the result is not actually constant. For example, `x / x` raises
22
+ # `ZeroDivisionError` when `x` is `0`, and returns `Float::NAN` (not `1`)
23
+ # when `x` is `0.0`; replacing it with `1` silences that.
24
+ #
18
25
  # @example
19
26
  #
20
27
  # # bad
@@ -36,9 +36,16 @@ module RuboCop
36
36
  # The following used `*_args` because `to_json(*args)` has
37
37
  # an offense of `Lint/UnusedMethodArgument` cop if `*args`
38
38
  # is not used.
39
- corrector.insert_after(node.loc.name, '(*_args)')
39
+ if (opening_parenthesis = node.each_child_node(:args).first.loc.begin)
40
+ # Explicit empty parentheses (`def to_json()`) are already present,
41
+ # so insert the argument inside them to avoid producing `to_json(*_args)()`.
42
+ corrector.insert_after(opening_parenthesis, '*_args')
43
+ else
44
+ corrector.insert_after(node.loc.name, '(*_args)')
45
+ end
40
46
  end
41
47
  end
48
+ alias on_defs on_def
42
49
  end
43
50
  end
44
51
  end
@@ -52,10 +52,15 @@ module RuboCop
52
52
  def on_begin(node)
53
53
  expressions = *node
54
54
 
55
- expressions.each_cons(2) do |expression1, expression2|
56
- next unless flow_expression?(expression1)
57
-
58
- add_offense(expression2)
55
+ # Once a flow-of-control statement is reached, every following statement
56
+ # in the block is unreachable, not just the one immediately after it.
57
+ flow_reached = false
58
+ expressions.each_with_index do |expression, index|
59
+ if flow_reached
60
+ add_offense(expression)
61
+ elsif index < expressions.size - 1 && flow_expression?(expression)
62
+ flow_reached = true
63
+ end
59
64
  end
60
65
  end
61
66
 
@@ -36,31 +36,31 @@ module RuboCop
36
36
  RESTRICT_ON_SEND = %i[+ - * / **].freeze
37
37
 
38
38
  # @!method useless_operation?(node)
39
- def_node_matcher :useless_operation?, '(call (call nil? $_) $_ (int $_))'
39
+ def_node_matcher :useless_operation?, <<~PATTERN
40
+ (call ${lvar ivar cvar gvar const (send nil? _)} $_ (int $_))
41
+ PATTERN
40
42
 
41
43
  # @!method useless_abbreviated_assignment?(node)
42
- def_node_matcher :useless_abbreviated_assignment?, '(op-asgn (lvasgn $_) $_ (int $_))'
44
+ def_node_matcher :useless_abbreviated_assignment?, <<~PATTERN
45
+ (op-asgn ${lvasgn ivasgn cvasgn gvasgn casgn} $_ (int $_))
46
+ PATTERN
43
47
 
44
48
  def on_send(node)
45
- return unless useless_operation?(node)
46
-
47
- variable, operation, number = useless_operation?(node)
49
+ return unless (receiver, operation, number = useless_operation?(node))
48
50
  return unless useless?(operation, number)
49
51
 
50
52
  add_offense(node) do |corrector|
51
- corrector.replace(node, variable)
53
+ corrector.replace(node, receiver.source)
52
54
  end
53
55
  end
54
56
  alias on_csend on_send
55
57
 
56
58
  def on_op_asgn(node)
57
- return unless useless_abbreviated_assignment?(node)
58
-
59
- variable, operation, number = useless_abbreviated_assignment?(node)
59
+ return unless (variable, operation, number = useless_abbreviated_assignment?(node))
60
60
  return unless useless?(operation, number)
61
61
 
62
62
  add_offense(node) do |corrector|
63
- corrector.replace(node, "#{variable} = #{variable}")
63
+ corrector.replace(node, "#{variable.source} = #{variable.source}")
64
64
  end
65
65
  end
66
66
 
@@ -212,7 +212,7 @@ module RuboCop
212
212
  end
213
213
 
214
214
  def check_nonmutating(node)
215
- return unless node.type?(:send, :any_block)
215
+ return unless node.type?(:call, :any_block)
216
216
 
217
217
  method_name = node.method_name
218
218
  return unless NONMUTATING_METHODS.include?(method_name)
@@ -265,7 +265,7 @@ module RuboCop
265
265
  end
266
266
 
267
267
  def autocorrect_nonmutating_send(corrector, node, suggestion)
268
- send_node = if node.send_type?
268
+ send_node = if node.call_type?
269
269
  node
270
270
  else
271
271
  node.send_node
@@ -25,11 +25,11 @@ module RuboCop
25
25
 
26
26
  # @api public
27
27
  def allowed_methods
28
- if cop_config_deprecated_values.any?(Regexp)
29
- cop_config_allowed_methods
30
- else
31
- cop_config_allowed_methods + cop_config_deprecated_values
32
- end
28
+ @allowed_methods ||= if cop_config_deprecated_values.any?(Regexp)
29
+ cop_config_allowed_methods
30
+ else
31
+ cop_config_allowed_methods + cop_config_deprecated_values
32
+ end
33
33
  end
34
34
 
35
35
  def cop_config_allowed_methods
@@ -27,7 +27,7 @@ module RuboCop
27
27
  end
28
28
 
29
29
  def matches_allowed_pattern?(line)
30
- allowed_patterns.any? { |pattern| Regexp.new(pattern).match?(line) }
30
+ allowed_pattern_regexps.any? { |pattern| pattern.match?(line) }
31
31
  end
32
32
 
33
33
  # @deprecated Use matches_allowed_pattern? instead
@@ -49,6 +49,10 @@ module RuboCop
49
49
  end
50
50
  end
51
51
 
52
+ def allowed_pattern_regexps
53
+ @allowed_pattern_regexps ||= allowed_patterns.map { |pattern| Regexp.new(pattern) }
54
+ end
55
+
52
56
  def cop_config_patterns_values
53
57
  @cop_config_patterns_values ||=
54
58
  Array(cop_config.fetch('AllowedPatterns', [])) +
@@ -5,12 +5,16 @@ module RuboCop
5
5
  # This module encapsulates the ability to forbid certain patterns in a cop.
6
6
  module ForbiddenPattern
7
7
  def forbidden_pattern?(name)
8
- forbidden_patterns.any? { |pattern| Regexp.new(pattern).match?(name) }
8
+ forbidden_pattern_regexps.any? { |pattern| pattern.match?(name) }
9
9
  end
10
10
 
11
11
  def forbidden_patterns
12
12
  cop_config.fetch('ForbiddenPatterns', [])
13
13
  end
14
+
15
+ def forbidden_pattern_regexps
16
+ @forbidden_pattern_regexps ||= forbidden_patterns.map { |pattern| Regexp.new(pattern) }
17
+ end
14
18
  end
15
19
  end
16
20
  end
@@ -75,7 +75,7 @@ module RuboCop
75
75
  end
76
76
 
77
77
  def first_line_comment(node)
78
- comment = processed_source.comments.find { |c| same_line?(c, node) }
78
+ comment = processed_source.comment_at_line(node.first_line)
79
79
  return unless comment
80
80
 
81
81
  comment_source = comment.source
@@ -153,7 +153,7 @@ module RuboCop
153
153
  BLOCK_MSG = 'Use anonymous block arguments forwarding (`&`).'
154
154
 
155
155
  def self.autocorrect_incompatible_with
156
- [Naming::BlockForwarding]
156
+ [Naming::BlockForwarding, Style::MethodDefParentheses]
157
157
  end
158
158
 
159
159
  def on_def(node)
@@ -47,7 +47,7 @@ module RuboCop
47
47
  #
48
48
  # # bad
49
49
  # array1.any? { |elem| array2.member?(elem) }
50
- # array1.none? { |elem| array2.member?(elem) }
50
+ # array1.none? { |elem| array2.include?(elem) }
51
51
  #
52
52
  # # good
53
53
  # array1.intersect?(array2)
@@ -121,15 +121,15 @@ module RuboCop
121
121
  (block
122
122
  (call $_receiver ${:any? :none?})
123
123
  (args (arg _key))
124
- (send $_argument :member? (lvar _key))
124
+ (send $_argument {:member? :include?} (lvar _key))
125
125
  )
126
126
  (numblock
127
127
  (call $_receiver ${:any? :none?}) 1
128
- (send $_argument :member? (lvar :_1))
128
+ (send $_argument {:member? :include?} (lvar :_1))
129
129
  )
130
130
  (itblock
131
131
  (call $_receiver ${:any? :none?}) :it
132
- (send $_argument :member? (lvar :it))
132
+ (send $_argument {:member? :include?} (lvar :it))
133
133
  )
134
134
  }
135
135
  PATTERN
@@ -96,7 +96,7 @@ module RuboCop
96
96
 
97
97
  # @!method grep_v_with_nil?(node)
98
98
  def_node_matcher :grep_v_with_nil?, <<~PATTERN
99
- (send _ :grep_v {(nil) (const {nil? cbase} :NilClass)})
99
+ (call _ :grep_v {(nil) (const {nil? cbase} :NilClass)})
100
100
  PATTERN
101
101
 
102
102
  def on_send(node)
@@ -57,11 +57,15 @@ module RuboCop
57
57
  private
58
58
 
59
59
  def parentheses_required?(node, arguments_range)
60
- return true if node.single_line? && !node.endless?
61
-
62
60
  end_pos = arguments_range.end.end_pos
63
61
  token_after_argument = range_between(end_pos, end_pos + 1).source
64
62
 
63
+ # A `;` after the parentheses (e.g. `def foo(); end`) already separates the
64
+ # signature from the body, so the parentheses can be removed (`def foo; end`).
65
+ return false if token_after_argument == ';'
66
+
67
+ return true if node.single_line? && !node.endless?
68
+
65
69
  token_after_argument == '='
66
70
  end
67
71
  end
@@ -140,7 +140,11 @@ module RuboCop
140
140
  end
141
141
 
142
142
  def method_allowed?(node)
143
- allowed_methods.include?(node.method_name)
143
+ # For a modifier form like `module_function def foo; end`, `node` is the
144
+ # `module_function`/`ruby2_keywords` send; the real method name is on its
145
+ # `def`/`defs` argument, not the modifier itself.
146
+ method_name = node.send_type? ? node.first_argument.method_name : node.method_name
147
+ allowed_methods.include?(method_name)
144
148
  end
145
149
 
146
150
  def allowed_methods
@@ -121,16 +121,24 @@ module RuboCop
121
121
  end
122
122
 
123
123
  def multi_argument(node)
124
+ # A splat argument can expand to any number of elements, so the pairs
125
+ # can't be built statically and there is no literal hash to suggest.
126
+ return if node.arguments.any?(&:splat_type?)
127
+
124
128
  if node.arguments.count.odd?
125
129
  add_offense(node, message: MSG_LITERAL_MULTI_ARG)
126
130
  else
127
- add_offense(node, message: MSG_LITERAL_MULTI_ARG) do |corrector|
128
- corrector.replace(node, args_to_hash(node.arguments))
131
+ correct_multi_argument(node)
132
+ end
133
+ end
129
134
 
130
- parent = node.parent
131
- if parent&.send_type? && !parent.method?(:to_h) && !parent.parenthesized?
132
- add_parentheses(parent, corrector)
133
- end
135
+ def correct_multi_argument(node)
136
+ add_offense(node, message: MSG_LITERAL_MULTI_ARG) do |corrector|
137
+ corrector.replace(node, args_to_hash(node.arguments))
138
+
139
+ parent = node.parent
140
+ if parent&.send_type? && !parent.method?(:to_h) && !parent.parenthesized?
141
+ add_parentheses(parent, corrector)
134
142
  end
135
143
  end
136
144
  end
@@ -74,7 +74,7 @@ module RuboCop
74
74
 
75
75
  def offense_for_brackets?(node)
76
76
  style == :brackets && node.receiver && node.method?(:fetch) && node.arguments.one? &&
77
- !node.block_literal?
77
+ !node.block_literal? && !node.csend_type?
78
78
  end
79
79
 
80
80
  def offense_for_fetch?(node)
@@ -84,11 +84,7 @@ module RuboCop
84
84
  def correct_fetch_to_brackets(corrector, node)
85
85
  key = node.first_argument.source
86
86
 
87
- if node.csend_type?
88
- corrector.replace(node, "(#{node.receiver.source}[#{key}])")
89
- else
90
- corrector.replace(node.loc.dot.join(node.source_range.end), "[#{key}]")
91
- end
87
+ corrector.replace(node.loc.dot.join(node.source_range.end), "[#{key}]")
92
88
  end
93
89
 
94
90
  def correct_brackets_to_fetch(corrector, node)
@@ -216,23 +216,34 @@ module RuboCop
216
216
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
217
217
 
218
218
  def correct_assignment(corrector, node, expression, insert_position)
219
+ indentation = indentation_of(node.parent)
220
+
219
221
  if insert_position == :after_condition
220
222
  assignment = node.parent.source_range.with(end_pos: node.source_range.begin_pos)
221
223
  corrector.remove(assignment)
222
- corrector.insert_after(node, "\n#{assignment.source}#{expression.source}")
224
+ corrector.insert_after(node, "\n#{indentation}#{assignment.source}#{expression.source}")
223
225
  else
224
- corrector.insert_before(node.parent, "#{expression.source}\n")
226
+ corrector.insert_before(node.parent, "#{expression.source}\n#{indentation}")
225
227
  end
226
228
  end
227
229
 
228
230
  def correct_no_assignment(corrector, node, expression, insert_position)
231
+ indentation = indentation_of(node)
232
+
229
233
  if insert_position == :after_condition
230
- corrector.insert_after(node, "\n#{expression.source}")
234
+ corrector.insert_after(node, "\n#{indentation}#{expression.source}")
231
235
  else
232
- corrector.insert_before(node, "#{expression.source}\n")
236
+ corrector.insert_before(node, "#{expression.source}\n#{indentation}")
233
237
  end
234
238
  end
235
239
 
240
+ # The leading indentation of the line the conditional starts on, so a
241
+ # hoisted expression keeps the surrounding nesting instead of landing
242
+ # at column zero.
243
+ def indentation_of(node)
244
+ ' ' * node.source_range.column
245
+ end
246
+
236
247
  def last_child_of_parent?(node)
237
248
  return true unless (parent = node.parent)
238
249
 
@@ -91,7 +91,7 @@ module RuboCop
91
91
 
92
92
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
93
93
  def on_if(node)
94
- return if endless_method?(node.body) || node.ancestors.any?(&:dstr_type?)
94
+ return if endless_method?(node.body) || node.each_ancestor(:dstr).any?
95
95
 
96
96
  condition = node.condition
97
97
  return if defined_nodes(condition).any? { |n| defined_argument_is_undefined?(node, n) } ||
@@ -119,7 +119,7 @@ module RuboCop
119
119
  if condition.defined_type?
120
120
  [condition]
121
121
  else
122
- condition.each_descendant.select(&:defined_type?)
122
+ condition.each_descendant(:defined?)
123
123
  end
124
124
  end
125
125
 
@@ -137,7 +137,7 @@ module RuboCop
137
137
  if condition.any_match_pattern_type?
138
138
  [condition]
139
139
  else
140
- condition.each_descendant.select(&:any_match_pattern_type?)
140
+ condition.each_descendant(:any_match_pattern)
141
141
  end
142
142
  end
143
143
 
@@ -350,7 +350,7 @@ module RuboCop
350
350
  end
351
351
 
352
352
  def comment_on_node_line(node)
353
- processed_source.comments.find { |c| same_line?(c, node) }
353
+ processed_source.comment_at_line(node.first_line)
354
354
  end
355
355
 
356
356
  def remove_comment(corrector, _node, comment)
@@ -75,7 +75,9 @@ module RuboCop
75
75
  def invertible?(node) # rubocop:disable Metrics/CyclomaticComplexity
76
76
  case node&.type
77
77
  when :begin
78
- invertible?(node.children.first)
78
+ # A multi-statement `begin` evaluates to its last expression, so it
79
+ # cannot be inverted by negating a single child.
80
+ node.children.one? && invertible?(node.children.first)
79
81
  when :send
80
82
  return false if inheritance_check?(node)
81
83
 
@@ -124,12 +126,17 @@ module RuboCop
124
126
  end
125
127
 
126
128
  def preferred_logical_condition(node)
127
- preferred_lhs = preferred_condition(node.lhs)
128
- preferred_rhs = preferred_condition(node.rhs)
129
+ preferred_lhs = preferred_operand(node, node.lhs)
130
+ preferred_rhs = preferred_operand(node, node.rhs)
129
131
 
130
132
  "#{preferred_lhs} #{node.inverse_operator} #{preferred_rhs}"
131
133
  end
132
134
 
135
+ def preferred_operand(node, operand)
136
+ preferred = preferred_condition(operand)
137
+ parenthesize_inverted_operand?(node, operand) ? "(#{preferred})" : preferred
138
+ end
139
+
133
140
  def autocorrect(corrector, node)
134
141
  case node.type
135
142
  when :begin
@@ -138,11 +145,24 @@ module RuboCop
138
145
  autocorrect_send_node(corrector, node)
139
146
  when :or, :and
140
147
  corrector.replace(node.loc.operator, node.inverse_operator)
141
- autocorrect(corrector, node.lhs)
142
- autocorrect(corrector, node.rhs)
148
+ autocorrect_operand(corrector, node, node.lhs)
149
+ autocorrect_operand(corrector, node, node.rhs)
143
150
  end
144
151
  end
145
152
 
153
+ def autocorrect_operand(corrector, node, operand)
154
+ autocorrect(corrector, operand)
155
+ corrector.wrap(operand, '(', ')') if parenthesize_inverted_operand?(node, operand)
156
+ end
157
+
158
+ # When an `and` is nested in an `or`, inverting both operators turns the
159
+ # `and` into an `or` that now binds looser than its parent, so it must be
160
+ # parenthesized to keep the original grouping (`a && b || c` inverts to
161
+ # `(!a || !b) && !c`, not `!a || !b && !c`).
162
+ def parenthesize_inverted_operand?(node, operand)
163
+ node.or_type? && operand.and_type?
164
+ end
165
+
146
166
  def autocorrect_send_node(corrector, node)
147
167
  if node.method?(:!)
148
168
  corrector.remove(node.loc.selector)
@@ -27,6 +27,9 @@ module RuboCop
27
27
 
28
28
  def on_send(node)
29
29
  return unless node.receiver
30
+ # Rewriting the call rebuilds it as a single expression, which would drop
31
+ # any comments inside the argument list, so leave it to be fixed manually.
32
+ return if comments_in_node?(node)
30
33
 
31
34
  if offense?(node)
32
35
  prefer = prefer(node)
@@ -48,6 +51,14 @@ module RuboCop
48
51
 
49
52
  private
50
53
 
54
+ def comments_in_node?(node)
55
+ range = node.source_range
56
+ processed_source.comments.any? do |comment|
57
+ range.begin_pos <= comment.source_range.begin_pos &&
58
+ comment.source_range.end_pos <= range.end_pos
59
+ end
60
+ end
61
+
51
62
  def offense?(node)
52
63
  (explicit_style? && node.implicit_call?) || (implicit_style? && !node.implicit_call?)
53
64
  end
@@ -50,9 +50,11 @@ module RuboCop
50
50
  private
51
51
 
52
52
  def register_offense(node)
53
- add_offense(offense_range(node)) do |corrector|
54
- corrector.remove(node.loc.begin)
55
- corrector.remove(node.loc.end)
53
+ range = offense_range(node)
54
+ return if processed_source.contains_comment?(range)
55
+
56
+ add_offense(range) do |corrector|
57
+ corrector.remove(range)
56
58
  end
57
59
  end
58
60
 
@@ -102,6 +102,10 @@ module RuboCop
102
102
  MSG_PRESENT = 'Use def without parentheses.'
103
103
  MSG_MISSING = 'Use def with parentheses when there are parameters.'
104
104
 
105
+ def self.autocorrect_incompatible_with
106
+ [Style::ArgumentsForwarding]
107
+ end
108
+
105
109
  def on_def(node)
106
110
  args = node.arguments
107
111
 
@@ -65,16 +65,19 @@ module RuboCop
65
65
  private
66
66
 
67
67
  def implements_respond_to_missing?(node)
68
- return false unless (grand_parent = node.parent.parent)
68
+ scope = enclosing_scope(node)
69
+ search_root = scope || node.parent
70
+ return false unless search_root
69
71
 
70
- grand_parent.each_descendant(node.type) do |descendant|
71
- return true if descendant.method?(:respond_to_missing?)
72
-
73
- child = descendant.children.first
74
- return true if child.respond_to?(:method?) && child.method?(:respond_to_missing?)
72
+ search_root.each_descendant(node.type).any? do |descendant|
73
+ descendant.method?(:respond_to_missing?) && enclosing_scope(descendant).equal?(scope)
75
74
  end
75
+ end
76
76
 
77
- false
77
+ # The class/module/`class << self` body that lexically contains `node`,
78
+ # or `nil` when `node` is defined at the top level.
79
+ def enclosing_scope(node)
80
+ node.each_ancestor(:class, :module, :sclass).first
78
81
  end
79
82
  end
80
83
  end
@@ -47,7 +47,7 @@ module RuboCop
47
47
  # @!method include_statement(node)
48
48
  def_node_matcher :include_statement, <<~PATTERN
49
49
  (send nil? ${:include :extend :prepend}
50
- const)
50
+ const+)
51
51
  PATTERN
52
52
 
53
53
  # @!method in_top_level_scope?(node)
@@ -110,9 +110,11 @@ module RuboCop
110
110
  def_node_matcher :private_directive?, '(send nil? :private ...)'
111
111
 
112
112
  def on_module(node)
113
- return unless node.body&.begin_type?
113
+ return unless node.body
114
114
 
115
- each_wrong_style(node.body.children) do |child_node|
115
+ body_nodes = node.body.begin_type? ? node.body.children : [node.body]
116
+
117
+ each_wrong_style(body_nodes) do |child_node|
116
118
  add_offense(child_node) do |corrector|
117
119
  next if style == :forbidden
118
120
 
@@ -26,7 +26,9 @@ module RuboCop
26
26
  def on_send(node)
27
27
  return unless (first_argument = node.first_argument)
28
28
  return unless (index = first_argument.source.index(CURRENT_DIRECTORY_PREFIX))
29
- return unless (redundant_length = redundant_path_length(first_argument.str_content))
29
+
30
+ content = leading_path_content(first_argument)
31
+ return unless (redundant_length = redundant_path_length(content))
30
32
 
31
33
  begin_pos = first_argument.source_range.begin.begin_pos + index
32
34
  end_pos = begin_pos + redundant_length
@@ -39,6 +41,16 @@ module RuboCop
39
41
 
40
42
  private
41
43
 
44
+ # The literal text at the start of the path, which is the whole string
45
+ # for a plain string and the leading literal segment for an interpolated
46
+ # one (`nil` when it starts with interpolation).
47
+ def leading_path_content(node)
48
+ return node.str_content if node.str_type?
49
+ return unless node.dstr_type? && (first = node.children.first)&.str_type?
50
+
51
+ first.str_content
52
+ end
53
+
42
54
  def redundant_path_length(path)
43
55
  return unless (match = path&.match(REDUNDANT_CURRENT_DIRECTORY_PREFIX))
44
56
 
@@ -48,8 +48,13 @@ module RuboCop
48
48
  def need_heredoc_delimiter_quotes?(node)
49
49
  heredoc_delimiter = node.source.delete(heredoc_type(node))
50
50
  return true unless heredoc_delimiter.start_with?("'", '"')
51
+ return true if node.loc.heredoc_end.source.strip.match?(/\W/)
51
52
 
52
- node.loc.heredoc_end.source.strip.match?(/\W/) ||
53
+ # A double-quoted delimiter interpolates exactly like an unquoted one,
54
+ # so its quotes are always redundant. A single-quoted delimiter is
55
+ # required when the body contains interpolation or escapes that would
56
+ # otherwise be evaluated.
57
+ heredoc_delimiter.start_with?("'") &&
53
58
  node.loc.heredoc_body.source.match?(STRING_INTERPOLATION_OR_ESCAPED_CHARACTER_PATTERN)
54
59
  end
55
60
  end
@@ -41,7 +41,9 @@ module RuboCop
41
41
  return unless struct_constructor?(node.parent_class)
42
42
 
43
43
  add_offense(node.parent_class) do |corrector|
44
- corrector.remove(range_with_surrounding_space(node.loc.keyword, newlines: false))
44
+ corrector.remove(
45
+ range_with_surrounding_space(node.loc.keyword, side: :right, newlines: false)
46
+ )
45
47
  corrector.replace(node.loc.operator, '=')
46
48
 
47
49
  correct_parent(node.parent_class, corrector)
@@ -234,7 +234,7 @@ module RuboCop
234
234
  def autocorrect_class(corrector, node)
235
235
  kind = trivial_accessor_kind(node)
236
236
 
237
- return unless names_match?(node) && kind
237
+ return unless names_match?(node) && !node.predicate_method? && kind
238
238
 
239
239
  indent = ' ' * node.loc.column
240
240
  corrector.replace(
@@ -59,6 +59,16 @@ module RuboCop
59
59
 
60
60
  attr_reader :errors, :warnings, :updated_source_file, :cops
61
61
 
62
+ # When set to true, the corrected source is not written back to the
63
+ # inspected file; it is exposed through `#updated_source` instead.
64
+ # @api private
65
+ attr_accessor :defer_corrections
66
+
67
+ # The corrected source of the last investigation, if corrections were
68
+ # made with `#defer_corrections` enabled.
69
+ # @api private
70
+ attr_reader :updated_source
71
+
62
72
  alias updated_source_file? updated_source_file
63
73
 
64
74
  def initialize(cops, config = nil, options = {})
@@ -135,20 +145,24 @@ module RuboCop
135
145
 
136
146
  def autocorrect(processed_source, corrector)
137
147
  @updated_source_file = false
148
+ @updated_source = nil
138
149
  return unless autocorrect?
139
150
  return unless corrector
140
151
  return if corrector.empty?
141
152
 
142
- new_source = corrector.rewrite
153
+ apply_correction(processed_source, corrector.rewrite)
154
+ @updated_source_file = true
155
+ end
143
156
 
157
+ def apply_correction(processed_source, new_source)
144
158
  if @options[:stdin]
145
159
  # holds source read in from stdin, when --stdin option is used
146
160
  @options[:stdin] = new_source
161
+ elsif defer_corrections
162
+ @updated_source = new_source
147
163
  else
148
- filename = processed_source.file_path
149
- File.write(filename, new_source)
164
+ File.write(processed_source.file_path, new_source)
150
165
  end
151
- @updated_source_file = true
152
166
  end
153
167
 
154
168
  def be_ready
@@ -346,16 +346,18 @@ module RuboCop
346
346
  # inspection iteration. This is used to output meaningful infinite loop
347
347
  # error message.
348
348
  offenses_by_iteration = []
349
+ corrected_source = nil
349
350
 
350
- # When running with --autocorrect, we need to inspect the file (which
351
- # includes writing a corrected version of it) until no more corrections
352
- # are made. This is because automatic corrections can introduce new
353
- # offenses. In the normal case the loop is only executed once.
351
+ # When running with --autocorrect, we need to inspect the file until no
352
+ # more corrections are made. This is because automatic corrections can
353
+ # introduce new offenses. In the normal case the loop is only executed
354
+ # once. The corrections are kept in memory while iterating and written
355
+ # back to the file when the loop is done.
354
356
  iterate_until_no_changes(processed_source, offenses_by_iteration) do
355
357
  # The offenses that couldn't be corrected will be found again so we
356
358
  # only keep the corrected ones in order to avoid duplicate reporting.
357
359
  !offenses_by_iteration.empty? && offenses_by_iteration.last.select!(&:corrected?)
358
- new_offenses, updated_source_file = inspect_file(processed_source)
360
+ team, new_offenses, updated_source_file = inspect_iteration(processed_source)
359
361
  offenses_by_iteration.push(new_offenses)
360
362
 
361
363
  # We have to reprocess the source to pickup the changes. Since the
@@ -364,12 +366,43 @@ module RuboCop
364
366
  break unless updated_source_file
365
367
 
366
368
  # Autocorrect has happened, don't use the prism result since it is stale.
367
- processed_source = get_processed_source(file, nil)
369
+ # With --stdin the corrected source is kept in @options[:stdin] instead.
370
+ corrected_source = team.updated_source
371
+ processed_source = get_processed_source(file, nil, source: corrected_source)
368
372
  end
369
373
 
370
374
  # Return summary of corrected offenses after all iterations
371
- offenses = offenses_by_iteration.flatten.uniq
372
- [processed_source, offenses]
375
+ [processed_source, offenses_by_iteration.flatten.uniq]
376
+ ensure
377
+ # Write the file once, even when the loop was left through an exception
378
+ # (e.g. an infinite correction loop), like the per-iteration writes
379
+ # used to be.
380
+ File.write(file, corrected_source) if corrected_source
381
+ end
382
+
383
+ def inspect_iteration(processed_source)
384
+ team = mobilize_team(processed_source)
385
+ team.defer_corrections = in_memory_corrections_possible?
386
+ offenses, updated_source_file = inspect_file(processed_source, team)
387
+ [team, offenses, updated_source_file]
388
+ end
389
+
390
+ # When corrections were written to disk and read back between iterations,
391
+ # the text-mode write converted LF to CRLF on Windows, and cops like
392
+ # `Layout/EndOfLine` rely on seeing the source as it would be on disk.
393
+ # Apply the same conversion to the in-memory source. The final `File.write`
394
+ # still performs it for the file itself.
395
+ def emulate_write_read_cycle(source)
396
+ return source unless Platform.windows?
397
+
398
+ source.encode(source.encoding, crlf_newline: true)
399
+ end
400
+
401
+ # Custom ruby extractors may derive their fragments from the file on
402
+ # disk rather than from the passed processed source, so corrections can
403
+ # only be kept in memory when the default extractor is used.
404
+ def in_memory_corrections_possible?
405
+ self.class.ruby_extractors.one?
373
406
  end
374
407
 
375
408
  def iterate_until_no_changes(source, offenses_by_iteration)
@@ -552,12 +585,19 @@ module RuboCop
552
585
  end
553
586
 
554
587
  # rubocop:disable Metrics/MethodLength
555
- def get_processed_source(file, prism_result)
588
+ def get_processed_source(file, prism_result, source: nil)
556
589
  config = @config_store.for_file(file)
557
590
  ruby_version = config.target_ruby_version
558
591
  parser_engine = config.parser_engine
559
592
 
560
- processed_source = if @options[:stdin]
593
+ processed_source = if source
594
+ ProcessedSource.new(
595
+ emulate_write_read_cycle(source),
596
+ ruby_version,
597
+ file,
598
+ parser_engine: parser_engine
599
+ )
600
+ elsif @options[:stdin]
561
601
  ProcessedSource.new(
562
602
  @options[:stdin],
563
603
  ruby_version,
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  # This module holds the RuboCop version information.
5
5
  module Version
6
- STRING = '1.88.1'
6
+ STRING = '1.88.2'
7
7
 
8
8
  MSG = '%<version>s (using %<parser_version>s, ' \
9
9
  'rubocop-ast %<rubocop_ast_version>s, ' \
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.88.1
4
+ version: 1.88.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bozhidar Batsov
@@ -1119,7 +1119,7 @@ licenses:
1119
1119
  - MIT
1120
1120
  metadata:
1121
1121
  homepage_uri: https://rubocop.org/
1122
- changelog_uri: https://github.com/rubocop/rubocop/releases/tag/v1.88.1
1122
+ changelog_uri: https://github.com/rubocop/rubocop/releases/tag/v1.88.2
1123
1123
  source_code_uri: https://github.com/rubocop/rubocop/
1124
1124
  documentation_uri: https://docs.rubocop.org/rubocop/1.88/
1125
1125
  bug_tracker_uri: https://github.com/rubocop/rubocop/issues