rubocop 1.75.8 → 1.80.1

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 (131) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -16
  3. data/config/default.yml +107 -26
  4. data/config/obsoletion.yml +6 -3
  5. data/exe/rubocop +1 -8
  6. data/lib/rubocop/cli.rb +17 -1
  7. data/lib/rubocop/config_loader.rb +1 -38
  8. data/lib/rubocop/cop/bundler/ordered_gems.rb +1 -1
  9. data/lib/rubocop/cop/correctors/alignment_corrector.rb +6 -3
  10. data/lib/rubocop/cop/correctors/for_to_each_corrector.rb +7 -2
  11. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +5 -2
  12. data/lib/rubocop/cop/gemspec/attribute_assignment.rb +91 -0
  13. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +0 -22
  14. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +1 -1
  15. data/lib/rubocop/cop/gemspec/require_mfa.rb +15 -1
  16. data/lib/rubocop/cop/internal_affairs/example_description.rb +1 -1
  17. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +4 -4
  18. data/lib/rubocop/cop/internal_affairs/node_pattern_groups.rb +1 -0
  19. data/lib/rubocop/cop/internal_affairs/node_type_group.rb +3 -2
  20. data/lib/rubocop/cop/internal_affairs/useless_restrict_on_send.rb +1 -1
  21. data/lib/rubocop/cop/layout/closing_parenthesis_indentation.rb +1 -1
  22. data/lib/rubocop/cop/layout/empty_lines_after_module_inclusion.rb +101 -0
  23. data/lib/rubocop/cop/layout/empty_lines_around_access_modifier.rb +1 -1
  24. data/lib/rubocop/cop/layout/empty_lines_around_arguments.rb +8 -29
  25. data/lib/rubocop/cop/layout/empty_lines_around_class_body.rb +1 -1
  26. data/lib/rubocop/cop/layout/line_length.rb +26 -5
  27. data/lib/rubocop/cop/layout/multiline_operation_indentation.rb +2 -0
  28. data/lib/rubocop/cop/layout/space_around_keyword.rb +6 -1
  29. data/lib/rubocop/cop/layout/space_around_operators.rb +8 -0
  30. data/lib/rubocop/cop/layout/space_before_brackets.rb +2 -9
  31. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +7 -2
  32. data/lib/rubocop/cop/lint/ambiguous_range.rb +5 -0
  33. data/lib/rubocop/cop/lint/duplicate_methods.rb +25 -4
  34. data/lib/rubocop/cop/lint/duplicate_regexp_character_class_element.rb +5 -42
  35. data/lib/rubocop/cop/lint/empty_interpolation.rb +3 -1
  36. data/lib/rubocop/cop/lint/float_comparison.rb +4 -4
  37. data/lib/rubocop/cop/lint/identity_comparison.rb +19 -15
  38. data/lib/rubocop/cop/lint/literal_as_condition.rb +34 -28
  39. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +1 -2
  40. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +1 -0
  41. data/lib/rubocop/cop/lint/redundant_regexp_quantifiers.rb +1 -1
  42. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +101 -2
  43. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +4 -4
  44. data/lib/rubocop/cop/lint/require_range_parentheses.rb +1 -1
  45. data/lib/rubocop/cop/lint/rescue_type.rb +1 -1
  46. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +4 -4
  47. data/lib/rubocop/cop/lint/self_assignment.rb +30 -4
  48. data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +5 -0
  49. data/lib/rubocop/cop/lint/useless_access_modifier.rb +29 -4
  50. data/lib/rubocop/cop/lint/useless_default_value_argument.rb +90 -0
  51. data/lib/rubocop/cop/lint/useless_numeric_operation.rb +1 -0
  52. data/lib/rubocop/cop/lint/useless_or.rb +98 -0
  53. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +3 -3
  54. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +121 -0
  55. data/lib/rubocop/cop/mixin/alignment.rb +1 -1
  56. data/lib/rubocop/cop/mixin/end_keyword_alignment.rb +1 -7
  57. data/lib/rubocop/cop/mixin/frozen_string_literal.rb +1 -1
  58. data/lib/rubocop/cop/mixin/gemspec_help.rb +22 -0
  59. data/lib/rubocop/cop/mixin/line_length_help.rb +24 -8
  60. data/lib/rubocop/cop/mixin/ordered_gem_node.rb +1 -1
  61. data/lib/rubocop/cop/naming/file_name.rb +2 -2
  62. data/lib/rubocop/cop/naming/method_name.rb +127 -13
  63. data/lib/rubocop/cop/naming/predicate_method.rb +306 -0
  64. data/lib/rubocop/cop/naming/{predicate_name.rb → predicate_prefix.rb} +4 -4
  65. data/lib/rubocop/cop/security/eval.rb +2 -1
  66. data/lib/rubocop/cop/security/open.rb +1 -0
  67. data/lib/rubocop/cop/style/access_modifier_declarations.rb +1 -1
  68. data/lib/rubocop/cop/style/accessor_grouping.rb +13 -1
  69. data/lib/rubocop/cop/style/arguments_forwarding.rb +11 -17
  70. data/lib/rubocop/cop/style/array_intersect.rb +98 -34
  71. data/lib/rubocop/cop/style/bitwise_predicate.rb +8 -1
  72. data/lib/rubocop/cop/style/block_delimiters.rb +1 -1
  73. data/lib/rubocop/cop/style/case_like_if.rb +1 -1
  74. data/lib/rubocop/cop/style/collection_querying.rb +167 -0
  75. data/lib/rubocop/cop/style/conditional_assignment.rb +4 -2
  76. data/lib/rubocop/cop/style/dig_chain.rb +1 -1
  77. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +100 -0
  78. data/lib/rubocop/cop/style/exponential_notation.rb +3 -2
  79. data/lib/rubocop/cop/style/fetch_env_var.rb +32 -6
  80. data/lib/rubocop/cop/style/hash_conversion.rb +16 -8
  81. data/lib/rubocop/cop/style/if_unless_modifier.rb +13 -6
  82. data/lib/rubocop/cop/style/infinite_loop.rb +1 -1
  83. data/lib/rubocop/cop/style/inverse_methods.rb +1 -1
  84. data/lib/rubocop/cop/style/it_assignment.rb +69 -12
  85. data/lib/rubocop/cop/style/it_block_parameter.rb +36 -15
  86. data/lib/rubocop/cop/style/map_to_hash.rb +1 -3
  87. data/lib/rubocop/cop/style/map_to_set.rb +1 -3
  88. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +4 -6
  89. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +16 -0
  90. data/lib/rubocop/cop/style/min_max_comparison.rb +13 -5
  91. data/lib/rubocop/cop/style/parallel_assignment.rb +32 -20
  92. data/lib/rubocop/cop/style/redundant_array_flatten.rb +50 -0
  93. data/lib/rubocop/cop/style/redundant_begin.rb +34 -0
  94. data/lib/rubocop/cop/style/redundant_condition.rb +1 -1
  95. data/lib/rubocop/cop/style/redundant_fetch_block.rb +1 -9
  96. data/lib/rubocop/cop/style/redundant_freeze.rb +1 -1
  97. data/lib/rubocop/cop/style/redundant_interpolation.rb +1 -1
  98. data/lib/rubocop/cop/style/redundant_line_continuation.rb +1 -1
  99. data/lib/rubocop/cop/style/redundant_parentheses.rb +42 -6
  100. data/lib/rubocop/cop/style/redundant_self.rb +8 -5
  101. data/lib/rubocop/cop/style/safe_navigation.rb +38 -12
  102. data/lib/rubocop/cop/style/single_line_methods.rb +7 -4
  103. data/lib/rubocop/cop/style/sole_nested_conditional.rb +32 -2
  104. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +1 -1
  105. data/lib/rubocop/cop/style/string_concatenation.rb +1 -1
  106. data/lib/rubocop/cop/style/symbol_array.rb +1 -1
  107. data/lib/rubocop/cop/style/symbol_proc.rb +1 -1
  108. data/lib/rubocop/cop/style/trailing_comma_in_block_args.rb +1 -1
  109. data/lib/rubocop/cop/variable_force/variable.rb +1 -1
  110. data/lib/rubocop/cop/variable_force.rb +25 -8
  111. data/lib/rubocop/cops_documentation_generator.rb +1 -0
  112. data/lib/rubocop/formatter/disabled_config_formatter.rb +18 -5
  113. data/lib/rubocop/formatter/fuubar_style_formatter.rb +1 -1
  114. data/lib/rubocop/formatter/markdown_formatter.rb +1 -0
  115. data/lib/rubocop/formatter/offense_count_formatter.rb +1 -1
  116. data/lib/rubocop/formatter/pacman_formatter.rb +1 -0
  117. data/lib/rubocop/lsp/diagnostic.rb +4 -4
  118. data/lib/rubocop/lsp/routes.rb +35 -6
  119. data/lib/rubocop/pending_cops_reporter.rb +56 -0
  120. data/lib/rubocop/result_cache.rb +14 -12
  121. data/lib/rubocop/rspec/expect_offense.rb +9 -3
  122. data/lib/rubocop/runner.rb +6 -4
  123. data/lib/rubocop/server/cache.rb +4 -2
  124. data/lib/rubocop/server/client_command/base.rb +10 -0
  125. data/lib/rubocop/server/client_command/exec.rb +2 -1
  126. data/lib/rubocop/server/client_command/start.rb +11 -1
  127. data/lib/rubocop/target_finder.rb +9 -9
  128. data/lib/rubocop/version.rb +1 -1
  129. data/lib/rubocop.rb +11 -1
  130. data/lib/ruby_lsp/rubocop/addon.rb +2 -2
  131. metadata +17 -7
@@ -6,6 +6,10 @@ module RuboCop
6
6
  # Checks for duplicated instance (or singleton) method
7
7
  # definitions.
8
8
  #
9
+ # NOTE: Aliasing a method to itself is allowed, as it indicates that
10
+ # the developer intends to suppress Ruby's method redefinition warnings.
11
+ # See https://bugs.ruby-lang.org/issues/13574.
12
+ #
9
13
  # @example
10
14
  #
11
15
  # # bad
@@ -40,6 +44,18 @@ module RuboCop
40
44
  #
41
45
  # alias bar foo
42
46
  #
47
+ # # good
48
+ # alias foo foo
49
+ # def foo
50
+ # 1
51
+ # end
52
+ #
53
+ # # good
54
+ # alias_method :foo, :foo
55
+ # def foo
56
+ # 1
57
+ # end
58
+ #
43
59
  # @example AllCops:ActiveSupportExtensionsEnabled: false (default)
44
60
  #
45
61
  # # good
@@ -113,11 +129,13 @@ module RuboCop
113
129
 
114
130
  # @!method method_alias?(node)
115
131
  def_node_matcher :method_alias?, <<~PATTERN
116
- (alias (sym $_name) sym)
132
+ (alias (sym $_name) (sym $_original_name))
117
133
  PATTERN
118
134
 
119
135
  def on_alias(node)
120
- return unless (name = method_alias?(node))
136
+ name, original_name = method_alias?(node)
137
+ return unless name && original_name
138
+ return if name == original_name
121
139
  return if node.ancestors.any?(&:if_type?)
122
140
 
123
141
  found_instance_method(node, name)
@@ -125,7 +143,7 @@ module RuboCop
125
143
 
126
144
  # @!method alias_method?(node)
127
145
  def_node_matcher :alias_method?, <<~PATTERN
128
- (send nil? :alias_method (sym $_name) _)
146
+ (send nil? :alias_method (sym $_name) (sym $_original_name))
129
147
  PATTERN
130
148
 
131
149
  # @!method delegate_method?(node)
@@ -140,7 +158,10 @@ module RuboCop
140
158
  def_node_matcher :sym_name, '(sym $_name)'
141
159
 
142
160
  def on_send(node) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
143
- if (name = alias_method?(node))
161
+ name, original_name = alias_method?(node)
162
+
163
+ if name && original_name
164
+ return if name == original_name
144
165
  return if node.ancestors.any?(&:if_type?)
145
166
 
146
167
  found_instance_method(node, name)
@@ -24,8 +24,6 @@ module RuboCop
24
24
 
25
25
  MSG_REPEATED_ELEMENT = 'Duplicate element inside regexp character class'
26
26
 
27
- OCTAL_DIGITS_AFTER_ESCAPE = 2
28
-
29
27
  def on_regexp(node)
30
28
  each_repeated_character_class_element_loc(node) do |loc|
31
29
  add_offense(loc, message: MSG_REPEATED_ELEMENT) do |corrector|
@@ -40,9 +38,9 @@ module RuboCop
40
38
 
41
39
  seen = Set.new
42
40
  group_expressions(node, expr.expressions) do |group|
43
- group_source = group.map(&:to_s).join
41
+ group_source = group.to_s
44
42
 
45
- yield source_range(group) if seen.include?(group_source)
43
+ yield group.expression if seen.include?(group_source)
46
44
 
47
45
  seen << group_source
48
46
  end
@@ -52,40 +50,13 @@ module RuboCop
52
50
  private
53
51
 
54
52
  def group_expressions(node, expressions)
55
- # Create a mutable list to simplify state tracking while we iterate.
56
- expressions = expressions.to_a
57
-
58
- until expressions.empty?
59
- # With we may need to compose a group of multiple expressions.
60
- group = [expressions.shift]
61
- next if within_interpolation?(node, group.first)
62
-
63
- # With regexp_parser < 2.7 escaped octal sequences may be up to 3
64
- # separate expressions ("\\0", "0", "1").
65
- pop_octal_digits(group, expressions) if escaped_octal?(group.first.to_s)
66
-
67
- yield(group)
68
- end
69
- end
70
-
71
- def pop_octal_digits(current_child, expressions)
72
- OCTAL_DIGITS_AFTER_ESCAPE.times do
73
- next_child = expressions.first
74
- break unless octal?(next_child.to_s)
53
+ expressions.each do |expression|
54
+ next if within_interpolation?(node, expression)
75
55
 
76
- current_child << expressions.shift
56
+ yield(expression)
77
57
  end
78
58
  end
79
59
 
80
- def source_range(children)
81
- return children.first.expression if children.size == 1
82
-
83
- range_between(
84
- children.first.expression.begin_pos,
85
- children.last.expression.begin_pos + children.last.to_s.length
86
- )
87
- end
88
-
89
60
  def skip_expression?(expr)
90
61
  expr.type != :set || expr.token == :intersection
91
62
  end
@@ -99,14 +70,6 @@ module RuboCop
99
70
  interpolation_locs(node).any? { |il| il.overlaps?(parse_tree_child_loc) }
100
71
  end
101
72
 
102
- def escaped_octal?(string)
103
- string.length == 2 && string[0] == '\\' && octal?(string[1])
104
- end
105
-
106
- def octal?(char)
107
- ('0'..'7').cover?(char)
108
- end
109
-
110
73
  def interpolation_locs(node)
111
74
  @interpolation_locs ||= {}
112
75
 
@@ -19,7 +19,9 @@ module RuboCop
19
19
  MSG = 'Empty interpolation detected.'
20
20
 
21
21
  def on_interpolation(begin_node)
22
- return unless begin_node.children.empty?
22
+ node_children = begin_node.children.dup
23
+ node_children.delete_if { |e| e.nil_type? || (e.basic_literal? && e.str_content&.empty?) }
24
+ return unless node_children.empty?
23
25
 
24
26
  add_offense(begin_node) { |corrector| corrector.remove(begin_node) }
25
27
  end
@@ -94,7 +94,7 @@ module RuboCop
94
94
  when :float
95
95
  true
96
96
  when :send
97
- check_send(node)
97
+ float_send?(node)
98
98
  when :begin
99
99
  float?(node.children.first)
100
100
  else
@@ -108,18 +108,18 @@ module RuboCop
108
108
  (node.numeric_type? && node.value.zero?) || node.nil_type?
109
109
  end
110
110
 
111
- def check_send(node)
111
+ def float_send?(node)
112
112
  if node.arithmetic_operation?
113
113
  float?(node.receiver) || float?(node.first_argument)
114
114
  elsif FLOAT_RETURNING_METHODS.include?(node.method_name)
115
115
  true
116
116
  elsif node.receiver&.float_type?
117
117
  FLOAT_INSTANCE_METHODS.include?(node.method_name) ||
118
- check_numeric_returning_method(node)
118
+ numeric_returning_method?(node)
119
119
  end
120
120
  end
121
121
 
122
- def check_numeric_returning_method(node)
122
+ def numeric_returning_method?(node)
123
123
  return false unless node.receiver
124
124
 
125
125
  case node.method_name
@@ -11,39 +11,43 @@ module RuboCop
11
11
  # @example
12
12
  # # bad
13
13
  # foo.object_id == bar.object_id
14
+ # foo.object_id != baz.object_id
14
15
  #
15
16
  # # good
16
17
  # foo.equal?(bar)
18
+ # !foo.equal?(baz)
17
19
  #
18
20
  class IdentityComparison < Base
19
21
  extend AutoCorrector
20
22
 
21
- MSG = 'Use `equal?` instead `==` when comparing `object_id`.'
22
- RESTRICT_ON_SEND = %i[==].freeze
23
+ MSG = 'Use `%<bang>sequal?` instead of `%<comparison_method>s` when comparing `object_id`.'
24
+ RESTRICT_ON_SEND = %i[== !=].freeze
25
+
26
+ # @!method object_id_comparison(node)
27
+ def_node_matcher :object_id_comparison, <<~PATTERN
28
+ (send
29
+ (send
30
+ _lhs_receiver :object_id) ${:== :!=}
31
+ (send
32
+ _rhs_receiver :object_id))
33
+ PATTERN
23
34
 
24
35
  def on_send(node)
25
- return unless compare_between_object_id_by_double_equal?(node)
36
+ return unless (comparison_method = object_id_comparison(node))
26
37
 
27
- add_offense(node) do |corrector|
38
+ bang = comparison_method == :== ? '' : '!'
39
+ add_offense(node,
40
+ message: format(MSG, comparison_method: comparison_method,
41
+ bang: bang)) do |corrector|
28
42
  receiver = node.receiver.receiver
29
43
  argument = node.first_argument.receiver
30
44
  return unless receiver && argument
31
45
 
32
- replacement = "#{receiver.source}.equal?(#{argument.source})"
46
+ replacement = "#{bang}#{receiver.source}.equal?(#{argument.source})"
33
47
 
34
48
  corrector.replace(node, replacement)
35
49
  end
36
50
  end
37
-
38
- private
39
-
40
- def compare_between_object_id_by_double_equal?(node)
41
- object_id_method?(node.receiver) && object_id_method?(node.first_argument)
42
- end
43
-
44
- def object_id_method?(node)
45
- node.send_type? && node.method?(:object_id)
46
- end
47
51
  end
48
52
  end
49
53
  end
@@ -54,6 +54,18 @@ module RuboCop
54
54
  end
55
55
  end
56
56
 
57
+ def on_or(node)
58
+ return unless node.lhs.falsey_literal?
59
+
60
+ add_offense(node.lhs) do |corrector|
61
+ # Don't autocorrect `'foo' && return` because having `return` as
62
+ # the leftmost node can lead to a void value expression syntax error.
63
+ next if node.rhs.type?(:return, :break, :next)
64
+
65
+ corrector.replace(node, node.rhs.source)
66
+ end
67
+ end
68
+
57
69
  def on_if(node)
58
70
  cond = condition(node)
59
71
 
@@ -123,7 +135,9 @@ module RuboCop
123
135
  # rubocop:enable Metrics/AbcSize
124
136
 
125
137
  def on_case(case_node)
126
- if case_node.condition
138
+ if (cond = case_node.condition)
139
+ return if !cond.falsey_literal? && !cond.truthy_literal?
140
+
127
141
  check_case(case_node)
128
142
  else
129
143
  case_node.when_branches.each do |when_node|
@@ -228,7 +242,7 @@ module RuboCop
228
242
  )
229
243
  end
230
244
 
231
- def condition_evaluation(node, cond)
245
+ def condition_evaluation?(node, cond)
232
246
  if node.unless?
233
247
  cond.falsey_literal?
234
248
  else
@@ -238,32 +252,24 @@ module RuboCop
238
252
 
239
253
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
240
254
  def correct_if_node(node, cond)
241
- result = condition_evaluation(node, cond)
242
-
243
- if node.elsif? && result
244
- add_offense(cond) do |corrector|
245
- corrector.replace(node, "else\n #{node.if_branch.source}")
246
- end
247
- elsif node.elsif? && !result
248
- add_offense(cond) do |corrector|
249
- corrector.replace(node, "else\n #{node.else_branch.source}")
250
- end
251
- elsif node.if_branch && result
252
- add_offense(cond) do |corrector|
253
- corrector.replace(node, node.if_branch.source)
254
- end
255
- elsif node.elsif_conditional?
256
- add_offense(cond) do |corrector|
257
- corrector.replace(node, "#{node.else_branch.source.sub('elsif', 'if')}\nend")
258
- end
259
- elsif node.else? || node.ternary?
260
- add_offense(cond) do |corrector|
261
- corrector.replace(node, node.else_branch.source)
262
- end
263
- else
264
- add_offense(cond) do |corrector|
265
- corrector.remove(node)
266
- end
255
+ result = condition_evaluation?(node, cond)
256
+
257
+ new_node = if node.elsif? && result
258
+ "else\n #{range_with_comments(node.if_branch).source}"
259
+ elsif node.elsif? && !result
260
+ "else\n #{node.else_branch.source}"
261
+ elsif node.if_branch && result
262
+ node.if_branch.source
263
+ elsif node.elsif_conditional?
264
+ "#{node.else_branch.source.sub('elsif', 'if')}\nend"
265
+ elsif node.else? || node.ternary?
266
+ node.else_branch.source
267
+ else
268
+ '' # Equivalent to removing the node
269
+ end
270
+
271
+ add_offense(cond) do |corrector|
272
+ corrector.replace(node, new_node)
267
273
  end
268
274
  end
269
275
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -52,10 +52,9 @@ module RuboCop
52
52
  each_missing_enable do |cop, line_range|
53
53
  next if acceptable_range?(cop, line_range)
54
54
 
55
- range = source_range(processed_source.buffer, line_range.min, 0..0)
56
55
  comment = processed_source.comment_at_line(line_range.begin)
57
56
 
58
- add_offense(range, message: message(cop, comment))
57
+ add_offense(comment, message: message(cop, comment))
59
58
  end
60
59
  end
61
60
 
@@ -45,6 +45,7 @@ module RuboCop
45
45
  #
46
46
  class NumericOperationWithConstantResult < Base
47
47
  extend AutoCorrector
48
+
48
49
  MSG = 'Numeric operation with a constant result detected.'
49
50
  RESTRICT_ON_SEND = %i[* / **].freeze
50
51
 
@@ -73,7 +73,7 @@ module RuboCop
73
73
  end
74
74
 
75
75
  def redundant_group?(expr)
76
- expr.is?(:passive, :group) && expr.count { |child| child.type != :free_space } == 1
76
+ expr.is?(:passive, :group) && expr.one? { |child| child.type != :free_space }
77
77
  end
78
78
 
79
79
  def redundantly_quantifiable?(node)
@@ -20,6 +20,15 @@ module RuboCop
20
20
  # In the example below, the safe navigation operator (`&.`) is unnecessary
21
21
  # because `NilClass` has methods like `respond_to?` and `is_a?`.
22
22
  #
23
+ # The `InferNonNilReceiver` option specifies whether to look into previous code
24
+ # paths to infer if the receiver can't be nil. This check is unsafe because the receiver
25
+ # can be redefined between the safe navigation call and previous regular method call.
26
+ # It does the inference only in the current scope, e.g. within the same method definition etc.
27
+ #
28
+ # The `AdditionalNilMethods` option specifies additional custom methods which are
29
+ # defined on `NilClass`. When `InferNonNilReceiver` is set, they are used to determine
30
+ # whether the receiver can be nil.
31
+ #
23
32
  # @safety
24
33
  # This cop is unsafe, because autocorrection can change the return type of
25
34
  # the expression. An offending expression that previously could return `nil`
@@ -33,6 +42,20 @@ module RuboCop
33
42
  # CamelCaseConst.do_something
34
43
  #
35
44
  # # bad
45
+ # foo.to_s&.strip
46
+ # foo.to_i&.zero?
47
+ # foo.to_f&.zero?
48
+ # foo.to_a&.size
49
+ # foo.to_h&.size
50
+ #
51
+ # # good
52
+ # foo.to_s.strip
53
+ # foo.to_i.zero?
54
+ # foo.to_f.zero?
55
+ # foo.to_a.size
56
+ # foo.to_h.size
57
+ #
58
+ # # bad
36
59
  # do_something if attrs&.respond_to?(:[])
37
60
  #
38
61
  # # good
@@ -81,17 +104,59 @@ module RuboCop
81
104
  # do_something if attrs.nil_safe_method(:[])
82
105
  # do_something if attrs&.not_nil_safe_method(:[])
83
106
  #
107
+ # @example InferNonNilReceiver: false (default)
108
+ # # good
109
+ # foo.bar
110
+ # foo&.baz
111
+ #
112
+ # @example InferNonNilReceiver: true
113
+ # # bad
114
+ # foo.bar
115
+ # foo&.baz # would raise on previous line if `foo` is nil
116
+ #
117
+ # # good
118
+ # foo.bar
119
+ # foo.baz
120
+ #
121
+ # # bad
122
+ # if foo.condition?
123
+ # foo&.bar
124
+ # end
125
+ #
126
+ # # good
127
+ # if foo.condition?
128
+ # foo.bar
129
+ # end
130
+ #
131
+ # # good (different scopes)
132
+ # def method1
133
+ # foo.bar
134
+ # end
135
+ #
136
+ # def method2
137
+ # foo&.bar
138
+ # end
139
+ #
140
+ # @example AdditionalNilMethods: [present?]
141
+ # # good
142
+ # foo.present?
143
+ # foo&.bar
144
+ #
84
145
  class RedundantSafeNavigation < Base
85
146
  include AllowedMethods
86
147
  extend AutoCorrector
87
148
 
88
149
  MSG = 'Redundant safe navigation detected, use `.` instead.'
89
150
  MSG_LITERAL = 'Redundant safe navigation with default literal detected.'
151
+ MSG_NON_NIL = 'Redundant safe navigation on non-nil receiver (detected by analyzing ' \
152
+ 'previous code/method invocations).'
90
153
 
91
154
  NIL_SPECIFIC_METHODS = (nil.methods - Object.new.methods).to_set.freeze
92
155
 
93
156
  SNAKE_CASE = /\A[[:digit:][:upper:]_]+\z/.freeze
94
157
 
158
+ GUARANTEED_INSTANCE_METHODS = %i[to_s to_i to_f to_a to_h].freeze
159
+
95
160
  # @!method respond_to_nil_specific_method?(node)
96
161
  def_node_matcher :respond_to_nil_specific_method?, <<~PATTERN
97
162
  (csend _ :respond_to? (sym %NIL_SPECIFIC_METHODS))
@@ -111,15 +176,27 @@ module RuboCop
111
176
 
112
177
  # rubocop:disable Metrics/AbcSize
113
178
  def on_csend(node)
179
+ range = node.loc.dot
180
+
181
+ if infer_non_nil_receiver?
182
+ checker = Lint::Utils::NilReceiverChecker.new(node.receiver, additional_nil_methods)
183
+
184
+ if checker.cant_be_nil?
185
+ add_offense(range, message: MSG_NON_NIL) { |corrector| corrector.replace(range, '.') }
186
+ return
187
+ end
188
+ end
189
+
114
190
  unless assume_receiver_instance_exists?(node.receiver)
115
- return unless check?(node) && allowed_method?(node.method_name)
191
+ return if !guaranteed_instance?(node.receiver) && !check?(node)
116
192
  return if respond_to_nil_specific_method?(node)
117
193
  end
118
194
 
119
- range = node.loc.dot
120
195
  add_offense(range) { |corrector| corrector.replace(range, '.') }
121
196
  end
197
+ # rubocop:enable Metrics/AbcSize
122
198
 
199
+ # rubocop:disable Metrics/AbcSize
123
200
  def on_or(node)
124
201
  conversion_with_default?(node) do |send_node|
125
202
  range = send_node.loc.dot.begin.join(node.source_range.end)
@@ -142,7 +219,20 @@ module RuboCop
142
219
  receiver.self_type? || (receiver.literal? && !receiver.nil_type?)
143
220
  end
144
221
 
222
+ def guaranteed_instance?(node)
223
+ receiver = if node.any_block_type?
224
+ node.send_node
225
+ else
226
+ node
227
+ end
228
+ return false unless receiver.send_type?
229
+
230
+ GUARANTEED_INSTANCE_METHODS.include?(receiver.method_name)
231
+ end
232
+
145
233
  def check?(node)
234
+ return false unless allowed_method?(node.method_name)
235
+
146
236
  parent = node.parent
147
237
  return false unless parent
148
238
 
@@ -154,6 +244,15 @@ module RuboCop
154
244
  def condition?(parent, node)
155
245
  (parent.conditional? || parent.post_condition_loop?) && parent.condition == node
156
246
  end
247
+
248
+ def infer_non_nil_receiver?
249
+ cop_config['InferNonNilReceiver']
250
+ end
251
+
252
+ def additional_nil_methods
253
+ @additional_nil_methods ||=
254
+ Array(cop_config.fetch('AdditionalNilMethods', []).map(&:to_sym))
255
+ end
157
256
  end
158
257
  end
159
258
  end
@@ -185,9 +185,9 @@ module RuboCop
185
185
  (hash (pair (sym :exception) false))
186
186
  PATTERN
187
187
 
188
- # rubocop:disable Metrics/AbcSize
188
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
189
189
  def on_send(node)
190
- return if hash_or_set_with_block?(node)
190
+ return if node.arguments.any? || hash_or_set_with_block?(node)
191
191
 
192
192
  receiver = find_receiver(node)
193
193
  return unless literal_receiver?(node, receiver) ||
@@ -198,10 +198,10 @@ module RuboCop
198
198
  message = format(MSG, method: node.method_name)
199
199
 
200
200
  add_offense(node.loc.selector, message: message) do |corrector|
201
- corrector.remove(node.loc.dot.join(node.loc.selector))
201
+ corrector.remove(node.loc.dot.join(node.loc.end || node.loc.selector))
202
202
  end
203
203
  end
204
- # rubocop:enable Metrics/AbcSize
204
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
205
205
  alias on_csend on_send
206
206
 
207
207
  private
@@ -43,7 +43,7 @@ module RuboCop
43
43
  def on_irange(node)
44
44
  return if node.parent&.begin_type?
45
45
  return unless node.begin && node.end
46
- return if same_line?(node.begin, node.end)
46
+ return if same_line?(node.loc.operator, node.end)
47
47
 
48
48
  message = format(MSG, range: "#{node.begin.source}#{node.loc.operator.source}")
49
49
 
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Lint
6
- # Check for arguments to `rescue` that will result in a `TypeError`
6
+ # Checks for arguments to `rescue` that will result in a `TypeError`
7
7
  # if an exception is raised.
8
8
  #
9
9
  # @example
@@ -97,7 +97,7 @@ module RuboCop
97
97
  end
98
98
 
99
99
  def require_parentheses?(send_node)
100
- return true if operator_inside_hash?(send_node)
100
+ return true if operator_inside_collection_literal?(send_node)
101
101
  return false unless send_node.comparison_method?
102
102
  return false unless (node = send_node.parent)
103
103
 
@@ -105,10 +105,10 @@ module RuboCop
105
105
  (node.respond_to?(:comparison_method?) && node.comparison_method?)
106
106
  end
107
107
 
108
- def operator_inside_hash?(send_node)
109
- # If an operator call (without a dot) is inside a hash, it needs
108
+ def operator_inside_collection_literal?(send_node)
109
+ # If an operator call (without a dot) is inside an array or a hash, it needs
110
110
  # to be parenthesized when converted to safe navigation.
111
- send_node.parent&.pair_type? && !send_node.loc.dot
111
+ send_node.parent&.type?(:array, :pair) && !send_node.loc.dot
112
112
  end
113
113
  end
114
114
  end