rubocop 1.86.0 → 1.86.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +14 -3
  3. data/lib/rubocop/cli/command/auto_generate_config.rb +27 -1
  4. data/lib/rubocop/cli/command/list_enabled_cops_for.rb +40 -0
  5. data/lib/rubocop/cli/command/show_docs_url.rb +3 -7
  6. data/lib/rubocop/cli/command/suggest_extensions.rb +1 -1
  7. data/lib/rubocop/cli.rb +4 -7
  8. data/lib/rubocop/comment_config.rb +12 -15
  9. data/lib/rubocop/config.rb +13 -9
  10. data/lib/rubocop/config_loader_resolver.rb +2 -1
  11. data/lib/rubocop/cop/autocorrect_logic.rb +2 -1
  12. data/lib/rubocop/cop/correctors/multiline_literal_brace_corrector.rb +1 -5
  13. data/lib/rubocop/cop/correctors.rb +28 -0
  14. data/lib/rubocop/cop/documentation.rb +2 -3
  15. data/lib/rubocop/cop/exclude_limit.rb +31 -5
  16. data/lib/rubocop/cop/gemspec/require_mfa.rb +3 -3
  17. data/lib/rubocop/cop/internal_affairs/location_line_equality_comparison.rb +1 -0
  18. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +1 -1
  19. data/lib/rubocop/cop/layout/end_alignment.rb +4 -2
  20. data/lib/rubocop/cop/layout/line_length.rb +5 -3
  21. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +1 -1
  22. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +26 -1
  23. data/lib/rubocop/cop/lint/duplicate_methods.rb +10 -5
  24. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +3 -13
  25. data/lib/rubocop/cop/lint/require_relative_self_path.rb +2 -0
  26. data/lib/rubocop/cop/lint/syntax.rb +25 -1
  27. data/lib/rubocop/cop/lint/unused_method_argument.rb +10 -0
  28. data/lib/rubocop/cop/lint/useless_assignment.rb +3 -8
  29. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +33 -8
  30. data/lib/rubocop/cop/mixin/configurable_max.rb +6 -5
  31. data/lib/rubocop/cop/mixin.rb +85 -0
  32. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +1 -1
  33. data/lib/rubocop/cop/offense.rb +8 -0
  34. data/lib/rubocop/cop/registry.rb +19 -24
  35. data/lib/rubocop/cop/style/access_modifier_declarations.rb +14 -2
  36. data/lib/rubocop/cop/style/copyright.rb +21 -10
  37. data/lib/rubocop/cop/style/date_time.rb +2 -2
  38. data/lib/rubocop/cop/style/document_dynamic_eval_definition.rb +6 -1
  39. data/lib/rubocop/cop/style/guard_clause.rb +9 -6
  40. data/lib/rubocop/cop/style/hash_lookup_method.rb +12 -7
  41. data/lib/rubocop/cop/style/if_inside_else.rb +16 -7
  42. data/lib/rubocop/cop/style/module_member_existence_check.rb +7 -14
  43. data/lib/rubocop/cop/style/one_class_per_file.rb +24 -4
  44. data/lib/rubocop/cop/style/reduce_to_hash.rb +16 -0
  45. data/lib/rubocop/cop/style/redundant_line_continuation.rb +16 -0
  46. data/lib/rubocop/cop/style/redundant_parentheses.rb +4 -1
  47. data/lib/rubocop/cop/style/redundant_self.rb +2 -2
  48. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +10 -0
  49. data/lib/rubocop/cop/style/regexp_literal.rb +29 -0
  50. data/lib/rubocop/cop/style/sole_nested_conditional.rb +4 -2
  51. data/lib/rubocop/cop/style/symbol_proc.rb +3 -3
  52. data/lib/rubocop/cop/style/while_until_modifier.rb +16 -0
  53. data/lib/rubocop/cop/team.rb +86 -35
  54. data/lib/rubocop/formatter/disabled_config_formatter.rb +4 -1
  55. data/lib/rubocop/lsp/runtime.rb +1 -2
  56. data/lib/rubocop/mcp/server.rb +2 -0
  57. data/lib/rubocop/options.rb +8 -4
  58. data/lib/rubocop/rspec/cop_helper.rb +8 -0
  59. data/lib/rubocop/rspec/shared_contexts.rb +21 -0
  60. data/lib/rubocop/runner.rb +77 -55
  61. data/lib/rubocop/target_finder.rb +13 -6
  62. data/lib/rubocop/version.rb +1 -1
  63. data/lib/rubocop.rb +7 -96
  64. metadata +7 -4
@@ -38,6 +38,8 @@ module RuboCop
38
38
  private
39
39
 
40
40
  def same_file?(file_path, required_feature)
41
+ return false unless File.extname(file_path) == '.rb'
42
+
41
43
  file_path == required_feature || remove_ext(file_path) == required_feature
42
44
  end
43
45
 
@@ -26,7 +26,31 @@ module RuboCop
26
26
  "#{diagnostic.message}\n(Using Ruby #{ruby_version} parser; " \
27
27
  'configure using `TargetRubyVersion` parameter, under `AllCops`)'
28
28
  end
29
- add_offense(diagnostic.location, message: message, severity: diagnostic.level)
29
+ location = diagnostic_location(diagnostic.location)
30
+ add_offense(location, message: message, severity: diagnostic.level)
31
+ end
32
+
33
+ # Expand zero-length diagnostic ranges so that editors and formatters
34
+ # can display them. This typically occurs when the parser reports
35
+ # `unexpected token $end` at EOF.
36
+ def diagnostic_location(location)
37
+ return location if location.size.positive?
38
+
39
+ source_buffer = location.source_buffer
40
+ if location.end_pos < source_buffer.source.size
41
+ location.resize(1)
42
+ elsif location.begin_pos.positive?
43
+ location.adjust(begin_pos: -1)
44
+ else
45
+ location
46
+ end
47
+ end
48
+
49
+ # Override to skip multiline_ranges check which requires AST.
50
+ # Syntax errors mean the AST is nil, so we go directly to
51
+ # the EOL comment insertion path.
52
+ def disable_offense(offense_range)
53
+ disable_offense_with_eol_or_surround_comment(offense_range)
30
54
  end
31
55
 
32
56
  def add_offense_from_error(error)
@@ -95,10 +95,20 @@ module RuboCop
95
95
  return unless variable.method_argument?
96
96
  return if variable.keyword_argument? && cop_config['AllowUnusedKeywordArguments']
97
97
  return if ignored_method?(variable.scope.node.body)
98
+ return if block_argument_with_yield?(variable)
98
99
 
99
100
  super
100
101
  end
101
102
 
103
+ def block_argument_with_yield?(variable)
104
+ return false unless variable.declaration_node.blockarg_type?
105
+
106
+ method_body = variable.scope.node.body
107
+ return false unless method_body
108
+
109
+ method_body.yield_type? || method_body.each_descendant(:yield).any?
110
+ end
111
+
102
112
  def ignored_method?(body)
103
113
  (cop_config['IgnoreEmptyMethods'] && body.nil?) ||
104
114
  (cop_config['IgnoreNotImplementedMethods'] && not_implemented?(body))
@@ -40,8 +40,6 @@ module RuboCop
40
40
  class UselessAssignment < Base
41
41
  extend AutoCorrector
42
42
 
43
- include RangeHelp
44
-
45
43
  MSG = 'Useless assignment to variable - `%<variable>s`.'
46
44
 
47
45
  def self.joining_forces
@@ -189,12 +187,9 @@ module RuboCop
189
187
  # rubocop:enable Metrics/AbcSize
190
188
 
191
189
  def remove_exception_assignment_part(corrector, node)
192
- corrector.remove(
193
- range_between(
194
- (node.parent.children.first&.source_range || node.parent.location.keyword).end_pos,
195
- node.source_range.end_pos
196
- )
197
- )
190
+ range = node.parent.children.first&.source_range || node.parent.location.keyword
191
+
192
+ corrector.remove(range.end.join(node.source_range.end))
198
193
  end
199
194
 
200
195
  def rename_variable_with_underscore(corrector, node)
@@ -60,9 +60,7 @@ module RuboCop
60
60
  return true if _cant_be_nil?(node.expression, receiver)
61
61
  end
62
62
 
63
- # Due to how `if/else` are implemented (`elsif` is a child of `if` or another `elsif`),
64
- # using left_siblings will not work correctly for them.
65
- if !else_branch?(node) || (node.if_type? && !node.elsif?)
63
+ if sequentially_reached?(node)
66
64
  node.left_siblings.reverse_each do |sibling|
67
65
  next unless sibling.is_a?(AST::Node)
68
66
 
@@ -82,15 +80,16 @@ module RuboCop
82
80
  !NIL_METHODS.include?(method_name) && !@additional_nil_methods.include?(method_name)
83
81
  end
84
82
 
85
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
83
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
86
84
  def sole_condition_of_parent_if?(node)
85
+ child = node
87
86
  parent = node.parent
88
87
 
89
88
  while parent
90
89
  if parent.if_type?
91
- condition = parent.condition
92
- if condition == node || (condition.csend_type? && !condition.receiver.equal?(node))
93
- return true
90
+ unless parent.unless?
91
+ condition = parent.condition
92
+ return true if !child.equal?(condition) && non_nil_condition?(condition, node)
94
93
  end
95
94
 
96
95
  parent = find_top_if(parent) if parent.elsif?
@@ -99,12 +98,30 @@ module RuboCop
99
98
  parent = parent.parent
100
99
  end
101
100
 
101
+ child = parent
102
102
  parent = parent&.parent
103
103
  end
104
104
 
105
105
  false
106
106
  end
107
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
107
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
108
+
109
+ def non_nil_condition?(condition, node)
110
+ return true if condition == node
111
+
112
+ condition.csend_type? && csend_root_receiver(condition) == node
113
+ end
114
+
115
+ # Whether control reaches `node` by falling through its left siblings rather than by
116
+ # a non-sequential entry. A `resbody` is entered via an exception, the `ensure` branch
117
+ # runs even after a partway raise, and the `else` arm of an `if/elsif` chain is reached by
118
+ # branching (its `if/case` siblings are walked via parent recursion instead).
119
+ def sequentially_reached?(node)
120
+ return false if node.resbody_type?
121
+ return false if node.parent&.ensure_type? && node.parent.branch.equal?(node)
122
+
123
+ !else_branch?(node) || (node.if_type? && !node.elsif?)
124
+ end
108
125
 
109
126
  def else_branch?(node)
110
127
  node.parent&.if_type? && node.parent.else_branch == node
@@ -115,6 +132,14 @@ module RuboCop
115
132
 
116
133
  node
117
134
  end
135
+
136
+ def csend_root_receiver(node)
137
+ return unless (receiver = node.receiver)
138
+
139
+ receiver = receiver.receiver while receiver.call_type? && receiver.receiver
140
+
141
+ receiver
142
+ end
118
143
  end
119
144
  end
120
145
  end
@@ -13,11 +13,12 @@ module RuboCop
13
13
  `max=` is deprecated. Use `exclude_limit <ParameterName>` instead.
14
14
  WARNING
15
15
 
16
- cfg = config_to_allow_offenses
17
- cfg[:exclude_limit] ||= {}
18
- current_max = cfg[:exclude_limit][max_parameter_name]
19
- value = [current_max, value].max if current_max
20
- cfg[:exclude_limit][max_parameter_name] = value
16
+ cop_dir = RuboCop::ExcludeLimit.cop_dir_for(self.class.badge.to_s)
17
+ return unless cop_dir
18
+
19
+ cop_dir.mkpath
20
+ filepath = cop_dir.join(max_parameter_name)
21
+ filepath.write("#{value}\n", mode: 'a')
21
22
  end
22
23
 
23
24
  def max_parameter_name
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop # rubocop:disable Style/Documentation
5
+ # Autoloads mixin modules included by cops. Mixins are autoloaded to reduce the number of
6
+ # requires because they're used only when the relevant cop class is loaded.
7
+
8
+ autoload :ArrayMinSize, 'rubocop/cop/mixin/array_min_size'
9
+ autoload :ArraySyntax, 'rubocop/cop/mixin/array_syntax'
10
+ autoload :Alignment, 'rubocop/cop/mixin/alignment'
11
+ autoload :AllowedIdentifiers, 'rubocop/cop/mixin/allowed_identifiers'
12
+ autoload :AllowedMethods, 'rubocop/cop/mixin/allowed_methods'
13
+ autoload :AllowedPattern, 'rubocop/cop/mixin/allowed_pattern'
14
+ autoload :AllowedReceivers, 'rubocop/cop/mixin/allowed_receivers'
15
+ autoload :ForbiddenIdentifiers, 'rubocop/cop/mixin/forbidden_identifiers'
16
+ autoload :ForbiddenPattern, 'rubocop/cop/mixin/forbidden_pattern'
17
+ autoload :AutoCorrector, 'rubocop/cop/mixin/auto_corrector' # rubocop:todo Naming/InclusiveLanguage
18
+ autoload :CheckAssignment, 'rubocop/cop/mixin/check_assignment'
19
+ autoload :CheckLineBreakable, 'rubocop/cop/mixin/check_line_breakable'
20
+ autoload :CheckSingleLineSuitability, 'rubocop/cop/mixin/check_single_line_suitability'
21
+ autoload :ConfigurableMax, 'rubocop/cop/mixin/configurable_max'
22
+ autoload :CodeLength, 'rubocop/cop/mixin/code_length'
23
+ autoload :ConfigurableEnforcedStyle, 'rubocop/cop/mixin/configurable_enforced_style'
24
+ autoload :ConfigurableFormatting, 'rubocop/cop/mixin/configurable_formatting'
25
+ autoload :ConfigurableNaming, 'rubocop/cop/mixin/configurable_naming'
26
+ autoload :ConfigurableNumbering, 'rubocop/cop/mixin/configurable_numbering'
27
+ autoload :DigHelp, 'rubocop/cop/mixin/dig_help'
28
+ autoload :DocumentationComment, 'rubocop/cop/mixin/documentation_comment'
29
+ autoload :Duplication, 'rubocop/cop/mixin/duplication'
30
+ autoload :RangeHelp, 'rubocop/cop/mixin/range_help'
31
+ autoload :AnnotationComment, 'rubocop/cop/mixin/annotation_comment'
32
+ autoload :EmptyParameter, 'rubocop/cop/mixin/empty_parameter'
33
+ autoload :EndKeywordAlignment, 'rubocop/cop/mixin/end_keyword_alignment'
34
+ autoload :EndlessMethodRewriter, 'rubocop/cop/mixin/endless_method_rewriter'
35
+ autoload :EnforceSuperclass, 'rubocop/cop/mixin/enforce_superclass'
36
+ autoload :FirstElementLineBreak, 'rubocop/cop/mixin/first_element_line_break'
37
+ autoload :FrozenStringLiteral, 'rubocop/cop/mixin/frozen_string_literal'
38
+ autoload :GemDeclaration, 'rubocop/cop/mixin/gem_declaration'
39
+ autoload :GemspecHelp, 'rubocop/cop/mixin/gemspec_help'
40
+ autoload :HashAlignmentStyles, 'rubocop/cop/mixin/hash_alignment_styles'
41
+ autoload :HashSubset, 'rubocop/cop/mixin/hash_subset'
42
+ autoload :HashTransformMethod, 'rubocop/cop/mixin/hash_transform_method'
43
+ autoload :IntegerNode, 'rubocop/cop/mixin/integer_node'
44
+ autoload :Interpolation, 'rubocop/cop/mixin/interpolation'
45
+ autoload :LineLengthHelp, 'rubocop/cop/mixin/line_length_help'
46
+ autoload :MatchRange, 'rubocop/cop/mixin/match_range'
47
+ autoload :HashShorthandSyntax, 'rubocop/cop/mixin/hash_shorthand_syntax'
48
+ autoload :MethodComplexity, 'rubocop/cop/mixin/method_complexity'
49
+ autoload :MethodPreference, 'rubocop/cop/mixin/method_preference'
50
+ autoload :MinBodyLength, 'rubocop/cop/mixin/min_body_length'
51
+ autoload :MinBranchesCount, 'rubocop/cop/mixin/min_branches_count'
52
+ autoload :MultilineElementIndentation, 'rubocop/cop/mixin/multiline_element_indentation'
53
+ autoload :MultilineElementLineBreaks, 'rubocop/cop/mixin/multiline_element_line_breaks'
54
+ autoload :MultilineExpressionIndentation, 'rubocop/cop/mixin/multiline_expression_indentation'
55
+ autoload :MultilineLiteralBraceLayout, 'rubocop/cop/mixin/multiline_literal_brace_layout'
56
+ autoload :NegativeConditional, 'rubocop/cop/mixin/negative_conditional'
57
+ autoload :Heredoc, 'rubocop/cop/mixin/heredoc'
58
+ autoload :NilMethods, 'rubocop/cop/mixin/nil_methods'
59
+ autoload :OnNormalIfUnless, 'rubocop/cop/mixin/on_normal_if_unless'
60
+ autoload :OrderedGemNode, 'rubocop/cop/mixin/ordered_gem_node'
61
+ autoload :Parentheses, 'rubocop/cop/mixin/parentheses'
62
+ autoload :PercentArray, 'rubocop/cop/mixin/percent_array'
63
+ autoload :PercentLiteral, 'rubocop/cop/mixin/percent_literal'
64
+ autoload :PrecedingFollowingAlignment, 'rubocop/cop/mixin/preceding_following_alignment'
65
+ autoload :PreferredDelimiters, 'rubocop/cop/mixin/preferred_delimiters'
66
+ autoload :RationalLiteral, 'rubocop/cop/mixin/rational_literal'
67
+ autoload :RequireLibrary, 'rubocop/cop/mixin/require_library'
68
+ autoload :RescueNode, 'rubocop/cop/mixin/rescue_node'
69
+ autoload :SafeAssignment, 'rubocop/cop/mixin/safe_assignment'
70
+ autoload :SpaceAfterPunctuation, 'rubocop/cop/mixin/space_after_punctuation'
71
+ autoload :SpaceBeforePunctuation, 'rubocop/cop/mixin/space_before_punctuation'
72
+ autoload :SurroundingSpace, 'rubocop/cop/mixin/surrounding_space'
73
+ autoload :StatementModifier, 'rubocop/cop/mixin/statement_modifier'
74
+ autoload :StringHelp, 'rubocop/cop/mixin/string_help'
75
+ autoload :StringLiteralsHelp, 'rubocop/cop/mixin/string_literals_help'
76
+ autoload :SymbolHelp, 'rubocop/cop/mixin/symbol_help'
77
+ autoload :TargetRubyVersion, 'rubocop/cop/mixin/target_ruby_version'
78
+ autoload :TrailingBody, 'rubocop/cop/mixin/trailing_body'
79
+ autoload :TrailingComma, 'rubocop/cop/mixin/trailing_comma'
80
+ autoload :UncommunicativeName, 'rubocop/cop/mixin/uncommunicative_name'
81
+ autoload :VisibilityHelp, 'rubocop/cop/mixin/visibility_help'
82
+ autoload :CommentsHelp, 'rubocop/cop/mixin/comments_help'
83
+ autoload :DefNode, 'rubocop/cop/mixin/def_node'
84
+ end
85
+ end
@@ -109,7 +109,7 @@ module RuboCop
109
109
  # @_foo = calculate_expensive_thing
110
110
  # end
111
111
  #
112
- # @example EnforcedStyleForLeadingUnderscores :optional
112
+ # @example EnforcedStyleForLeadingUnderscores: optional
113
113
  # # bad
114
114
  # def foo
115
115
  # @something ||= calculate_expensive_thing
@@ -98,6 +98,14 @@ module RuboCop
98
98
  freeze
99
99
  end
100
100
 
101
+ def marshal_dump
102
+ [@severity, @location, @message, @cop_name, @status]
103
+ end
104
+
105
+ def marshal_load(array)
106
+ @severity, @location, @message, @cop_name, @status = array
107
+ end
108
+
101
109
  # @api public
102
110
  #
103
111
  # @!attribute [r] correctable?
@@ -49,9 +49,8 @@ module RuboCop
49
49
  attr_reader :options, :warnings
50
50
 
51
51
  def initialize(cops = [], options = {})
52
- @registry = {}
53
- @departments = {}
54
- @cops_by_cop_name = Hash.new { |hash, key| hash[key] = [] }
52
+ @departments = Set.new
53
+ @cops_by_badge = {}
55
54
 
56
55
  @enrollment_queue = cops
57
56
  @options = options
@@ -72,22 +71,17 @@ module RuboCop
72
71
  # @return [Array<Symbol>] list of departments for current cops.
73
72
  def departments
74
73
  clear_enrollment_queue
75
- @departments.keys
74
+ @departments.to_a
76
75
  end
77
76
 
78
77
  # @return [Registry] Cops for that specific department.
79
78
  def with_department(department)
80
- clear_enrollment_queue
81
- with(@departments.fetch(department, []))
79
+ with(cops.select { |cop| cop.department == department })
82
80
  end
83
81
 
84
82
  # @return [Registry] Cops not for a specific department.
85
83
  def without_department(department)
86
- clear_enrollment_queue
87
- without_department = @departments.dup
88
- without_department.delete(department)
89
-
90
- with(without_department.values.flatten)
84
+ with(cops.reject { |cop| cop.department == department })
91
85
  end
92
86
 
93
87
  # @return [Boolean] Checks if given name is department
@@ -160,31 +154,31 @@ module RuboCop
160
154
  def unqualified_cop_names
161
155
  clear_enrollment_queue
162
156
  @unqualified_cop_names ||=
163
- Set.new(@cops_by_cop_name.keys.map { |qn| File.basename(qn) }) <<
157
+ Set.new(@cops_by_badge.keys.map { |badge| File.basename(badge.to_s) }) <<
164
158
  'RedundantCopDisableDirective'
165
159
  end
166
160
 
167
161
  def qualify_badge(badge)
168
162
  clear_enrollment_queue
169
163
  @departments
170
- .map { |department, _| badge.with_department(department) }
164
+ .map { |department| badge.with_department(department) }
171
165
  .select { |potential_badge| registered?(potential_badge) }
172
166
  end
173
167
 
174
168
  # @return [Hash{String => Array<Class>}]
175
169
  def to_h
176
170
  clear_enrollment_queue
177
- @cops_by_cop_name
171
+ @cops_by_badge.to_h { |_badge, cop| [cop.cop_name, [cop]] }
178
172
  end
179
173
 
180
174
  def cops
181
175
  clear_enrollment_queue
182
- @registry.values
176
+ @cops_by_badge.values
183
177
  end
184
178
 
185
179
  def length
186
180
  clear_enrollment_queue
187
- @registry.size
181
+ @cops_by_badge.size
188
182
  end
189
183
 
190
184
  def enabled(config)
@@ -219,7 +213,8 @@ module RuboCop
219
213
  end
220
214
 
221
215
  def names
222
- cops.map(&:cop_name)
216
+ clear_enrollment_queue
217
+ @cops_by_badge.keys.map(&:to_s)
223
218
  end
224
219
 
225
220
  def cops_for_department(department)
@@ -236,7 +231,7 @@ module RuboCop
236
231
 
237
232
  def sort!
238
233
  clear_enrollment_queue
239
- @registry = @registry.sort_by { |badge, _| badge.cop_name }.to_h
234
+ @cops_by_badge = @cops_by_badge.sort_by { |badge, _cop| badge.cop_name }.to_h
240
235
 
241
236
  self
242
237
  end
@@ -252,7 +247,9 @@ module RuboCop
252
247
  # @param [String] cop_name
253
248
  # @return [Class, nil]
254
249
  def find_by_cop_name(cop_name)
255
- to_h[cop_name].first
250
+ clear_enrollment_queue
251
+ badge = Badge.parse(cop_name)
252
+ @cops_by_badge[badge]
256
253
  end
257
254
 
258
255
  # When a cop name is given returns a single-element array with the cop class.
@@ -289,10 +286,8 @@ module RuboCop
289
286
  return if @enrollment_queue.empty?
290
287
 
291
288
  @enrollment_queue.each do |cop|
292
- @registry[cop.badge] = cop
293
- @departments[cop.department] ||= []
294
- @departments[cop.department] << cop
295
- @cops_by_cop_name[cop.cop_name] << cop
289
+ @cops_by_badge[cop.badge] = cop
290
+ @departments << cop.department
296
291
  end
297
292
  @enrollment_queue = []
298
293
  end
@@ -318,7 +313,7 @@ module RuboCop
318
313
 
319
314
  def registered?(badge)
320
315
  clear_enrollment_queue
321
- @registry.key?(badge)
316
+ @cops_by_badge.key?(badge)
322
317
  end
323
318
  end
324
319
  end
@@ -348,8 +348,20 @@ module RuboCop
348
348
 
349
349
  def remove_modifier_node_within_begin(corrector, modifier_node, begin_node)
350
350
  def_node = begin_node.children[begin_node.children.index(modifier_node) + 1]
351
- range = modifier_node.source_range.begin.join(def_node.source_range.begin)
352
- corrector.remove(range)
351
+ # Stop the removal range at the first comment that precedes the def, if
352
+ # any exist. Without this, comments between the modifier and the def are
353
+ # dropped because they fall inside the removed range.
354
+ end_pos = first_comment_or_node_start(def_node)
355
+ corrector.remove(modifier_node.source_range.begin.join(end_pos))
356
+ end
357
+
358
+ def first_comment_or_node_start(node)
359
+ preceding = processed_source.ast_with_comments[node].select do |comment|
360
+ comment.loc.line < node.loc.line
361
+ end
362
+ return node.source_range.begin if preceding.empty?
363
+
364
+ preceding.first.source_range.begin
353
365
  end
354
366
 
355
367
  def def_source(node, def_nodes)
@@ -46,15 +46,16 @@ module RuboCop
46
46
  token = insert_notice_before(processed_source)
47
47
  range = token.nil? ? range_between(0, 0) : token.pos
48
48
 
49
- corrector.insert_before(range, "#{autocorrect_notice}\n")
49
+ corrector.insert_before(range, "#{normalized_autocorrect_notice}\n")
50
50
  end
51
51
 
52
- def notice
53
- cop_config['Notice']
54
- end
52
+ def normalized_autocorrect_notice
53
+ autocorrect_notice.lines.map do |line|
54
+ next line if line.start_with?('#')
55
+ next "#\n" if line.chomp.empty?
55
56
 
56
- def autocorrect_notice
57
- cop_config['AutocorrectNotice']
57
+ "# #{line}"
58
+ end.join
58
59
  end
59
60
 
60
61
  def verify_autocorrect_notice!
@@ -62,8 +63,7 @@ module RuboCop
62
63
  raise Warning, "#{cop_name}: #{AUTOCORRECT_EMPTY_WARNING}"
63
64
  end
64
65
 
65
- regex = Regexp.new(notice)
66
- return if autocorrect_notice.gsub(/^# */, '').match?(regex)
66
+ return if normalized_autocorrect_notice.gsub(/^# */, '').match?(notice_regexp)
67
67
 
68
68
  message = "AutocorrectNotice '#{autocorrect_notice}' must match Notice /#{notice}/"
69
69
  raise Warning, "#{cop_name}: #{message}"
@@ -91,18 +91,29 @@ module RuboCop
91
91
  end
92
92
 
93
93
  def notice_found?(processed_source)
94
- notice_regexp = Regexp.new(notice.lines.map(&:strip).join)
95
94
  multiline_notice = +''
96
95
  processed_source.tokens.each do |token|
97
96
  break unless token.comment?
98
97
 
99
- multiline_notice << token.text.sub(/\A# */, '')
98
+ multiline_notice << token.text.sub(/\A# */, '') << "\n"
100
99
 
101
100
  break if notice_regexp.match?(token.text)
102
101
  end
103
102
 
104
103
  multiline_notice.match?(notice_regexp)
105
104
  end
105
+
106
+ def notice_regexp
107
+ @notice_regexp ||= Regexp.new(notice.sub(/\A(?:\\A|\^)?#(?:\\s[*+?]?|\s)*/, ''))
108
+ end
109
+
110
+ def notice
111
+ cop_config['Notice']
112
+ end
113
+
114
+ def autocorrect_notice
115
+ cop_config['AutocorrectNotice']
116
+ end
106
117
  end
107
118
  end
108
119
  end
@@ -3,8 +3,8 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Style
6
- # Checks for consistent usage of the `DateTime` class over the
7
- # `Time` class. This cop is disabled by default since these classes,
6
+ # Checks for consistent usage of the `Time` class over the
7
+ # `DateTime` class. This cop is disabled by default since these classes,
8
8
  # although highly overlapping, have particularities that make them not
9
9
  # replaceable in certain situations when dealing with multiple timezones
10
10
  # and/or DST.
@@ -161,7 +161,12 @@ module RuboCop
161
161
  source = source.gsub(COMMENT_REGEXP, '')
162
162
  return if source.blank?
163
163
 
164
- /\s*#{Regexp.escape(source.strip)}/
164
+ # Treat `\#` (an escaped interpolation marker in the heredoc) as matching
165
+ # either `\#` or `#` in the comment, since the comment may show either
166
+ # the literal source form or the runtime appearance.
167
+ segments = source.strip.split('\\#', -1).map { |segment| Regexp.escape(segment) }
168
+
169
+ /\s*#{segments.join('\\\\?#')}/
165
170
  end
166
171
  end
167
172
  end
@@ -224,15 +224,18 @@ module RuboCop
224
224
  end
225
225
 
226
226
  def find_heredoc_argument(node)
227
- return unless node&.call_type?
227
+ return unless node
228
228
 
229
- last_arg = node.last_argument
229
+ node = node.children.first while node.begin_type?
230
+ return node if heredoc?(node)
231
+ return unless node.call_type?
230
232
 
231
- if heredoc?(last_arg)
232
- last_arg
233
- elsif last_arg&.call_type?
234
- find_heredoc_argument(last_arg)
233
+ node.arguments.reverse_each do |argument|
234
+ heredoc_argument = find_heredoc_argument(argument)
235
+ return heredoc_argument if heredoc_argument
235
236
  end
237
+
238
+ find_heredoc_argument(node.receiver)
236
239
  end
237
240
 
238
241
  def autocorrect_heredoc_argument(corrector, node, heredoc_node, leave_branch, guard)
@@ -82,18 +82,23 @@ module RuboCop
82
82
  end
83
83
 
84
84
  def correct_fetch_to_brackets(corrector, node)
85
- receiver = node.receiver.source
86
85
  key = node.first_argument.source
87
- replacement = "#{receiver}[#{key}]"
88
- replacement = "(#{replacement})" if node.csend_type?
89
- corrector.replace(node, replacement)
86
+
87
+ if node.csend_type?
88
+ corrector.replace(node, "(#{node.receiver.source}[#{key}])")
89
+ else
90
+ corrector.replace(node.loc.dot.join(node.source_range.end), "[#{key}]")
91
+ end
90
92
  end
91
93
 
92
94
  def correct_brackets_to_fetch(corrector, node)
93
- receiver = node.receiver.source
94
95
  key = node.first_argument.source
95
- operator = node.csend_type? ? '&.' : '.'
96
- corrector.replace(node, "#{receiver}#{operator}fetch(#{key})")
96
+
97
+ if node.csend_type?
98
+ corrector.replace(node.loc.dot.join(node.source_range.end), "&.fetch(#{key})")
99
+ else
100
+ corrector.replace(node.loc.selector.join(node.source_range.end), ".fetch(#{key})")
101
+ end
97
102
  end
98
103
  end
99
104
  end
@@ -64,7 +64,7 @@ module RuboCop
64
64
 
65
65
  MSG = 'Convert `if` nested inside `else` to `elsif`.'
66
66
 
67
- # rubocop:disable Metrics/CyclomaticComplexity
67
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
68
68
  def on_if(node)
69
69
  return if node.ternary? || node.unless?
70
70
 
@@ -72,6 +72,7 @@ module RuboCop
72
72
 
73
73
  return unless else_branch&.if_type? && else_branch.if?
74
74
  return if allow_if_modifier_in_else_branch?(else_branch)
75
+ return if comments_between_else_and_if?(node, else_branch)
75
76
 
76
77
  add_offense(else_branch.loc.keyword) do |corrector|
77
78
  next if part_of_ignored_node?(node)
@@ -80,12 +81,12 @@ module RuboCop
80
81
  ignore_node(node)
81
82
  end
82
83
  end
83
- # rubocop:enable Metrics/CyclomaticComplexity
84
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
84
85
 
85
86
  private
86
87
 
87
88
  def autocorrect(corrector, node)
88
- if then?(node)
89
+ if node.then?
89
90
  # If the nested `if` is a then node, correct it first,
90
91
  # then the next pass will use `correct_to_elsif_from_if_inside_else_form`
91
92
  IfThenCorrector.new(node, indentation: 0).call(corrector)
@@ -124,10 +125,6 @@ module RuboCop
124
125
  corrector.remove(range_by_whole_lines(find_end_range(node), include_final_newline: true))
125
126
  end
126
127
 
127
- def then?(node)
128
- node.loc.begin&.source == 'then'
129
- end
130
-
131
128
  def find_end_range(node)
132
129
  end_range = node.loc.end
133
130
  return end_range if end_range
@@ -139,6 +136,18 @@ module RuboCop
139
136
  range_between(node.loc.keyword.begin_pos, condition.source_range.end_pos)
140
137
  end
141
138
 
139
+ def comments_between_else_and_if?(node, else_branch)
140
+ return false if else_branch.modifier_form?
141
+
142
+ else_end = node.loc.else.end_pos
143
+ if_begin = else_branch.loc.keyword.begin_pos
144
+
145
+ processed_source.comments.any? do |comment|
146
+ comment_pos = comment.source_range.begin_pos
147
+ comment_pos > else_end && comment_pos < if_begin
148
+ end
149
+ end
150
+
142
151
  def allow_if_modifier_in_else_branch?(else_branch)
143
152
  allow_if_modifier? && else_branch&.modifier_form?
144
153
  end