rubocop 1.66.1 → 1.68.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +55 -6
  4. data/config/internal_affairs.yml +11 -0
  5. data/lib/rubocop/cached_data.rb +12 -4
  6. data/lib/rubocop/cli/command/auto_generate_config.rb +6 -7
  7. data/lib/rubocop/cli/command/execute_runner.rb +1 -1
  8. data/lib/rubocop/cli/command/lsp.rb +2 -2
  9. data/lib/rubocop/cli/command/version.rb +2 -2
  10. data/lib/rubocop/config_loader_resolver.rb +3 -3
  11. data/lib/rubocop/config_validator.rb +2 -1
  12. data/lib/rubocop/cop/autocorrect_logic.rb +22 -2
  13. data/lib/rubocop/cop/base.rb +6 -2
  14. data/lib/rubocop/cop/bundler/gem_version.rb +1 -0
  15. data/lib/rubocop/cop/cop.rb +8 -0
  16. data/lib/rubocop/cop/correctors/alignment_corrector.rb +1 -12
  17. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +1 -1
  18. data/lib/rubocop/cop/correctors/percent_literal_corrector.rb +10 -0
  19. data/lib/rubocop/cop/internal_affairs/cop_description.rb +0 -4
  20. data/lib/rubocop/cop/internal_affairs/redundant_message_argument.rb +6 -21
  21. data/lib/rubocop/cop/internal_affairs/redundant_source_range.rb +8 -1
  22. data/lib/rubocop/cop/internal_affairs/useless_message_assertion.rb +0 -5
  23. data/lib/rubocop/cop/internal_affairs.rb +16 -0
  24. data/lib/rubocop/cop/layout/access_modifier_indentation.rb +5 -1
  25. data/lib/rubocop/cop/layout/def_end_alignment.rb +1 -1
  26. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +1 -1
  27. data/lib/rubocop/cop/layout/first_method_argument_line_break.rb +8 -0
  28. data/lib/rubocop/cop/layout/indentation_width.rb +4 -5
  29. data/lib/rubocop/cop/layout/leading_comment_space.rb +56 -1
  30. data/lib/rubocop/cop/layout/space_before_brackets.rb +5 -5
  31. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +4 -0
  32. data/lib/rubocop/cop/lint/ambiguous_range.rb +4 -1
  33. data/lib/rubocop/cop/lint/big_decimal_new.rb +4 -7
  34. data/lib/rubocop/cop/lint/boolean_symbol.rb +1 -1
  35. data/lib/rubocop/cop/lint/duplicate_branch.rb +39 -4
  36. data/lib/rubocop/cop/lint/duplicate_set_element.rb +74 -0
  37. data/lib/rubocop/cop/lint/ensure_return.rb +0 -3
  38. data/lib/rubocop/cop/lint/erb_new_arguments.rb +1 -1
  39. data/lib/rubocop/cop/lint/float_comparison.rb +1 -1
  40. data/lib/rubocop/cop/lint/implicit_string_concatenation.rb +10 -4
  41. data/lib/rubocop/cop/lint/it_without_arguments_in_block.rb +5 -14
  42. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +25 -2
  43. data/lib/rubocop/cop/lint/non_atomic_file_operation.rb +7 -0
  44. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +5 -6
  45. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +1 -1
  46. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +9 -0
  47. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +107 -41
  48. data/lib/rubocop/cop/lint/symbol_conversion.rb +1 -1
  49. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +88 -0
  50. data/lib/rubocop/cop/lint/uri_regexp.rb +25 -7
  51. data/lib/rubocop/cop/metrics/cyclomatic_complexity.rb +4 -1
  52. data/lib/rubocop/cop/mixin/check_line_breakable.rb +10 -0
  53. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +24 -0
  54. data/lib/rubocop/cop/mixin/frozen_string_literal.rb +3 -1
  55. data/lib/rubocop/cop/mixin/percent_array.rb +1 -1
  56. data/lib/rubocop/cop/mixin/statement_modifier.rb +3 -2
  57. data/lib/rubocop/cop/naming/block_forwarding.rb +1 -1
  58. data/lib/rubocop/cop/naming/inclusive_language.rb +12 -3
  59. data/lib/rubocop/cop/naming/predicate_name.rb +1 -1
  60. data/lib/rubocop/cop/offense.rb +4 -5
  61. data/lib/rubocop/cop/style/access_modifier_declarations.rb +12 -2
  62. data/lib/rubocop/cop/style/accessor_grouping.rb +10 -2
  63. data/lib/rubocop/cop/style/ambiguous_endless_method_definition.rb +79 -0
  64. data/lib/rubocop/cop/style/arguments_forwarding.rb +46 -6
  65. data/lib/rubocop/cop/style/bitwise_predicate.rb +100 -0
  66. data/lib/rubocop/cop/style/block_delimiters.rb +31 -3
  67. data/lib/rubocop/cop/style/collection_compact.rb +10 -10
  68. data/lib/rubocop/cop/style/combinable_defined.rb +115 -0
  69. data/lib/rubocop/cop/style/combinable_loops.rb +7 -0
  70. data/lib/rubocop/cop/style/commented_keyword.rb +7 -1
  71. data/lib/rubocop/cop/style/conditional_assignment.rb +1 -1
  72. data/lib/rubocop/cop/style/data_inheritance.rb +1 -1
  73. data/lib/rubocop/cop/style/endless_method.rb +1 -14
  74. data/lib/rubocop/cop/style/eval_with_location.rb +1 -1
  75. data/lib/rubocop/cop/style/guard_clause.rb +15 -2
  76. data/lib/rubocop/cop/style/hash_each_methods.rb +6 -0
  77. data/lib/rubocop/cop/style/hash_syntax.rb +2 -2
  78. data/lib/rubocop/cop/style/if_inside_else.rb +1 -1
  79. data/lib/rubocop/cop/style/if_with_semicolon.rb +7 -3
  80. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +67 -0
  81. data/lib/rubocop/cop/style/lambda.rb +1 -1
  82. data/lib/rubocop/cop/style/map_into_array.rb +59 -8
  83. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +12 -7
  84. data/lib/rubocop/cop/style/multiline_memoization.rb +1 -1
  85. data/lib/rubocop/cop/style/multiple_comparison.rb +28 -39
  86. data/lib/rubocop/cop/style/nested_modifier.rb +1 -1
  87. data/lib/rubocop/cop/style/nested_parenthesized_calls.rb +1 -1
  88. data/lib/rubocop/cop/style/one_line_conditional.rb +4 -0
  89. data/lib/rubocop/cop/style/operator_method_call.rb +25 -6
  90. data/lib/rubocop/cop/style/redundant_begin.rb +4 -0
  91. data/lib/rubocop/cop/style/redundant_condition.rb +1 -1
  92. data/lib/rubocop/cop/style/redundant_line_continuation.rb +23 -5
  93. data/lib/rubocop/cop/style/redundant_parentheses.rb +9 -11
  94. data/lib/rubocop/cop/style/require_order.rb +1 -1
  95. data/lib/rubocop/cop/style/rescue_modifier.rb +13 -1
  96. data/lib/rubocop/cop/style/return_nil_in_predicate_method_definition.rb +54 -12
  97. data/lib/rubocop/cop/style/safe_navigation.rb +104 -50
  98. data/lib/rubocop/cop/style/safe_navigation_chain_length.rb +52 -0
  99. data/lib/rubocop/cop/style/select_by_regexp.rb +9 -6
  100. data/lib/rubocop/cop/style/semicolon.rb +1 -1
  101. data/lib/rubocop/cop/style/struct_inheritance.rb +1 -1
  102. data/lib/rubocop/cop/style/ternary_parentheses.rb +26 -5
  103. data/lib/rubocop/cop/style/trivial_accessors.rb +1 -1
  104. data/lib/rubocop/cop/team.rb +8 -1
  105. data/lib/rubocop/cop/util.rb +1 -1
  106. data/lib/rubocop/cop/variable_force/assignment.rb +18 -3
  107. data/lib/rubocop/cop/variable_force/branch.rb +1 -1
  108. data/lib/rubocop/cop/variable_force/variable.rb +5 -1
  109. data/lib/rubocop/cop/variable_force/variable_table.rb +2 -2
  110. data/lib/rubocop/cops_documentation_generator.rb +81 -40
  111. data/lib/rubocop/file_finder.rb +9 -4
  112. data/lib/rubocop/formatter/disabled_config_formatter.rb +1 -1
  113. data/lib/rubocop/lsp/runtime.rb +2 -0
  114. data/lib/rubocop/lsp/server.rb +0 -1
  115. data/lib/rubocop/rspec/expect_offense.rb +1 -0
  116. data/lib/rubocop/runner.rb +17 -6
  117. data/lib/rubocop/server/cache.rb +6 -1
  118. data/lib/rubocop/server/core.rb +1 -0
  119. data/lib/rubocop/target_ruby.rb +13 -13
  120. data/lib/rubocop/version.rb +28 -7
  121. data/lib/rubocop/yaml_duplication_checker.rb +20 -27
  122. data/lib/rubocop.rb +10 -0
  123. metadata +13 -4
@@ -69,6 +69,8 @@ module RuboCop
69
69
  extend AutoCorrector
70
70
 
71
71
  MSG = 'Redundant line continuation.'
72
+ LINE_CONTINUATION = "\\\n"
73
+ LINE_CONTINUATION_PATTERN = /(\\\n)/.freeze
72
74
  ALLOWED_STRING_TOKENS = %i[tSTRING tSTRING_CONTENT].freeze
73
75
  ARGUMENT_TYPES = %i[
74
76
  kFALSE kNIL kSELF kTRUE tCONSTANT tCVAR tFLOAT tGVAR tIDENTIFIER tINTEGER tIVAR
@@ -79,7 +81,7 @@ module RuboCop
79
81
  def on_new_investigation
80
82
  return unless processed_source.ast
81
83
 
82
- each_match_range(processed_source.ast.source_range, /(\\\n)/) do |range|
84
+ each_match_range(processed_source.ast.source_range, LINE_CONTINUATION_PATTERN) do |range|
83
85
  next if require_line_continuation?(range)
84
86
  next unless redundant_line_continuation?(range)
85
87
 
@@ -87,6 +89,8 @@ module RuboCop
87
89
  corrector.remove_leading(range, 1)
88
90
  end
89
91
  end
92
+
93
+ inspect_eof_line_continuation
90
94
  end
91
95
 
92
96
  private
@@ -122,13 +126,27 @@ module RuboCop
122
126
  end
123
127
 
124
128
  def redundant_line_continuation?(range)
125
- return true unless (node = find_node_for_line(range.line))
129
+ return true unless (node = find_node_for_line(range.last_line))
126
130
  return false if argument_newline?(node)
127
131
 
128
- source = node.parent ? node.parent.source : node.source
132
+ source = node.source
133
+ while (node = node.parent)
134
+ source = node.source
135
+ end
129
136
  parse(source.gsub("\\\n", "\n")).valid_syntax?
130
137
  end
131
138
 
139
+ def inspect_eof_line_continuation
140
+ return unless processed_source.raw_source.end_with?(LINE_CONTINUATION)
141
+
142
+ rindex = processed_source.raw_source.rindex(LINE_CONTINUATION)
143
+ line_continuation_range = range_between(rindex, rindex + 1)
144
+
145
+ add_offense(line_continuation_range) do |corrector|
146
+ corrector.remove_trailing(line_continuation_range, 1)
147
+ end
148
+ end
149
+
132
150
  def inside_string_literal?(range, token)
133
151
  ALLOWED_STRING_TOKENS.include?(token.type) && token.pos.overlaps?(range)
134
152
  end
@@ -160,9 +178,9 @@ module RuboCop
160
178
  end
161
179
  # rubocop:enable Metrics/AbcSize
162
180
 
163
- def find_node_for_line(line)
181
+ def find_node_for_line(last_line)
164
182
  processed_source.ast.each_node do |node|
165
- return node if same_line?(node, line)
183
+ return node if node.respond_to?(:expression) && node.expression&.last_line == last_line
166
184
  end
167
185
  end
168
186
 
@@ -31,9 +31,6 @@ module RuboCop
31
31
  # @!method allowed_pin_operator?(node)
32
32
  def_node_matcher :allowed_pin_operator?, '^(pin (begin !{lvar ivar cvar gvar}))'
33
33
 
34
- # @!method arg_in_call_with_block?(node)
35
- def_node_matcher :arg_in_call_with_block?, '^^(block (send _ _ equal?(%0) ...) ...)'
36
-
37
34
  def on_begin(node)
38
35
  return if !parentheses?(node) || parens_allowed?(node) || ignore_syntax?(node)
39
36
 
@@ -59,7 +56,6 @@ module RuboCop
59
56
 
60
57
  def allowed_expression?(node)
61
58
  allowed_ancestor?(node) ||
62
- allowed_method_call?(node) ||
63
59
  allowed_multiple_expression?(node) ||
64
60
  allowed_ternary?(node) ||
65
61
  node.parent&.range_type?
@@ -70,11 +66,6 @@ module RuboCop
70
66
  keyword_ancestor?(node) && parens_required?(node)
71
67
  end
72
68
 
73
- def allowed_method_call?(node)
74
- # Don't flag `method (arg) { }`
75
- arg_in_call_with_block?(node) && !parentheses?(node.parent)
76
- end
77
-
78
69
  def allowed_multiple_expression?(node)
79
70
  return false if node.children.one?
80
71
 
@@ -160,7 +151,7 @@ module RuboCop
160
151
  return if node.semantic_operator? && begin_node.parent
161
152
  return if node.multiline? && allow_in_multiline_conditions?
162
153
  return if ALLOWED_NODE_TYPES.include?(begin_node.parent&.type)
163
- return if begin_node.parent&.if_type? && begin_node.parent&.ternary?
154
+ return if begin_node.parent&.if_type? && begin_node.parent.ternary?
164
155
 
165
156
  'a logical expression'
166
157
  elsif node.respond_to?(:comparison_method?) && node.comparison_method?
@@ -185,7 +176,8 @@ module RuboCop
185
176
  return check_unary(begin_node, node) if node.unary_operation?
186
177
 
187
178
  return unless method_call_with_redundant_parentheses?(node)
188
- return if call_chain_starts_with_int?(begin_node, node)
179
+ return if call_chain_starts_with_int?(begin_node, node) ||
180
+ do_end_block_in_method_chain?(begin_node, node)
189
181
 
190
182
  offense(begin_node, 'a method call')
191
183
  end
@@ -285,6 +277,12 @@ module RuboCop
285
277
  recv&.int_type? && (parent = begin_node.parent) &&
286
278
  parent.send_type? && (parent.method?(:-@) || parent.method?(:+@))
287
279
  end
280
+
281
+ def do_end_block_in_method_chain?(begin_node, node)
282
+ return false unless (block = node.each_descendant(:block, :numblock).first)
283
+
284
+ block.keywords? && begin_node.each_ancestor(:send, :csend).any?
285
+ end
288
286
  end
289
287
  end
290
288
  end
@@ -103,7 +103,7 @@ module RuboCop
103
103
  next unless sibling.is_a?(AST::Node)
104
104
 
105
105
  sibling = sibling_node(sibling)
106
- break unless sibling&.send_type? && sibling&.method?(node.method_name)
106
+ break unless sibling&.send_type? && sibling.method?(node.method_name)
107
107
  break unless sibling.arguments? && !sibling.receiver
108
108
  break unless in_same_section?(sibling, node)
109
109
  break unless node.first_argument.str_type? && sibling.first_argument.str_type?
@@ -75,7 +75,7 @@ module RuboCop
75
75
 
76
76
  corrector.remove(range_between(operation.source_range.end_pos, node.source_range.end_pos))
77
77
  corrector.insert_before(operation, "begin\n#{node_indentation}")
78
- corrector.insert_after(operation, <<~RESCUE_CLAUSE.chop)
78
+ corrector.insert_after(heredoc_end(operation) || operation, <<~RESCUE_CLAUSE.chop)
79
79
 
80
80
  #{node_offset}rescue
81
81
  #{node_indentation}#{rescue_args.source}
@@ -92,6 +92,18 @@ module RuboCop
92
92
  end
93
93
  [node_indentation, node_offset]
94
94
  end
95
+
96
+ def heredoc_end(node)
97
+ return unless node.call_type?
98
+
99
+ heredoc = node.arguments.reverse.find do |argument|
100
+ argument.respond_to?(:heredoc?) && argument.heredoc?
101
+ end
102
+
103
+ return unless heredoc
104
+
105
+ heredoc.loc.heredoc_end
106
+ end
95
107
  end
96
108
  end
97
109
  end
@@ -3,7 +3,8 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Style
6
- # Checks if `return` or `return nil` is used in predicate method definitions.
6
+ # Checks for predicate method definitions that return `nil`.
7
+ # A predicate method should only return a boolean value.
7
8
  #
8
9
  # @safety
9
10
  # Autocorrection is marked as unsafe because the change of the return value
@@ -31,6 +32,24 @@ module RuboCop
31
32
  # do_something?
32
33
  # end
33
34
  #
35
+ # # bad
36
+ # def foo?
37
+ # if condition
38
+ # nil
39
+ # else
40
+ # true
41
+ # end
42
+ # end
43
+ #
44
+ # # good
45
+ # def foo?
46
+ # if condition
47
+ # false
48
+ # else
49
+ # true
50
+ # end
51
+ # end
52
+ #
34
53
  # @example AllowedMethods: ['foo?']
35
54
  # # good
36
55
  # def foo?
@@ -64,24 +83,25 @@ module RuboCop
64
83
  return if allowed_method?(node.method_name) || matches_allowed_pattern?(node.method_name)
65
84
  return unless (body = node.body)
66
85
 
67
- body.each_descendant(:return) do |return_node|
68
- register_offense(return_node, 'return false') if return_nil?(return_node)
69
- end
70
-
71
- return unless (nil_node = nil_node_at_the_end_of_method_body(body))
86
+ body.each_descendant(:return) { |return_node| handle_return(return_node) }
72
87
 
73
- register_offense(nil_node, 'false')
88
+ handle_implicit_return_values(body)
74
89
  end
75
90
  alias on_defs on_def
76
91
 
77
92
  private
78
93
 
79
- def nil_node_at_the_end_of_method_body(body)
80
- return body if body.nil_type?
81
- return unless body.begin_type?
82
- return unless (last_child = body.children.last)
94
+ def last_node_of_type(node, type)
95
+ return unless node
96
+ return node if node_type?(node, type)
97
+ return unless node.begin_type?
98
+ return unless (last_child = node.children.last)
99
+
100
+ last_child if last_child.is_a?(AST::Node) && node_type?(last_child, type)
101
+ end
83
102
 
84
- last_child if last_child.is_a?(AST::Node) && last_child.nil_type?
103
+ def node_type?(node, type)
104
+ node.type == type.to_sym
85
105
  end
86
106
 
87
107
  def register_offense(offense_node, replacement)
@@ -89,6 +109,28 @@ module RuboCop
89
109
  corrector.replace(offense_node, replacement)
90
110
  end
91
111
  end
112
+
113
+ def handle_implicit_return_values(node)
114
+ handle_if(last_node_of_type(node, :if))
115
+ handle_nil(last_node_of_type(node, :nil))
116
+ end
117
+
118
+ def handle_return(return_node)
119
+ register_offense(return_node, 'return false') if return_nil?(return_node)
120
+ end
121
+
122
+ def handle_nil(nil_node)
123
+ return unless nil_node
124
+
125
+ register_offense(nil_node, 'false')
126
+ end
127
+
128
+ def handle_if(if_node)
129
+ return unless if_node
130
+
131
+ handle_implicit_return_values(if_node.if_branch)
132
+ handle_implicit_return_values(if_node.else_branch)
133
+ end
92
134
  end
93
135
  end
94
136
  end
@@ -21,6 +21,11 @@ module RuboCop
21
21
  # We have limited the cop to not register an offense for method chains
22
22
  # that exceed this option's value.
23
23
  #
24
+ # NOTE: This cop will recognize offenses but not autocorrect code when the
25
+ # right hand side (RHS) of the `&&` statement is an `||` statement
26
+ # (eg. `foo && (foo.bar? || foo.baz?)`). It can be corrected
27
+ # manually by removing the `foo &&` and adding `&.` to each `foo` on the RHS.
28
+ #
24
29
  # @safety
25
30
  # Autocorrection is unsafe because if a value is `false`, the resulting
26
31
  # code will have different behavior or raise an error.
@@ -81,7 +86,7 @@ module RuboCop
81
86
  # foo.baz = bar if foo
82
87
  # foo.baz + bar if foo
83
88
  # foo.bar > 2 if foo
84
- class SafeNavigation < Base
89
+ class SafeNavigation < Base # rubocop:disable Metrics/ClassLength
85
90
  include NilMethods
86
91
  include RangeHelp
87
92
  extend AutoCorrector
@@ -121,50 +126,122 @@ module RuboCop
121
126
  }
122
127
  PATTERN
123
128
 
129
+ # @!method and_with_rhs_or?(node)
130
+ def_node_matcher :and_with_rhs_or?, '(and _ {or (begin or)})'
131
+
124
132
  # @!method not_nil_check?(node)
125
133
  def_node_matcher :not_nil_check?, '(send (send $_ :nil?) :!)'
126
134
 
135
+ # @!method and_inside_begin?(node)
136
+ def_node_matcher :and_inside_begin?, '`(begin and ...)'
137
+
138
+ # @!method strip_begin(node)
139
+ def_node_matcher :strip_begin, '{ (begin $!begin) $!(begin) }'
140
+
127
141
  def on_if(node)
128
142
  return if allowed_if_condition?(node)
129
143
 
130
- check_node(node)
144
+ checked_variable, receiver, method_chain, _method = extract_parts_from_if(node)
145
+ return unless offending_node?(node, checked_variable, method_chain, receiver)
146
+
147
+ body = extract_if_body(node)
148
+ method_call = receiver.parent
149
+
150
+ removal_ranges = [begin_range(node, body), end_range(node, body)]
151
+
152
+ report_offense(node, method_chain, method_call, *removal_ranges) do |corrector|
153
+ corrector.insert_before(method_call.loc.dot, '&') unless method_call.safe_navigation?
154
+ end
131
155
  end
132
156
 
133
- def on_and(node)
134
- check_node(node)
157
+ def on_and(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
158
+ collect_and_clauses(node).each do |(lhs, lhs_operator_range), (rhs, _rhs_operator_range)|
159
+ lhs_not_nil_check = not_nil_check?(lhs)
160
+ lhs_receiver = lhs_not_nil_check || lhs
161
+ rhs_receiver = find_matching_receiver_invocation(strip_begin(rhs), lhs_receiver)
162
+
163
+ next if !cop_config['ConvertCodeThatCanStartToReturnNil'] && lhs_not_nil_check
164
+ next unless offending_node?(node, lhs_receiver, rhs, rhs_receiver)
165
+
166
+ # Since we are evaluating every clause in potentially a complex chain of `and` nodes,
167
+ # we need to ensure that there isn't an object check happening
168
+ lhs_method_chain = find_method_chain(lhs_receiver)
169
+ next unless lhs_method_chain == lhs_receiver || lhs_not_nil_check
170
+
171
+ report_offense(
172
+ node,
173
+ rhs, rhs_receiver,
174
+ range_with_surrounding_space(range: lhs.source_range, side: :right),
175
+ range_with_surrounding_space(range: lhs_operator_range, side: :right),
176
+ offense_range: range_between(lhs.source_range.begin_pos, rhs.source_range.end_pos)
177
+ )
178
+ end
179
+ end
180
+
181
+ def report_offense(node, rhs, rhs_receiver, *removal_ranges, offense_range: node)
182
+ add_offense(offense_range) do |corrector|
183
+ # If the RHS is an `or` we cannot safely autocorrect because in order to remove
184
+ # the non-nil check we need to add safe-navs to all clauses where the receiver is used
185
+ next if and_with_rhs_or?(node)
186
+
187
+ removal_ranges.each { |range| corrector.remove(range) }
188
+ yield corrector if block_given?
189
+
190
+ handle_comments(corrector, node, rhs)
191
+
192
+ add_safe_nav_to_all_methods_in_chain(corrector, rhs_receiver, rhs)
193
+ end
135
194
  end
136
195
 
137
196
  private
138
197
 
139
- def check_node(node)
140
- checked_variable, receiver, method_chain, method = extract_parts(node)
141
- return if receiver != checked_variable || receiver.nil?
142
- return if use_var_only_in_unless_modifier?(node, checked_variable)
143
- return if chain_length(method_chain, method) > max_chain_length
144
- return if unsafe_method_used?(method_chain, method)
145
- return if method_chain.method?(:empty?)
198
+ def find_method_chain(node)
199
+ return node unless node&.parent&.call_type?
146
200
 
147
- add_offense(node) { |corrector| autocorrect(corrector, node) }
201
+ find_method_chain(node.parent)
148
202
  end
149
203
 
150
- def use_var_only_in_unless_modifier?(node, variable)
151
- node.if_type? && node.unless? && !method_called?(variable)
204
+ def collect_and_clauses(node)
205
+ # Collect the lhs, operator and rhs of all `and` nodes
206
+ # `and` nodes can be nested and can contain `begin` nodes
207
+ # This gives us a source-ordered list of clauses that is then used to look
208
+ # for matching receivers as well as operator locations for offense and corrections
209
+ node.each_descendant(:and)
210
+ .inject(and_parts(node)) { |nodes, and_node| concat_nodes(nodes, and_node) }
211
+ .sort_by { |a| a.is_a?(RuboCop::AST::Node) ? a.source_range.begin_pos : a.begin_pos }
212
+ .each_slice(2)
213
+ .each_cons(2)
152
214
  end
153
215
 
154
- def autocorrect(corrector, node)
155
- body = extract_body(node)
156
- method_call = method_call(node)
216
+ def concat_nodes(nodes, and_node)
217
+ return nodes if and_node.each_ancestor(:block).any?
157
218
 
158
- corrector.remove(begin_range(node, body))
159
- corrector.remove(end_range(node, body))
160
- corrector.insert_before(method_call.loc.dot, '&') unless method_call.safe_navigation?
161
- handle_comments(corrector, node, method_call)
219
+ nodes.concat(and_parts(and_node))
220
+ end
162
221
 
163
- add_safe_nav_to_all_methods_in_chain(corrector, method_call, body)
222
+ def and_parts(node)
223
+ parts = [node.loc.operator]
224
+ parts << node.rhs unless and_inside_begin?(node.rhs)
225
+ parts << node.lhs unless node.lhs.and_type? || and_inside_begin?(node.lhs)
226
+ parts
164
227
  end
165
228
 
166
- def extract_body(node)
167
- if node.if_type? && node.ternary?
229
+ def offending_node?(node, lhs_receiver, rhs, rhs_receiver) # rubocop:disable Metrics/CyclomaticComplexity
230
+ return false if lhs_receiver != rhs_receiver || rhs_receiver.nil?
231
+ return false if use_var_only_in_unless_modifier?(node, lhs_receiver)
232
+ return false if chain_length(rhs, rhs_receiver) > max_chain_length
233
+ return false if unsafe_method_used?(rhs, rhs_receiver.parent)
234
+ return false if rhs.send_type? && rhs.method?(:empty?)
235
+
236
+ true
237
+ end
238
+
239
+ def use_var_only_in_unless_modifier?(node, variable)
240
+ node.if_type? && node.unless? && !method_called?(variable)
241
+ end
242
+
243
+ def extract_if_body(node)
244
+ if node.ternary?
168
245
  node.branches.find { |branch| !branch.nil_type? }
169
246
  else
170
247
  node.node_parts[1]
@@ -201,20 +278,6 @@ module RuboCop
201
278
  node.else? || node.elsif?
202
279
  end
203
280
 
204
- def method_call(node)
205
- _checked_variable, matching_receiver, = extract_parts(node)
206
- matching_receiver.parent
207
- end
208
-
209
- def extract_parts(node)
210
- case node.type
211
- when :if
212
- extract_parts_from_if(node)
213
- when :and
214
- extract_parts_from_and(node)
215
- end
216
- end
217
-
218
281
  def extract_parts_from_if(node)
219
282
  variable, receiver =
220
283
  if node.ternary?
@@ -230,16 +293,6 @@ module RuboCop
230
293
  [checked_variable, matching_receiver, receiver, method]
231
294
  end
232
295
 
233
- def extract_parts_from_and(node)
234
- checked_variable, rhs = *node
235
- if cop_config['ConvertCodeThatCanStartToReturnNil']
236
- checked_variable = not_nil_check?(checked_variable) || checked_variable
237
- end
238
-
239
- checked_variable, matching_receiver, method = extract_common_parts(rhs, checked_variable)
240
- [checked_variable, matching_receiver, rhs, method]
241
- end
242
-
243
296
  def extract_common_parts(method_chain, checked_variable)
244
297
  matching_receiver = find_matching_receiver_invocation(method_chain, checked_variable)
245
298
 
@@ -249,7 +302,7 @@ module RuboCop
249
302
  end
250
303
 
251
304
  def find_matching_receiver_invocation(method_chain, checked_variable)
252
- return nil unless method_chain
305
+ return nil unless method_chain.respond_to?(:receiver)
253
306
 
254
307
  receiver = method_chain.receiver
255
308
 
@@ -259,7 +312,7 @@ module RuboCop
259
312
  end
260
313
 
261
314
  def chain_length(method_chain, method)
262
- method.each_ancestor(:send).inject(1) do |total, ancestor|
315
+ method.each_ancestor(:send).inject(0) do |total, ancestor|
263
316
  break total + 1 if ancestor == method_chain
264
317
 
265
318
  total + 1
@@ -310,6 +363,7 @@ module RuboCop
310
363
  start_method.each_ancestor do |ancestor|
311
364
  break unless %i[send block].include?(ancestor.type)
312
365
  next unless ancestor.send_type?
366
+ next if ancestor.safe_navigation?
313
367
 
314
368
  corrector.insert_before(ancestor.loc.dot, '&')
315
369
 
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Enforces safe navigation chains length to not exceed the configured maximum.
7
+ # The longer the chain is, the harder it becomes to track what on it could be
8
+ # returning `nil`.
9
+ #
10
+ # There is a potential interplay with `Style/SafeNavigation` - if both are enabled
11
+ # and their settings are "incompatible", one of the cops will complain about what
12
+ # the other proposes.
13
+ #
14
+ # E.g. if `Style/SafeNavigation` is configured with `MaxChainLength: 2` (default)
15
+ # and this cop is configured with `Max: 1`, then for `foo.bar.baz if foo` the former
16
+ # will suggest `foo&.bar&.baz`, which is an offense for the latter.
17
+ #
18
+ # @example Max: 2 (default)
19
+ # # bad
20
+ # user&.address&.zip&.upcase
21
+ #
22
+ # # good
23
+ # user&.address&.zip
24
+ # user.address.zip if user
25
+ #
26
+ class SafeNavigationChainLength < Base
27
+ MSG = 'Avoid safe navigation chains longer than %<max>d calls.'
28
+
29
+ def on_csend(node)
30
+ safe_navigation_chains = safe_navigation_chains(node)
31
+ return if safe_navigation_chains.size < max
32
+
33
+ add_offense(safe_navigation_chains.last, message: format(MSG, max: max))
34
+ end
35
+
36
+ private
37
+
38
+ def safe_navigation_chains(node)
39
+ node.each_ancestor.with_object([]) do |parent, chains|
40
+ break chains unless parent.csend_type?
41
+
42
+ chains << parent
43
+ end
44
+ end
45
+
46
+ def max
47
+ cop_config['Max'] || 2
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -27,7 +27,7 @@ module RuboCop
27
27
  # so the correction may not be actually equivalent.
28
28
  #
29
29
  # @example
30
- # # bad (select or find_all)
30
+ # # bad (select, filter, or find_all)
31
31
  # array.select { |x| x.match? /regexp/ }
32
32
  # array.select { |x| /regexp/.match?(x) }
33
33
  # array.select { |x| x =~ /regexp/ }
@@ -47,9 +47,11 @@ module RuboCop
47
47
  include RangeHelp
48
48
 
49
49
  MSG = 'Prefer `%<replacement>s` to `%<original_method>s` with a regexp match.'
50
- RESTRICT_ON_SEND = %i[select find_all reject].freeze
51
- REPLACEMENTS = { select: 'grep', find_all: 'grep', reject: 'grep_v' }.freeze
52
- OPPOSITE_REPLACEMENTS = { select: 'grep_v', find_all: 'grep_v', reject: 'grep' }.freeze
50
+ RESTRICT_ON_SEND = %i[select filter find_all reject].freeze
51
+ REPLACEMENTS = { select: 'grep', filter: 'grep', find_all: 'grep', reject: 'grep_v' }.freeze
52
+ OPPOSITE_REPLACEMENTS = {
53
+ select: 'grep_v', filter: 'grep_v', find_all: 'grep_v', reject: 'grep'
54
+ }.freeze
53
55
  REGEXP_METHODS = %i[match? =~ !~].to_set.freeze
54
56
 
55
57
  # @!method regexp_match?(node)
@@ -84,8 +86,9 @@ module RuboCop
84
86
  }
85
87
  PATTERN
86
88
 
87
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
89
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
88
90
  def on_send(node)
91
+ return if target_ruby_version < 2.6 && node.method?(:filter)
89
92
  return unless (block_node = node.block_node)
90
93
  return if block_node.body&.begin_type?
91
94
  return if receiver_allowed?(block_node.receiver)
@@ -99,7 +102,7 @@ module RuboCop
99
102
 
100
103
  register_offense(node, block_node, regexp, replacement)
101
104
  end
102
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
105
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
103
106
  alias on_csend on_send
104
107
 
105
108
  private
@@ -141,7 +141,7 @@ module RuboCop
141
141
 
142
142
  def expressions_per_line(exprs)
143
143
  # create a map matching lines to the number of expressions on them
144
- exprs_lines = exprs.map(&:first_line)
144
+ exprs_lines = exprs.map(&:last_line)
145
145
  exprs_lines.group_by(&:itself)
146
146
  end
147
147
 
@@ -33,7 +33,7 @@ module RuboCop
33
33
  def on_class(node)
34
34
  return unless struct_constructor?(node.parent_class)
35
35
 
36
- add_offense(node.parent_class.source_range) do |corrector|
36
+ add_offense(node.parent_class) do |corrector|
37
37
  corrector.remove(range_with_surrounding_space(node.loc.keyword, newlines: false))
38
38
  corrector.replace(node.loc.operator, '=')
39
39
 
@@ -75,7 +75,7 @@ module RuboCop
75
75
 
76
76
  message = message(node)
77
77
 
78
- add_offense(node.source_range, message: message) do |corrector|
78
+ add_offense(node, message: message) do |corrector|
79
79
  autocorrect(corrector, node)
80
80
  end
81
81
  end
@@ -171,9 +171,7 @@ module RuboCop
171
171
  end
172
172
 
173
173
  def unsafe_autocorrect?(condition)
174
- condition.children.any? do |child|
175
- unparenthesized_method_call?(child) || below_ternary_precedence?(child)
176
- end
174
+ condition.children.any? { |child| below_ternary_precedence?(child) }
177
175
  end
178
176
 
179
177
  def unparenthesized_method_call?(child)
@@ -192,7 +190,7 @@ module RuboCop
192
190
  # @!method method_name(node)
193
191
  def_node_matcher :method_name, <<~PATTERN
194
192
  {($:defined? _ ...)
195
- (send {_ nil?} $_ _ ...)}
193
+ (call {_ nil?} $_ _ ...)}
196
194
  PATTERN
197
195
 
198
196
  def correct_parenthesized(corrector, condition)
@@ -203,16 +201,39 @@ module RuboCop
203
201
  # If we remove the parentheses, we need to add a space or we'll
204
202
  # generate invalid code.
205
203
  corrector.insert_after(condition.loc.end, ' ') unless whitespace_after?(condition)
204
+
205
+ if (send_node = condition.child_nodes.last) && node_args_need_parens?(send_node)
206
+ parenthesize_condition_arguments(corrector, send_node)
207
+ end
206
208
  end
207
209
 
208
210
  def correct_unparenthesized(corrector, condition)
209
211
  corrector.wrap(condition, '(', ')')
210
212
  end
211
213
 
214
+ def parenthesize_condition_arguments(corrector, send_node)
215
+ range_start = send_node.defined_type? ? send_node.loc.keyword : send_node.loc.selector
216
+ opening_range = range_start.end.join(send_node.first_argument.source_range.begin)
217
+
218
+ corrector.replace(opening_range, '(')
219
+ corrector.insert_after(send_node.last_argument, ')')
220
+ end
221
+
212
222
  def whitespace_after?(node)
213
223
  last_token = processed_source.last_token_of(node)
214
224
  last_token.space_after?
215
225
  end
226
+
227
+ def node_args_need_parens?(send_node)
228
+ return false unless node_with_args?(send_node)
229
+ return false if send_node.arguments.none? || send_node.parenthesized?
230
+
231
+ send_node.dot? || send_node.safe_navigation? || unparenthesized_method_call?(send_node)
232
+ end
233
+
234
+ def node_with_args?(node)
235
+ node.call_type? || node.defined_type?
236
+ end
216
237
  end
217
238
  end
218
239
  end