rubocop 1.88.2 → 1.89.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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +67 -11
  3. data/lib/rubocop/comment_config.rb +35 -19
  4. data/lib/rubocop/config.rb +53 -0
  5. data/lib/rubocop/config_validator.rb +38 -1
  6. data/lib/rubocop/cop/badge.rb +8 -0
  7. data/lib/rubocop/cop/base.rb +6 -4
  8. data/lib/rubocop/cop/bundler.rb +19 -0
  9. data/lib/rubocop/cop/corrector.rb +1 -1
  10. data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
  11. data/lib/rubocop/cop/gemspec.rb +22 -0
  12. data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
  13. data/lib/rubocop/cop/generator.rb +4 -0
  14. data/lib/rubocop/cop/layout/class_structure.rb +8 -2
  15. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
  16. data/lib/rubocop/cop/layout/indentation_width.rb +3 -1
  17. data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
  18. data/lib/rubocop/cop/layout/line_length.rb +85 -12
  19. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +12 -2
  20. data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
  21. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +33 -34
  22. data/lib/rubocop/cop/layout.rb +114 -0
  23. data/lib/rubocop/cop/lazy_loader.rb +41 -0
  24. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
  25. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
  26. data/lib/rubocop/cop/lint/constant_reassignment.rb +1 -10
  27. data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
  28. data/lib/rubocop/cop/lint/deprecated_reference.rb +185 -0
  29. data/lib/rubocop/cop/lint/duplicate_methods.rb +202 -22
  30. data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
  31. data/lib/rubocop/cop/lint/literal_as_condition.rb +5 -0
  32. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +1 -1
  33. data/lib/rubocop/cop/lint/missing_super.rb +31 -1
  34. data/lib/rubocop/cop/lint/name_typo.rb +223 -0
  35. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +0 -4
  36. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +7 -0
  37. data/lib/rubocop/cop/lint/to_enum_arguments.rb +37 -1
  38. data/lib/rubocop/cop/lint/unused_private_method.rb +157 -0
  39. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
  40. data/lib/rubocop/cop/lint/void.rb +17 -6
  41. data/lib/rubocop/cop/lint.rb +176 -0
  42. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  43. data/lib/rubocop/cop/metrics.rb +31 -0
  44. data/lib/rubocop/cop/migration/department_name.rb +0 -4
  45. data/lib/rubocop/cop/migration.rb +13 -0
  46. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
  47. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
  48. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
  49. data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
  50. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
  51. data/lib/rubocop/cop/mixin/statement_modifier.rb +42 -6
  52. data/lib/rubocop/cop/mixin.rb +78 -77
  53. data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
  54. data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
  55. data/lib/rubocop/cop/naming/variable_number.rb +3 -1
  56. data/lib/rubocop/cop/naming.rb +31 -0
  57. data/lib/rubocop/cop/offense.rb +10 -6
  58. data/lib/rubocop/cop/registry.rb +153 -43
  59. data/lib/rubocop/cop/security.rb +19 -0
  60. data/lib/rubocop/cop/style/array_intersect.rb +24 -5
  61. data/lib/rubocop/cop/style/class_and_module_children.rb +78 -18
  62. data/lib/rubocop/cop/style/conditional_assignment.rb +22 -0
  63. data/lib/rubocop/cop/style/copyright.rb +6 -1
  64. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +41 -3
  65. data/lib/rubocop/cop/style/documentation.rb +47 -0
  66. data/lib/rubocop/cop/style/endless_method.rb +0 -10
  67. data/lib/rubocop/cop/style/if_unless_modifier.rb +7 -48
  68. data/lib/rubocop/cop/style/lambda.rb +4 -0
  69. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +20 -3
  70. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +30 -0
  71. data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
  72. data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
  73. data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
  74. data/lib/rubocop/cop/style/negative_array_index.rb +3 -1
  75. data/lib/rubocop/cop/style/redundant_assignment.rb +11 -0
  76. data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
  77. data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
  78. data/lib/rubocop/cop/style/redundant_format.rb +12 -3
  79. data/lib/rubocop/cop/style/redundant_line_continuation.rb +78 -135
  80. data/lib/rubocop/cop/style/redundant_parentheses.rb +63 -63
  81. data/lib/rubocop/cop/style/safe_navigation.rb +10 -0
  82. data/lib/rubocop/cop/style/sole_nested_conditional.rb +20 -3
  83. data/lib/rubocop/cop/style/static_class.rb +21 -0
  84. data/lib/rubocop/cop/style.rb +310 -0
  85. data/lib/rubocop/cop/team.rb +48 -3
  86. data/lib/rubocop/cop/variable_force/variable.rb +18 -6
  87. data/lib/rubocop/cops_documentation_generator.rb +1 -1
  88. data/lib/rubocop/directive_comment.rb +2 -0
  89. data/lib/rubocop/feature_loader.rb +0 -6
  90. data/lib/rubocop/lsp/routes.rb +41 -3
  91. data/lib/rubocop/lsp/runtime.rb +4 -0
  92. data/lib/rubocop/lsp/server.rb +4 -0
  93. data/lib/rubocop/lsp/stdin_runner.rb +20 -0
  94. data/lib/rubocop/pending_cops_reporter.rb +1 -6
  95. data/lib/rubocop/project_index_loader.rb +16 -0
  96. data/lib/rubocop/result_cache.rb +6 -4
  97. data/lib/rubocop/rspec/shared_contexts.rb +14 -2
  98. data/lib/rubocop/runner.rb +45 -20
  99. data/lib/rubocop/server/cache.rb +10 -5
  100. data/lib/rubocop/target_ruby.rb +1 -1
  101. data/lib/rubocop/version.rb +1 -1
  102. data/lib/rubocop.rb +17 -626
  103. metadata +18 -3
@@ -5,10 +5,13 @@ module RuboCop
5
5
  module Style
6
6
  # Checks for redundant line continuation.
7
7
  #
8
- # This cop marks a line continuation as redundant if removing the backslash
9
- # does not result in a syntax error.
10
- # However, a backslash at the end of a comment or
11
- # for string concatenation is not redundant and is not considered an offense.
8
+ # A line continuation is redundant when removing the backslash does not
9
+ # change how the program parses: the source is reparsed without the
10
+ # backslash and the resulting AST is compared to the original. Only
11
+ # backslashes that are pure noise are reported; backslashes that are
12
+ # significant — inside strings, for string concatenation, before an
13
+ # operator or argument that would otherwise start a new statement, and
14
+ # so on — are left alone, as are backslashes in comments.
12
15
  #
13
16
  # @example
14
17
  # # bad
@@ -66,31 +69,23 @@ module RuboCop
66
69
  #
67
70
  class RedundantLineContinuation < Base
68
71
  include MatchRange
72
+ include ReparsedEquivalence
69
73
  extend AutoCorrector
70
74
 
71
75
  MSG = 'Redundant line continuation.'
72
76
  LINE_CONTINUATION = '\\'
73
77
  LINE_CONTINUATION_PATTERN = /(\\\n)/.freeze
74
- ALLOWED_STRING_TOKENS = %i[tSTRING tSTRING_CONTENT].freeze
75
- ARGUMENT_TYPES = %i[
76
- kDEF kDEFINED kFALSE kNIL kSELF kTRUE tAMPER tBANG tCARET tCHARACTER tCOLON3 tCONSTANT
77
- tCVAR tDOT2 tDOT3 tFLOAT tGVAR tIDENTIFIER tINTEGER tIVAR tLAMBDA tLBRACK tLCURLY
78
- tLPAREN_ARG tPIPE tQSYMBOLS_BEG tQWORDS_BEG tREGEXP_BEG tSTAR tSTRING tSTRING_BEG tSYMBEG
79
- tSYMBOL tSYMBOLS_BEG tTILDE tUMINUS tUNARY_NUM tUPLUS tWORDS_BEG tXSTRING_BEG
80
- ].freeze
81
- ARGUMENT_TAKING_FLOW_TOKEN_TYPES = %i[
82
- tIDENTIFIER kBREAK kNEXT kRETURN kSUPER kYIELD
83
- ].freeze
84
- ARITHMETIC_OPERATOR_TOKENS = %i[tDIVIDE tDSTAR tMINUS tPERCENT tPLUS tSTAR2].freeze
85
- STRING_LITERAL_BEGIN_TOKENS = %i[tSTRING_BEG tXSTRING_BEG tREGEXP_BEG tSYMBEG].freeze
78
+ STRING_TOKEN_TYPES = %i[tSTRING tSTRING_CONTENT].freeze
79
+ STRING_LITERAL_ENDING_TOKEN_TYPES = %i[tSTRING tSTRING_END].freeze
80
+ STRING_LITERAL_BEGINNING_TOKEN_TYPES = %i[tSTRING tSTRING_BEG].freeze
86
81
 
87
82
  def on_new_investigation
88
83
  return unless processed_source.ast
89
84
 
90
- each_match_range(processed_source.ast.source_range, LINE_CONTINUATION_PATTERN) do |range|
91
- next if require_line_continuation?(range)
92
- next unless redundant_line_continuation?(range)
93
-
85
+ # A backslash is redundant if the source parses to the same AST
86
+ # without it; verification is the offense logic itself, so oversized
87
+ # scopes are reparsed regardless of size.
88
+ verified_by_reparse(line_continuation_candidates, oversized: :verify).each do |range|
94
89
  add_offense(range) do |corrector|
95
90
  corrector.remove_leading(range, 1)
96
91
  end
@@ -101,87 +96,100 @@ module RuboCop
101
96
 
102
97
  private
103
98
 
104
- def require_line_continuation?(range)
105
- !ends_with_uncommented_backslash?(range) ||
106
- string_concatenation?(range.source_line) ||
107
- start_with_arithmetic_operator?(range) ||
108
- inside_string_literal_or_method_with_argument?(range) ||
109
- inside_string_literal_with_interpolation?(range) ||
110
- leading_dot_method_chain_with_blank_line?(range)
99
+ def apply_reparse_correction(corrector, range)
100
+ corrector.remove_leading(range, 1)
111
101
  end
112
102
 
113
- def ends_with_uncommented_backslash?(range)
114
- # A line continuation always needs to be the last character on the line, which
115
- # means that it is impossible to have a comment following a continuation.
116
- # Therefore, if the line contains a comment, it cannot end with a continuation.
117
- return false if processed_source.line_with_comment?(range.line)
103
+ def line_continuation_candidates
104
+ candidates = []
105
+
106
+ each_match_range(processed_source.ast.source_range, LINE_CONTINUATION_PATTERN) do |range|
107
+ next if within_comment?(range) || implicit_string_concatenation?(range)
108
+ next if within_string_content?(range)
109
+ next if leading_dot_method_chain_with_blank_line?(range)
110
+
111
+ candidates << range
112
+ end
118
113
 
119
- range.source_line.end_with?(LINE_CONTINUATION)
114
+ candidates
120
115
  end
121
116
 
122
- def string_concatenation?(source_line)
123
- /["']\s*\\\z/.match?(source_line)
117
+ def within_comment?(range)
118
+ processed_source.comments.any? do |comment|
119
+ comment.source_range.overlaps?(range)
120
+ end
124
121
  end
125
122
 
126
- def inside_string_literal_or_method_with_argument?(range)
127
- line_range = range_by_whole_lines(range)
123
+ # A backslash directly joining two string literals is never redundant:
124
+ # removing it moves the second literal onto its own logical line,
125
+ # severing the implicit string concatenation. Skipping these up front
126
+ # avoids reparse verification, which is prohibitively slow on generated
127
+ # files gluing thousands of string fragments with line continuations.
128
+ def implicit_string_concatenation?(range)
129
+ tokens = processed_source.sorted_tokens
128
130
 
129
- processed_source.tokens.each_cons(2).any? do |token, next_token|
130
- next if token.line == next_token.line
131
+ token_before = last_token_before(tokens, range)
132
+ return false unless string_literal_ending_on_line?(token_before, range.line)
131
133
 
132
- inside_string_literal?(range, token) ||
133
- method_with_argument?(line_range, token, next_token)
134
- end
134
+ string_literal_beginning_on_line?(first_token_after(tokens, range), range.line + 1)
135
135
  end
136
136
 
137
- def inside_string_literal_with_interpolation?(range)
138
- string_depth = 0
139
- processed_source.tokens.each do |token|
140
- break if token.pos.begin_pos >= range.begin_pos
141
-
142
- if STRING_LITERAL_BEGIN_TOKENS.include?(token.type)
143
- string_depth += 1
144
- elsif token.type == :tSTRING_END
145
- string_depth -= 1
146
- end
137
+ # A backslash in string content is never a redundant line continuation
138
+ # (removing it changes the string's value, which the reparse check would
139
+ # detect); skipping it up front just avoids a useless reparse.
140
+ def within_string_content?(range)
141
+ @string_content_ranges ||= processed_source.tokens.filter_map do |token|
142
+ token.pos if STRING_TOKEN_TYPES.include?(token.type)
147
143
  end
148
- string_depth.positive?
144
+
145
+ @string_content_ranges.any? { |pos| pos.overlaps?(range) }
149
146
  end
150
147
 
148
+ # The `parser` gem, unlike Ruby itself, accepts a leading-dot method
149
+ # chain continued across a blank line, so the reparse check alone would
150
+ # deem the backslash redundant even though removing it breaks the code
151
+ # on MRI. Prism matches Ruby here and does not need this guard.
151
152
  def leading_dot_method_chain_with_blank_line?(range)
152
153
  return false unless range.source_line.strip.start_with?('.', '&.')
153
154
 
154
155
  processed_source[range.line].strip.empty?
155
156
  end
156
157
 
157
- def redundant_line_continuation?(range)
158
- return true unless (node = find_node_for_line(range.last_line))
159
- return false if argument_newline?(node)
158
+ def last_token_before(tokens, range)
159
+ index = tokens.bsearch_index { |token| token.end_pos > range.begin_pos }
160
+ index&.positive? ? tokens[index - 1] : nil
161
+ end
162
+
163
+ def first_token_after(tokens, range)
164
+ tokens.bsearch { |token| token.begin_pos >= range.end_pos }
165
+ end
166
+
167
+ def string_literal_ending_on_line?(token, line)
168
+ return false unless token
169
+
170
+ STRING_LITERAL_ENDING_TOKEN_TYPES.include?(token.type) && token.pos.last_line == line
171
+ end
172
+
173
+ def string_literal_beginning_on_line?(token, line)
174
+ return false unless token
160
175
 
161
- # Check if source is still valid without the continuation
162
- source = processed_source.raw_source.dup
163
- source[range.begin_pos, range.length] = "\n"
164
- parse(source).valid_syntax?
176
+ STRING_LITERAL_BEGINNING_TOKEN_TYPES.include?(token.type) && token.line == line
165
177
  end
166
178
 
167
179
  def inspect_end_of_ruby_code_line_continuation
168
180
  last_line_number = processed_source.ast.last_line
169
181
  last_line = processed_source.lines[last_line_number - 1]
170
- return unless code_ends_with_continuation?(last_line)
182
+ return unless last_line&.end_with?(LINE_CONTINUATION)
171
183
 
172
- line_continuation_range = trailing_line_continuation_range(last_line_number)
184
+ range = trailing_line_continuation_range(last_line_number)
185
+ return if within_comment?(range)
186
+ return if verified_by_reparse([range], oversized: :verify).empty?
173
187
 
174
- add_offense(line_continuation_range) do |corrector|
175
- corrector.remove_trailing(line_continuation_range, 1)
188
+ add_offense(range) do |corrector|
189
+ corrector.remove_trailing(range, 1)
176
190
  end
177
191
  end
178
192
 
179
- def code_ends_with_continuation?(last_line)
180
- return false if processed_source.line_with_comment?(processed_source.ast.last_line)
181
-
182
- last_line.end_with?(LINE_CONTINUATION)
183
- end
184
-
185
193
  # The backslash is the last character of the line; locate it by the line's
186
194
  # position in the buffer rather than treating the column as an absolute offset
187
195
  # (which corrupts an earlier line in multi-line files).
@@ -189,71 +197,6 @@ module RuboCop
189
197
  line_range = processed_source.buffer.line_range(line_number)
190
198
  range_between(line_range.end_pos - 1, line_range.end_pos)
191
199
  end
192
-
193
- def inside_string_literal?(range, token)
194
- ALLOWED_STRING_TOKENS.include?(token.type) && token.pos.overlaps?(range)
195
- end
196
-
197
- # A method call without parentheses such as the following cannot remove `\`:
198
- #
199
- # do_something \
200
- # argument
201
- def method_with_argument?(line_range, current_token, next_token)
202
- return false unless ARGUMENT_TAKING_FLOW_TOKEN_TYPES.include?(current_token.type)
203
- return false unless current_token.pos.overlaps?(line_range)
204
-
205
- ARGUMENT_TYPES.include?(next_token.type)
206
- end
207
-
208
- def argument_newline?(node)
209
- return false if node.parenthesized_call?
210
-
211
- node = node.children.first if node.root? && node.begin_type?
212
-
213
- if argument_is_method?(node)
214
- argument_newline?(node.first_argument)
215
- else
216
- return false unless method_call_with_arguments?(node)
217
-
218
- node.loc.selector.line != node.first_argument.loc.line
219
- end
220
- end
221
-
222
- def find_node_for_line(last_line)
223
- processed_source.ast.each_node do |node|
224
- return node if same_line?(node, last_line)
225
- end
226
- end
227
-
228
- def same_line?(node, line)
229
- return false unless (source_range = node.source_range)
230
-
231
- if node.is_a?(AST::StrNode)
232
- if node.heredoc?
233
- (node.loc.heredoc_body.line..node.loc.heredoc_body.last_line).cover?(line)
234
- else
235
- (source_range.line..source_range.last_line).cover?(line)
236
- end
237
- else
238
- source_range.line == line
239
- end
240
- end
241
-
242
- def argument_is_method?(node)
243
- return false unless node.send_type?
244
- return false unless (first_argument = node.first_argument)
245
-
246
- method_call_with_arguments?(first_argument)
247
- end
248
-
249
- def method_call_with_arguments?(node)
250
- node.call_type? && !node.arguments.empty?
251
- end
252
-
253
- def start_with_arithmetic_operator?(range)
254
- line_range = processed_source.buffer.line_range(range.line + 1)
255
- ARITHMETIC_OPERATOR_TOKENS.include?(processed_source.first_token_of(line_range).type)
256
- end
257
200
  end
258
201
  end
259
202
  end
@@ -15,6 +15,7 @@ module RuboCop
15
15
  #
16
16
  class RedundantParentheses < Base # rubocop:disable Metrics/ClassLength
17
17
  include Parentheses
18
+ include ReparsedEquivalence
18
19
  extend AutoCorrector
19
20
 
20
21
  ALLOWED_NODE_TYPES = %i[or send splat kwsplat].freeze
@@ -30,12 +31,30 @@ module RuboCop
30
31
  # @!method allowed_pin_operator?(node)
31
32
  def_node_matcher :allowed_pin_operator?, '^(pin (begin !{lvar ivar cvar gvar}))'
32
33
 
34
+ def on_new_investigation
35
+ @pending_offenses = {}.compare_by_identity
36
+ super
37
+ end
38
+
33
39
  def on_begin(node)
34
40
  return if !parentheses?(node) || parens_allowed?(node) || ignore_syntax?(node)
35
41
 
36
42
  check(node)
37
43
  end
38
44
 
45
+ def on_investigation_end
46
+ # Each candidate's exact correction is verified by reparsing before
47
+ # the offense is registered, so redundancy never depends on
48
+ # hand-maintained knowledge of Ruby's grammar.
49
+ verified_by_reparse(@pending_offenses.keys).each do |node|
50
+ add_offense(node, message: @pending_offenses[node]) do |corrector|
51
+ ParenthesesCorrector.correct(corrector, node)
52
+ end
53
+ end
54
+
55
+ super
56
+ end
57
+
39
58
  private
40
59
 
41
60
  def variable?(node)
@@ -44,7 +63,6 @@ module RuboCop
44
63
 
45
64
  def parens_allowed?(node)
46
65
  empty_parentheses?(node) ||
47
- first_arg_begins_with_hash_literal?(node) ||
48
66
  rescue?(node) ||
49
67
  in_pattern_matching_in_method_argument?(node) ||
50
68
  allowed_pin_operator?(node) ||
@@ -111,15 +129,6 @@ module RuboCop
111
129
  node.children.empty?
112
130
  end
113
131
 
114
- def first_arg_begins_with_hash_literal?(node)
115
- # Don't flag `method ({key: value})` or `method ({key: value}.method)`
116
- hash_literal = method_chain_begins_with_hash_literal(node.children.first)
117
- if (root_method = node.each_ancestor(:call).to_a.last)
118
- parenthesized = root_method.parenthesized_call?
119
- end
120
- hash_literal && first_argument?(node) && !parentheses?(hash_literal) && !parenthesized
121
- end
122
-
123
132
  def in_pattern_matching_in_method_argument?(begin_node)
124
133
  return false unless begin_node.parent&.call_type?
125
134
  return false unless (node = begin_node.children.first)
@@ -127,14 +136,6 @@ module RuboCop
127
136
  target_ruby_version <= 2.7 ? node.match_pattern_type? : node.match_pattern_p_type?
128
137
  end
129
138
 
130
- def method_chain_begins_with_hash_literal(node)
131
- return if node.nil?
132
- return node if node.hash_type?
133
- return unless node.send_type?
134
-
135
- method_chain_begins_with_hash_literal(node.children.first)
136
- end
137
-
138
139
  def check(begin_node)
139
140
  node = begin_node.children.first
140
141
 
@@ -231,8 +232,6 @@ module RuboCop
231
232
  return check_unary(begin_node, node) if node.unary_operation?
232
233
 
233
234
  return unless method_call_with_redundant_parentheses?(begin_node, node)
234
- return if call_chain_starts_with_int?(begin_node, node) ||
235
- do_end_block_in_method_chain?(begin_node, node)
236
235
 
237
236
  offense(begin_node, 'a method call')
238
237
  end
@@ -247,11 +246,50 @@ module RuboCop
247
246
  end
248
247
 
249
248
  def offense(node, msg)
250
- add_offense(node, message: "Don't use parentheses around #{msg}.") do |corrector|
251
- ParenthesesCorrector.correct(corrector, node)
249
+ @pending_offenses[node] = "Don't use parentheses around #{msg}."
250
+ end
251
+
252
+ def apply_reparse_correction(corrector, node)
253
+ ParenthesesCorrector.correct(corrector, node)
254
+ end
255
+
256
+ # Grouping parentheses are transparent to the comparison: single-child
257
+ # `begin` nodes are collapsed, a parenthesized statement sequence
258
+ # inside another sequence is spliced in place, and chains of the same
259
+ # `&&`/`||` operator are normalized to left association (`x && (y &&
260
+ # z)` and `x && y && z` differ as trees but `&&` and `||` cannot be
261
+ # redefined, so same-operator regrouping is semantically transparent).
262
+ def normalize_reparsed_ast(node)
263
+ return node unless node.is_a?(::Parser::AST::Node)
264
+
265
+ children = node.children.map { |child| normalize_reparsed_ast(child) }
266
+ children = splice_nested_sequences(children) if %i[begin kwbegin].include?(node.type)
267
+
268
+ node = node.updated(nil, children)
269
+ if node.begin_type? && children.one? && children.first.is_a?(::Parser::AST::Node)
270
+ children.first
271
+ else
272
+ rotate_same_operator(node)
273
+ end
274
+ end
275
+
276
+ def splice_nested_sequences(children)
277
+ children.flat_map do |child|
278
+ child.is_a?(::Parser::AST::Node) && child.begin_type? ? child.children : [child]
252
279
  end
253
280
  end
254
281
 
282
+ def rotate_same_operator(node)
283
+ return node unless node.type?(:and, :or)
284
+
285
+ right = node.rhs
286
+ return node unless right.is_a?(::Parser::AST::Node) && right.type == node.type
287
+
288
+ rotated_left = rotate_same_operator(node.updated(nil, [node.lhs, right.lhs]))
289
+
290
+ rotate_same_operator(node.updated(nil, [rotated_left, right.rhs]))
291
+ end
292
+
255
293
  def suspect_unary?(node)
256
294
  node.send_type? && node.unary_operation? && !node.prefix_not?
257
295
  end
@@ -261,13 +299,10 @@ module RuboCop
261
299
  end
262
300
 
263
301
  def disallowed_literal?(begin_node, node)
264
- if node.range_type?
265
- return false unless (parent = begin_node.parent)
302
+ return true unless node.range_type?
303
+ return false unless (parent = begin_node.parent)
266
304
 
267
- parent.begin_type? && parent.children.one?
268
- else
269
- !raised_to_power_negative_numeric?(begin_node, node)
270
- end
305
+ parent.begin_type? && parent.children.one?
271
306
  end
272
307
 
273
308
  # rubocop:disable Metrics/CyclomaticComplexity
@@ -291,17 +326,6 @@ module RuboCop
291
326
  node.any_match_pattern_type? && node.each_ancestor.none?(&:operator_keyword?)
292
327
  end
293
328
 
294
- def raised_to_power_negative_numeric?(begin_node, node)
295
- return false unless node.numeric_type?
296
-
297
- next_sibling = begin_node.right_sibling
298
- return false unless next_sibling
299
-
300
- base_value = node.children.first
301
-
302
- base_value.negative? && next_sibling == :**
303
- end
304
-
305
329
  def keyword_with_redundant_parentheses?(node)
306
330
  return false unless node.keyword?
307
331
  return true if node.special_keyword?
@@ -333,30 +357,6 @@ module RuboCop
333
357
  def only_begin_arg?(args)
334
358
  args.one? && args.first&.begin_type?
335
359
  end
336
-
337
- def first_argument?(node)
338
- return true if first_call_argument?(node)
339
-
340
- node.each_ancestor.any? { |ancestor| first_argument?(ancestor) }
341
- end
342
-
343
- # @!method first_call_argument?(node)
344
- def_node_matcher :first_call_argument?, <<~PATTERN
345
- {^(call _ _ equal?(%0) ...)
346
- ^({super yield} equal?(%0) ...)}
347
- PATTERN
348
-
349
- def call_chain_starts_with_int?(begin_node, send_node)
350
- recv = first_part_of_call_chain(send_node)
351
- recv&.int_type? && (parent = begin_node.parent) &&
352
- parent.send_type? && (parent.method?(:-@) || parent.method?(:+@))
353
- end
354
-
355
- def do_end_block_in_method_chain?(begin_node, node)
356
- return false unless (block = node.each_descendant(:any_block).first)
357
-
358
- block.keywords? && begin_node.each_ancestor(:call).any?
359
- end
360
360
  end
361
361
  end
362
362
  end
@@ -37,6 +37,16 @@ module RuboCop
37
37
  # x&.foo # raises NoMethodError
38
38
  # ----
39
39
  #
40
+ # Additionally, when a method chain is converted, a `NoMethodError` that
41
+ # the original code raised for an intermediate `nil` value is suppressed:
42
+ #
43
+ # [source,ruby]
44
+ # ----
45
+ # x = Struct.new(:foo).new(nil)
46
+ # x && x.foo.bar # raises NoMethodError
47
+ # x&.foo&.bar # returns nil
48
+ # ----
49
+ #
40
50
  # @example
41
51
  # # bad
42
52
  # foo.bar if foo
@@ -48,6 +48,7 @@ module RuboCop
48
48
  # end if condition_a
49
49
  class SoleNestedConditional < Base
50
50
  include RangeHelp
51
+ include ReparsedEquivalence
51
52
  extend AutoCorrector
52
53
 
53
54
  MSG = 'Consider merging nested conditions into outer `%<conditional_type>s` conditions.'
@@ -57,11 +58,10 @@ module RuboCop
57
58
  end
58
59
 
59
60
  def on_if(node)
60
- return if node.ternary? || node.else? || node.elsif?
61
+ return unless offending_conditional?(node)
61
62
 
62
63
  if_branch = node.if_branch
63
- return if use_variable_assignment_in_condition?(node.condition, if_branch)
64
- return unless offending_branch?(node, if_branch)
64
+ return unless correction_parses?(node)
65
65
 
66
66
  message = format(MSG, conditional_type: node.keyword)
67
67
  add_offense(if_branch.loc.keyword, message: message) do |corrector|
@@ -74,6 +74,23 @@ module RuboCop
74
74
 
75
75
  private
76
76
 
77
+ def offending_conditional?(node)
78
+ return false if node.ternary? || node.else? || node.elsif?
79
+
80
+ if_branch = node.if_branch
81
+ return false if use_variable_assignment_in_condition?(node.condition, if_branch)
82
+
83
+ offending_branch?(node, if_branch)
84
+ end
85
+
86
+ # Merging the conditionals intentionally produces a different AST, so
87
+ # full equivalence cannot be checked, but the corrected source must at
88
+ # least remain parseable (see `ReparsedEquivalence#correction_parses?`),
89
+ # which suppresses the entire produces-invalid-code bug class.
90
+ def apply_reparse_correction(corrector, node)
91
+ autocorrect(corrector, node, node.if_branch)
92
+ end
93
+
77
94
  def use_variable_assignment_in_condition?(condition, if_branch)
78
95
  assigned_variables = assigned_variables(condition)
79
96
 
@@ -12,6 +12,11 @@ module RuboCop
12
12
  # for some other subclass, monkey-patched with instance methods or
13
13
  # a dummy instance is instantiated from it somewhere.
14
14
  #
15
+ # When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is
16
+ # installed, classes that are subclassed anywhere in the project are
17
+ # not reported, since converting them to modules would break their
18
+ # subclasses.
19
+ #
15
20
  # @example
16
21
  # # bad
17
22
  # class SomeClass
@@ -44,6 +49,7 @@ module RuboCop
44
49
  # end
45
50
  #
46
51
  class StaticClass < Base
52
+ include ProjectIndexHelp
47
53
  include RangeHelp
48
54
  include VisibilityHelp
49
55
  extend AutoCorrector
@@ -53,6 +59,7 @@ module RuboCop
53
59
  def on_class(class_node)
54
60
  return if class_node.parent_class
55
61
  return unless class_convertible_to_module?(class_node)
62
+ return if subclassed_in_project?(class_node)
56
63
 
57
64
  add_offense(class_node) do |corrector|
58
65
  autocorrect(corrector, class_node)
@@ -61,6 +68,20 @@ module RuboCop
61
68
 
62
69
  private
63
70
 
71
+ # When `AllCops/UseProjectIndex` is enabled, a class with descendants
72
+ # anywhere in the project is not reported: converting it to a module
73
+ # would break its subclasses.
74
+ def subclassed_in_project?(class_node)
75
+ return false unless project_index
76
+
77
+ declaration = resolve_constant_in_index(class_node.identifier)
78
+ return false unless declaration.is_a?(Rubydex::Class)
79
+
80
+ declaration.descendants.any? { |descendant| descendant.name != declaration.name }
81
+ rescue StandardError
82
+ false
83
+ end
84
+
64
85
  def autocorrect(corrector, class_node)
65
86
  corrector.replace(class_node.loc.keyword, 'module')
66
87
  corrector.insert_after(class_node.loc.name, "\nmodule_function\n")