rubocop 1.89.0 → 1.90.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 (176) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +74 -7
  3. data/exe/rubocop +1 -1
  4. data/lib/rubocop/cached_data.rb +7 -2
  5. data/lib/rubocop/cli/command/execute_runner.rb +31 -0
  6. data/lib/rubocop/cli.rb +2 -4
  7. data/lib/rubocop/comment_config/directive_range.rb +32 -0
  8. data/lib/rubocop/comment_config/disable_next.rb +97 -0
  9. data/lib/rubocop/comment_config.rb +46 -30
  10. data/lib/rubocop/config.rb +23 -5
  11. data/lib/rubocop/config_loader.rb +1 -2
  12. data/lib/rubocop/config_loader_resolver.rb +1 -2
  13. data/lib/rubocop/config_validator.rb +1 -2
  14. data/lib/rubocop/cop/autocorrect_logic.rb +8 -0
  15. data/lib/rubocop/cop/base.rb +34 -2
  16. data/lib/rubocop/cop/corrector.rb +3 -3
  17. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +1 -2
  18. data/lib/rubocop/cop/internal_affairs/node_pattern_groups/ast_walker.rb +1 -2
  19. data/lib/rubocop/cop/layout/block_alignment.rb +1 -2
  20. data/lib/rubocop/cop/layout/class_structure.rb +102 -26
  21. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +1 -2
  22. data/lib/rubocop/cop/layout/empty_line_after_magic_comment.rb +63 -5
  23. data/lib/rubocop/cop/layout/end_alignment.rb +1 -2
  24. data/lib/rubocop/cop/layout/extra_spacing.rb +105 -3
  25. data/lib/rubocop/cop/layout/first_argument_indentation.rb +21 -1
  26. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +1 -2
  27. data/lib/rubocop/cop/layout/line_length.rb +1 -2
  28. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +1 -2
  29. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +5 -5
  30. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +1 -2
  31. data/lib/rubocop/cop/layout/space_around_operators.rb +7 -0
  32. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +2 -4
  33. data/lib/rubocop/cop/lint/ambiguous_range.rb +1 -2
  34. data/lib/rubocop/cop/lint/argument_mismatch.rb +89 -0
  35. data/lib/rubocop/cop/lint/constant_reassignment.rb +17 -11
  36. data/lib/rubocop/cop/lint/cop_directive_syntax.rb +84 -8
  37. data/lib/rubocop/cop/lint/deprecated_reference.rb +11 -2
  38. data/lib/rubocop/cop/lint/duplicate_methods.rb +112 -14
  39. data/lib/rubocop/cop/lint/implicit_string_concatenation.rb +1 -2
  40. data/lib/rubocop/cop/lint/ineffective_access_modifier.rb +1 -2
  41. data/lib/rubocop/cop/lint/interpolation_check.rb +1 -2
  42. data/lib/rubocop/cop/lint/literal_as_condition.rb +3 -6
  43. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +3 -6
  44. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +25 -12
  45. data/lib/rubocop/cop/lint/name_typo.rb +53 -6
  46. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +15 -0
  47. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +98 -19
  48. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +47 -7
  49. data/lib/rubocop/cop/lint/redundant_require_statement.rb +1 -2
  50. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +1 -2
  51. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +1 -2
  52. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +1 -2
  53. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -2
  54. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +1 -2
  55. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +2 -4
  56. data/lib/rubocop/cop/lint/super_argument_mismatch.rb +179 -0
  57. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +1 -2
  58. data/lib/rubocop/cop/lint/to_enum_arguments.rb +1 -2
  59. data/lib/rubocop/cop/lint/unreachable_code.rb +1 -2
  60. data/lib/rubocop/cop/lint/unused_private_method.rb +94 -17
  61. data/lib/rubocop/cop/lint/useless_access_modifier.rb +1 -2
  62. data/lib/rubocop/cop/lint/useless_assignment.rb +1 -2
  63. data/lib/rubocop/cop/lint/useless_constant_scoping.rb +36 -0
  64. data/lib/rubocop/cop/lint/useless_method_definition.rb +1 -1
  65. data/lib/rubocop/cop/lint/useless_rescue.rb +1 -2
  66. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +61 -7
  67. data/lib/rubocop/cop/lint/void.rb +1 -2
  68. data/lib/rubocop/cop/lint.rb +2 -0
  69. data/lib/rubocop/cop/metrics/perceived_complexity.rb +1 -2
  70. data/lib/rubocop/cop/migration/department_name.rb +1 -2
  71. data/lib/rubocop/cop/mixin/comments_help.rb +1 -2
  72. data/lib/rubocop/cop/mixin/configurable_enforced_style.rb +1 -2
  73. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +1 -2
  74. data/lib/rubocop/cop/mixin/hash_subset.rb +1 -2
  75. data/lib/rubocop/cop/mixin/indexed_method_arity.rb +94 -0
  76. data/lib/rubocop/cop/mixin/line_length_help.rb +7 -1
  77. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +51 -6
  78. data/lib/rubocop/cop/mixin/range_help.rb +1 -2
  79. data/lib/rubocop/cop/mixin/statement_modifier.rb +13 -7
  80. data/lib/rubocop/cop/mixin/trailing_comma.rb +2 -4
  81. data/lib/rubocop/cop/mixin.rb +4 -0
  82. data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +16 -0
  83. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +2 -4
  84. data/lib/rubocop/cop/naming/method_name.rb +5 -2
  85. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +1 -2
  86. data/lib/rubocop/cop/offense.rb +27 -4
  87. data/lib/rubocop/cop/registry.rb +34 -10
  88. data/lib/rubocop/cop/style/accessor_grouping.rb +1 -2
  89. data/lib/rubocop/cop/style/arguments_forwarding.rb +3 -6
  90. data/lib/rubocop/cop/style/array_first_last.rb +1 -2
  91. data/lib/rubocop/cop/style/bitwise_predicate.rb +1 -2
  92. data/lib/rubocop/cop/style/block_delimiters.rb +3 -6
  93. data/lib/rubocop/cop/style/case_like_if.rb +2 -4
  94. data/lib/rubocop/cop/style/class_and_module_children.rb +1 -2
  95. data/lib/rubocop/cop/style/combinable_loops.rb +1 -2
  96. data/lib/rubocop/cop/style/concat_array_literals.rb +1 -2
  97. data/lib/rubocop/cop/style/conditional_assignment.rb +1 -2
  98. data/lib/rubocop/cop/style/constant_visibility.rb +1 -2
  99. data/lib/rubocop/cop/style/copyright.rb +1 -2
  100. data/lib/rubocop/cop/style/directive_scope.rb +171 -0
  101. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +36 -10
  102. data/lib/rubocop/cop/style/empty_case_condition.rb +1 -2
  103. data/lib/rubocop/cop/style/format_string_token.rb +1 -2
  104. data/lib/rubocop/cop/style/guard_clause.rb +1 -2
  105. data/lib/rubocop/cop/style/hash_each_methods.rb +1 -2
  106. data/lib/rubocop/cop/style/hash_syntax.rb +1 -2
  107. data/lib/rubocop/cop/style/identical_conditional_branches.rb +2 -4
  108. data/lib/rubocop/cop/style/if_inside_else.rb +1 -2
  109. data/lib/rubocop/cop/style/if_unless_modifier.rb +1 -2
  110. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +1 -2
  111. data/lib/rubocop/cop/style/map_to_hash.rb +1 -2
  112. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +2 -4
  113. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +1 -2
  114. data/lib/rubocop/cop/style/multiline_method_signature.rb +1 -2
  115. data/lib/rubocop/cop/style/negated_if_else_condition.rb +1 -2
  116. data/lib/rubocop/cop/style/negative_array_index.rb +2 -4
  117. data/lib/rubocop/cop/style/nil_comparison.rb +1 -2
  118. data/lib/rubocop/cop/style/operator_method_call.rb +1 -2
  119. data/lib/rubocop/cop/style/parallel_assignment.rb +1 -2
  120. data/lib/rubocop/cop/style/redundant_assignment.rb +1 -2
  121. data/lib/rubocop/cop/style/redundant_condition.rb +2 -4
  122. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +1 -2
  123. data/lib/rubocop/cop/style/redundant_each.rb +1 -2
  124. data/lib/rubocop/cop/style/redundant_format.rb +2 -4
  125. data/lib/rubocop/cop/style/redundant_parentheses.rb +2 -4
  126. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +1 -2
  127. data/lib/rubocop/cop/style/redundant_return.rb +1 -2
  128. data/lib/rubocop/cop/style/redundant_self_assignment.rb +1 -2
  129. data/lib/rubocop/cop/style/redundant_string_escape.rb +1 -2
  130. data/lib/rubocop/cop/style/rescue_modifier.rb +1 -2
  131. data/lib/rubocop/cop/style/safe_navigation.rb +1 -2
  132. data/lib/rubocop/cop/style/sample.rb +11 -6
  133. data/lib/rubocop/cop/style/select_by_regexp.rb +1 -2
  134. data/lib/rubocop/cop/style/semicolon.rb +2 -4
  135. data/lib/rubocop/cop/style/single_line_do_end_block.rb +1 -2
  136. data/lib/rubocop/cop/style/sole_nested_conditional.rb +1 -2
  137. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +2 -0
  138. data/lib/rubocop/cop/style/string_concatenation.rb +18 -5
  139. data/lib/rubocop/cop/style/super_arguments.rb +1 -2
  140. data/lib/rubocop/cop/style/symbol_proc.rb +1 -2
  141. data/lib/rubocop/cop/style/time_now.rb +41 -0
  142. data/lib/rubocop/cop/style/yoda_condition.rb +1 -2
  143. data/lib/rubocop/cop/style.rb +2 -0
  144. data/lib/rubocop/cop/team.rb +11 -3
  145. data/lib/rubocop/cop/util.rb +2 -4
  146. data/lib/rubocop/cop/variable_force/variable.rb +1 -2
  147. data/lib/rubocop/cops_documentation_generator.rb +3 -6
  148. data/lib/rubocop/directive_comment.rb +37 -3
  149. data/lib/rubocop/formatter/emacs_style_formatter.rb +3 -1
  150. data/lib/rubocop/formatter/html_formatter.rb +2 -4
  151. data/lib/rubocop/formatter/json_formatter.rb +11 -6
  152. data/lib/rubocop/formatter/junit_formatter.rb +26 -9
  153. data/lib/rubocop/formatter/offense_count_formatter.rb +1 -2
  154. data/lib/rubocop/formatter/simple_text_formatter.rb +4 -3
  155. data/lib/rubocop/formatter/tap_formatter.rb +3 -1
  156. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -2
  157. data/lib/rubocop/lockfile.rb +18 -0
  158. data/lib/rubocop/lsp/diagnostic.rb +4 -8
  159. data/lib/rubocop/lsp/routes.rb +48 -19
  160. data/lib/rubocop/mcp/server.rb +1 -2
  161. data/lib/rubocop/options.rb +10 -6
  162. data/lib/rubocop/path_util.rb +1 -2
  163. data/lib/rubocop/plugin/configuration_integrator.rb +6 -3
  164. data/lib/rubocop/rake_task.rb +1 -2
  165. data/lib/rubocop/rspec/expect_offense.rb +2 -4
  166. data/lib/rubocop/rspec/shared_contexts.rb +4 -2
  167. data/lib/rubocop/runner.rb +26 -23
  168. data/lib/rubocop/server/cache.rb +1 -2
  169. data/lib/rubocop/server/cli.rb +2 -4
  170. data/lib/rubocop/server.rb +14 -0
  171. data/lib/rubocop/target_finder.rb +4 -1
  172. data/lib/rubocop/todo_audit.rb +111 -0
  173. data/lib/rubocop/version.rb +3 -5
  174. data/lib/rubocop.rb +3 -0
  175. data/lib/ruby_lsp/rubocop/addon.rb +1 -2
  176. metadata +13 -5
@@ -5,6 +5,7 @@ module RuboCop
5
5
  # and provides a way to check if each cop is enabled at arbitrary line.
6
6
  class CommentConfig
7
7
  extend SimpleForwardable
8
+ include DisableNext
8
9
 
9
10
  CONFIG_DISABLED_LINE_RANGE_MIN = -Float::INFINITY
10
11
 
@@ -25,8 +26,6 @@ module RuboCop
25
26
  end
26
27
  end
27
28
 
28
- CopAnalysis = Struct.new(:line_ranges, :start_line_number)
29
-
30
29
  attr_reader :processed_source
31
30
 
32
31
  def_delegators :@processed_source, :config, :registry
@@ -35,14 +34,25 @@ module RuboCop
35
34
  @processed_source = processed_source
36
35
  @no_directives = !processed_source.raw_source.include?('rubocop')
37
36
  @stack = []
37
+ @detached_next_directives = []
38
38
  end
39
39
 
40
40
  def cop_enabled_at_line?(cop, line_number)
41
+ cop_enabled_at_lines?(cop, line_number, line_number)
42
+ end
43
+
44
+ # Whether the cop is enabled for all of the given line span. The cop
45
+ # counts as disabled for the span when any of its disabled ranges
46
+ # overlaps it, so that a directive on any line of a multi-line offense
47
+ # suppresses the offense.
48
+ def cop_enabled_at_lines?(cop, first_line, last_line)
41
49
  cop = cop.cop_name if cop.respond_to?(:cop_name)
42
50
  disabled_line_ranges = cop_disabled_line_ranges[cop]
43
51
  return true unless disabled_line_ranges
44
52
 
45
- disabled_line_ranges.none? { |range| range.include?(line_number) }
53
+ disabled_line_ranges.none? do |range|
54
+ range.end >= first_line && range.begin <= last_line
55
+ end
46
56
  end
47
57
 
48
58
  def cop_opted_in?(cop)
@@ -88,7 +98,9 @@ module RuboCop
88
98
  def extra_enabled_comments_with_names(extras:, names:)
89
99
  each_directive do |directive|
90
100
  next unless comment_only_line?(directive.line_number)
91
- next if directive.push? || directive.pop?
101
+ # Push/pop and next-statement directives close themselves, so they
102
+ # play no part in the disable/enable pairing.
103
+ next if directive.push? || directive.pop? || directive.disable_next?
92
104
 
93
105
  if directive.enabled_all?
94
106
  handle_enable_all(directive, names, extras)
@@ -100,7 +112,7 @@ module RuboCop
100
112
  extras
101
113
  end
102
114
 
103
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
115
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
104
116
  def analyze
105
117
  return {} if @no_directives
106
118
 
@@ -111,9 +123,11 @@ module RuboCop
111
123
  if directive.push?
112
124
  restore_point = analyses.transform_values(&:dup)
113
125
  @stack.push(restore_point)
114
- apply_push(analyses, resolve_push_cops(directive), directive.line_number)
126
+ apply_push(analyses, resolve_push_cops(directive), directive)
115
127
  elsif directive.pop?
116
128
  pop_state(analyses, directive.line_number) if @stack.any?
129
+ elsif directive.disable_next?
130
+ apply_disable_next(analyses, directive)
117
131
  else
118
132
  directive.cop_names.each do |cop_name|
119
133
  cop_name = qualified_cop_name(cop_name)
@@ -129,7 +143,6 @@ module RuboCop
129
143
  hash[cop_name] = cop_line_ranges(analysis)
130
144
  end
131
145
  end
132
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
133
146
 
134
147
  def resolve_push_cops(directive)
135
148
  directive.push_args.transform_values do |names|
@@ -143,32 +156,36 @@ module RuboCop
143
156
  cops.map { |c| qualified_cop_name(c) }
144
157
  end
145
158
 
146
- def apply_push(analyses, resolved_cops, line)
159
+ def apply_push(analyses, resolved_cops, directive)
147
160
  resolved_cops.each do |op, cops|
148
- cops.each { |cop| apply_cop_op(analyses, op, cop, line) }
161
+ cops.each { |cop| apply_cop_op(analyses, op, cop, directive) }
149
162
  end
150
163
  end
151
164
 
152
- def apply_cop_op(analyses, operation, cop, line)
165
+ def apply_cop_op(analyses, operation, cop, directive)
153
166
  analysis = analyses[cop]
167
+ line = directive.line_number
154
168
  if operation == '-' && !analysis.start_line_number
155
- analyses[cop] = CopAnalysis.new(analysis.line_ranges, line)
169
+ analyses[cop] = CopAnalysis.new(analysis.line_ranges, line, directive)
156
170
  elsif operation == '+' && analysis.start_line_number
157
- analyses[cop] =
158
- CopAnalysis.new(analysis.line_ranges + [analysis.start_line_number..line], nil)
171
+ analyses[cop] = CopAnalysis.new(analysis.close(line), nil)
159
172
  end
160
173
  end
161
174
 
162
175
  def pop_state(analyses, line)
163
176
  restore_point = @stack.pop
164
177
  (restore_point.keys | analyses.keys).each do |cop|
165
- current = analyses[cop]
166
- new_range = current.start_line_number ? [current.start_line_number..(line - 1)] : []
167
- new_start = restore_point[cop]&.start_line_number ? line : nil
168
- analyses[cop] = CopAnalysis.new(current.line_ranges + new_range, new_start)
178
+ analyses[cop] = popped_analysis(analyses[cop], restore_point[cop], line)
169
179
  end
170
180
  end
171
181
 
182
+ def popped_analysis(current, restored, line)
183
+ ranges = current.close(line - 1)
184
+ return CopAnalysis.new(ranges, nil) unless restored&.start_line_number
185
+
186
+ CopAnalysis.new(ranges, line, restored.start_directive)
187
+ end
188
+
172
189
  def inject_disabled_cops_directives(analyses)
173
190
  registry.disabled_names(config).each do |cop_name|
174
191
  analyses[cop_name] = analyze_cop(
@@ -193,27 +210,21 @@ module RuboCop
193
210
  return analysis unless directive.disabled?
194
211
 
195
212
  line = directive.line_number
196
- CopAnalysis.new(analysis.line_ranges + [(line..line)], analysis.start_line_number)
213
+ range = DirectiveRange.new(line, line, directive)
214
+ CopAnalysis.new(analysis.line_ranges + [range], analysis.start_line_number,
215
+ analysis.start_directive)
197
216
  end
198
217
 
199
218
  def analyze_disabled(analysis, directive)
200
- line = directive.line_number
201
- start_line = analysis.start_line_number
202
- new_ranges = start_line ? analysis.line_ranges + [start_line..line] : analysis.line_ranges
203
- CopAnalysis.new(new_ranges, line)
219
+ CopAnalysis.new(analysis.close(directive.line_number), directive.line_number, directive)
204
220
  end
205
221
 
206
222
  def analyze_rest(analysis, directive)
207
- line = directive.line_number
208
- start_line = analysis.start_line_number
209
- new_ranges = start_line ? analysis.line_ranges + [start_line..line] : analysis.line_ranges
210
- CopAnalysis.new(new_ranges, nil)
223
+ CopAnalysis.new(analysis.close(directive.line_number), nil)
211
224
  end
212
225
 
213
226
  def cop_line_ranges(analysis)
214
- return analysis.line_ranges unless analysis.start_line_number
215
-
216
- analysis.line_ranges + [(analysis.start_line_number..Float::INFINITY)]
227
+ analysis.close(Float::INFINITY)
217
228
  end
218
229
 
219
230
  def each_directive
@@ -226,7 +237,12 @@ module RuboCop
226
237
  end
227
238
 
228
239
  def qualified_cop_name(cop_name)
229
- Cop::Registry.qualified_cop_name(cop_name.strip, processed_source.file_path)
240
+ # A directive naming a cop under the wrong department is not honored -
241
+ # the mistake is surfaced by `Lint/RedundantCopDisableDirective` (with a
242
+ # did-you-mean hint) instead of a stderr warning that is easy to miss
243
+ # and disappears entirely on cached runs.
244
+ Cop::Registry.qualified_cop_name(cop_name.strip, processed_source.file_path,
245
+ correct_namespace: false)
230
246
  end
231
247
 
232
248
  # `Style/DisableCopsWithinSourceCodeDirective` cannot be disabled via
@@ -2,7 +2,7 @@
2
2
 
3
3
  # FIXME: Moving Rails department code to RuboCop Rails will remove
4
4
  # the following rubocop:disable comment.
5
- # rubocop:disable Metrics/ClassLength
5
+ # rubocop:disable-next Metrics/ClassLength
6
6
  module RuboCop
7
7
  # This class represents the configuration of the RuboCop application
8
8
  # and all its cops. A Config is associated with a YAML configuration
@@ -27,7 +27,7 @@ module RuboCop
27
27
  config
28
28
  end
29
29
 
30
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
30
+ # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength
31
31
  def initialize(hash = RuboCop::ConfigLoader.default_configuration, loaded_path = nil)
32
32
  @loaded_path = loaded_path
33
33
  @for_cop = Hash.new do |h, cop|
@@ -72,7 +72,6 @@ module RuboCop
72
72
  @badge_config_cache = {}.compare_by_identity
73
73
  @clusivity_config_exists_cache = {}
74
74
  end
75
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
76
75
 
77
76
  def loaded_plugins
78
77
  @loaded_plugins ||= ConfigLoader.loaded_plugins
@@ -168,7 +167,11 @@ module RuboCop
168
167
  department_config = self[badge.department_name]
169
168
  cop_config = for_cop(badge.to_s)
170
169
  if department_config
171
- department_config.merge(cop_config)
170
+ merged_config = department_config.merge(cop_config)
171
+ if department_config['Exclude'] && cop_config['Exclude']
172
+ merged_config['Exclude'] = department_config['Exclude'] | cop_config['Exclude']
173
+ end
174
+ merged_config
172
175
  else
173
176
  cop_config
174
177
  end
@@ -352,6 +355,14 @@ module RuboCop
352
355
  @gem_versions_in_target ||= read_gem_versions_from_target_lockfile
353
356
  end
354
357
 
358
+ # Returns the names of the target's gems that are sourced from a local path
359
+ # (i.e. `path:` dependencies and the project's own gem when the `Gemfile`
360
+ # uses `gemspec`), whose code therefore lives in the project itself.
361
+ # @returns [Array<String>, nil] The gem names, or nil without a lockfile.
362
+ def path_sourced_gems_in_target
363
+ @path_sourced_gems_in_target ||= read_path_sourced_gems_from_target_lockfile
364
+ end
365
+
355
366
  def inspect # :nodoc:
356
367
  "#<#{self.class.name}:#{object_id} @loaded_path=#{loaded_path}>"
357
368
  end
@@ -396,6 +407,14 @@ module RuboCop
396
407
  Lockfile.new(lockfile_path).gem_versions
397
408
  end
398
409
 
410
+ # @returns [Array<String>, nil] The names of the gems sourced from a local path.
411
+ def read_path_sourced_gems_from_target_lockfile
412
+ lockfile_path = bundler_lock_file_path
413
+ return nil unless lockfile_path
414
+
415
+ Lockfile.new(lockfile_path).path_sourced_gem_names
416
+ end
417
+
399
418
  def enable_cop?(qualified_cop_name, cop_options)
400
419
  # If the cop is explicitly enabled or `Lint/Syntax`, the other checks can be skipped.
401
420
  return true if cop_options['Enabled'] == true || qualified_cop_name == 'Lint/Syntax'
@@ -454,4 +473,3 @@ module RuboCop
454
473
  end
455
474
  end
456
475
  end
457
- # rubocop:enable Metrics/ClassLength
@@ -49,7 +49,7 @@ module RuboCop
49
49
  FileFinder.root_level = nil
50
50
  end
51
51
 
52
- # rubocop:disable Metrics/AbcSize
52
+ # rubocop:disable-next Metrics/AbcSize
53
53
  def load_file(file, check: true)
54
54
  path = file_path(file)
55
55
 
@@ -73,7 +73,6 @@ module RuboCop
73
73
 
74
74
  Config.create(hash, path, check: check)
75
75
  end
76
- # rubocop:enable Metrics/AbcSize
77
76
 
78
77
  def load_yaml_configuration(absolute_path)
79
78
  file_contents = read_file(absolute_path)
@@ -115,7 +115,7 @@ module RuboCop
115
115
  # with the addition that any value that is a hash, and occurs in both
116
116
  # arguments, will also be merged. And so on.
117
117
  #
118
- # rubocop:disable Metrics/AbcSize
118
+ # rubocop:disable-next Metrics/AbcSize
119
119
  def merge(base_hash, derived_hash, **opts)
120
120
  result = base_hash.merge(derived_hash)
121
121
  keys_appearing_in_both = base_hash.keys & derived_hash.keys
@@ -132,7 +132,6 @@ module RuboCop
132
132
  end
133
133
  result
134
134
  end
135
- # rubocop:enable Metrics/AbcSize
136
135
 
137
136
  # An `Enabled: true` setting in user configuration for a cop overrides an
138
137
  # `Enabled: false` setting for its department.
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  # Handles validation of configuration, for example cop names, parameter
5
5
  # names, and Ruby versions.
6
- # rubocop:disable Metrics/ClassLength
6
+ # rubocop:disable-next Metrics/ClassLength
7
7
  class ConfigValidator
8
8
  extend SimpleForwardable
9
9
 
@@ -324,5 +324,4 @@ module RuboCop
324
324
  "is supposed to be #{supposed_values} and #{Rainbow(value).yellow} is not."
325
325
  end
326
326
  end
327
- # rubocop:enable Metrics/ClassLength
328
327
  end
@@ -12,6 +12,14 @@ module RuboCop
12
12
  autocorrect_requested? && disable_uncorrectable? && autocorrect_enabled?
13
13
  end
14
14
 
15
+ # Whether a correction exists but is skipped because it is unsafe in a
16
+ # safe autocorrect run, while `--disable-uncorrectable` asks for a todo
17
+ # comment on everything this run cannot correct.
18
+ def skipped_unsafe_correction_with_disable_uncorrectable?
19
+ autocorrect_requested? && disable_uncorrectable? &&
20
+ @options.fetch(:safe_autocorrect, false) && !safe_autocorrect?
21
+ end
22
+
15
23
  def autocorrect_requested?
16
24
  @options.fetch(:autocorrect, false)
17
25
  end
@@ -127,6 +127,8 @@ module RuboCop
127
127
  # in the `on_new_investigation` callback.
128
128
  # If your cop does autocorrections, be aware that your instance may be called
129
129
  # multiple times with the same `processed_source.path` but different content.
130
+ # Note that under `--parallel` each worker process has its own cop
131
+ # instances, so state persists only within a worker's share of the files.
130
132
  def self.support_multiple_source?
131
133
  false
132
134
  end
@@ -208,13 +210,15 @@ module RuboCop
208
210
  severity = find_severity(range_to_pass, severity)
209
211
  message = find_message(range_to_pass, message)
210
212
 
211
- status, corrector = enabled_line?(range.line) ? correct(range, &block) : :disabled
213
+ status, corrector = enabled_lines?(range) ? correct(range, &block) : :disabled
214
+ justification = suppression_reason(range) if status == :disabled
212
215
 
213
216
  # Since this range may be generated from Ruby code embedded in some
214
217
  # template file, we convert it to location info in the original file.
215
218
  range = range_for_original(range)
216
219
 
217
- current_offenses << Offense.new(severity, range, message, name, status, corrector)
220
+ current_offenses << Offense.new(severity, range, message, name, status, corrector,
221
+ justification: justification)
218
222
  end
219
223
 
220
224
  # This method should be overridden when a cop's behavior depends
@@ -445,6 +449,10 @@ module RuboCop
445
449
  def use_corrector(range, corrector)
446
450
  if autocorrect?
447
451
  attempt_correction(range, corrector)
452
+ elsif skipped_unsafe_correction_with_disable_uncorrectable?
453
+ # The unsafe correction will not run, so the offense is
454
+ # uncorrectable for this run and gets a todo comment.
455
+ attempt_correction(range, nil)
448
456
  elsif corrector && (always_autocorrect? || (contextual_autocorrect? && !LSP.enabled?))
449
457
  :uncorrected
450
458
  else
@@ -524,6 +532,30 @@ module RuboCop
524
532
  @processed_source.comment_config.cop_enabled_at_line?(self, line_number)
525
533
  end
526
534
 
535
+ # A multi-line offense is suppressed by a directive on any line of its
536
+ # range, not only its first line, matching the intuition that the
537
+ # directive is attached to the offending code.
538
+ def enabled_lines?(range)
539
+ return true if @options[:ignore_disable_comments] || !@processed_source
540
+
541
+ comment_config = @processed_source.comment_config
542
+ comment_config.cop_enabled_at_lines?(self, range.first_line, range.last_line)
543
+ end
544
+
545
+ # The `--` reason on the directive that suppresses offenses on this
546
+ # range, or `nil` when the directive carries none.
547
+ def suppression_reason(range)
548
+ covering = covering_disabled_range(range)
549
+ covering.directive.reason if covering.respond_to?(:directive)
550
+ end
551
+
552
+ def covering_disabled_range(range)
553
+ disabled_ranges = @processed_source.comment_config.cop_disabled_line_ranges[cop_name]
554
+ disabled_ranges&.find do |disabled_range|
555
+ disabled_range.end >= range.first_line && disabled_range.begin <= range.last_line
556
+ end
557
+ end
558
+
527
559
  def find_severity(_range, severity)
528
560
  custom_severity || severity || default_severity
529
561
  end
@@ -46,7 +46,7 @@ module RuboCop
46
46
 
47
47
  # Removes `size` characters prior to the source range.
48
48
  #
49
- # @param [Parser::Source::Range, RuboCop::AST::Node] range or node
49
+ # @param [Parser::Source::Range, RuboCop::AST::Node] node_or_range
50
50
  # @param [Integer] size
51
51
  def remove_preceding(node_or_range, size)
52
52
  range = to_range(node_or_range)
@@ -58,7 +58,7 @@ module RuboCop
58
58
  # If `size` is greater than the size of `range`, the removed region can
59
59
  # overrun the end of `range`.
60
60
  #
61
- # @param [Parser::Source::Range, RuboCop::AST::Node] range or node
61
+ # @param [Parser::Source::Range, RuboCop::AST::Node] node_or_range
62
62
  # @param [Integer] size
63
63
  def remove_leading(node_or_range, size)
64
64
  range = to_range(node_or_range)
@@ -70,7 +70,7 @@ module RuboCop
70
70
  # If `size` is greater than the size of `range`, the removed region can
71
71
  # overrun the beginning of `range`.
72
72
  #
73
- # @param [Parser::Source::Range, RuboCop::AST::Node] range or node
73
+ # @param [Parser::Source::Range, RuboCop::AST::Node] node_or_range
74
74
  # @param [Integer] size
75
75
  def remove_trailing(node_or_range, size)
76
76
  range = to_range(node_or_range)
@@ -146,7 +146,7 @@ module RuboCop
146
146
  end
147
147
  end
148
148
 
149
- # rubocop:disable Metrics/MethodLength
149
+ # rubocop:disable-next Metrics/MethodLength
150
150
  def formatted_message(offense_type, directive, actual_name, method_name)
151
151
  case offense_type
152
152
  when :wrong_name
@@ -166,7 +166,6 @@ module RuboCop
166
166
  format(MSG, method: method_name)
167
167
  end
168
168
  end
169
- # rubocop:enable Metrics/MethodLength
170
169
 
171
170
  def remove_receiver(current)
172
171
  current.delete_prefix('self.')
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module InternalAffairs
6
- # rubocop:disable InternalAffairs/RedundantSourceRange -- node here is a `NodePattern::Node`
6
+ # rubocop:disable-next InternalAffairs/RedundantSourceRange -- node here is a `NodePattern::Node`
7
7
  class NodePatternGroups
8
8
  # Walks an AST that has been processed by `InternalAffairs::NodePatternGroups::Processor`
9
9
  # in order to find `node_type` and `node_sequence` nodes that can be replaced with a node
@@ -125,7 +125,6 @@ module RuboCop
125
125
  end
126
126
  end
127
127
  end
128
- # rubocop:enable InternalAffairs/RedundantSourceRange
129
128
  end
130
129
  end
131
130
  end
@@ -281,7 +281,7 @@ module RuboCop
281
281
  end
282
282
  end
283
283
 
284
- # rubocop:disable Metrics/AbcSize
284
+ # rubocop:disable-next Metrics/AbcSize
285
285
  def do_line_begins_inside_argument?(node, do_loc)
286
286
  line_begin_pos = do_loc.begin_pos - do_loc.column
287
287
  first_char_pos = line_begin_pos + (do_loc.source_line =~ /\S/)
@@ -292,7 +292,6 @@ module RuboCop
292
292
  first_char_pos < argument.source_range.end_pos
293
293
  end
294
294
  end
295
- # rubocop:enable Metrics/AbcSize
296
295
 
297
296
  # The continuation line indentation is only an unmeaningful alignment target when
298
297
  # it is dictated by an opening delimiter, i.e. the line begins inside `(` or `[`.
@@ -190,33 +190,58 @@ module RuboCop
190
190
 
191
191
  # Validates code style on class declaration.
192
192
  # Add offense when find a node out of expected order.
193
+ # A node is out of order when its category is expected earlier than
194
+ # the highest-priority category seen so far, so that a low-priority element
195
+ # (even an unmovable one) cannot mask disorder among the elements that follow it.
196
+ # Consecutive elements of the same category are reported only once,
197
+ # on the first element of the group.
193
198
  def on_class(class_node)
194
- previous = -1
195
- walk_over_nested_class_definition(class_node) do |node, category|
196
- index = expected_order.index(category)
197
- if index < previous
198
- message = format(MSG, category: category, previous: expected_order[previous])
199
- add_offense(node, message: message) { |corrector| autocorrect(corrector, node) }
199
+ # Corrections are registered in reverse source order because an insertion at
200
+ # a given position lands before any insertion already made there;
201
+ # this keeps the source order of nodes moved before the same anchor.
202
+ out_of_order_elements(class_node).reverse_each do |node, category, previous|
203
+ message = format(MSG, category: category, previous: previous)
204
+
205
+ add_offense(node, message: message) do |corrector|
206
+ autocorrect(corrector, node)
200
207
  end
201
- previous = index
202
208
  end
203
209
  end
204
210
  alias on_sclass on_class
205
211
 
206
212
  private
207
213
 
208
- # Autocorrect by swapping between two nodes autocorrecting them
209
- def autocorrect(corrector, node)
210
- previous = node.left_siblings.reverse.find do |sibling|
211
- !ignore_for_autocorrect?(node, sibling)
214
+ def out_of_order_elements(class_node)
215
+ out_of_order = []
216
+ max_index = -1
217
+ previous_category = nil
218
+ walk_over_nested_class_definition(class_node) do |node, category|
219
+ index = expected_order.index(category)
220
+ if index < max_index && category != previous_category
221
+ out_of_order << [node, category, expected_order[max_index]]
222
+ end
223
+ max_index = index if index > max_index
224
+ previous_category = category
212
225
  end
213
- return unless previous
214
-
215
- current_range = source_range_with_comment(node)
216
- previous_range = source_range_with_comment(previous)
226
+ out_of_order
227
+ end
217
228
 
218
- corrector.insert_before(previous_range, current_range.source)
219
- corrector.remove(current_range)
229
+ # Autocorrect by moving the node, together with the contiguous group of
230
+ # same-category elements that follows it, to its expected position.
231
+ def autocorrect(corrector, node)
232
+ return if dynamic_constant?(node)
233
+
234
+ anchor = insertion_anchor(node)
235
+ return unless anchor
236
+
237
+ anchor_range = source_range_with_comment(anchor)
238
+ # Reversed for the same reason offenses are registered in reverse source order:
239
+ # the last insertion at a position comes first.
240
+ movable_group(node).reverse_each do |group_node|
241
+ current_range = source_range_with_comment(group_node)
242
+ corrector.insert_before(anchor_range, current_range.source)
243
+ corrector.remove(current_range)
244
+ end
220
245
  end
221
246
 
222
247
  # Classifies a node to match with something in the {expected_order}
@@ -302,15 +327,6 @@ module RuboCop
302
327
  end
303
328
  end
304
329
 
305
- def ignore_for_autocorrect?(node, sibling)
306
- classification = classify(node)
307
- sibling_class = classify(sibling)
308
-
309
- ignore?(sibling, sibling_class) ||
310
- classification == sibling_class ||
311
- dynamic_constant?(node)
312
- end
313
-
314
330
  def humanize_node(node)
315
331
  if node.def_type?
316
332
  return :initializer if node.method?(:initialize)
@@ -328,6 +344,66 @@ module RuboCop
328
344
  !(expression.method?(:freeze) && expression.receiver&.recursive_basic_literal?)
329
345
  end
330
346
 
347
+ # The expected position of the node: the first left sibling within its movable span
348
+ # whose category is expected to appear after the node's.
349
+ # Requiring a strictly later category keeps the order of elements sharing a category stable.
350
+ def insertion_anchor(node)
351
+ index = expected_order.index(classify(node))
352
+
353
+ movable_span(node).find do |sibling|
354
+ classification = classify(sibling)
355
+
356
+ !ignore?(sibling, classification) && expected_order.index(classification) > index
357
+ end
358
+ end
359
+
360
+ # The node together with the contiguous same-category right siblings,
361
+ # so that the whole group moves in a single pass while the offense is
362
+ # reported only on its first element.
363
+ def movable_group(node)
364
+ classification = classify(node)
365
+ group = [node]
366
+
367
+ node.right_siblings.each do |sibling|
368
+ break unless classify(sibling) == classification
369
+ break if ignore?(sibling, classification) || dynamic_constant?(sibling)
370
+
371
+ group << sibling
372
+ end
373
+
374
+ group
375
+ end
376
+
377
+ # Left siblings the node may be reordered with: those after the last barrier.
378
+ # Ignored elements within the span are simply jumped over.
379
+ def movable_span(node)
380
+ left_siblings = node.left_siblings
381
+ barrier_index = left_siblings.rindex { |sibling| barrier?(node, sibling) }
382
+
383
+ barrier_index ? left_siblings[(barrier_index + 1)..] : left_siblings
384
+ end
385
+
386
+ # A dynamic constant blocks every element: the cop does not move such constants,
387
+ # and letting other elements jump over one would change execution order just the same.
388
+ # A bare visibility modifier blocks only the elements whose meaning depends on
389
+ # the visibility section they appear in.
390
+ def barrier?(node, sibling)
391
+ dynamic_constant?(sibling) || (visibility_dependent?(node) && visibility_block?(sibling))
392
+ end
393
+
394
+ # Whether moving the node across a bare visibility modifier would change its meaning.
395
+ # This is the case for `def` nodes and for macros whose category is classified by visibility
396
+ # (e.g. `attr_accessor` when the expected order lists `private_attribute_macros`).
397
+ # Inline visibility (`private def foo`, `def self.foo`) travels with the node.
398
+ def visibility_dependent?(node)
399
+ return true if node.def_type?
400
+ return false if !node.send_type? || node.def_modifier?
401
+
402
+ key = find_category(node.method_name) || node.method_name.to_s
403
+
404
+ VISIBILITY_SCOPES.any? { |visibility| expected_order.include?("#{visibility}_#{key}") }
405
+ end
406
+
331
407
  def private_constant?(node)
332
408
  return false unless node.casgn_type? && node.namespace.nil?
333
409
  return false unless (parent = node.parent)
@@ -161,7 +161,7 @@ module RuboCop
161
161
  next_sibling.if_type? && contains_guard_clause?(next_sibling)
162
162
  end
163
163
 
164
- # rubocop:disable Metrics/CyclomaticComplexity
164
+ # rubocop:disable-next Metrics/CyclomaticComplexity
165
165
  def last_heredoc_argument(node)
166
166
  n = last_heredoc_argument_node(node)
167
167
  n = n.children.first while n.respond_to?(:begin_type?) && n.begin_type?
@@ -176,7 +176,6 @@ module RuboCop
176
176
 
177
177
  last_heredoc_argument(n.receiver) if n.respond_to?(:receiver)
178
178
  end
179
- # rubocop:enable Metrics/CyclomaticComplexity
180
179
 
181
180
  def last_heredoc_argument_node(node)
182
181
  return node unless node.respond_to?(:if_branch)