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
@@ -11,7 +11,7 @@ module RuboCop
11
11
  COMMON_PARAMS = %w[Exclude Include Severity inherit_mode AutoCorrect StyleGuide Details
12
12
  Enabled Reference References Safe SafeAutoCorrect].freeze
13
13
  # @api private
14
- INTERNAL_PARAMS = %w[Description StyleGuide
14
+ INTERNAL_PARAMS = %w[Description StyleGuide Preview
15
15
  VersionAdded VersionChanged VersionRemoved
16
16
  Reference References Safe SafeAutoCorrect].freeze
17
17
  # @api private
@@ -21,7 +21,7 @@ module RuboCop
21
21
 
22
22
  # @api private
23
23
  CONFIG_CHECK_KEYS = %w[Enabled Safe SafeAutoCorrect AutoCorrect References].to_set.freeze
24
- CONFIG_CHECK_DEPARTMENTS = %w[pending override_department].freeze
24
+ CONFIG_CHECK_DEPARTMENTS = %w[pending preview override_department].freeze
25
25
  CONFIG_CHECK_AUTOCORRECTS = %w[always contextual disabled].freeze
26
26
  private_constant :CONFIG_CHECK_KEYS, :CONFIG_CHECK_DEPARTMENTS
27
27
 
@@ -48,7 +48,7 @@ module RuboCop
48
48
 
49
49
  check_obsoletions
50
50
  alert_about_unrecognized_cops(invalid_cop_names)
51
- validate_new_cops_parameter
51
+ validate_all_cops_parameters
52
52
  validate_parameter_names(valid_cop_names)
53
53
  validate_enforced_styles(valid_cop_names)
54
54
  validate_syntax_cop
@@ -170,6 +170,21 @@ module RuboCop
170
170
  validate_department_new_cops_parameters
171
171
  end
172
172
 
173
+ def validate_all_cops_parameters
174
+ validate_new_cops_parameter
175
+ validate_fail_level_parameter
176
+ end
177
+
178
+ def validate_fail_level_parameter
179
+ fail_level = @config.for_all_cops['FailLevel']
180
+ return if fail_level.nil? || Cop::Severity::NAMES.include?(fail_level.to_sym)
181
+
182
+ message = "invalid #{fail_level} for `FailLevel` found in #{smart_loaded_path}\n" \
183
+ "Valid choices are: #{Cop::Severity::NAMES.join(', ')}"
184
+
185
+ raise ValidationError, message
186
+ end
187
+
173
188
  def validate_all_cops_new_cops_parameter
174
189
  new_cop_parameter = @config.for_all_cops['NewCops']
175
190
  return if new_cop_parameter.nil? || NEW_COPS_VALUES.include?(new_cop_parameter)
@@ -45,7 +45,7 @@ module RuboCop
45
45
 
46
46
  def hash
47
47
  # Do hashing manually to reduce Array allocations.
48
- department.hash ^ cop_name.hash # rubocop:disable Security/CompoundHash
48
+ department.hash ^ cop_name.hash # rubocop:disable Security/CompoundHash -- hashing manually avoids the Array allocation
49
49
  end
50
50
 
51
51
  def match?(other)
@@ -49,7 +49,12 @@ module RuboCop
49
49
  InvestigationReport = Struct.new(:cop, :processed_source, :offenses, :corrector)
50
50
 
51
51
  # List of methods names to restrict calls for `on_send` / `on_csend`
52
- RESTRICT_ON_SEND = Set[].freeze # rubocop:disable InternalAffairs/UselessRestrictOnSend
52
+ RESTRICT_ON_SEND = Set[].freeze # rubocop:disable InternalAffairs/UselessRestrictOnSend -- the base class default, which cops override
53
+
54
+ # Departments whose cops report above `convention` unless a cop says
55
+ # otherwise. The exceptions live in `config/default.yml` as an explicit
56
+ # `Severity`, such as the lint-like cops in `Bundler` and `Gemspec`.
57
+ DEPARTMENT_SEVERITIES = { Lint: :warning, Security: :warning, Metrics: :refactor }.freeze
53
58
 
54
59
  # List of cops that should not try to autocorrect at the same
55
60
  # time as this cop
@@ -288,6 +293,13 @@ module RuboCop
288
293
  @config.string_literals_frozen_by_default?
289
294
  end
290
295
 
296
+ # Whether the user opted in to unstable behavior, with `--preview` or
297
+ # `AllCops: Preview`. Cops branch on this to ship a change that is not
298
+ # ready to be the default yet.
299
+ def preview?
300
+ @config.preview?(@options)
301
+ end
302
+
291
303
  def relevant_file?(file)
292
304
  return false unless target_satisfies_all_gem_version_requirements?
293
305
  return true unless @config.clusivity_config_for_badge?(self.class.badge)
@@ -326,7 +338,7 @@ module RuboCop
326
338
 
327
339
  ### Reserved for Commissioner
328
340
 
329
- # rubocop:disable Layout/ClassStructure
341
+ # rubocop:disable Layout/ClassStructure -- grouped under the Commissioner heading above
330
342
  # @api private
331
343
  def callbacks_needed
332
344
  self.class.callbacks_needed
@@ -561,7 +573,7 @@ module RuboCop
561
573
  end
562
574
 
563
575
  def default_severity
564
- self.class.lint? ? :warning : :convention
576
+ DEPARTMENT_SEVERITIES.fetch(self.class.department, :convention)
565
577
  end
566
578
 
567
579
  def custom_severity
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RuboCop
4
- module Cop # rubocop:disable Style/Documentation
4
+ module Cop # rubocop:disable Style/Documentation -- a namespace that only autoloads the correctors below
5
5
  # Autoloads corrector classes used by cops. Classes are autoloaded to reduce the number of
6
6
  # required classes because they're referenced only when autocorrection is performed.
7
7
 
8
- # rubocop:disable Layout/LineLength
8
+ # rubocop:disable Layout/LineLength -- the autoload paths do not wrap
9
9
  autoload :AlignmentCorrector, 'rubocop/cop/correctors/alignment_corrector'
10
10
  autoload :ConditionCorrector, 'rubocop/cop/correctors/condition_corrector'
11
11
  autoload :EachToForCorrector, 'rubocop/cop/correctors/each_to_for_corrector'
@@ -92,7 +92,7 @@ module RuboCop
92
92
  (str "true")
93
93
  PATTERN
94
94
 
95
- def on_block(node) # rubocop:disable Metrics/MethodLength, InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
95
+ def on_block(node) # rubocop:disable Metrics/MethodLength, InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler -- the gemspec block is matched by its named block variable
96
96
  gem_specification(node) do |block_var|
97
97
  metadata_value = metadata(node)
98
98
  mfa_value = mfa_value(metadata_value)
@@ -147,7 +147,7 @@ module RuboCop
147
147
  badge: badge,
148
148
  version_added: version_added)
149
149
 
150
- injector.inject do # rubocop:disable Lint/UnexpectedBlockArity
150
+ injector.inject do # rubocop:disable Lint/UnexpectedBlockArity -- this `inject` is the injector API, not `Enumerable#inject`
151
151
  output.puts(format(CONFIGURATION_ADDED_MESSAGE,
152
152
  configuration_file_path: config_file_path))
153
153
  end
@@ -75,7 +75,7 @@ module RuboCop
75
75
  @on_csend_definition = node if node.new_identifier.value == :on_csend
76
76
  end
77
77
 
78
- def on_send(node) # rubocop:disable InternalAffairs/OnSendWithoutOnCSend
78
+ def on_send(node) # rubocop:disable InternalAffairs/OnSendWithoutOnCSend -- this cop is what enforces the rule, and does not apply to itself
79
79
  return unless (new_identifier = node.first_argument)
80
80
  return unless new_identifier.basic_literal?
81
81
 
@@ -81,6 +81,8 @@ module RuboCop
81
81
  last_arg = node.last_argument
82
82
 
83
83
  if last_arg.hash_type? && !last_arg.braces?
84
+ return items if enforce_hash_argument_with_separator?
85
+
84
86
  items += last_arg.pairs
85
87
  else
86
88
  items << last_arg
@@ -64,10 +64,10 @@ module RuboCop
64
64
  autocorrect_one(corrector, comment)
65
65
  end
66
66
 
67
- # Corrects all comment lines that occur immediately before the given
68
- # comment and have the same indentation. This is to avoid a long chain
69
- # of correcting, saving the file, parsing and inspecting again, and
70
- # then correcting one more line, and so on.
67
+ # Corrects all preceding comment lines that have the same indentation
68
+ # and are separated from the given comment by nothing but blank lines.
69
+ # This is to avoid a long chain of correcting, saving the file, parsing
70
+ # and inspecting again, and then correcting one more line, and so on.
71
71
  def autocorrect_preceding_comments(corrector, comment)
72
72
  comments = processed_source.comments
73
73
  index = comments.index(comment)
@@ -82,7 +82,10 @@ module RuboCop
82
82
  def should_correct?(preceding_comment, reference_comment)
83
83
  loc = preceding_comment.loc
84
84
  ref_loc = reference_comment.loc
85
- loc.line == ref_loc.line - 1 && loc.column == ref_loc.column
85
+ return false unless loc.column == ref_loc.column
86
+ return false unless own_line_comment?(preceding_comment)
87
+
88
+ ((loc.line + 1)...ref_loc.line).all? { |line| processed_source[line - 1].blank? }
86
89
  end
87
90
 
88
91
  def autocorrect_one(corrector, comment)
@@ -93,6 +93,7 @@ module RuboCop
93
93
  when :def, :defs then base_for_method_definition(parent)
94
94
  when :kwbegin then parent.loc.begin
95
95
  when :block, :numblock, :itblock then start_line_range(parent)
96
+ when :class, :module, :sclass then parent.loc.keyword
96
97
  else node.loc.keyword
97
98
  end
98
99
  end
@@ -91,7 +91,7 @@ module RuboCop
91
91
  end
92
92
 
93
93
  def next_line_node(node)
94
- return if node.parent.if_type?
94
+ return if node.parent.type?(:if, :rescue, :ensure)
95
95
 
96
96
  node.right_sibling
97
97
  end
@@ -89,15 +89,16 @@ module RuboCop
89
89
  'in an array, relative to %<base_description>s.'
90
90
 
91
91
  def on_array(node)
92
- return if style != :consistent && enforce_first_argument_with_fixed_indentation?
92
+ return unless node.loc.begin
93
+ return if autocorrect_incompatible_with_other_cops?(node, nil)
93
94
 
94
- check(node, nil) if node.loc.begin
95
+ check(node, nil)
95
96
  end
96
97
 
97
98
  def on_send(node)
98
- return if style != :consistent && enforce_first_argument_with_fixed_indentation?
99
-
100
99
  each_argument_node(node, :array) do |array_node, left_parenthesis|
100
+ next if autocorrect_incompatible_with_other_cops?(array_node, left_parenthesis)
101
+
101
102
  check(array_node, left_parenthesis)
102
103
  end
103
104
  end
@@ -105,6 +106,18 @@ module RuboCop
105
106
 
106
107
  private
107
108
 
109
+ def autocorrect_incompatible_with_other_cops?(array_node, left_parenthesis)
110
+ return false unless enforce_first_argument_with_fixed_indentation?
111
+ return true if style != :consistent
112
+
113
+ # `Layout/ArrayAlignment` does not align single-element arrays.
114
+ return false if array_node.children.size < 2
115
+
116
+ _base_column, indent_base_type =
117
+ indent_base(array_node.loc.begin, array_node.values.first, left_parenthesis)
118
+ indent_base_type != :start_of_line
119
+ end
120
+
108
121
  def autocorrect(corrector, node)
109
122
  AlignmentCorrector.correct(corrector, processed_source, node, @column_delta)
110
123
  end
@@ -53,6 +53,7 @@ module RuboCop
53
53
  def on_def(node)
54
54
  return if node.arguments.empty?
55
55
  return if node.arguments.loc.begin.nil?
56
+ return if autocorrect_incompatible_with_other_cops?(node)
56
57
 
57
58
  check(node)
58
59
  end
@@ -60,6 +61,11 @@ module RuboCop
60
61
 
61
62
  private
62
63
 
64
+ def autocorrect_incompatible_with_other_cops?(node)
65
+ node.arguments.size >= 2 &&
66
+ style == :align_parentheses && enforce_parameter_with_fixed_indentation?
67
+ end
68
+
63
69
  def autocorrect(corrector, node)
64
70
  AlignmentCorrector.correct(corrector, processed_source, node, @column_delta)
65
71
  end
@@ -95,6 +101,11 @@ module RuboCop
95
101
  base_description: base_description
96
102
  )
97
103
  end
104
+
105
+ def enforce_parameter_with_fixed_indentation?
106
+ parameter_alignment_config = config.for_enabled_cop('Layout/ParameterAlignment')
107
+ parameter_alignment_config['EnforcedStyle'] == 'with_fixed_indentation'
108
+ end
98
109
  end
99
110
  end
100
111
  end
@@ -330,7 +330,11 @@ module RuboCop
330
330
  end
331
331
 
332
332
  def correct_no_value(corrector, key_delta, key)
333
- adjust(corrector, key_delta, key)
333
+ adjust(corrector, clamped_key_delta(key_delta, key), key)
334
+ end
335
+
336
+ def clamped_key_delta(key_delta, key)
337
+ [key_delta, -key.column].max
334
338
  end
335
339
 
336
340
  def correct_key_value(corrector, delta, key, value, separator)
@@ -341,10 +345,7 @@ module RuboCop
341
345
  value_delta = delta[:value] || 0
342
346
  key_delta = delta[:key] || 0
343
347
 
344
- key_column = key.column
345
- key_delta = -key_column if key_delta < -key_column
346
-
347
- adjust(corrector, key_delta, key)
348
+ adjust(corrector, clamped_key_delta(key_delta, key), key)
348
349
  adjust(corrector, separator_delta, separator)
349
350
  adjust(corrector, value_delta, value)
350
351
  end
@@ -400,7 +400,7 @@ module RuboCop
400
400
  if body_node.rescue_type?
401
401
  check_rescue?(body_node)
402
402
  elsif body_node.ensure_type?
403
- block_body, = *body_node # rubocop:disable InternalAffairs/NodeDestructuring
403
+ block_body, = *body_node # rubocop:disable InternalAffairs/NodeDestructuring -- `EnsureNode` has no accessor for the protected body
404
404
  if block_body&.rescue_type?
405
405
  check_rescue?(block_body)
406
406
  else
@@ -440,7 +440,7 @@ module RuboCop
440
440
  begin_pos - indentation
441
441
  end
442
442
 
443
- pos = indentation >= 0 ? ind..begin_pos : begin_pos..ind
443
+ pos = ind <= begin_pos ? ind..begin_pos : begin_pos..ind
444
444
  range_between(pos.begin, pos.end)
445
445
  end
446
446
 
@@ -104,6 +104,11 @@ module RuboCop
104
104
  alias on_defs on_def
105
105
 
106
106
  def on_new_investigation
107
+ @breakable_range_by_line_index = {}
108
+ @breakable_string_delimiters = {}
109
+ @endless_methods_by_line = {}
110
+ @heredocs = nil
111
+
107
112
  return unless processed_source.raw_source.include?(';')
108
113
 
109
114
  check_for_breakable_semicolons(processed_source)
@@ -288,7 +293,7 @@ module RuboCop
288
293
  source_range = node.source_range
289
294
  relevant_substr = largest_possible_string(node)
290
295
 
291
- if (space_pos = relevant_substr.rindex(/\s/))
296
+ if (space_pos = breakable_space_position(node, relevant_substr))
292
297
  source_range.resize(space_pos + 1)
293
298
  elsif (escape_pos = relevant_substr.rindex(/\\(u[\da-f]{0,4}|x[\da-f]{0,2})?\z/))
294
299
  source_range.resize(escape_pos)
@@ -300,6 +305,19 @@ module RuboCop
300
305
  end
301
306
  end
302
307
 
308
+ def breakable_space_position(node, substr)
309
+ limit = string_content_length(node) - 1
310
+ space_pos = substr.rindex(/\s/)
311
+ space_pos = substr[0, limit].rindex(/\s/) if space_pos && space_pos + 1 > limit
312
+ space_pos
313
+ end
314
+
315
+ def string_content_length(node)
316
+ content_end = node.loc?(:end) ? node.loc.end.begin_pos : node.source_range.end_pos
317
+
318
+ content_end - node.source_range.begin_pos
319
+ end
320
+
303
321
  def breakable_dstr_begin_position(node)
304
322
  source_range = node.source_range
305
323
  source_range.begin_pos if source_range.column < max && source_range.last_column >= max
@@ -5,7 +5,8 @@ module RuboCop
5
5
  module Layout
6
6
  # Checks the spacing inside and after block parameters pipes. Line breaks
7
7
  # inside parameter pipes are checked by `Layout/MultilineBlockLayout` and
8
- # not by this cop.
8
+ # not by this cop. Spaces inside a lambda's parameter parentheses are left
9
+ # to `Layout/SpaceInsideParens` when that cop enforces a conflicting style.
9
10
  #
10
11
  # @example EnforcedStyleInsidePipes: no_space (default)
11
12
  # # bad
@@ -29,12 +30,12 @@ module RuboCop
29
30
  include RangeHelp
30
31
  extend AutoCorrector
31
32
 
32
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
33
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler -- checks spacing inside a block parameter list
33
34
  arguments = node.arguments
34
35
 
35
36
  return unless node.arguments? && pipes?(arguments)
36
37
 
37
- check_inside_pipes(arguments)
38
+ check_inside_pipes(arguments) unless conflicting_space_inside_parens_style?(arguments)
38
39
  check_after_closing_pipe(arguments) if node.body
39
40
  check_each_arg(arguments)
40
41
  end
@@ -53,6 +54,16 @@ module RuboCop
53
54
  'EnforcedStyleInsidePipes'
54
55
  end
55
56
 
57
+ def conflicting_space_inside_parens_style?(arguments)
58
+ return false unless arguments.loc.begin.source == '('
59
+
60
+ case config.for_enabled_cop('Layout/SpaceInsideParens')['EnforcedStyle']
61
+ when 'no_space' then style == :space
62
+ when 'space', 'compact' then style == :no_space
63
+ else false
64
+ end
65
+ end
66
+
56
67
  def check_inside_pipes(arguments)
57
68
  case style
58
69
  when :no_space
@@ -258,7 +258,7 @@ module RuboCop
258
258
  return false unless allow_for_alignment?
259
259
  return false unless with_space.source.start_with?(EXCESSIVE_SPACE)
260
260
 
261
- return !aligned_with_operator?(operator) unless type == :assignment
261
+ return !aligned_with_operator?(operator) unless grouped_alignment_type?(type)
262
262
 
263
263
  token = Token.new(operator, nil, operator.source)
264
264
  align_preceding = aligned_with_preceding_equals_operator(token)
@@ -269,6 +269,10 @@ module RuboCop
269
269
  aligned_with_subsequent_equals_operator(token) != :yes
270
270
  end
271
271
 
272
+ def grouped_alignment_type?(type)
273
+ type == :assignment || (type == :special_asgn && force_equal_sign_alignment?)
274
+ end
275
+
272
276
  def excess_trailing_space?(right_operand, with_space)
273
277
  with_space.source.end_with?(EXCESSIVE_SPACE) &&
274
278
  (!allow_for_alignment? || !aligned_with_something?(right_operand))
@@ -21,7 +21,9 @@ module RuboCop
21
21
  #
22
22
  # Constructor calls (`Foo.new`) and keyword-argument validation are out of
23
23
  # scope: only positional arity of explicitly defined singleton methods is
24
- # checked.
24
+ # checked. `new` is skipped outright, because `Class#new` is not in the
25
+ # index: any `new` the index does find sits below it in the method
26
+ # resolution order, where Ruby would never reach it.
25
27
  #
26
28
  # @example
27
29
  # # Given the project defines:
@@ -45,8 +47,7 @@ module RuboCop
45
47
  MSG = 'Wrong number of arguments to `%<method>s` (given %<given>d, expected %<expected>s).'
46
48
 
47
49
  def on_send(node)
48
- return unless project_index
49
- return unless node.receiver&.const_type?
50
+ return unless checkable_call?(node)
50
51
 
51
52
  shape = resolved_signature_shape(node)
52
53
  return unless shape
@@ -61,6 +62,10 @@ module RuboCop
61
62
 
62
63
  private
63
64
 
65
+ def checkable_call?(node)
66
+ project_index && node.receiver&.const_type? && !node.method?(:new)
67
+ end
68
+
64
69
  def message(node, given, min, max)
65
70
  format(MSG, method: node.method_name, given: given, expected: expected_range(min, max))
66
71
  end
@@ -77,9 +82,14 @@ module RuboCop
77
82
  nil
78
83
  end
79
84
 
85
+ # A call with an explicit receiver cannot reach a private method, so its
86
+ # signature says nothing about the call. This matters because `Object`
87
+ # closes the singleton ancestry of every constant, and a bare `def` at
88
+ # the top level of any file — what every block-based DSL produces — is a
89
+ # private instance method of `Object`.
80
90
  def singleton_signature_shape(declaration, method_name)
81
91
  member = indexed_singleton_member(declaration, "#{method_name}()")
82
- return nil unless member
92
+ return nil unless member&.public?
83
93
 
84
94
  indexed_member_shape(member)
85
95
  end
@@ -106,6 +106,8 @@ module RuboCop
106
106
  def traverse_node(node, &block)
107
107
  # if the node is a block, any assignments are irrelevant
108
108
  return if node.any_block_type?
109
+ # an assignment inside `defined?` is never executed
110
+ return if node.defined_type?
109
111
 
110
112
  result = yield node if ASGN_TYPES.include?(node.type)
111
113
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Lint/RedundantCopDisableDirective
4
- # rubocop:disable Style/DoubleCopDisableDirective
5
3
  module RuboCop
6
4
  module Cop
7
5
  module Lint
@@ -23,12 +21,11 @@ module RuboCop
23
21
  # # good
24
22
  # # rubocop:disable all
25
23
  #
26
- # # bad
24
+ # # bad - only the first directive takes effect
27
25
  # # rubocop:disable Layout/LineLength # rubocop:disable Style/Encoding
28
26
  #
29
27
  # # good
30
- # # rubocop:disable Layout/LineLength
31
- # # rubocop:disable Style/Encoding
28
+ # # rubocop:disable Layout/LineLength, Style/Encoding
32
29
  #
33
30
  # # bad
34
31
  # # rubocop:wrongmode Layout/LineLength
@@ -55,13 +52,19 @@ module RuboCop
55
52
  # # rubocop:disable Layout/LineLength
56
53
  #
57
54
  class CopDirectiveSyntax < Base
55
+ extend AutoCorrector
56
+
58
57
  COMMON_MSG = 'Malformed directive comment detected.'
59
58
 
60
59
  MISSING_MODE_NAME_MSG = 'The mode name is missing.'
61
- INVALID_MODE_NAME_MSG = 'The mode name must be one of `enable`, `disable`, `disable-next`, `todo`, `todo-next`, `push`, or `pop`.' # rubocop:disable Layout/LineLength
60
+ INVALID_MODE_NAME_MSG = 'The mode name must be one of `enable`, `disable`, `disable-next`, `enable-next`, `todo`, `todo-next`, `next`, `push`, or `pop`.' # rubocop:disable Layout/LineLength -- the message lists every mode and does not wrap
62
61
  MISSING_COP_NAME_MSG = 'The cop name is missing.'
62
+ MULTIPLE_DIRECTIVES_MSG = 'Only the first directive on a line takes effect. ' \
63
+ 'List the cop names in a single directive instead.'
63
64
  MALFORMED_COP_NAMES_MSG = 'Cop names must be separated by commas. ' \
64
65
  'Comment in the directive must start with `--`.'
66
+ INVALID_SIGNED_ARGS_MSG = '`push` and `next` arguments must be `+`- or `-`-prefixed ' \
67
+ 'cop names, and `pop` takes no arguments.'
65
68
  NEXT_DIRECTIVE_AT_EOL_MSG = 'A `-next` directive must be on its own line, above the ' \
66
69
  'statement it applies to.'
67
70
  INVALID_KEYWORD_MSG = 'The directive keyword must be `rubocop`, not `%<keyword>s`.'
@@ -89,7 +92,11 @@ module RuboCop
89
92
  private
90
93
 
91
94
  def check_directive(comment, directive_comment)
92
- if directive_comment.malformed?
95
+ if (directives = multiple_directives(comment, directive_comment))
96
+ add_offense(comment, message: "#{COMMON_MSG} #{MULTIPLE_DIRECTIVES_MSG}") do |corrector|
97
+ merge_directives(corrector, comment, directives)
98
+ end
99
+ elsif directive_comment.malformed?
93
100
  add_offense(comment, message: offense_message(directive_comment))
94
101
  elsif misplaced_next_directive?(directive_comment)
95
102
  add_offense(comment, message: NEXT_DIRECTIVE_AT_EOL_MSG)
@@ -98,13 +105,72 @@ module RuboCop
98
105
  end
99
106
  end
100
107
 
101
- # An EOL `disable-next` is not honored - it must fail loudly instead
102
- # of silently doing nothing.
108
+ # Everything after the first directive in a comment is trailing text as
109
+ # far as the parser is concerned, so a second directive written on the
110
+ # same line disables nothing at all. A valid `--` reason may mention a
111
+ # directive as plain text, which should not be treated as another one.
112
+ def multiple_directives(comment, directive_comment)
113
+ matches = comment.text.to_enum(:scan, DirectiveComment::DIRECTIVE_COMMENT_REGEXP)
114
+ .map { Regexp.last_match }
115
+ matches = filter_reason_matches(matches, directive_comment)
116
+
117
+ matches if matches.size > 1
118
+ end
119
+
120
+ def filter_reason_matches(matches, directive_comment)
121
+ return matches unless directive_comment.reason
122
+
123
+ [matches.first, *matches.drop(1).reject { |match| match.post_match.match?(/\S/) }]
124
+ end
125
+
126
+ def merge_directives(corrector, comment, directives)
127
+ return unless directives_adjacent?(comment, directives)
128
+ return unless (mode = merged_mode(directives))
129
+ return unless (cops = merged_cops(directives))
130
+
131
+ corrector.replace(comment, "# rubocop:#{mode} #{cops.join(', ')}")
132
+ end
133
+
134
+ # `all` covers every cop already, so folding it into a list of names
135
+ # would be a rewrite rather than a merge.
136
+ def merged_cops(directives)
137
+ cops = directives.flat_map { |match| match[2].to_s.split(',').map(&:strip) }
138
+ cops = cops.reject(&:empty?).uniq
139
+ cops unless cops.empty? || cops.include?('all')
140
+ end
141
+
142
+ # Anything written between the directives is a `--` reason or prose,
143
+ # and merging around it would either move it or drop it.
144
+ def directives_adjacent?(comment, directives)
145
+ text = comment.text
146
+ bounds = directives.map { |match| match.end(0) }
147
+ .zip(directives.map { |match| match.begin(0) }[1..] + [text.length])
148
+ bounds.all? { |from, to| text[from...to].strip.empty? }
149
+ end
150
+
151
+ # `todo` is an alias of `disable`, so those two combine. Anything else
152
+ # (an `enable` beside a `disable`, or directives of different scopes)
153
+ # means different things per directive and can't be folded into one.
154
+ def merged_mode(directives)
155
+ modes = directives.map { |match| match[1] }.uniq
156
+ return modes.first if modes.size == 1
157
+ return modes.first if (modes - %w[disable todo]).empty?
158
+
159
+ nil
160
+ end
161
+
162
+ # An EOL next-statement directive is not honored - it must fail
163
+ # loudly instead of silently doing nothing.
103
164
  def misplaced_next_directive?(directive_comment)
104
- directive_comment.disable_next? &&
165
+ next_statement_directive?(directive_comment) &&
105
166
  !processed_source.comment_config.comment_only_line?(directive_comment.line_number)
106
167
  end
107
168
 
169
+ def next_statement_directive?(directive_comment)
170
+ directive_comment.disable_next? || directive_comment.enable_next? ||
171
+ directive_comment.next?
172
+ end
173
+
108
174
  # rubocop:disable-next Metrics/MethodLength
109
175
  def offense_message(directive_comment)
110
176
  comment = directive_comment.comment
@@ -116,6 +182,8 @@ module RuboCop
116
182
  INVALID_MODE_NAME_MSG
117
183
  elsif directive_comment.missing_cop_name?
118
184
  MISSING_COP_NAME_MSG
185
+ elsif directive_comment.invalid_signed_args?
186
+ INVALID_SIGNED_ARGS_MSG
119
187
  else
120
188
  MALFORMED_COP_NAMES_MSG
121
189
  end
@@ -142,10 +210,10 @@ module RuboCop
142
210
  end
143
211
 
144
212
  def unknown_cop_name(directive_comment)
145
- return if directive_comment.push? || directive_comment.pop? || directive_comment.all_cops?
213
+ return if directive_comment.pop? || directive_comment.all_cops?
146
214
 
147
215
  registry = directive_comment.cop_registry
148
- directive_comment.raw_cop_names.find do |name|
216
+ directive_names(directive_comment).find do |name|
149
217
  next false if registry.department?(name)
150
218
 
151
219
  qualified = registry.qualified_cop_name(name, nil, warn: false)
@@ -153,6 +221,14 @@ module RuboCop
153
221
  end
154
222
  end
155
223
 
224
+ def directive_names(directive_comment)
225
+ if directive_comment.push? || directive_comment.next?
226
+ directive_comment.signed_args.values.flatten
227
+ else
228
+ directive_comment.raw_cop_names
229
+ end
230
+ end
231
+
156
232
  def unknown_cop_message(directive_comment, name)
157
233
  similar = NameSimilarity.find_similar_name(name, directive_comment.cop_registry.names)
158
234
  suggestion = similar ? " (did you mean `#{similar}`?)" : ''
@@ -162,5 +238,3 @@ module RuboCop
162
238
  end
163
239
  end
164
240
  end
165
- # rubocop:enable Lint/RedundantCopDisableDirective
166
- # rubocop:enable Style/DoubleCopDisableDirective