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.
- checksums.yaml +4 -4
- data/config/default.yml +67 -11
- data/lib/rubocop/comment_config.rb +35 -19
- data/lib/rubocop/config.rb +53 -0
- data/lib/rubocop/config_validator.rb +38 -1
- data/lib/rubocop/cop/badge.rb +8 -0
- data/lib/rubocop/cop/base.rb +6 -4
- data/lib/rubocop/cop/bundler.rb +19 -0
- data/lib/rubocop/cop/corrector.rb +1 -1
- data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
- data/lib/rubocop/cop/gemspec.rb +22 -0
- data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
- data/lib/rubocop/cop/generator.rb +4 -0
- data/lib/rubocop/cop/layout/class_structure.rb +8 -2
- data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
- data/lib/rubocop/cop/layout/indentation_width.rb +3 -1
- data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
- data/lib/rubocop/cop/layout/line_length.rb +85 -12
- data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +12 -2
- data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
- data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +33 -34
- data/lib/rubocop/cop/layout.rb +114 -0
- data/lib/rubocop/cop/lazy_loader.rb +41 -0
- data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
- data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
- data/lib/rubocop/cop/lint/constant_reassignment.rb +1 -10
- data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
- data/lib/rubocop/cop/lint/deprecated_reference.rb +185 -0
- data/lib/rubocop/cop/lint/duplicate_methods.rb +202 -22
- data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
- data/lib/rubocop/cop/lint/literal_as_condition.rb +5 -0
- data/lib/rubocop/cop/lint/literal_in_interpolation.rb +1 -1
- data/lib/rubocop/cop/lint/missing_super.rb +31 -1
- data/lib/rubocop/cop/lint/name_typo.rb +223 -0
- data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +0 -4
- data/lib/rubocop/cop/lint/safe_navigation_chain.rb +7 -0
- data/lib/rubocop/cop/lint/to_enum_arguments.rb +37 -1
- data/lib/rubocop/cop/lint/unused_private_method.rb +157 -0
- data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
- data/lib/rubocop/cop/lint/void.rb +17 -6
- data/lib/rubocop/cop/lint.rb +176 -0
- data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
- data/lib/rubocop/cop/metrics.rb +31 -0
- data/lib/rubocop/cop/migration/department_name.rb +0 -4
- data/lib/rubocop/cop/migration.rb +13 -0
- data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
- data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
- data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
- data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
- data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
- data/lib/rubocop/cop/mixin/statement_modifier.rb +42 -6
- data/lib/rubocop/cop/mixin.rb +78 -77
- data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
- data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
- data/lib/rubocop/cop/naming/variable_number.rb +3 -1
- data/lib/rubocop/cop/naming.rb +31 -0
- data/lib/rubocop/cop/offense.rb +10 -6
- data/lib/rubocop/cop/registry.rb +153 -43
- data/lib/rubocop/cop/security.rb +19 -0
- data/lib/rubocop/cop/style/array_intersect.rb +24 -5
- data/lib/rubocop/cop/style/class_and_module_children.rb +78 -18
- data/lib/rubocop/cop/style/conditional_assignment.rb +22 -0
- data/lib/rubocop/cop/style/copyright.rb +6 -1
- data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +41 -3
- data/lib/rubocop/cop/style/documentation.rb +47 -0
- data/lib/rubocop/cop/style/endless_method.rb +0 -10
- data/lib/rubocop/cop/style/if_unless_modifier.rb +7 -48
- data/lib/rubocop/cop/style/lambda.rb +4 -0
- data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +20 -3
- data/lib/rubocop/cop/style/missing_respond_to_missing.rb +30 -0
- data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
- data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
- data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
- data/lib/rubocop/cop/style/negative_array_index.rb +3 -1
- data/lib/rubocop/cop/style/redundant_assignment.rb +11 -0
- data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
- data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
- data/lib/rubocop/cop/style/redundant_format.rb +12 -3
- data/lib/rubocop/cop/style/redundant_line_continuation.rb +78 -135
- data/lib/rubocop/cop/style/redundant_parentheses.rb +63 -63
- data/lib/rubocop/cop/style/safe_navigation.rb +10 -0
- data/lib/rubocop/cop/style/sole_nested_conditional.rb +20 -3
- data/lib/rubocop/cop/style/static_class.rb +21 -0
- data/lib/rubocop/cop/style.rb +310 -0
- data/lib/rubocop/cop/team.rb +48 -3
- data/lib/rubocop/cop/variable_force/variable.rb +18 -6
- data/lib/rubocop/cops_documentation_generator.rb +1 -1
- data/lib/rubocop/directive_comment.rb +2 -0
- data/lib/rubocop/feature_loader.rb +0 -6
- data/lib/rubocop/lsp/routes.rb +41 -3
- data/lib/rubocop/lsp/runtime.rb +4 -0
- data/lib/rubocop/lsp/server.rb +4 -0
- data/lib/rubocop/lsp/stdin_runner.rb +20 -0
- data/lib/rubocop/pending_cops_reporter.rb +1 -6
- data/lib/rubocop/project_index_loader.rb +16 -0
- data/lib/rubocop/result_cache.rb +6 -4
- data/lib/rubocop/rspec/shared_contexts.rb +14 -2
- data/lib/rubocop/runner.rb +45 -20
- data/lib/rubocop/server/cache.rb +10 -5
- data/lib/rubocop/target_ruby.rb +1 -1
- data/lib/rubocop/version.rb +1 -1
- data/lib/rubocop.rb +17 -626
- metadata +18 -3
|
@@ -5,10 +5,13 @@ module RuboCop
|
|
|
5
5
|
module Style
|
|
6
6
|
# Checks for redundant line continuation.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
105
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
114
|
+
candidates
|
|
120
115
|
end
|
|
121
116
|
|
|
122
|
-
def
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
+
token_before = last_token_before(tokens, range)
|
|
132
|
+
return false unless string_literal_ending_on_line?(token_before, range.line)
|
|
131
133
|
|
|
132
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if
|
|
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
|
-
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
|
182
|
+
return unless last_line&.end_with?(LINE_CONTINUATION)
|
|
171
183
|
|
|
172
|
-
|
|
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(
|
|
175
|
-
corrector.remove_trailing(
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
265
|
-
|
|
302
|
+
return true unless node.range_type?
|
|
303
|
+
return false unless (parent = begin_node.parent)
|
|
266
304
|
|
|
267
|
-
|
|
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
|
|
61
|
+
return unless offending_conditional?(node)
|
|
61
62
|
|
|
62
63
|
if_branch = node.if_branch
|
|
63
|
-
return
|
|
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")
|