rubocop 1.82.0 → 1.84.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +1 -1
  4. data/config/default.yml +44 -0
  5. data/lib/rubocop/cli/command/lsp.rb +1 -1
  6. data/lib/rubocop/cli.rb +2 -1
  7. data/lib/rubocop/comment_config.rb +1 -0
  8. data/lib/rubocop/cop/autocorrect_logic.rb +2 -0
  9. data/lib/rubocop/cop/bundler/gem_version.rb +28 -28
  10. data/lib/rubocop/cop/bundler/ordered_gems.rb +1 -2
  11. data/lib/rubocop/cop/correctors/alignment_corrector.rb +20 -2
  12. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +1 -2
  13. data/lib/rubocop/cop/internal_affairs/example_heredoc_delimiter.rb +8 -8
  14. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +9 -9
  15. data/lib/rubocop/cop/internal_affairs/useless_message_assertion.rb +4 -4
  16. data/lib/rubocop/cop/layout/case_indentation.rb +3 -1
  17. data/lib/rubocop/cop/layout/class_structure.rb +12 -5
  18. data/lib/rubocop/cop/layout/first_argument_indentation.rb +32 -1
  19. data/lib/rubocop/cop/layout/first_array_element_line_break.rb +26 -0
  20. data/lib/rubocop/cop/layout/first_hash_element_line_break.rb +25 -25
  21. data/lib/rubocop/cop/layout/heredoc_indentation.rb +35 -1
  22. data/lib/rubocop/cop/layout/indentation_width.rb +111 -7
  23. data/lib/rubocop/cop/layout/line_length.rb +6 -2
  24. data/lib/rubocop/cop/layout/multiline_array_brace_layout.rb +57 -57
  25. data/lib/rubocop/cop/layout/multiline_block_layout.rb +2 -0
  26. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +56 -56
  27. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +5 -1
  28. data/lib/rubocop/cop/layout/space_after_comma.rb +2 -10
  29. data/lib/rubocop/cop/layout/space_after_semicolon.rb +1 -1
  30. data/lib/rubocop/cop/layout/space_in_lambda_literal.rb +8 -8
  31. data/lib/rubocop/cop/lint/duplicate_match_pattern.rb +4 -4
  32. data/lib/rubocop/cop/lint/duplicate_methods.rb +57 -5
  33. data/lib/rubocop/cop/lint/float_comparison.rb +1 -1
  34. data/lib/rubocop/cop/lint/literal_as_condition.rb +1 -1
  35. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +1 -1
  36. data/lib/rubocop/cop/lint/struct_new_override.rb +17 -1
  37. data/lib/rubocop/cop/lint/to_json.rb +12 -16
  38. data/lib/rubocop/cop/lint/useless_assignment.rb +44 -16
  39. data/lib/rubocop/cop/lint/useless_or.rb +1 -1
  40. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +1 -1
  41. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +2 -0
  42. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +4 -4
  43. data/lib/rubocop/cop/mixin/space_after_punctuation.rb +5 -4
  44. data/lib/rubocop/cop/mixin/trailing_comma.rb +5 -1
  45. data/lib/rubocop/cop/naming/predicate_prefix.rb +11 -11
  46. data/lib/rubocop/cop/offense.rb +2 -1
  47. data/lib/rubocop/cop/security/json_load.rb +1 -1
  48. data/lib/rubocop/cop/style/access_modifier_declarations.rb +1 -2
  49. data/lib/rubocop/cop/style/documentation.rb +6 -6
  50. data/lib/rubocop/cop/style/documentation_method.rb +8 -8
  51. data/lib/rubocop/cop/style/empty_class_definition.rb +144 -0
  52. data/lib/rubocop/cop/style/guard_clause.rb +7 -4
  53. data/lib/rubocop/cop/style/hash_lookup_method.rb +94 -0
  54. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +12 -12
  55. data/lib/rubocop/cop/style/lambda_call.rb +8 -8
  56. data/lib/rubocop/cop/style/module_member_existence_check.rb +56 -13
  57. data/lib/rubocop/cop/style/multiline_method_signature.rb +2 -0
  58. data/lib/rubocop/cop/style/negative_array_index.rb +218 -0
  59. data/lib/rubocop/cop/style/operator_method_call.rb +11 -2
  60. data/lib/rubocop/cop/style/preferred_hash_methods.rb +12 -12
  61. data/lib/rubocop/cop/style/redundant_condition.rb +1 -1
  62. data/lib/rubocop/cop/style/reverse_find.rb +51 -0
  63. data/lib/rubocop/cop/team.rb +3 -3
  64. data/lib/rubocop/cop/variable_force/branch.rb +28 -4
  65. data/lib/rubocop/formatter/clang_style_formatter.rb +5 -2
  66. data/lib/rubocop/formatter/formatter_set.rb +1 -1
  67. data/lib/rubocop/formatter/tap_formatter.rb +5 -2
  68. data/lib/rubocop/remote_config.rb +5 -2
  69. data/lib/rubocop/result_cache.rb +38 -27
  70. data/lib/rubocop/rspec/shared_contexts.rb +4 -0
  71. data/lib/rubocop/rspec/support.rb +1 -0
  72. data/lib/rubocop/runner.rb +4 -0
  73. data/lib/rubocop/target_ruby.rb +3 -1
  74. data/lib/rubocop/version.rb +1 -1
  75. data/lib/rubocop.rb +4 -0
  76. metadata +11 -7
@@ -77,7 +77,7 @@ module RuboCop
77
77
 
78
78
  def on_case(node)
79
79
  node.when_branches.each do |when_branch|
80
- when_branch.each_condition do |condition|
80
+ when_branch.conditions.each do |condition|
81
81
  next if !float?(condition) || literal_safe?(condition)
82
82
 
83
83
  add_offense(condition, message: MSG_CASE)
@@ -157,7 +157,7 @@ module RuboCop
157
157
 
158
158
  check_case(case_match_node)
159
159
  else
160
- case_match_node.each_in_pattern do |in_pattern_node|
160
+ case_match_node.in_pattern_branches.each do |in_pattern_node|
161
161
  next unless in_pattern_node.condition.literal?
162
162
 
163
163
  add_offense(in_pattern_node)
@@ -77,7 +77,7 @@ module RuboCop
77
77
  PERCENT_CAPITAL_W = '%W'
78
78
  PERCENT_I = '%i'
79
79
  PERCENT_CAPITAL_I = '%I'
80
- ASSIGNMENT_TYPES = %i[lvasgn ivasgn cvasgn gvasgn].freeze
80
+ ASSIGNMENT_TYPES = %i[lvasgn ivasgn cvasgn gvasgn casgn].freeze
81
81
 
82
82
  # @!method array_new?(node)
83
83
  def_node_matcher :array_new?, <<~PATTERN
@@ -26,7 +26,23 @@ module RuboCop
26
26
  'and it may be unexpected.'
27
27
  RESTRICT_ON_SEND = %i[new].freeze
28
28
 
29
- STRUCT_METHOD_NAMES = Struct.instance_methods
29
+ # This is based on `Struct.instance_methods.sort` in Ruby 4.0.0.
30
+ STRUCT_METHOD_NAMES = %i[
31
+ ! != !~ <=> == === [] []= __id__ __send__ all? any? chain chunk chunk_while class clone
32
+ collect collect_concat compact count cycle deconstruct deconstruct_keys
33
+ define_singleton_method detect dig display drop drop_while dup each each_cons each_entry
34
+ each_pair each_slice each_with_index each_with_object entries enum_for eql? equal? extend
35
+ filter filter_map find find_all find_index first flat_map freeze frozen? grep grep_v
36
+ group_by hash include? inject inspect instance_eval instance_exec instance_of?
37
+ instance_variable_defined? instance_variable_get instance_variable_set instance_variables
38
+ is_a? itself kind_of? lazy length map max max_by member? members method methods
39
+ min min_by minmax minmax_by nil? none? object_id one? partition private_methods
40
+ protected_methods public_method public_methods public_send reduce reject
41
+ remove_instance_variable respond_to? reverse_each select send singleton_class
42
+ singleton_method singleton_methods size slice_after slice_before slice_when sort sort_by
43
+ sum take take_while tally tap then to_a to_enum to_h to_s to_set uniq values values_at
44
+ yield_self zip
45
+ ].freeze
30
46
  STRUCT_MEMBER_NAME_TYPES = %i[sym str].freeze
31
47
 
32
48
  # @!method struct_new(node)
@@ -5,27 +5,23 @@ module RuboCop
5
5
  module Lint
6
6
  # Checks to make sure `#to_json` includes an optional argument.
7
7
  # When overriding `#to_json`, callers may invoke JSON
8
- # generation via `JSON.generate(your_obj)`. Since `JSON#generate` allows
8
+ # generation via `JSON.generate(your_obj)`. Since `JSON#generate` allows
9
9
  # for an optional argument, your method should too.
10
10
  #
11
11
  # @example
12
- # class Point
13
- # attr_reader :x, :y
14
- #
15
- # # bad, incorrect arity
16
- # def to_json
17
- # JSON.generate([x, y])
18
- # end
12
+ # # bad - incorrect arity
13
+ # def to_json
14
+ # JSON.generate([x, y])
15
+ # end
19
16
  #
20
- # # good, preserving args
21
- # def to_json(*args)
22
- # JSON.generate([x, y], *args)
23
- # end
17
+ # # good - preserving args
18
+ # def to_json(*args)
19
+ # JSON.generate([x, y], *args)
20
+ # end
24
21
  #
25
- # # good, discarding args
26
- # def to_json(*_args)
27
- # JSON.generate([x, y])
28
- # end
22
+ # # good - discarding args
23
+ # def to_json(*_args)
24
+ # JSON.generate([x, y])
29
25
  # end
30
26
  #
31
27
  class ToJSON < Base
@@ -52,32 +52,39 @@ module RuboCop
52
52
  scope.variables.each_value { |variable| check_for_unused_assignments(variable) }
53
53
  end
54
54
 
55
- # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
56
55
  def check_for_unused_assignments(variable)
57
56
  return if variable.should_be_unused?
58
57
 
59
58
  variable.assignments.reverse_each do |assignment|
60
- assignment_node = assignment.node
61
- next if assignment.used? || part_of_ignored_node?(assignment_node)
59
+ check_for_unused_assignment(variable, assignment)
60
+ end
61
+ end
62
62
 
63
- message = message_for_useless_assignment(assignment)
64
- range = offense_range(assignment)
63
+ def check_for_unused_assignment(variable, assignment)
64
+ assignment_node = assignment.node
65
65
 
66
- add_offense(range, message: message) do |corrector|
67
- # In cases like `x = 1, y = 2`, where removing a variable would cause a syntax error,
68
- # and where changing `x ||= 1` to `x = 1` would cause `NameError`,
69
- # the autocorrect will be skipped, even if the variable is unused.
70
- if sequential_assignment?(assignment_node) || assignment_node.parent&.or_asgn_type?
71
- next
72
- end
66
+ return if ignored_assignment?(variable, assignment_node, assignment)
73
67
 
74
- autocorrect(corrector, assignment)
75
- end
68
+ message = message_for_useless_assignment(assignment)
69
+ range = offense_range(assignment)
76
70
 
77
- ignore_node(assignment_node) if chained_assignment?(assignment_node)
71
+ add_offense(range, message: message) do |corrector|
72
+ # In cases like `x = 1, y = 2`, where removing a variable would cause a syntax error,
73
+ # and where changing `x ||= 1` to `x = 1` would cause `NameError`,
74
+ # the autocorrect will be skipped, even if the variable is unused.
75
+ next if sequential_assignment?(assignment_node) ||
76
+ assignment_node.parent&.or_asgn_type?
77
+
78
+ autocorrect(corrector, assignment)
78
79
  end
80
+
81
+ ignore_node(assignment_node) if chained_assignment?(assignment_node)
82
+ end
83
+
84
+ def ignored_assignment?(variable, assignment_node, assignment)
85
+ assignment.used? || part_of_ignored_node?(assignment_node) ||
86
+ variable_in_loop_condition?(assignment_node, variable)
79
87
  end
80
- # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
81
88
 
82
89
  def message_for_useless_assignment(assignment)
83
90
  variable = assignment.variable
@@ -208,6 +215,27 @@ module RuboCop
208
215
  def remove_local_variable_assignment_part(corrector, node)
209
216
  corrector.replace(node, node.expression.source)
210
217
  end
218
+
219
+ def variable_in_loop_condition?(assignment_node, variable)
220
+ return false if assignment_node.each_ancestor(:any_def).any?
221
+
222
+ loop_node = assignment_node.each_ancestor.find do |ancestor|
223
+ ancestor.type?(*VariableForce::LOOP_TYPES)
224
+ end
225
+
226
+ return false unless loop_node.respond_to?(:condition)
227
+
228
+ condition_node = loop_node.condition
229
+ variable_name = variable.name
230
+
231
+ return true if condition_node.lvar_type? && condition_node.children.first == variable_name
232
+
233
+ condition_node.each_descendant(:lvar) do |lvar_node|
234
+ return true if lvar_node.children.first == variable_name
235
+ end
236
+
237
+ false
238
+ end
211
239
  end
212
240
  end
213
241
  end
@@ -11,7 +11,7 @@ module RuboCop
11
11
  #
12
12
  # @safety
13
13
  # As shown in the examples below, there are generally two possible ways to correct the
14
- # offense, but this cops autocorrection always chooses the option that preserves the
14
+ # offense, but this cop's autocorrection always chooses the option that preserves the
15
15
  # current behavior. While this does not change how the code behaves, that option is not
16
16
  # necessarily the appropriate fix in every situation. For this reason, the autocorrection
17
17
  # provided by this cop is considered unsafe.
@@ -53,7 +53,7 @@ module RuboCop
53
53
  return true
54
54
  end
55
55
  when :when
56
- node.each_condition do |condition|
56
+ node.conditions.each do |condition|
57
57
  return true if _cant_be_nil?(condition, receiver)
58
58
  end
59
59
  when :lvasgn, :ivasgn, :cvasgn, :gvasgn, :casgn
@@ -14,6 +14,8 @@ module RuboCop
14
14
  private
15
15
 
16
16
  def too_long?(node)
17
+ return false unless max_line_length
18
+
17
19
  lines = processed_source.lines[(node.first_line - 1)...node.last_line]
18
20
  to_single_line(lines.join("\n")).length > max_line_length
19
21
  end
@@ -125,7 +125,7 @@ module RuboCop
125
125
  return if dispatch_node.assignment_method?
126
126
  return if dispatch_node.parenthesized?
127
127
  return if dispatch_node.parent && parentheses?(dispatch_node.parent)
128
- return if last_expression?(dispatch_node) && !method_dispatch_as_argument?(dispatch_node)
128
+ return if last_expression?(dispatch_node) && !requires_parentheses_context?(dispatch_node)
129
129
 
130
130
  def_node = node.each_ancestor(:call, :super, :yield).first
131
131
 
@@ -164,11 +164,11 @@ module RuboCop
164
164
  !assignment_node.right_sibling
165
165
  end
166
166
 
167
- def method_dispatch_as_argument?(method_dispatch_node)
168
- parent = method_dispatch_node.parent
167
+ def requires_parentheses_context?(node)
168
+ parent = node.parent
169
169
  return false unless parent
170
170
 
171
- parent.type?(:call, :super, :yield)
171
+ parent.type?(:call, :if, :super, :until, :while, :yield)
172
172
  end
173
173
 
174
174
  def breakdown_value_types_of_hash(hash_node)
@@ -8,8 +8,8 @@ module RuboCop
8
8
  MSG = 'Space missing after %<token>s.'
9
9
 
10
10
  def on_new_investigation
11
- each_missing_space(processed_source.tokens) do |token|
12
- add_offense(token.pos, message: format(MSG, token: kind(token))) do |corrector|
11
+ each_missing_space(processed_source.tokens) do |token, kind|
12
+ add_offense(token.pos, message: format(MSG, token: kind)) do |corrector|
13
13
  PunctuationCorrector.add_space(corrector, token)
14
14
  end
15
15
  end
@@ -19,11 +19,12 @@ module RuboCop
19
19
 
20
20
  def each_missing_space(tokens)
21
21
  tokens.each_cons(2) do |token1, token2|
22
- next unless kind(token1)
22
+ kind = kind(token1, token2)
23
+ next unless kind
23
24
  next unless space_missing?(token1, token2)
24
25
  next unless space_required_before?(token2)
25
26
 
26
- yield token1
27
+ yield token1, kind
27
28
  end
28
29
  end
29
30
 
@@ -95,12 +95,16 @@ module RuboCop
95
95
  node.multiline? && !allowed_multiline_argument?(node)
96
96
  end
97
97
 
98
+ # rubocop:disable Metrics/AbcSize
98
99
  def method_name_and_arguments_on_same_line?(node)
99
100
  return false if !node.call_type? || node.last_line != node.last_argument.last_line
100
101
  return true if node.last_argument.hash_type? && node.last_argument.braces?
101
102
 
102
- node.loc.selector.line == node.last_argument.last_line
103
+ line = node.loc.selector&.line || node.loc.line
104
+
105
+ line == node.last_argument.last_line
103
106
  end
107
+ # rubocop:enable Metrics/AbcSize
104
108
 
105
109
  # A single argument with the closing bracket on the same line as the end
106
110
  # of the argument is not considered multiline, even if the argument
@@ -63,17 +63,17 @@ module RuboCop
63
63
  # end
64
64
  #
65
65
  # @example UseSorbetSigs: false (default)
66
- # # bad
67
- # sig { returns(String) }
68
- # def is_this_thing_on
69
- # "yes"
70
- # end
71
- #
72
- # # good - Sorbet signature is not evaluated
73
- # sig { returns(String) }
74
- # def is_this_thing_on?
75
- # "yes"
76
- # end
66
+ # # bad
67
+ # sig { returns(String) }
68
+ # def is_this_thing_on
69
+ # "yes"
70
+ # end
71
+ #
72
+ # # good - Sorbet signature is not evaluated
73
+ # sig { returns(String) }
74
+ # def is_this_thing_on?
75
+ # "yes"
76
+ # end
77
77
  #
78
78
  # @example UseSorbetSigs: true
79
79
  # # bad
@@ -139,7 +139,8 @@ module RuboCop
139
139
  # @return [Parser::Source::Range]
140
140
  # the range of the code that is highlighted
141
141
  def highlighted_area
142
- Parser::Source::Range.new(source_line, column, column + column_length)
142
+ source_buffer = Parser::Source::Buffer.new(location.source_buffer.name, source: source_line)
143
+ Parser::Source::Range.new(source_buffer, column, column + column_length)
143
144
  end
144
145
 
145
146
  # @api private
@@ -52,7 +52,7 @@ module RuboCop
52
52
  (
53
53
  send (const {nil? cbase} :JSON) ${:load :restore}
54
54
  ...
55
- !(hash `(sym $:create_additions))
55
+ !`(pair (sym :create_additions) _)
56
56
  )
57
57
  PATTERN
58
58
 
@@ -326,8 +326,7 @@ module RuboCop
326
326
  argument_less_modifier_node = find_argument_less_modifier_node(node)
327
327
  if argument_less_modifier_node
328
328
  corrector.insert_after(argument_less_modifier_node, "\n\n#{source}")
329
- elsif (ancestor = node.each_ancestor(:class, :module).first)
330
-
329
+ elsif (ancestor = node.each_ancestor(:class, :module, :sclass).first)
331
330
  corrector.insert_before(ancestor.loc.end, "#{node.method_name}\n\n#{source}\n")
332
331
  else
333
332
  corrector.replace(node, "#{node.method_name}\n\n#{source}")
@@ -62,12 +62,12 @@ module RuboCop
62
62
  #
63
63
  # @example AllowedConstants: ['ClassMethods']
64
64
  #
65
- # # good
66
- # module A
67
- # module ClassMethods
68
- # # ...
69
- # end
70
- # end
65
+ # # good
66
+ # module A
67
+ # module ClassMethods
68
+ # # ...
69
+ # end
70
+ # end
71
71
  #
72
72
  class Documentation < Base
73
73
  include DocumentationComment
@@ -97,14 +97,14 @@ module RuboCop
97
97
  #
98
98
  # @example AllowedMethods: ['method_missing', 'respond_to_missing?']
99
99
  #
100
- # # good
101
- # class Foo
102
- # def method_missing(name, *args)
103
- # end
104
- #
105
- # def respond_to_missing?(symbol, include_private)
106
- # end
107
- # end
100
+ # # good
101
+ # class Foo
102
+ # def method_missing(name, *args)
103
+ # end
104
+ #
105
+ # def respond_to_missing?(symbol, include_private)
106
+ # end
107
+ # end
108
108
  #
109
109
  class DocumentationMethod < Base
110
110
  include DocumentationComment
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Enforces consistent style for empty class definitions.
7
+ #
8
+ # This cop can enforce either a two-line class definition or `Class.new`
9
+ # for classes with no body.
10
+ #
11
+ # The supported styles are:
12
+ #
13
+ # * class_definition (default) - prefer two-line class definition over `Class.new`
14
+ # * class_new - prefer `Class.new` over class definition
15
+ #
16
+ # @example EnforcedStyle: class_definition (default)
17
+ # # bad
18
+ # FooError = Class.new(StandardError)
19
+ #
20
+ # # okish
21
+ # class FooError < StandardError; end
22
+ #
23
+ # # good
24
+ # class FooError < StandardError
25
+ # end
26
+ #
27
+ # @example EnforcedStyle: class_new
28
+ # # bad
29
+ # class FooError < StandardError
30
+ # end
31
+ #
32
+ # # bad
33
+ # class FooError < StandardError; end
34
+ #
35
+ # # good
36
+ # FooError = Class.new(StandardError)
37
+ #
38
+ class EmptyClassDefinition < Base
39
+ include ConfigurableEnforcedStyle
40
+ include Alignment
41
+ include RangeHelp
42
+ extend AutoCorrector
43
+
44
+ MSG_CLASS_DEFINITION =
45
+ 'Prefer a two-line class definition over `Class.new` for classes with no body.'
46
+ MSG_CLASS_NEW = 'Prefer `Class.new` over class definition for classes with no body.'
47
+
48
+ # @!method class_new_assignment?(node)
49
+ def_node_matcher :class_new_assignment?, <<~PATTERN
50
+ (casgn _ _ (send (const _ :Class) :new ...))
51
+ PATTERN
52
+
53
+ def on_casgn(node)
54
+ return unless style == :class_definition
55
+ return unless node.expression
56
+
57
+ class_new_node = find_class_new_node(node.expression)
58
+ return if chained_with_any_method?(node.expression, class_new_node)
59
+ return if variable_parent_class?(class_new_node)
60
+
61
+ add_offense(node, message: MSG_CLASS_DEFINITION) do |corrector|
62
+ autocorrect_class_new(corrector, node)
63
+ end
64
+ end
65
+
66
+ def on_class(node)
67
+ return unless style == :class_new
68
+ return unless empty_class?(node)
69
+
70
+ add_offense(node, message: MSG_CLASS_NEW) do |corrector|
71
+ autocorrect_class_definition(corrector, node)
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def autocorrect_class_new(corrector, node)
78
+ indent = ' ' * node.loc.column
79
+ class_name = node.name
80
+ class_new_node = find_class_new_node(node.expression)
81
+ parent_class = extract_parent_class(class_new_node)
82
+
83
+ replacement = if parent_class
84
+ "class #{class_name} < #{parent_class}\n#{indent}end"
85
+ else
86
+ "class #{class_name}\n#{indent}end"
87
+ end
88
+
89
+ corrector.replace(node, replacement)
90
+ end
91
+
92
+ def autocorrect_class_definition(corrector, node)
93
+ source_line = processed_source.buffer.source_line(node.loc.line)
94
+ indent = source_line[/\A */]
95
+ class_name = node.identifier.source
96
+ parent_class = node.parent_class&.source
97
+ range = range_by_whole_lines(node.source_range, include_final_newline: true)
98
+
99
+ replacement = if parent_class
100
+ "#{indent}#{class_name} = Class.new(#{parent_class})\n"
101
+ else
102
+ "#{indent}#{class_name} = Class.new\n"
103
+ end
104
+
105
+ corrector.replace(range, replacement)
106
+ end
107
+
108
+ def extract_parent_class(class_new_node)
109
+ first_arg = class_new_node.first_argument
110
+ first_arg&.source
111
+ end
112
+
113
+ def variable_parent_class?(class_new_node)
114
+ first_arg = class_new_node.first_argument
115
+ return false unless first_arg
116
+
117
+ !first_arg.const_type?
118
+ end
119
+
120
+ def find_class_new_node(node)
121
+ return nil unless node.send_type?
122
+ return nil unless node.receiver&.const_type?
123
+
124
+ return node if node.receiver.const_name.to_sym == :Class && node.method?(:new)
125
+
126
+ nil
127
+ end
128
+
129
+ def chained_with_any_method?(expression_node, class_new_node)
130
+ return true unless expression_node == class_new_node
131
+
132
+ false
133
+ end
134
+
135
+ def empty_class?(node)
136
+ body = node.body
137
+ return true unless body
138
+
139
+ body.begin_type? && body.children.empty?
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -227,12 +227,15 @@ module RuboCop
227
227
  remove_whole_lines(corrector, node.loc.end)
228
228
  return unless node.else?
229
229
 
230
- remove_whole_lines(corrector, leave_branch.source_range)
230
+ if leave_branch
231
+ remove_whole_lines(corrector, leave_branch.source_range)
232
+ corrector.insert_after(
233
+ heredoc_branch.last_argument.loc.heredoc_end, "\n#{leave_branch.source}"
234
+ )
235
+ end
236
+
231
237
  remove_whole_lines(corrector, node.loc.else)
232
238
  remove_whole_lines(corrector, range_of_branch_to_remove(node, guard))
233
- corrector.insert_after(
234
- heredoc_branch.last_argument.loc.heredoc_end, "\n#{leave_branch.source}"
235
- )
236
239
  end
237
240
 
238
241
  def range_of_branch_to_remove(node, guard)
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Enforces the use of either `Hash#[]` or `Hash#fetch` for hash lookup.
7
+ #
8
+ # This cop can be configured to prefer either bracket-style (`[]`)
9
+ # or fetch-style lookup. It is disabled by default.
10
+ #
11
+ # When enforcing `fetch` style, only single-argument bracket access is flagged.
12
+ # When enforcing `brackets` style, only `fetch` calls with a single key
13
+ # argument are flagged (not those with default values or blocks).
14
+ #
15
+ # @safety
16
+ # This cop is unsafe because `Hash#[]` and `Hash#fetch` have different
17
+ # semantics. `Hash#[]` returns `nil` for missing keys, while `Hash#fetch`
18
+ # raises a `KeyError`. Replacing one with the other can change program
19
+ # behavior in cases where the key is missing.
20
+ #
21
+ # Additionally, it cannot be guaranteed that the receiver is a `Hash`
22
+ # or responds to the replacement method.
23
+ #
24
+ # @example EnforcedStyle: brackets (default)
25
+ # # bad
26
+ # hash.fetch(key)
27
+ #
28
+ # # good
29
+ # hash[key]
30
+ #
31
+ # # good - fetch with default value is allowed
32
+ # hash.fetch(key, default)
33
+ #
34
+ # # good - fetch with block is allowed
35
+ # hash.fetch(key) { default }
36
+ #
37
+ # @example EnforcedStyle: fetch
38
+ # # bad
39
+ # hash[key]
40
+ #
41
+ # # good
42
+ # hash.fetch(key)
43
+ #
44
+ class HashLookupMethod < Base
45
+ include ConfigurableEnforcedStyle
46
+ extend AutoCorrector
47
+
48
+ BRACKET_MSG = 'Use `Hash#[]` instead of `Hash#fetch`.'
49
+ FETCH_MSG = 'Use `Hash#fetch` instead of `Hash#[]`.'
50
+
51
+ RESTRICT_ON_SEND = %i[[] fetch].freeze
52
+
53
+ def on_send(node)
54
+ if offense_for_brackets?(node)
55
+ add_offense(node.loc.selector, message: BRACKET_MSG) do |corrector|
56
+ correct_fetch_to_brackets(corrector, node)
57
+ end
58
+ elsif offense_for_fetch?(node)
59
+ add_offense(node, message: FETCH_MSG) do |corrector|
60
+ correct_brackets_to_fetch(corrector, node)
61
+ end
62
+ end
63
+ end
64
+ alias on_csend on_send
65
+
66
+ private
67
+
68
+ def offense_for_brackets?(node)
69
+ style == :brackets && node.receiver && node.method?(:fetch) && node.arguments.one? &&
70
+ !node.block_literal?
71
+ end
72
+
73
+ def offense_for_fetch?(node)
74
+ style == :fetch && node.method?(:[]) && node.arguments.one?
75
+ end
76
+
77
+ def correct_fetch_to_brackets(corrector, node)
78
+ receiver = node.receiver.source
79
+ key = node.first_argument.source
80
+ replacement = "#{receiver}[#{key}]"
81
+ replacement = "(#{replacement})" if node.csend_type?
82
+ corrector.replace(node, replacement)
83
+ end
84
+
85
+ def correct_brackets_to_fetch(corrector, node)
86
+ receiver = node.receiver.source
87
+ key = node.first_argument.source
88
+ operator = node.csend_type? ? '&.' : '.'
89
+ corrector.replace(node, "#{receiver}#{operator}fetch(#{key})")
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end