rubocop 1.90.0 → 1.91.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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +238 -10
  3. data/config/obsoletion.yml +9 -0
  4. data/lib/rubocop/changed_files.rb +112 -0
  5. data/lib/rubocop/cli/command/execute_runner.rb +21 -1
  6. data/lib/rubocop/cli.rb +9 -3
  7. data/lib/rubocop/comment_config/directive_range.rb +3 -1
  8. data/lib/rubocop/comment_config/disable_next.rb +49 -3
  9. data/lib/rubocop/comment_config/push_pop.rb +55 -0
  10. data/lib/rubocop/comment_config.rb +21 -51
  11. data/lib/rubocop/config.rb +10 -0
  12. data/lib/rubocop/config_loader.rb +12 -4
  13. data/lib/rubocop/config_loader_resolver.rb +49 -7
  14. data/lib/rubocop/config_obsoletion/cop_rule.rb +2 -1
  15. data/lib/rubocop/config_obsoletion/removed_cop.rb +8 -0
  16. data/lib/rubocop/config_obsoletion.rb +1 -1
  17. data/lib/rubocop/config_validator.rb +18 -3
  18. data/lib/rubocop/cop/badge.rb +1 -1
  19. data/lib/rubocop/cop/base.rb +15 -3
  20. data/lib/rubocop/cop/correctors.rb +2 -2
  21. data/lib/rubocop/cop/gemspec/require_mfa.rb +1 -1
  22. data/lib/rubocop/cop/generator.rb +1 -1
  23. data/lib/rubocop/cop/internal_affairs/on_send_without_on_csend.rb +1 -1
  24. data/lib/rubocop/cop/layout/argument_alignment.rb +2 -0
  25. data/lib/rubocop/cop/layout/comment_indentation.rb +8 -5
  26. data/lib/rubocop/cop/layout/else_alignment.rb +1 -0
  27. data/lib/rubocop/cop/layout/empty_lines_after_module_inclusion.rb +1 -1
  28. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +17 -4
  29. data/lib/rubocop/cop/layout/first_parameter_indentation.rb +11 -0
  30. data/lib/rubocop/cop/layout/hash_alignment.rb +6 -5
  31. data/lib/rubocop/cop/layout/indentation_width.rb +2 -2
  32. data/lib/rubocop/cop/layout/line_length.rb +19 -1
  33. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +14 -3
  34. data/lib/rubocop/cop/layout/space_around_operators.rb +5 -1
  35. data/lib/rubocop/cop/lint/argument_mismatch.rb +14 -4
  36. data/lib/rubocop/cop/lint/assignment_in_condition.rb +2 -0
  37. data/lib/rubocop/cop/lint/cop_directive_syntax.rb +88 -14
  38. data/lib/rubocop/cop/lint/duplicate_methods.rb +1 -1
  39. data/lib/rubocop/cop/lint/empty_block.rb +1 -1
  40. data/lib/rubocop/cop/lint/literal_as_condition.rb +8 -2
  41. data/lib/rubocop/cop/lint/misplaced_magic_comment.rb +185 -0
  42. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +1 -1
  43. data/lib/rubocop/cop/lint/nested_method_definition.rb +1 -1
  44. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +1 -1
  45. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +17 -1
  46. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +30 -16
  47. data/lib/rubocop/cop/lint.rb +1 -0
  48. data/lib/rubocop/cop/migration/department_name.rb +1 -1
  49. data/lib/rubocop/cop/mixin/alignment.rb +1 -1
  50. data/lib/rubocop/cop/mixin/annotation_comment.rb +1 -1
  51. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +11 -1
  52. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +6 -2
  53. data/lib/rubocop/cop/mixin/hash_transform_method.rb +1 -1
  54. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +12 -1
  55. data/lib/rubocop/cop/mixin.rb +2 -2
  56. data/lib/rubocop/cop/naming/block_forwarding.rb +4 -1
  57. data/lib/rubocop/cop/naming/block_parameter_name.rb +1 -1
  58. data/lib/rubocop/cop/registry.rb +9 -1
  59. data/lib/rubocop/cop/severity.rb +12 -0
  60. data/lib/rubocop/cop/style/access_modifier_declarations.rb +3 -2
  61. data/lib/rubocop/cop/style/arguments_forwarding.rb +2 -2
  62. data/lib/rubocop/cop/style/case_like_if.rb +3 -3
  63. data/lib/rubocop/cop/style/constant_visibility.rb +6 -2
  64. data/lib/rubocop/cop/style/data_inheritance.rb +14 -0
  65. data/lib/rubocop/cop/style/directive_scope.rb +154 -21
  66. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +53 -9
  67. data/lib/rubocop/cop/style/documentation_method.rb +1 -9
  68. data/lib/rubocop/cop/style/each_for_simple_loop.rb +1 -1
  69. data/lib/rubocop/cop/style/empty_block_parameter.rb +1 -1
  70. data/lib/rubocop/cop/style/empty_lambda_parameter.rb +1 -1
  71. data/lib/rubocop/cop/style/empty_method.rb +12 -2
  72. data/lib/rubocop/cop/style/endless_method.rb +31 -12
  73. data/lib/rubocop/cop/style/file_null.rb +2 -2
  74. data/lib/rubocop/cop/style/float_division.rb +10 -16
  75. data/lib/rubocop/cop/style/for.rb +5 -0
  76. data/lib/rubocop/cop/style/format_string_token.rb +1 -1
  77. data/lib/rubocop/cop/style/guard_clause.rb +4 -0
  78. data/lib/rubocop/cop/style/hash_lookup_method.rb +3 -0
  79. data/lib/rubocop/cop/style/magic_comment_format.rb +1 -1
  80. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +1 -1
  81. data/lib/rubocop/cop/style/nested_modifier.rb +9 -4
  82. data/lib/rubocop/cop/style/nil_comparison.rb +36 -8
  83. data/lib/rubocop/cop/style/nil_lambda.rb +1 -1
  84. data/lib/rubocop/cop/style/non_nil_check.rb +18 -2
  85. data/lib/rubocop/cop/style/not.rb +3 -1
  86. data/lib/rubocop/cop/style/numeric_literal_prefix.rb +11 -1
  87. data/lib/rubocop/cop/style/one_line_conditional.rb +1 -1
  88. data/lib/rubocop/cop/style/parallel_assignment.rb +1 -1
  89. data/lib/rubocop/cop/style/parentheses_around_condition.rb +6 -1
  90. data/lib/rubocop/cop/style/redundant_condition.rb +4 -4
  91. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +10 -1
  92. data/lib/rubocop/cop/style/redundant_fetch_block.rb +1 -1
  93. data/lib/rubocop/cop/style/redundant_parentheses.rb +11 -1
  94. data/lib/rubocop/cop/style/redundant_regexp_character_class.rb +6 -5
  95. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +6 -6
  96. data/lib/rubocop/cop/style/single_line_block_params.rb +1 -1
  97. data/lib/rubocop/cop/style/special_global_vars.rb +1 -1
  98. data/lib/rubocop/cop/style/struct_inheritance.rb +14 -0
  99. data/lib/rubocop/cop/style/trailing_comma_in_block_args.rb +1 -1
  100. data/lib/rubocop/cop/style.rb +0 -1
  101. data/lib/rubocop/cop/util.rb +1 -1
  102. data/lib/rubocop/cops_documentation_generator.rb +23 -2
  103. data/lib/rubocop/directive_comment.rb +37 -8
  104. data/lib/rubocop/formatter/disabled_config_formatter.rb +19 -1
  105. data/lib/rubocop/formatter/formatter_set.rb +2 -1
  106. data/lib/rubocop/formatter/github_actions_formatter.rb +6 -5
  107. data/lib/rubocop/formatter/html_formatter.rb +2 -2
  108. data/lib/rubocop/formatter/junit_formatter.rb +1 -1
  109. data/lib/rubocop/formatter/sarif_formatter.rb +181 -0
  110. data/lib/rubocop/formatter.rb +1 -0
  111. data/lib/rubocop/magic_comment.rb +27 -0
  112. data/lib/rubocop/options.rb +59 -1
  113. data/lib/rubocop/path_util.rb +1 -1
  114. data/lib/rubocop/plugin/loader.rb +1 -1
  115. data/lib/rubocop/rake_task.rb +1 -1
  116. data/lib/rubocop/remote_config.rb +4 -4
  117. data/lib/rubocop/result_cache.rb +16 -5
  118. data/lib/rubocop/runner.rb +64 -21
  119. data/lib/rubocop/unified_diff.rb +257 -0
  120. data/lib/rubocop/version.rb +1 -1
  121. data/lib/rubocop.rb +4 -1
  122. metadata +8 -4
  123. data/lib/rubocop/cop/style/double_cop_disable_directive.rb +0 -46
@@ -106,7 +106,7 @@ module RuboCop
106
106
  when :or
107
107
  find_target(node.lhs)
108
108
  when :match_with_lvasgn
109
- lhs, rhs = *node # rubocop:disable InternalAffairs/NodeDestructuring
109
+ lhs, rhs = *node # rubocop:disable InternalAffairs/NodeDestructuring -- `match_with_lvasgn` has no named operand accessors
110
110
  if lhs.regexp_type?
111
111
  rhs
112
112
  elsif rhs.regexp_type?
@@ -171,7 +171,7 @@ module RuboCop
171
171
  return collect_conditions(node.lhs, target, conditions) &&
172
172
  collect_conditions(node.rhs, target, conditions)
173
173
  when :match_with_lvasgn
174
- lhs, rhs = *node # rubocop:disable InternalAffairs/NodeDestructuring
174
+ lhs, rhs = *node # rubocop:disable InternalAffairs/NodeDestructuring -- `match_with_lvasgn` has no named operand accessors
175
175
  condition_from_binary_op(lhs, rhs, target)
176
176
  when :send
177
177
  condition_from_send_node(node, target)
@@ -258,7 +258,7 @@ module RuboCop
258
258
  def regexp_with_working_captures?(node)
259
259
  case node.type
260
260
  when :match_with_lvasgn
261
- lhs, _rhs = *node # rubocop:disable InternalAffairs/NodeDestructuring
261
+ lhs, _rhs = *node # rubocop:disable InternalAffairs/NodeDestructuring -- `match_with_lvasgn` has no named operand accessors
262
262
  node.loc.selector.source == '=~' && regexp_with_named_captures?(lhs)
263
263
  when :send
264
264
  node.method?(:match) &&
@@ -82,12 +82,16 @@ module RuboCop
82
82
  end
83
83
  end
84
84
 
85
- # rubocop:disable-next Metrics/AbcSize
85
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity
86
86
  def visibility_declaration?(node)
87
87
  node.parent.each_child_node(:send).any? do |child|
88
88
  next false unless (arguments = visibility_declaration_for(child))
89
89
 
90
- arguments = arguments.first.children.first.to_a if arguments.first&.splat_type?
90
+ if (splat = arguments.first)&.splat_type?
91
+ next true unless splat.children.first.array_type?
92
+
93
+ arguments = splat.children.first.children
94
+ end
91
95
  constant_values = arguments.map do |argument|
92
96
  # `respond_to?(:value)` is too broad: `int`/`float` nodes respond to it
93
97
  # but their value is a `Numeric`, which has no `to_sym` (e.g.
@@ -10,6 +10,10 @@ module RuboCop
10
10
  # Autocorrection is unsafe because it will change the inheritance
11
11
  # tree (e.g. return value of `Module#ancestors`) of the constant.
12
12
  #
13
+ # It is also unsafe because constants that the class body resolves through its
14
+ # ancestors (e.g. one provided by an included module) fall out of scope inside
15
+ # the block.
16
+ #
13
17
  # @example
14
18
  # # bad
15
19
  # class Person < Data.define(:first_name, :last_name)
@@ -42,6 +46,7 @@ module RuboCop
42
46
 
43
47
  def on_class(node)
44
48
  return unless data_define?(node.parent_class)
49
+ return if defines_constants?(node.body)
45
50
 
46
51
  add_offense(node.parent_class) do |corrector|
47
52
  corrector.remove(range_with_surrounding_space(node.loc.keyword, newlines: false))
@@ -59,6 +64,15 @@ module RuboCop
59
64
 
60
65
  private
61
66
 
67
+ # Constants, including nested classes and modules, are scoped to the class when they
68
+ # are defined in a class body, but leak into the enclosing namespace once moved into a
69
+ # `Data.define` block, so such a class cannot be rewritten as an assignment.
70
+ def defines_constants?(class_body)
71
+ return false unless class_body
72
+
73
+ class_body.each_node(:casgn, :class, :module).any?
74
+ end
75
+
62
76
  def correct_parent(parent, corrector)
63
77
  if parent.block_type?
64
78
  # Convert a brace block to `do`, so the class's own `end` closes it
@@ -4,10 +4,10 @@ module RuboCop
4
4
  module Cop
5
5
  module Style
6
6
  # Checks for directive scopes that can be expressed with the tighter
7
- # `disable-next` form: a `disable`/`enable` pair wrapping exactly one
8
- # statement, or a `push`/`pop` that only disables cops around exactly
9
- # one statement. A statement-scoped directive cannot drift as the
10
- # surrounding code changes and needs no closing boundary.
7
+ # next-statement forms: a `disable`/`enable` pair, an
8
+ # `enable`/`disable` pair, or a `push`/`pop` with signed arguments
9
+ # wrapping exactly one statement. A statement-scoped directive cannot
10
+ # drift as the surrounding code changes and needs no closing boundary.
11
11
  #
12
12
  # @safety
13
13
  # The autocorrection is unsafe because the suppression scope shrinks
@@ -37,6 +37,21 @@ module RuboCop
37
37
  # def foo
38
38
  # end
39
39
  #
40
+ # # bad
41
+ # # rubocop:disable Metrics/AbcSize
42
+ # # rubocop:push +Metrics/AbcSize
43
+ # def foo
44
+ # end
45
+ # # rubocop:pop
46
+ # # rubocop:enable Metrics/AbcSize
47
+ #
48
+ # # good
49
+ # # rubocop:disable Metrics/AbcSize
50
+ # # rubocop:enable-next Metrics/AbcSize
51
+ # def foo
52
+ # end
53
+ # # rubocop:enable Metrics/AbcSize
54
+ #
40
55
  # # good - the region spans more than one statement
41
56
  # # rubocop:disable Metrics/AbcSize
42
57
  # def foo
@@ -52,7 +67,9 @@ module RuboCop
52
67
 
53
68
  MSG_PAIR = 'Use `%<mode>s-next` instead of a `%<mode>s`/`enable` pair ' \
54
69
  'around a single statement.'
55
- MSG_PUSH_POP = 'Use `disable-next` instead of `push`/`pop` around a single statement.'
70
+ MSG_ENABLE_PAIR = 'Use `enable-next` instead of an `enable`/`disable` pair ' \
71
+ 'around a single statement.'
72
+ MSG_PUSH_POP = 'Use `%<replacement>s` instead of `push`/`pop` around a single statement.'
56
73
 
57
74
  def on_new_investigation
58
75
  processed_source.comments.each do |comment|
@@ -61,8 +78,10 @@ module RuboCop
61
78
 
62
79
  if plain_disable?(directive)
63
80
  check_pair(directive)
64
- elsif disable_only_push?(directive)
81
+ elsif signed_push?(directive)
65
82
  check_push_pop(directive)
83
+ elsif plain_enable?(directive)
84
+ check_enable_pair(directive)
66
85
  end
67
86
  end
68
87
  end
@@ -77,8 +96,12 @@ module RuboCop
77
96
  directive.disabled? && !directive.disable_next?
78
97
  end
79
98
 
80
- def disable_only_push?(directive)
81
- directive.push? && !directive.push_args.empty? && directive.push_args.keys == ['-']
99
+ def plain_enable?(directive)
100
+ directive.enabled? && !directive.enable_next? && !directive.all_cops?
101
+ end
102
+
103
+ def signed_push?(directive)
104
+ directive.push? && !directive.signed_args.empty?
82
105
  end
83
106
 
84
107
  def check_pair(directive)
@@ -100,15 +123,132 @@ module RuboCop
100
123
  end
101
124
 
102
125
  def check_push_pop(directive)
103
- pop = single_statement_closing_directive(directive)
104
- return unless pop&.pop?
126
+ pop_line = balancing_pop_line(directive)
127
+ return unless pop_line && comment_config.comment_only_line?(pop_line)
128
+ return unless wraps_single_statement?(directive, pop_line)
105
129
 
106
- add_offense(directive.comment, message: MSG_PUSH_POP) do |corrector|
107
- corrector.replace(directive.comment, disable_next_replacement(directive))
108
- remove_line(corrector, pop.comment)
130
+ pop_comment = processed_source.comment_at_line(pop_line)
131
+ replacement = push_replacement(directive)
132
+ message = format(MSG_PUSH_POP, replacement: replacement_mode(directive))
133
+ add_offense(directive.comment, message: message) do |corrector|
134
+ corrector.replace(directive.comment, replacement)
135
+ remove_line(corrector, pop_comment)
109
136
  end
110
137
  end
111
138
 
139
+ # The line of the `pop` balancing this `push`, taking nesting into
140
+ # account, or `nil` when the push is never popped.
141
+ def balancing_pop_line(push_directive)
142
+ depth = 0
143
+ each_directive_after(push_directive) do |directive|
144
+ if directive.push?
145
+ depth += 1
146
+ elsif directive.pop?
147
+ return directive.line_number if depth.zero?
148
+
149
+ depth -= 1
150
+ end
151
+ end
152
+ nil
153
+ end
154
+
155
+ def each_directive_after(reference)
156
+ processed_source.comments.each do |comment|
157
+ directive = DirectiveComment.new(comment)
158
+ next unless directive.cop_names || directive.push? || directive.pop?
159
+
160
+ yield directive if directive.line_number > reference.line_number
161
+ end
162
+ end
163
+
164
+ def replacement_mode(directive)
165
+ ops = directive.signed_args.keys.sort
166
+ if ops == ['-']
167
+ 'disable-next'
168
+ elsif ops == ['+']
169
+ 'enable-next'
170
+ else
171
+ 'next'
172
+ end
173
+ end
174
+
175
+ def push_replacement(directive)
176
+ text = case replacement_mode(directive)
177
+ when 'disable-next'
178
+ "# rubocop:disable-next #{directive.signed_args['-'].join(', ')}"
179
+ when 'enable-next'
180
+ "# rubocop:enable-next #{directive.signed_args['+'].join(', ')}"
181
+ else
182
+ "# rubocop:next #{directive.cops}"
183
+ end
184
+ reason = directive.reason
185
+ reason ? "#{text} -- #{reason}" : text
186
+ end
187
+
188
+ # An `enable`/`disable` pair around one statement inside a disabled
189
+ # region re-enables the cops for just that statement - `enable-next`
190
+ # says the same without the closing boundary. The pair only counts
191
+ # when the `enable` really closed open disables (otherwise the
192
+ # trailing `disable` opens a new region and the conversion would
193
+ # change what is covered).
194
+ def check_enable_pair(directive)
195
+ closing = enable_pair_closing(directive)
196
+ return unless closing
197
+
198
+ add_offense(directive.comment, message: MSG_ENABLE_PAIR) do |corrector|
199
+ corrector.replace(directive.comment,
200
+ directive.comment.text.sub(/\benable\b/, 'enable-next'))
201
+ remove_line(corrector, closing.comment)
202
+ end
203
+ end
204
+
205
+ def enable_pair_closing(directive)
206
+ scope = comment_config.statement_scope_after(directive.line_number)
207
+ return nil unless scope && scope.begin == directive.line_number + 1
208
+
209
+ closing = re_disable_below(directive, scope.end + 1)
210
+ closing if closing && closed_open_disables?(directive, closing)
211
+ end
212
+
213
+ def re_disable_below(directive, line)
214
+ return nil unless comment_config.comment_only_line?(line)
215
+
216
+ comment = processed_source.comment_at_line(line)
217
+ return nil unless comment
218
+
219
+ closing = DirectiveComment.new(comment)
220
+ return nil unless closing.disabled? && !closing.disable_next?
221
+ return nil unless closing.raw_cop_names.sort == directive.raw_cop_names.sort
222
+
223
+ closing
224
+ end
225
+
226
+ # Every cop of the pair must have a range the `enable` closed and a
227
+ # range the trailing `disable` reopened - otherwise the `enable` was
228
+ # not inside a disabled region and the conversion would change what
229
+ # is covered.
230
+ def closed_open_disables?(directive, closing)
231
+ directive.cop_names.all? do |cop|
232
+ ranges = comment_config.cop_disabled_line_ranges[qualified_name(cop)]
233
+ ranges && closed_at?(ranges, directive.line_number) && reopened_by?(ranges, closing)
234
+ end
235
+ end
236
+
237
+ def closed_at?(ranges, line)
238
+ ranges.any? { |range| range.end == line }
239
+ end
240
+
241
+ def reopened_by?(ranges, closing)
242
+ ranges.any? do |range|
243
+ range.respond_to?(:directive) && range.directive.comment.equal?(closing.comment)
244
+ end
245
+ end
246
+
247
+ def qualified_name(cop_name)
248
+ Registry.qualified_cop_name(cop_name.strip, processed_source.file_path,
249
+ correct_namespace: false)
250
+ end
251
+
112
252
  # The directive closing this one's scope, when that scope wraps
113
253
  # exactly one statement - `nil` otherwise.
114
254
  def single_statement_closing_directive(directive)
@@ -133,7 +273,7 @@ module RuboCop
133
273
  ends = ranges.map(&:end).uniq
134
274
  return nil unless ends.size == 1 && ends.first.to_f.finite?
135
275
 
136
- directive.push? ? ends.first + 1 : ends.first
276
+ ends.first
137
277
  end
138
278
 
139
279
  def ranges_opened_by(directive)
@@ -155,13 +295,6 @@ module RuboCop
155
295
  !scope.nil? && scope.begin == directive.line_number + 1 && scope.end + 1 == closing_line
156
296
  end
157
297
 
158
- def disable_next_replacement(directive)
159
- cops = directive.push_args['-'].join(', ')
160
- reason = directive.reason
161
- text = "# rubocop:disable-next #{cops}"
162
- reason ? "#{text} -- #{reason}" : text
163
- end
164
-
165
298
  def remove_line(corrector, comment)
166
299
  corrector.remove(range_by_whole_lines(comment.source_range, include_final_newline: true))
167
300
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Lint/RedundantCopDisableDirective
3
+ # rubocop:disable Lint/RedundantCopDisableDirective -- the examples below read as real directives
4
4
 
5
5
  module RuboCop
6
6
  module Cop
@@ -12,6 +12,10 @@ module RuboCop
12
12
  # Specific cops can be allowed with the `AllowedCops` configuration. Note that
13
13
  # if this configuration is set, `rubocop:disable all` is still disallowed.
14
14
  #
15
+ # `AllowedCops` and `DisallowedCops` accept cop names and department names
16
+ # alike. Listing a cop does not cover its whole department: a directive
17
+ # disabling `Metrics` is broader than one disabling `Metrics/AbcSize`.
18
+ #
15
19
  # Alternatively, specific cops can be disallowed with the `DisallowedCops`
16
20
  # configuration. When `DisallowedCops` is set, only directives for the listed
17
21
  # cops (and `all`) will be flagged. This is useful when you want
@@ -19,7 +23,13 @@ module RuboCop
19
23
  # allowlisting all other cops. `AllowedCops` and `DisallowedCops` should not
20
24
  # both be set at the same time; if `DisallowedCops` is set, it takes precedence.
21
25
  #
22
- # With `AllowTrailingComment` set to `true`, a disable directive carrying
26
+ # `AllowedDirectives` names directive forms - `disable`, `todo`,
27
+ # `disable-next`, `todo-next`, `push`, `next` - and exempts them from the
28
+ # cop entirely. `rubocop:todo` is the usual candidate, since
29
+ # `--disable-uncorrectable` generates those and they record debt rather
30
+ # than a decision someone should be asked to justify.
31
+ #
32
+ # With `AllowWithReason` set to `true`, a disable directive carrying
23
33
  # a `--` trailing justification comment is allowed, so a team can require
24
34
  # every disable to be documented instead of banning them outright. Enable
25
35
  # directives are not checked in this mode, since they end a suppression
@@ -58,7 +68,21 @@ module RuboCop
58
68
  # foo
59
69
  # # rubocop:enable Metrics/AbcSize
60
70
  #
61
- # @example AllowTrailingComment: true
71
+ # @example AllowedDirectives: [todo]
72
+ # # good - the cop does not look at `todo` directives at all
73
+ # x = 0 # rubocop:todo Layout/SpaceAroundOperators
74
+ #
75
+ # # bad
76
+ # x = 0 # rubocop:disable Layout/SpaceAroundOperators
77
+ #
78
+ # @example AllowedCops: [Metrics]
79
+ # # good - every cop the directive disables is in an exempt department
80
+ # # rubocop:disable Metrics/AbcSize
81
+ # def foo
82
+ # end
83
+ # # rubocop:enable Metrics/AbcSize
84
+ #
85
+ # @example AllowWithReason: true
62
86
  # # bad
63
87
  # x = 0 # rubocop:disable Layout/SpaceAroundOperators
64
88
  #
@@ -77,7 +101,7 @@ module RuboCop
77
101
  def on_new_investigation
78
102
  processed_source.comments.each do |comment|
79
103
  directive = DirectiveComment.new(comment)
80
- next if allow_trailing_comment? && (directive.reason || directive.enabled?)
104
+ next if ignored?(directive)
81
105
 
82
106
  directive_cops = directive_cops(directive)
83
107
  disallowed_cops = compute_disallowed_cops(directive_cops)
@@ -95,10 +119,10 @@ module RuboCop
95
119
  if directive_cops.include?('all')
96
120
  directive_cops
97
121
  else
98
- directive_cops.uniq.select { |cop| disallowed_cops_config.include?(cop) }
122
+ directive_cops.uniq.select { |cop| listed?(disallowed_cops_config, cop) }
99
123
  end
100
124
  else
101
- directive_cops.reject { |cop| allowed_cops.include?(cop) }
125
+ directive_cops.reject { |cop| cop != 'all' && listed?(allowed_cops, cop) }
102
126
  end
103
127
  end
104
128
 
@@ -116,7 +140,7 @@ module RuboCop
116
140
  end
117
141
 
118
142
  def offense_message(disallowed_cops)
119
- if allow_trailing_comment?
143
+ if allow_with_reason?
120
144
  MSG_MISSING_REASON
121
145
  elsif any_cops_allowed? || disallowed_cops_config.any?
122
146
  format(MSG_FOR_COPS, cops: "`#{disallowed_cops.join('`, `')}`")
@@ -130,14 +154,34 @@ module RuboCop
130
154
  match_captures && match_captures[1] ? match_captures[1].split(',').map(&:strip) : []
131
155
  end
132
156
 
133
- def allow_trailing_comment?
134
- cop_config['AllowTrailingComment']
157
+ def ignored?(directive)
158
+ return true if exempt_mode?(directive)
159
+
160
+ allow_with_reason? && (directive.reason || directive.enabled?)
161
+ end
162
+
163
+ def exempt_mode?(directive)
164
+ allowed_directives.include?(directive.mode)
165
+ end
166
+
167
+ def allowed_directives
168
+ @allowed_directives ||= Array(cop_config['AllowedDirectives'])
169
+ end
170
+
171
+ def allow_with_reason?
172
+ cop_config['AllowWithReason']
135
173
  end
136
174
 
137
175
  def allowed_cops
138
176
  @allowed_cops ||= Array(cop_config['AllowedCops']).to_set
139
177
  end
140
178
 
179
+ # A name matches either outright or through its department, so
180
+ # `Metrics` covers `Metrics/AbcSize` but not the other way round.
181
+ def listed?(names, cop)
182
+ names.include?(cop) || names.include?(cop.split('/').first)
183
+ end
184
+
141
185
  def any_cops_allowed?
142
186
  allowed_cops.any?
143
187
  end
@@ -119,6 +119,7 @@ module RuboCop
119
119
 
120
120
  def on_def(node)
121
121
  return if node.method?(:initialize)
122
+ return if allowed_methods.include?(node.method_name)
122
123
 
123
124
  parent = node.parent
124
125
  modifier_node?(parent) ? check(parent) : check(node)
@@ -130,7 +131,6 @@ module RuboCop
130
131
  def check(node)
131
132
  return if non_public?(node) && !require_for_non_public_methods?
132
133
  return if documentation_comment?(node)
133
- return if method_allowed?(node)
134
134
 
135
135
  add_offense(node)
136
136
  end
@@ -139,14 +139,6 @@ module RuboCop
139
139
  cop_config['RequireForNonPublicMethods']
140
140
  end
141
141
 
142
- def method_allowed?(node)
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)
148
- end
149
-
150
142
  def allowed_methods
151
143
  @allowed_methods ||= cop_config.fetch('AllowedMethods', []).map(&:to_sym)
152
144
  end
@@ -26,7 +26,7 @@ module RuboCop
26
26
 
27
27
  MSG = 'Use `Integer#times` for a simple loop which iterates a fixed number of times.'
28
28
 
29
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
29
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler -- only matches blocks that take no parameters
30
30
  return unless offending?(node)
31
31
 
32
32
  send_node = node.send_node
@@ -28,7 +28,7 @@ module RuboCop
28
28
 
29
29
  MSG = 'Omit pipes for the empty block parameters.'
30
30
 
31
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
31
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler -- the offense is an empty `||`
32
32
  send_node = node.send_node
33
33
  check(node) unless send_node.send_type? && send_node.lambda_literal?
34
34
  end
@@ -23,7 +23,7 @@ module RuboCop
23
23
 
24
24
  MSG = 'Omit parentheses for the empty lambda parameters.'
25
25
 
26
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
26
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler -- the offense is an empty `()`
27
27
  send_node = node.send_node
28
28
  return unless send_node.send_type?
29
29
 
@@ -53,11 +53,11 @@ module RuboCop
53
53
 
54
54
  def on_def(node)
55
55
  return if node.body || processed_source.contains_comment?(node.source_range)
56
- return if correct_style?(node)
56
+ return if correct_style?(node) || compact_style_disallowed?
57
57
 
58
58
  add_offense(node) do |corrector|
59
59
  correction = corrected(node)
60
- next if compact_style? && max_line_length && correction.size > max_line_length
60
+ next if correction_exceeds_line_length?(correction)
61
61
 
62
62
  corrector.replace(node, correction)
63
63
  end
@@ -74,6 +74,12 @@ module RuboCop
74
74
  (compact_style? && compact?(node)) || (expanded_style? && expanded?(node))
75
75
  end
76
76
 
77
+ def compact_style_disallowed?
78
+ return false unless compact_style?
79
+
80
+ config.for_enabled_cop('Style/SingleLineMethods')['AllowIfMethodIsEmpty'] == false
81
+ end
82
+
77
83
  def corrected(node)
78
84
  scope = node.receiver ? "#{node.receiver.source}." : ''
79
85
  arguments = if node.arguments?
@@ -86,6 +92,10 @@ module RuboCop
86
92
  ["def #{signature}", 'end'].join(joint(node))
87
93
  end
88
94
 
95
+ def correction_exceeds_line_length?(correction)
96
+ compact_style? && max_line_length && correction.size > max_line_length
97
+ end
98
+
89
99
  def joint(node)
90
100
  indent = ' ' * node.loc.column
91
101
 
@@ -166,6 +166,8 @@ module RuboCop
166
166
  return if node.single_line? || style == :allow_always
167
167
 
168
168
  add_offense(node, message: MSG_MULTI_LINE) do |corrector|
169
+ next if endless_parent?(node)
170
+
169
171
  correct_to_multiline(corrector, node)
170
172
  end
171
173
  end
@@ -175,11 +177,11 @@ module RuboCop
175
177
  add_offense(node, message: MSG_MULTI_LINE) do |corrector|
176
178
  correct_to_multiline(corrector, node)
177
179
  end
178
- elsif !node.endless? && can_be_made_endless?(node) && node.body.single_line?
180
+ elsif !node.endless? && can_be_made_endless?(node) && single_line_when_made_endless?(node)
179
181
  return if too_long_when_made_endless?(node)
180
182
 
181
183
  add_offense(node, message: MSG_REQUIRE_SINGLE) do |corrector|
182
- corrector.replace(node, endless_replacement(node))
184
+ correct_to_endless(corrector, node)
183
185
  end
184
186
  end
185
187
  end
@@ -189,14 +191,22 @@ module RuboCop
189
191
  return if too_long_when_made_endless?(node)
190
192
 
191
193
  add_offense(node, message: MSG_REQUIRE_ALWAYS) do |corrector|
192
- corrector.replace(node, endless_replacement(node))
194
+ correct_to_endless(corrector, node)
193
195
  end
194
196
  end
195
197
 
196
198
  def handle_disallow_style(node)
197
199
  return unless node.endless?
198
200
 
199
- add_offense(node) { |corrector| correct_to_multiline(corrector, node) }
201
+ add_offense(node) do |corrector|
202
+ next if endless_parent?(node)
203
+
204
+ correct_to_multiline(corrector, node)
205
+ end
206
+ end
207
+
208
+ def endless_parent?(node)
209
+ node.parent&.any_def_type? && node.parent.endless?
200
210
  end
201
211
 
202
212
  def use_heredoc?(node)
@@ -206,6 +216,17 @@ module RuboCop
206
216
  body.each_descendant(:str).any?(&:heredoc?)
207
217
  end
208
218
 
219
+ def correct_to_endless(corrector, node)
220
+ corrector.replace(signature_to_body_range(node), ' = ')
221
+ corrector.remove(node.body.source_range.end.join(node.loc.end.end))
222
+ end
223
+
224
+ def signature_to_body_range(node)
225
+ signature_end = node.arguments.any? ? node.arguments.source_range.end : node.loc.name.end
226
+
227
+ signature_end.join(node.body.source_range.begin)
228
+ end
229
+
209
230
  def endless_replacement(node)
210
231
  <<~RUBY.strip
211
232
  def #{receiver(node)}#{node.method_name}#{arguments(node)} = #{node.body.source}
@@ -221,19 +242,17 @@ module RuboCop
221
242
  end
222
243
 
223
244
  def can_be_made_endless?(node)
224
- node.body && !node.body.begin_type? && !node.body.kwbegin_type?
245
+ node.body && !node.body.type?(:begin, :kwbegin, :rescue, :ensure)
246
+ end
247
+
248
+ def single_line_when_made_endless?(node)
249
+ !endless_replacement(node).include?("\n")
225
250
  end
226
251
 
227
252
  def too_long_when_made_endless?(node)
228
253
  return false unless config.cop_enabled?('Layout/LineLength')
229
254
 
230
- offset = modifier_offset(node)
231
-
232
- endless_replacement(node).length + offset > max_line_length
233
- end
234
-
235
- def modifier_offset(node)
236
- same_line?(node.parent, node) ? node.loc.column - node.parent.loc.column : 0
255
+ endless_replacement(node).length + node.loc.column > max_line_length
237
256
  end
238
257
  end
239
258
  end
@@ -54,7 +54,7 @@ module RuboCop
54
54
  @contain_dev_null_string_in_file = ast.each_descendant(:str).any? do |str|
55
55
  content = str.str_content
56
56
 
57
- valid_string?(content) && content.downcase == '/dev/null' # rubocop:disable Style/FileNull
57
+ valid_string?(content) && content.downcase == '/dev/null' # rubocop:disable Style/FileNull -- the cop needs the literal it is matching
58
58
  end
59
59
  end
60
60
 
@@ -62,7 +62,7 @@ module RuboCop
62
62
  value = node.value
63
63
  return unless valid_string?(value)
64
64
  return if acceptable?(node)
65
- return if value.downcase == 'nul' && !@contain_dev_null_string_in_file # rubocop:disable Style/FileNull
65
+ return if value.downcase == 'nul' && !@contain_dev_null_string_in_file # rubocop:disable Style/FileNull -- the cop needs the literal it is matching
66
66
  return unless REGEXP.match?(value)
67
67
 
68
68
  add_offense(node, message: format(MSG, source: value)) do |corrector|