rubocop 1.88.0 → 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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +5 -1
  3. data/lib/rubocop/cop/base.rb +13 -8
  4. data/lib/rubocop/cop/bundler/gem_comment.rb +3 -1
  5. data/lib/rubocop/cop/commissioner.rb +13 -11
  6. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  7. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
  8. data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
  9. data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
  10. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +12 -2
  11. data/lib/rubocop/cop/gemspec/require_mfa.rb +4 -1
  12. data/lib/rubocop/cop/layout/block_alignment.rb +17 -0
  13. data/lib/rubocop/cop/layout/class_structure.rb +7 -3
  14. data/lib/rubocop/cop/layout/comment_indentation.rb +9 -2
  15. data/lib/rubocop/cop/layout/condition_position.rb +13 -3
  16. data/lib/rubocop/cop/layout/else_alignment.rb +1 -14
  17. data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
  18. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +14 -1
  19. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +13 -14
  20. data/lib/rubocop/cop/layout/indentation_width.rb +28 -0
  21. data/lib/rubocop/cop/layout/line_length.rb +10 -5
  22. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +6 -1
  23. data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
  24. data/lib/rubocop/cop/lint/assignment_in_condition.rb +13 -1
  25. data/lib/rubocop/cop/lint/debugger.rb +13 -1
  26. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +7 -0
  27. data/lib/rubocop/cop/lint/to_enum_arguments.rb +7 -1
  28. data/lib/rubocop/cop/lint/to_json.rb +8 -1
  29. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +32 -8
  30. data/lib/rubocop/cop/lint/unreachable_code.rb +9 -4
  31. data/lib/rubocop/cop/lint/useless_numeric_operation.rb +10 -10
  32. data/lib/rubocop/cop/lint/void.rb +2 -2
  33. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  34. data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
  35. data/lib/rubocop/cop/mixin/allowed_methods.rb +5 -5
  36. data/lib/rubocop/cop/mixin/allowed_pattern.rb +5 -1
  37. data/lib/rubocop/cop/mixin/forbidden_pattern.rb +5 -1
  38. data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
  39. data/lib/rubocop/cop/mixin/hash_transform_method.rb +4 -0
  40. data/lib/rubocop/cop/mixin/statement_modifier.rb +1 -1
  41. data/lib/rubocop/cop/naming/file_name.rb +4 -3
  42. data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
  43. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +9 -0
  44. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +9 -3
  45. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  46. data/lib/rubocop/cop/security/marshal_load.rb +1 -1
  47. data/lib/rubocop/cop/style/accessor_grouping.rb +11 -1
  48. data/lib/rubocop/cop/style/arguments_forwarding.rb +1 -1
  49. data/lib/rubocop/cop/style/array_intersect.rb +4 -4
  50. data/lib/rubocop/cop/style/collection_compact.rb +1 -1
  51. data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
  52. data/lib/rubocop/cop/style/def_with_parentheses.rb +6 -2
  53. data/lib/rubocop/cop/style/dir_empty.rb +4 -0
  54. data/lib/rubocop/cop/style/documentation_method.rb +5 -1
  55. data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
  56. data/lib/rubocop/cop/style/empty_class_definition.rb +8 -1
  57. data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
  58. data/lib/rubocop/cop/style/empty_literal.rb +7 -2
  59. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
  60. data/lib/rubocop/cop/style/env_home.rb +4 -0
  61. data/lib/rubocop/cop/style/even_odd.rb +11 -1
  62. data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
  63. data/lib/rubocop/cop/style/file_null.rb +4 -2
  64. data/lib/rubocop/cop/style/format_string.rb +13 -1
  65. data/lib/rubocop/cop/style/hash_conversion.rb +14 -6
  66. data/lib/rubocop/cop/style/hash_lookup_method.rb +2 -6
  67. data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
  68. data/lib/rubocop/cop/style/identical_conditional_branches.rb +15 -4
  69. data/lib/rubocop/cop/style/if_unless_modifier.rb +4 -4
  70. data/lib/rubocop/cop/style/if_with_semicolon.rb +9 -1
  71. data/lib/rubocop/cop/style/inline_comment.rb +1 -1
  72. data/lib/rubocop/cop/style/invertible_unless_condition.rb +25 -5
  73. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
  74. data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
  75. data/lib/rubocop/cop/style/lambda.rb +7 -1
  76. data/lib/rubocop/cop/style/lambda_call.rb +11 -0
  77. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
  78. data/lib/rubocop/cop/style/map_into_array.rb +1 -1
  79. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +11 -5
  80. data/lib/rubocop/cop/style/method_def_parentheses.rb +5 -1
  81. data/lib/rubocop/cop/style/min_max_comparison.rb +3 -0
  82. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +10 -7
  83. data/lib/rubocop/cop/style/mixin_usage.rb +1 -1
  84. data/lib/rubocop/cop/style/module_function.rb +4 -2
  85. data/lib/rubocop/cop/style/multiline_if_then.rb +1 -1
  86. data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
  87. data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
  88. data/lib/rubocop/cop/style/nil_lambda.rb +8 -0
  89. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  90. data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
  91. data/lib/rubocop/cop/style/option_hash.rb +1 -1
  92. data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
  93. data/lib/rubocop/cop/style/parallel_assignment.rb +11 -2
  94. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  95. data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
  96. data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +13 -1
  97. data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
  98. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  99. data/lib/rubocop/cop/style/redundant_format.rb +28 -0
  100. data/lib/rubocop/cop/style/redundant_heredoc_delimiter_quotes.rb +6 -1
  101. data/lib/rubocop/cop/style/redundant_line_continuation.rb +11 -3
  102. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
  103. data/lib/rubocop/cop/style/redundant_self.rb +9 -0
  104. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +23 -4
  105. data/lib/rubocop/cop/style/semicolon.rb +4 -4
  106. data/lib/rubocop/cop/style/single_line_do_end_block.rb +17 -4
  107. data/lib/rubocop/cop/style/string_hash_keys.rb +1 -0
  108. data/lib/rubocop/cop/style/struct_inheritance.rb +3 -1
  109. data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
  110. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
  111. data/lib/rubocop/cop/style/trivial_accessors.rb +1 -1
  112. data/lib/rubocop/cop/team.rb +18 -4
  113. data/lib/rubocop/runner.rb +55 -13
  114. data/lib/rubocop/version.rb +1 -1
  115. 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: 8642dc64b958118e0563ed93c4837ac13aca29cb8f8a6267155ef450380ea1b3
4
+ data.tar.gz: 569ff18924510b28b60ff03563053d0c53983829d7f4755c0ad39cc213b41f64
5
5
  SHA512:
6
- metadata.gz: 8d2cb39bc17b788f176096a12ddf766dbdcfafe8a6fcd09e830544053665b3b42f204b0b7fe01a61d2be94d64fe859f6783710f01df71f136e28d9e46ec1ebf6
7
- data.tar.gz: 18067fc7914382cf8c13a07a34b9774647bd85ea34e85eac8a705da85af7039f31befae4e850b23989ddb91918f4cf10bccd4d578dc79a65de5422db86b51ad2
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.'
@@ -2844,7 +2846,7 @@ Metrics/PerceivedComplexity:
2844
2846
  Description: 'Checks that the perceived complexity of methods is not higher than the configured maximum.'
2845
2847
  Enabled: true
2846
2848
  VersionAdded: '0.25'
2847
- VersionChanged: '0.81'
2849
+ VersionChanged: '1.88'
2848
2850
  AllowedMethods: []
2849
2851
  AllowedPatterns: []
2850
2852
  Max: 8
@@ -4658,7 +4660,9 @@ Style/MagicCommentFormat:
4658
4660
  Style/MapCompactWithConditionalBlock:
4659
4661
  Description: 'Prefer `select` or `reject` over `map { ... }.compact`.'
4660
4662
  Enabled: pending
4663
+ SafeAutoCorrect: false
4661
4664
  VersionAdded: '1.30'
4665
+ VersionChanged: '1.88'
4662
4666
 
4663
4667
  Style/MapIntoArray:
4664
4668
  Description: 'Checks for usages of `each` with `<<`, `push`, or `append` which can be replaced by `map`.'
@@ -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
 
@@ -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
@@ -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)
@@ -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)
@@ -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)
@@ -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
 
@@ -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)
@@ -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
@@ -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
@@ -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
 
@@ -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)
@@ -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) ||