rubocop-sorted_methods_by_call 1.2.2 → 1.2.3
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/.rubocop.yml +4 -1
- data/.rubocop_todo.yml +2 -57
- data/CHANGELOG.md +42 -0
- data/CODE_OF_CONDUCT.md +1 -1
- data/CONTRIBUTING.md +38 -0
- data/README.md +5 -8
- data/SECURITY.md +9 -0
- data/Steepfile +6 -0
- data/docscribe.yml +18 -0
- data/lib/rubocop/cop/sorted_methods_by_call/waterfall.rb +394 -368
- data/lib/rubocop/sorted_methods_by_call/compare.rb +11 -41
- data/lib/rubocop/sorted_methods_by_call/plugin.rb +9 -18
- data/lib/rubocop/sorted_methods_by_call/version.rb +1 -1
- data/rbs_collection.lock.yaml +120 -0
- data/rbs_collection.yaml +19 -0
- data/sig/rubocop/cop/auto_corrector.rbs +11 -0
- data/sig/rubocop/cop/sorted_methods_by_call/waterfall.rbs +71 -0
- data/sig/rubocop/sorted_methods_by_call/compare.rbs +8 -0
- data/sig/rubocop/sorted_methods_by_call/plugin.rbs +9 -0
- data/sig/rubocop/sorted_methods_by_call/version.rbs +5 -0
- metadata +14 -3
- data/lib/rubocop/sorted_methods_by_call/extensions/util.rb +0 -32
|
@@ -89,37 +89,33 @@ module RuboCop
|
|
|
89
89
|
MSG_SIBLING_CYCLE_NOTE =
|
|
90
90
|
'%<base>s (Possible sibling cycle detected; autocorrect may be skipped.)'
|
|
91
91
|
|
|
92
|
-
# Entry point for
|
|
92
|
+
# Entry point for top-level `:begin` scopes; delegates to `analyze_scope`.
|
|
93
93
|
#
|
|
94
|
-
#
|
|
95
|
-
# by default we only analyze class/module/sclass scopes, but top-level
|
|
96
|
-
# is supported through this hook.
|
|
97
|
-
#
|
|
98
|
-
# @param node [RuboCop::AST::Node] root :begin node
|
|
94
|
+
# @param [RuboCop::AST::Node] node The `:begin` AST node representing the top-level scope.
|
|
99
95
|
# @return [void]
|
|
100
96
|
def on_begin(node)
|
|
101
97
|
analyze_scope(node)
|
|
102
98
|
end
|
|
103
99
|
|
|
104
|
-
# Entry point for class scopes
|
|
100
|
+
# Entry point for `:class` scopes; delegates to `analyze_scope`.
|
|
105
101
|
#
|
|
106
|
-
# @param
|
|
102
|
+
# @param [RuboCop::AST::Node] node The `:class` AST node to analyze.
|
|
107
103
|
# @return [void]
|
|
108
104
|
def on_class(node)
|
|
109
105
|
analyze_scope(node)
|
|
110
106
|
end
|
|
111
107
|
|
|
112
|
-
# Entry point for module scopes
|
|
108
|
+
# Entry point for `:module` scopes; delegates to `analyze_scope`.
|
|
113
109
|
#
|
|
114
|
-
# @param
|
|
110
|
+
# @param [RuboCop::AST::Node] node The `:module` AST node to analyze.
|
|
115
111
|
# @return [void]
|
|
116
112
|
def on_module(node)
|
|
117
113
|
analyze_scope(node)
|
|
118
114
|
end
|
|
119
115
|
|
|
120
|
-
# Entry point for singleton class
|
|
116
|
+
# Entry point for singleton class (`class << self`) scopes; delegates to `analyze_scope`.
|
|
121
117
|
#
|
|
122
|
-
# @param
|
|
118
|
+
# @param [RuboCop::AST::Node] node The `:sclass` AST node to analyze.
|
|
123
119
|
# @return [void]
|
|
124
120
|
def on_sclass(node)
|
|
125
121
|
analyze_scope(node)
|
|
@@ -127,58 +123,70 @@ module RuboCop
|
|
|
127
123
|
|
|
128
124
|
private
|
|
129
125
|
|
|
130
|
-
# Analyze a
|
|
131
|
-
# - Collect method defs in the scope body
|
|
132
|
-
# - Build direct call edges (caller → callee)
|
|
133
|
-
# - Optionally build sibling-order edges ("called together")
|
|
134
|
-
# - Find the first ordering violation and register an offense
|
|
135
|
-
# - Attempt autocorrect (under -A) within a contiguous visibility section
|
|
136
|
-
# - Recurse into nested scopes inside the body
|
|
126
|
+
# Analyze a scope node for waterfall ordering violations and recurse into nested scopes.
|
|
137
127
|
#
|
|
138
|
-
# @
|
|
128
|
+
# @private
|
|
129
|
+
# @param [RuboCop::AST::Node] scope_node The scope node (begin/class/module/sclass) to analyze.
|
|
139
130
|
# @return [void]
|
|
140
|
-
# @api private
|
|
141
131
|
def analyze_scope(scope_node)
|
|
142
|
-
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
def_nodes = method_def_nodes(body_nodes)
|
|
146
|
-
return if def_nodes.size <= 1
|
|
132
|
+
data = scope_data(scope_node)
|
|
133
|
+
return unless data
|
|
147
134
|
|
|
148
|
-
|
|
135
|
+
register_violation(data) if data[:edge]
|
|
136
|
+
analyze_nested_scopes(data[:body])
|
|
137
|
+
end
|
|
149
138
|
|
|
150
|
-
|
|
151
|
-
|
|
139
|
+
# Build a data hash with method definitions, edges, and the first violation (if any) for a scope.
|
|
140
|
+
#
|
|
141
|
+
# @private
|
|
142
|
+
# @param [RuboCop::AST::Node] scope_node The scope node to extract data from.
|
|
143
|
+
# @return [RuboCop::Cop::SortedMethodsByCall::Waterfall::data?]
|
|
144
|
+
def scope_data(scope_node)
|
|
145
|
+
body = scope_body_nodes(scope_node)
|
|
146
|
+
defs = method_def_nodes(body) if body.any?
|
|
147
|
+
return unless defs && defs.size > 1
|
|
152
148
|
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
names, name_set, idx = method_name_index(defs)
|
|
150
|
+
direct = build_direct_edges(defs, name_set)
|
|
151
|
+
sibling = build_sibling_edges(defs, name_set, direct, names)
|
|
152
|
+
adj = build_adj(names, direct)
|
|
155
153
|
|
|
156
|
-
|
|
157
|
-
if violation
|
|
158
|
-
_, callee_name = violation
|
|
159
|
-
callee_node = def_nodes[index_of.fetch(callee_name)]
|
|
154
|
+
type, edge = find_violation(direct, sibling, idx, adj)
|
|
160
155
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
names: names,
|
|
165
|
-
edges_for_sort: edges_for_sort,
|
|
166
|
-
body_nodes: body_nodes
|
|
167
|
-
)
|
|
156
|
+
{ body: body, idx: idx, defs: defs, names: names, edges: direct + sibling,
|
|
157
|
+
type: type, edge: edge }
|
|
158
|
+
end
|
|
168
159
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
160
|
+
# Register an offense for the given violation data.
|
|
161
|
+
#
|
|
162
|
+
# @private
|
|
163
|
+
# @param [RuboCop::Cop::SortedMethodsByCall::Waterfall::data] data The scope data hash containing violation and method information.
|
|
164
|
+
# @return [void]
|
|
165
|
+
def register_violation(data)
|
|
166
|
+
_, callee = data[:edge]
|
|
167
|
+
add_offense(data[:defs][data[:idx].fetch(callee)], message: build_offense_message(
|
|
168
|
+
violation_type: data[:type], violation: data[:edge], names: data[:names],
|
|
169
|
+
edges_for_sort: data[:edges], body_nodes: data[:body]
|
|
170
|
+
)) do |corrector|
|
|
171
|
+
auto_correct_violation(corrector, data)
|
|
172
172
|
end
|
|
173
|
+
end
|
|
173
174
|
|
|
174
|
-
|
|
175
|
+
# Delegate autocorrection to `try_autocorrect` with violation data.
|
|
176
|
+
#
|
|
177
|
+
# @private
|
|
178
|
+
# @param [RuboCop::Cop::Corrector] corrector The RuboCop corrector object used to apply corrections.
|
|
179
|
+
# @param [RuboCop::Cop::SortedMethodsByCall::Waterfall::data] data The scope data hash containing edges and violation info.
|
|
180
|
+
# @return [void]
|
|
181
|
+
def auto_correct_violation(corrector, data)
|
|
182
|
+
try_autocorrect(corrector, data[:body], data[:defs], data[:edges], data[:edge])
|
|
175
183
|
end
|
|
176
184
|
|
|
177
|
-
#
|
|
185
|
+
# Extract direct child nodes from a scope node's body.
|
|
178
186
|
#
|
|
179
|
-
# @
|
|
180
|
-
# @
|
|
181
|
-
# @
|
|
187
|
+
# @private
|
|
188
|
+
# @param [Object] node The scope node whose body children to extract.
|
|
189
|
+
# @return [Array<RuboCop::AST::Node>]
|
|
182
190
|
def scope_body_nodes(node)
|
|
183
191
|
case node.type
|
|
184
192
|
when :begin
|
|
@@ -193,31 +201,33 @@ module RuboCop
|
|
|
193
201
|
end
|
|
194
202
|
end
|
|
195
203
|
|
|
196
|
-
#
|
|
204
|
+
# Filter body nodes to only `:def`/`:defs` (method definition) nodes.
|
|
197
205
|
#
|
|
198
|
-
# @
|
|
199
|
-
# @
|
|
200
|
-
# @
|
|
206
|
+
# @private
|
|
207
|
+
# @param [Array<RuboCop::AST::Node>] body_nodes Array of child nodes from a scope body.
|
|
208
|
+
# @return [Array<RuboCop::AST::DefNode>]
|
|
201
209
|
def method_def_nodes(body_nodes)
|
|
202
|
-
|
|
210
|
+
# rubocop:disable Layout/LeadingCommentSpace
|
|
211
|
+
body_nodes.select { |n| %i[def defs].include?(n.type) } #: Array[::RuboCop::AST::DefNode]
|
|
212
|
+
# rubocop:enable Layout/LeadingCommentSpace
|
|
203
213
|
end
|
|
204
214
|
|
|
205
|
-
#
|
|
215
|
+
# Build index structures: array of names, set of names, and name-to-position hash.
|
|
206
216
|
#
|
|
207
|
-
# @
|
|
208
|
-
# @
|
|
209
|
-
# @
|
|
217
|
+
# @private
|
|
218
|
+
# @param [Array<RuboCop::AST::DefNode>] def_nodes Array of method definition nodes to index.
|
|
219
|
+
# @return [[ ::Array[::Symbol], ::Set[::Symbol], ::Hash[::Symbol, ::Integer] ]]
|
|
210
220
|
def method_name_index(def_nodes)
|
|
211
221
|
names = def_nodes.map(&:method_name)
|
|
212
222
|
[names, names.to_set, names.each_with_index.to_h]
|
|
213
223
|
end
|
|
214
224
|
|
|
215
|
-
# Build direct call edges
|
|
225
|
+
# Build direct call edges from each method definition to its local callees.
|
|
216
226
|
#
|
|
217
|
-
# @
|
|
218
|
-
# @param
|
|
219
|
-
# @
|
|
220
|
-
# @
|
|
227
|
+
# @private
|
|
228
|
+
# @param [Array<RuboCop::AST::DefNode>] def_nodes Array of method definition nodes to analyze.
|
|
229
|
+
# @param [Set<Symbol>] names_set Set of known method names within the current scope.
|
|
230
|
+
# @return [Array<[ ::Symbol, ::Symbol ]>]
|
|
221
231
|
def build_direct_edges(def_nodes, names_set)
|
|
222
232
|
def_nodes.flat_map do |def_node|
|
|
223
233
|
local_calls(def_node, names_set)
|
|
@@ -226,53 +236,56 @@ module RuboCop
|
|
|
226
236
|
end
|
|
227
237
|
end
|
|
228
238
|
|
|
229
|
-
# Build sibling-order edges
|
|
239
|
+
# Build sibling call-order edges for orchestration methods.
|
|
230
240
|
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
# @param
|
|
234
|
-
# @param
|
|
235
|
-
# @param
|
|
236
|
-
# @
|
|
237
|
-
# @return [Array<Array(Symbol, Symbol)>]
|
|
238
|
-
# @api private
|
|
241
|
+
# @private
|
|
242
|
+
# @param [Array<RuboCop::AST::DefNode>] def_nodes Array of method definition nodes to analyze.
|
|
243
|
+
# @param [Set<Symbol>] names_set Set of known method names within the current scope.
|
|
244
|
+
# @param [Array<[ ::Symbol, ::Symbol ]>] direct_edges Previously computed direct call edges.
|
|
245
|
+
# @param [Array<Symbol>] names Ordered array of method names in the current scope.
|
|
246
|
+
# @return [Array<[ ::Symbol, ::Symbol ]>]
|
|
239
247
|
def build_sibling_edges(def_nodes, names_set, direct_edges, names)
|
|
240
248
|
all_callees = direct_edges.to_set(&:last)
|
|
241
249
|
direct_pair_set = direct_edges.to_set
|
|
242
|
-
|
|
243
|
-
skip_cyclic_siblings = skip_cyclic_sibling_edges?
|
|
244
250
|
adj_for_siblings = build_adj(names, direct_edges)
|
|
245
251
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def_nodes.each do |def_node|
|
|
252
|
+
def_nodes.each_with_object([]) do |def_node, sibling_edges|
|
|
249
253
|
next if all_callees.include?(def_node.method_name)
|
|
250
254
|
|
|
251
|
-
|
|
252
|
-
calls.each_cons(2) do |a, b|
|
|
253
|
-
# If there is already a direct relationship between a and b (either direction),
|
|
254
|
-
# do not add a sibling-order edge.
|
|
255
|
-
next if direct_pair_set.include?([a, b]) || direct_pair_set.include?([b, a])
|
|
256
|
-
|
|
257
|
-
# Optional: do not add a sibling edge that would introduce a cycle.
|
|
258
|
-
next if skip_cyclic_siblings && path_exists?(b, a, adj_for_siblings)
|
|
259
|
-
|
|
260
|
-
sibling_edges << [a, b]
|
|
261
|
-
adj_for_siblings[a] << b unless adj_for_siblings[a].include?(b)
|
|
262
|
-
end
|
|
255
|
+
sibling_edges.concat(sibling_edges_for_method(def_node, names_set, direct_pair_set, adj_for_siblings))
|
|
263
256
|
end
|
|
257
|
+
end
|
|
264
258
|
|
|
265
|
-
|
|
259
|
+
# Generate sibling edges for consecutive calls within a single method body.
|
|
260
|
+
#
|
|
261
|
+
# @private
|
|
262
|
+
# @param [RuboCop::AST::DefNode] def_node The method definition node whose body to scan for consecutive calls.
|
|
263
|
+
# @param [Set<Symbol>] names_set Set of known method names within the current scope.
|
|
264
|
+
# @param [Set<[ ::Symbol, ::Symbol ]>] direct_pair_set Set of existing direct call pairs to avoid duplicating.
|
|
265
|
+
# @param [Hash<Symbol, Array<Symbol>>] adj_for_siblings Adjacency map of existing edges for cycle detection.
|
|
266
|
+
# @return [Array<[ ::Symbol, ::Symbol ]>]
|
|
267
|
+
def sibling_edges_for_method(def_node, names_set, direct_pair_set, adj_for_siblings)
|
|
268
|
+
calls = local_calls(def_node, names_set)
|
|
269
|
+
calls.each_cons(2).filter_map do |a, b|
|
|
270
|
+
# @type var a: Symbol
|
|
271
|
+
# @type var b: Symbol
|
|
272
|
+
|
|
273
|
+
next if direct_pair_set.include?([a, b]) || direct_pair_set.include?([b, a])
|
|
274
|
+
next if skip_cyclic_sibling_edges? && path_exists?(b, a, adj_for_siblings)
|
|
275
|
+
|
|
276
|
+
adj_for_siblings[a] << b unless adj_for_siblings[a].include?(b)
|
|
277
|
+
[a, b]
|
|
278
|
+
end
|
|
266
279
|
end
|
|
267
280
|
|
|
268
|
-
# Find the first
|
|
281
|
+
# Find the first backward edge in direct or sibling edges (waterfall order violation).
|
|
269
282
|
#
|
|
270
|
-
# @
|
|
271
|
-
# @param
|
|
272
|
-
# @param
|
|
273
|
-
# @param
|
|
274
|
-
# @
|
|
275
|
-
# @
|
|
283
|
+
# @private
|
|
284
|
+
# @param [Array<[ ::Symbol, ::Symbol ]>] direct_edges Direct call edges to check for violations.
|
|
285
|
+
# @param [Array<[ ::Symbol, ::Symbol ]>] sibling_edges Sibling call-order edges to check for violations.
|
|
286
|
+
# @param [Hash<Symbol, Integer>] index_of Map from method names to their definition position index.
|
|
287
|
+
# @param [Hash<Symbol, Array<Symbol>>] adj_direct Adjacency map of direct edges for recursion cycle detection.
|
|
288
|
+
# @return [[ ::Symbol, [ ::Symbol, ::Symbol ]? ]?]
|
|
276
289
|
def find_violation(direct_edges, sibling_edges, index_of, adj_direct)
|
|
277
290
|
allow_recursion = allowed_recursion?
|
|
278
291
|
|
|
@@ -282,18 +295,17 @@ module RuboCop
|
|
|
282
295
|
violation = first_backward_edge(sibling_edges, index_of, adj_direct, allow_recursion)
|
|
283
296
|
return [:sibling, violation] if violation
|
|
284
297
|
|
|
285
|
-
|
|
298
|
+
nil
|
|
286
299
|
end
|
|
287
300
|
|
|
288
|
-
#
|
|
289
|
-
# in recursion/cycles detectable in the direct-call graph (AllowedRecursion).
|
|
301
|
+
# Find the first edge where callee is defined before caller, optionally skipping recursive cycles.
|
|
290
302
|
#
|
|
291
|
-
# @
|
|
292
|
-
# @param
|
|
293
|
-
# @param
|
|
294
|
-
# @param
|
|
295
|
-
# @
|
|
296
|
-
# @
|
|
303
|
+
# @private
|
|
304
|
+
# @param [Array<[ ::Symbol, ::Symbol ]>] edges Edges to search for backward ordering.
|
|
305
|
+
# @param [Hash<Symbol, Integer>] index_of Map from method names to their definition position index.
|
|
306
|
+
# @param [Hash<Symbol, Array<Symbol>>] adj_direct Adjacency map for recursion cycle detection.
|
|
307
|
+
# @param [Boolean] allow_recursion Whether to skip edges that are part of a recursive call cycle.
|
|
308
|
+
# @return [[ ::Symbol, ::Symbol ]?]
|
|
297
309
|
def first_backward_edge(edges, index_of, adj_direct, allow_recursion)
|
|
298
310
|
edges.find do |caller, callee|
|
|
299
311
|
next unless index_of.key?(caller) && index_of.key?(callee)
|
|
@@ -303,30 +315,31 @@ module RuboCop
|
|
|
303
315
|
end
|
|
304
316
|
end
|
|
305
317
|
|
|
306
|
-
#
|
|
307
|
-
# - sibling cycle note (for sibling violations)
|
|
308
|
-
# - cross-visibility note (public/private/protected boundary)
|
|
318
|
+
# Build a full offense message with optional sibling-cycle and cross-visibility notes.
|
|
309
319
|
#
|
|
310
|
-
# @
|
|
311
|
-
# @param
|
|
312
|
-
# @param
|
|
313
|
-
# @param
|
|
314
|
-
# @param
|
|
320
|
+
# @private
|
|
321
|
+
# @param [Symbol] violation_type Either `:direct` or `:sibling` indicating the violation kind.
|
|
322
|
+
# @param [[ ::Symbol, ::Symbol ]] violation The violating edge as a `[caller, callee]` pair.
|
|
323
|
+
# @param [Array<Symbol>] names Ordered array of method names for adjacency construction.
|
|
324
|
+
# @param [Array<[ ::Symbol, ::Symbol ]>] edges_for_sort All edges in the scope used for building the full adjacency graph.
|
|
325
|
+
# @param [Array<RuboCop::AST::Node>] body_nodes Body nodes of the scope for cross-visibility detection.
|
|
315
326
|
# @return [String]
|
|
316
|
-
# @api private
|
|
317
327
|
def build_offense_message(violation_type:, violation:, names:, edges_for_sort:, body_nodes:)
|
|
318
328
|
caller_name, callee_name = violation
|
|
319
329
|
|
|
320
330
|
base = base_message_for(violation_type, caller_name, callee_name)
|
|
321
|
-
|
|
331
|
+
adj_all = build_adj(names, edges_for_sort)
|
|
332
|
+
base = add_sibling_cycle_note_if_needed(base, violation_type, caller_name, callee_name, adj_all)
|
|
322
333
|
add_cross_visibility_note_if_needed(base, body_nodes, caller_name, callee_name)
|
|
323
334
|
end
|
|
324
335
|
|
|
325
|
-
#
|
|
326
|
-
#
|
|
327
|
-
# @
|
|
336
|
+
# Return the base offense message template for a direct or sibling violation.
|
|
337
|
+
#
|
|
338
|
+
# @private
|
|
339
|
+
# @param [Symbol] violation_type Either `:direct` or `:sibling`.
|
|
340
|
+
# @param [Symbol] caller_name Name of the method that calls another.
|
|
341
|
+
# @param [Symbol] callee_name Name of the method being called.
|
|
328
342
|
# @return [String]
|
|
329
|
-
# @api private
|
|
330
343
|
def base_message_for(violation_type, caller_name, callee_name)
|
|
331
344
|
if violation_type == :sibling
|
|
332
345
|
format(SIBLING_MSG, callee: "##{callee_name}", caller: "##{caller_name}")
|
|
@@ -335,21 +348,18 @@ module RuboCop
|
|
|
335
348
|
end
|
|
336
349
|
end
|
|
337
350
|
|
|
338
|
-
#
|
|
351
|
+
# Append a sibling-cycle warning note if applicable.
|
|
339
352
|
#
|
|
340
|
-
# @
|
|
341
|
-
# @param
|
|
342
|
-
# @param
|
|
343
|
-
# @param
|
|
344
|
-
# @param
|
|
345
|
-
# @param
|
|
353
|
+
# @private
|
|
354
|
+
# @param [String] base_message The base offense message to potentially annotate.
|
|
355
|
+
# @param [Symbol] violation_type Either `:direct` or `:sibling`.
|
|
356
|
+
# @param [Symbol] caller_name Name of the caller method.
|
|
357
|
+
# @param [Symbol] callee_name Name of the callee method.
|
|
358
|
+
# @param [Hash<Symbol, Array<Symbol>>] adj_all Full adjacency map for cycle detection.
|
|
346
359
|
# @return [String]
|
|
347
|
-
|
|
348
|
-
def add_sibling_cycle_note_if_needed(base_message, violation_type, caller_name, callee_name, names,
|
|
349
|
-
edges_for_sort)
|
|
360
|
+
def add_sibling_cycle_note_if_needed(base_message, violation_type, caller_name, callee_name, adj_all)
|
|
350
361
|
return base_message unless violation_type == :sibling
|
|
351
362
|
|
|
352
|
-
adj_all = build_adj(names, edges_for_sort)
|
|
353
363
|
if path_exists?(callee_name, caller_name, adj_all)
|
|
354
364
|
format(MSG_SIBLING_CYCLE_NOTE, base: base_message)
|
|
355
365
|
else
|
|
@@ -357,128 +367,168 @@ module RuboCop
|
|
|
357
367
|
end
|
|
358
368
|
end
|
|
359
369
|
|
|
360
|
-
#
|
|
370
|
+
# Append a cross-visibility note if caller and callee are in different visibility sections.
|
|
361
371
|
#
|
|
362
|
-
# @
|
|
363
|
-
# @param
|
|
364
|
-
# @param
|
|
365
|
-
# @param
|
|
372
|
+
# @private
|
|
373
|
+
# @param [String] base_message The base offense message to potentially annotate.
|
|
374
|
+
# @param [Array<RuboCop::AST::Node>] body_nodes Body nodes of the scope for visibility section extraction.
|
|
375
|
+
# @param [Symbol] caller_name Name of the caller method.
|
|
376
|
+
# @param [Symbol] callee_name Name of the callee method.
|
|
366
377
|
# @return [String]
|
|
367
|
-
# @api private
|
|
368
378
|
def add_cross_visibility_note_if_needed(base_message, body_nodes, caller_name, callee_name)
|
|
369
379
|
sections = extract_visibility_sections(body_nodes)
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
caller_vis
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
base: base_message,
|
|
380
|
-
caller_visibility: caller_vis,
|
|
381
|
-
callee_visibility: callee_vis
|
|
382
|
-
)
|
|
383
|
-
else
|
|
384
|
-
base_message
|
|
385
|
-
end
|
|
380
|
+
caller_vis = visibility_label(section_for_method(sections, caller_name))
|
|
381
|
+
callee_vis = visibility_label(section_for_method(sections, callee_name))
|
|
382
|
+
|
|
383
|
+
return base_message unless caller_vis != callee_vis
|
|
384
|
+
|
|
385
|
+
format(MSG_CROSS_VISIBILITY_NOTE,
|
|
386
|
+
base: base_message,
|
|
387
|
+
caller_visibility: caller_vis,
|
|
388
|
+
callee_visibility: callee_vis)
|
|
386
389
|
end
|
|
387
390
|
|
|
388
|
-
#
|
|
391
|
+
# Attempt to autocorrect a violation by reordering methods within their visibility section.
|
|
389
392
|
#
|
|
390
|
-
#
|
|
391
|
-
#
|
|
392
|
-
#
|
|
393
|
-
#
|
|
394
|
-
#
|
|
395
|
-
# @param
|
|
396
|
-
# @param body_nodes [Array<RuboCop::AST::Node>]
|
|
397
|
-
# @param def_nodes [Array<RuboCop::AST::Node>]
|
|
398
|
-
# @param edges [Array<Array(Symbol, Symbol)>] direct + sibling edges for this scope
|
|
399
|
-
# @param initial_violation [Array(Symbol, Symbol), nil]
|
|
393
|
+
# @private
|
|
394
|
+
# @param [RuboCop::Cop::Corrector] corrector The RuboCop corrector object used to apply corrections.
|
|
395
|
+
# @param [Array<RuboCop::AST::Node>] body_nodes Body nodes of the scope for visibility section extraction.
|
|
396
|
+
# @param [Array<RuboCop::AST::DefNode>] def_nodes All method definition nodes in the scope.
|
|
397
|
+
# @param [Array<[ ::Symbol, ::Symbol ]>] edges All edges (direct + sibling) for the scope.
|
|
398
|
+
# @param [[ ::Symbol, ::Symbol ]?] initial_violation The specific violating edge to autocorrect, or nil to auto-detect.
|
|
400
399
|
# @return [void]
|
|
401
|
-
# @api private
|
|
402
400
|
def try_autocorrect(corrector, body_nodes, def_nodes, edges, initial_violation = nil)
|
|
403
|
-
|
|
401
|
+
data = auto_correct_data(def_nodes, edges, initial_violation) or return
|
|
402
|
+
target = section_containing(extract_visibility_sections(body_nodes), *data[:violation])
|
|
403
|
+
return unless target && target[:defs].size > 1
|
|
404
404
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
idx_of = names.each_with_index.to_h
|
|
405
|
+
sorted = correction_order(target[:defs], data, data[:violation])
|
|
406
|
+
return unless sorted
|
|
408
407
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
sibling_edges = edges - direct_edges
|
|
412
|
-
|
|
413
|
-
allow_recursion = allowed_recursion?
|
|
414
|
-
adj_direct = build_adj(names, direct_edges)
|
|
415
|
-
|
|
416
|
-
violation = initial_violation || first_backward_edge(edges, idx_of, adj_direct, allow_recursion)
|
|
417
|
-
return unless violation
|
|
408
|
+
replace_sorted_section(corrector, target[:defs], sorted)
|
|
409
|
+
end
|
|
418
410
|
|
|
411
|
+
# Compute the corrected method order via topological sort; returns nil if already correct.
|
|
412
|
+
#
|
|
413
|
+
# @private
|
|
414
|
+
# @param [Array<RuboCop::AST::DefNode>] defs Method definition nodes in the target visibility section.
|
|
415
|
+
# @param [RuboCop::Cop::SortedMethodsByCall::Waterfall::data] data Autocorrect data hash with direct, sibling edges, and violation.
|
|
416
|
+
# @param [[ ::Symbol, ::Symbol ]] violation The violating `[caller, callee]` pair to resolve.
|
|
417
|
+
# @return [Array<Symbol>?]
|
|
418
|
+
def correction_order(defs, data, violation)
|
|
419
|
+
names = defs.map(&:method_name)
|
|
419
420
|
caller_name, callee_name = violation
|
|
421
|
+
result = topo_sort(names, edges_for_section(data, names, caller_name, callee_name),
|
|
422
|
+
names.each_with_index.to_h)
|
|
423
|
+
result == names ? nil : result
|
|
424
|
+
end
|
|
420
425
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
+
# Build data for autocorrection, recomputing direct edges and finding the violation.
|
|
427
|
+
#
|
|
428
|
+
# @private
|
|
429
|
+
# @param [Array<RuboCop::AST::DefNode>] def_nodes All method definition nodes in the scope.
|
|
430
|
+
# @param [Array<[ ::Symbol, ::Symbol ]>] edges All edges (direct + sibling) for the scope.
|
|
431
|
+
# @param [[ ::Symbol, ::Symbol ]?] initial_violation The specific violating edge, or nil to auto-detect.
|
|
432
|
+
# @return [RuboCop::Cop::SortedMethodsByCall::Waterfall::data?]
|
|
433
|
+
def auto_correct_data(def_nodes, edges, initial_violation)
|
|
434
|
+
names = def_nodes.map(&:method_name)
|
|
435
|
+
name_set = names.to_set
|
|
436
|
+
direct = build_direct_edges(def_nodes, name_set)
|
|
437
|
+
adj = build_adj(names, direct)
|
|
438
|
+
violation = initial_violation || first_backward_edge(
|
|
439
|
+
edges, names.each_with_index.to_h, adj, allowed_recursion?
|
|
440
|
+
)
|
|
441
|
+
return unless violation
|
|
426
442
|
|
|
427
|
-
|
|
428
|
-
|
|
443
|
+
{ direct: direct, sibling: edges - direct, violation: violation }
|
|
444
|
+
end
|
|
429
445
|
|
|
430
|
-
|
|
431
|
-
|
|
446
|
+
# Filter edges to those relevant to a given visibility section and violation.
|
|
447
|
+
#
|
|
448
|
+
# @private
|
|
449
|
+
# @param [RuboCop::Cop::SortedMethodsByCall::Waterfall::data] data Autocorrect data hash with direct and sibling edge lists.
|
|
450
|
+
# @param [Array<Symbol>] section_names Method names in the target visibility section.
|
|
451
|
+
# @param [Symbol] caller_name Name of the caller method in the violation.
|
|
452
|
+
# @param [Symbol] callee_name Name of the callee method in the violation.
|
|
453
|
+
# @return [Array<[ ::Symbol, ::Symbol ]>]
|
|
454
|
+
def edges_for_section(data, section_names, caller_name, callee_name)
|
|
455
|
+
direct = filter_names(data[:direct], section_names)
|
|
456
|
+
sibling = filter_names(data[:sibling], section_names)
|
|
457
|
+
direct = reject_reciprocal(direct) if allowed_recursion?
|
|
458
|
+
data[:direct].any? { |u, v| u == caller_name && v == callee_name } ? direct : sibling + direct
|
|
459
|
+
end
|
|
432
460
|
|
|
433
|
-
|
|
461
|
+
# Filter edges to only those whose both endpoints are in the given name list.
|
|
462
|
+
#
|
|
463
|
+
# @private
|
|
464
|
+
# @param [Array<[ ::Symbol, ::Symbol ]>] edges Edges to filter.
|
|
465
|
+
# @param [Array<Symbol>] names Allowed method names; only edges between these names are kept.
|
|
466
|
+
# @return [Array<[ ::Symbol, ::Symbol ]>]
|
|
467
|
+
def filter_names(edges, names)
|
|
468
|
+
edges.select { |u, v| names.include?(u) && names.include?(v) }
|
|
469
|
+
end
|
|
434
470
|
|
|
435
|
-
|
|
436
|
-
|
|
471
|
+
# Remove pairs of reciprocal edges (a→b, b→a) from the edge list.
|
|
472
|
+
#
|
|
473
|
+
# @private
|
|
474
|
+
# @param [Array<[ ::Symbol, ::Symbol ]>] edges Edges to filter reciprocal pairs from.
|
|
475
|
+
# @return [Array<[ ::Symbol, ::Symbol ]>]
|
|
476
|
+
def reject_reciprocal(edges)
|
|
477
|
+
pair_set = edges.to_set
|
|
478
|
+
edges.reject { |u, v| pair_set.include?([v, u]) }
|
|
479
|
+
end
|
|
437
480
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
481
|
+
# Find the visibility section that contains all given method names.
|
|
482
|
+
#
|
|
483
|
+
# @private
|
|
484
|
+
# @param [Array<RuboCop::Cop::SortedMethodsByCall::Waterfall::section>] sections Visibility sections to search through.
|
|
485
|
+
# @param [Array<Symbol>] method_names Method names to locate within a single section.
|
|
486
|
+
# @return [RuboCop::Cop::SortedMethodsByCall::Waterfall::section?]
|
|
487
|
+
def section_containing(sections, *method_names)
|
|
488
|
+
sections.find do |section|
|
|
489
|
+
section_names = section[:defs].map(&:method_name)
|
|
490
|
+
method_names.all? { |name| section_names.include?(name) }
|
|
441
491
|
end
|
|
492
|
+
end
|
|
442
493
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
# IMPORTANT: only rewrite the contiguous def/defs block itself.
|
|
457
|
-
# Do NOT include the visibility line (private/protected/public) in the rewritten region,
|
|
458
|
-
# otherwise non-method statements between the visibility modifier and the first `def`
|
|
459
|
-
# (e.g., `helper_method :x`) can be deleted. See issue #10.
|
|
460
|
-
new_content = sorted_def_sources.join("\n\n")
|
|
461
|
-
|
|
462
|
-
section_begin = defs.map { |d| ranges_by_name.fetch(d.method_name).begin_pos }.min
|
|
463
|
-
section_end = defs.map { |d| ranges_by_name.fetch(d.method_name).end_pos }.max
|
|
494
|
+
# Replace the source code of a section of method definitions with the new sorted order.
|
|
495
|
+
#
|
|
496
|
+
# @private
|
|
497
|
+
# @param [RuboCop::Cop::Corrector] corrector The RuboCop corrector object used to apply corrections.
|
|
498
|
+
# @param [Array<RuboCop::AST::DefNode>] defs Method definition nodes in the section being reordered.
|
|
499
|
+
# @param [Array<Symbol>] sorted_names Method names in their corrected order.
|
|
500
|
+
# @return [void]
|
|
501
|
+
def replace_sorted_section(corrector, defs, sorted_names)
|
|
502
|
+
ranges = defs.to_h { |d| [d.method_name, range_with_leading_comments(d)] }
|
|
503
|
+
content = sorted_names.map { |n| ranges.fetch(n).source }.join("\n\n")
|
|
504
|
+
corrector.replace(bounds(ranges, defs), content)
|
|
505
|
+
end
|
|
464
506
|
|
|
465
|
-
|
|
466
|
-
|
|
507
|
+
# Compute a source range spanning all given method definitions by their stored ranges.
|
|
508
|
+
#
|
|
509
|
+
# @private
|
|
510
|
+
# @param [Hash<Symbol, Object>] ranges Hash mapping method names to their source ranges (including leading comments).
|
|
511
|
+
# @param [Array<RuboCop::AST::DefNode>] defs Method definition nodes whose span to compute.
|
|
512
|
+
# @return [Parser::Source::Range]
|
|
513
|
+
def bounds(ranges, defs)
|
|
514
|
+
range_between(defs.map { |d| ranges.fetch(d.method_name).begin_pos }.min,
|
|
515
|
+
defs.map { |d| ranges.fetch(d.method_name).end_pos }.max)
|
|
467
516
|
end
|
|
468
517
|
|
|
469
|
-
# Collect local method calls (receiver
|
|
470
|
-
# restricted to known method names in this scope.
|
|
518
|
+
# Collect local method calls (no receiver or self) within a method body that match known names.
|
|
471
519
|
#
|
|
472
|
-
# @
|
|
473
|
-
# @param
|
|
474
|
-
# @
|
|
475
|
-
# @
|
|
520
|
+
# @private
|
|
521
|
+
# @param [RuboCop::AST::DefNode] def_node The method definition node whose body to scan.
|
|
522
|
+
# @param [Set<Symbol>] names_set Set of known method names to match against.
|
|
523
|
+
# @return [Array<Symbol>]
|
|
476
524
|
def local_calls(def_node, names_set)
|
|
477
525
|
body = def_node.body
|
|
478
526
|
return [] unless body
|
|
479
527
|
|
|
528
|
+
# @type var res: Array[Symbol]
|
|
480
529
|
res = []
|
|
481
530
|
body.each_node(:send) do |send|
|
|
531
|
+
# @type var send: ::RuboCop::AST::SendNode
|
|
482
532
|
recv = send.receiver
|
|
483
533
|
next unless recv.nil? || recv&.self_type?
|
|
484
534
|
|
|
@@ -488,14 +538,15 @@ module RuboCop
|
|
|
488
538
|
res.uniq
|
|
489
539
|
end
|
|
490
540
|
|
|
491
|
-
# Build an adjacency list
|
|
541
|
+
# Build an adjacency list (caller → [callees]) from edges, restricted to known names.
|
|
492
542
|
#
|
|
493
|
-
# @
|
|
494
|
-
# @param
|
|
495
|
-
# @
|
|
496
|
-
# @
|
|
543
|
+
# @private
|
|
544
|
+
# @param [Array<Symbol>] names Ordered array of method names to restrict the adjacency to.
|
|
545
|
+
# @param [Array<[ ::Symbol, ::Symbol ]>] edges Edges to build the adjacency list from.
|
|
546
|
+
# @return [Hash<Symbol, Array<Symbol>>]
|
|
497
547
|
def build_adj(names, edges)
|
|
498
548
|
allowed = names.to_set
|
|
549
|
+
# @type var adj: Hash[Symbol, Array[Symbol]]
|
|
499
550
|
adj = Hash.new { |h, k| h[k] = [] }
|
|
500
551
|
|
|
501
552
|
edges.each do |u, v|
|
|
@@ -508,114 +559,81 @@ module RuboCop
|
|
|
508
559
|
adj
|
|
509
560
|
end
|
|
510
561
|
|
|
511
|
-
#
|
|
562
|
+
# Check if a path exists from `src` to `dst` in the adjacency graph (BFS with limit).
|
|
512
563
|
#
|
|
513
|
-
# @
|
|
514
|
-
# @param
|
|
515
|
-
# @param
|
|
516
|
-
# @param
|
|
564
|
+
# @private
|
|
565
|
+
# @param [Symbol] src The starting node for the path search.
|
|
566
|
+
# @param [Symbol] dst The target node to find a path to.
|
|
567
|
+
# @param [Hash<Symbol, Array<Symbol>>] adj Adjacency map of the graph to search.
|
|
568
|
+
# @param [Integer] limit Maximum number of iterations (nodes visited) for the BFS.
|
|
517
569
|
# @return [Boolean]
|
|
518
|
-
# @api private
|
|
519
570
|
def path_exists?(src, dst, adj, limit = 200)
|
|
520
|
-
|
|
521
|
-
|
|
571
|
+
# @type var visited: Hash[Symbol, bool]
|
|
522
572
|
visited = {}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
573
|
+
queue = [src]
|
|
574
|
+
limit.times do
|
|
575
|
+
break if queue.empty?
|
|
526
576
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
return false if steps > limit
|
|
530
|
-
|
|
531
|
-
u = q[i]
|
|
532
|
-
i += 1
|
|
533
|
-
next if visited[u]
|
|
534
|
-
|
|
535
|
-
visited[u] = true
|
|
577
|
+
u = queue.shift
|
|
578
|
+
visited[u] ? next : (visited[u] = true)
|
|
536
579
|
return true if u == dst
|
|
537
580
|
|
|
538
|
-
adj[u].each { |v|
|
|
581
|
+
adj[u].each { |v| queue << v unless visited[v] }
|
|
539
582
|
end
|
|
540
|
-
|
|
541
583
|
false
|
|
542
584
|
end
|
|
543
585
|
|
|
544
|
-
# Split
|
|
545
|
-
# by the visibility modifier immediately preceding them (private/protected/public).
|
|
546
|
-
#
|
|
547
|
-
# A section is represented as a Hash with:
|
|
548
|
-
# - :visibility [RuboCop::AST::Node, nil] the bare visibility send, or nil
|
|
549
|
-
# - :defs [Array<RuboCop::AST::Node>] contiguous def/defs nodes
|
|
550
|
-
# - :start_pos [Integer]
|
|
551
|
-
# - :end_pos [Integer]
|
|
586
|
+
# Split body nodes into contiguous groups separated by non-def nodes, each with a visibility.
|
|
552
587
|
#
|
|
553
|
-
# @
|
|
554
|
-
# @
|
|
555
|
-
# @
|
|
588
|
+
# @private
|
|
589
|
+
# @param [Array<RuboCop::AST::Node>] body_nodes Body nodes of the scope to partition into visibility sections.
|
|
590
|
+
# @return [Array<RuboCop::Cop::SortedMethodsByCall::Waterfall::section>]
|
|
556
591
|
def extract_visibility_sections(body_nodes)
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
current_defs << node
|
|
566
|
-
section_start ||= node.source_range.begin_pos
|
|
567
|
-
when :send
|
|
568
|
-
flush_visibility_section!(sections, current_visibility, current_defs, section_start, body_nodes, idx - 1)
|
|
569
|
-
current_defs = []
|
|
570
|
-
section_start = nil
|
|
571
|
-
current_visibility = node if bare_visibility_send?(node)
|
|
572
|
-
else
|
|
573
|
-
flush_visibility_section!(sections, current_visibility, current_defs, section_start, body_nodes, idx - 1)
|
|
574
|
-
current_defs = []
|
|
575
|
-
section_start = nil
|
|
576
|
-
current_visibility = nil
|
|
577
|
-
end
|
|
578
|
-
end
|
|
579
|
-
|
|
580
|
-
unless current_defs.empty?
|
|
581
|
-
sections << {
|
|
582
|
-
visibility: current_visibility,
|
|
583
|
-
defs: current_defs,
|
|
584
|
-
start_pos: section_start,
|
|
585
|
-
end_pos: current_defs.last.source_range.end_pos
|
|
586
|
-
}
|
|
592
|
+
vis = nil
|
|
593
|
+
body_nodes.slice_when { |_, b| not_def_node?(b) }.filter_map do |group|
|
|
594
|
+
# @type var defs: Array[::RuboCop::AST::DefNode]
|
|
595
|
+
defs = group.reject { |n| not_def_node?(n) }
|
|
596
|
+
next if defs.empty?
|
|
597
|
+
|
|
598
|
+
vis = vis_node(group) || vis
|
|
599
|
+
make_section(vis, defs)
|
|
587
600
|
end
|
|
601
|
+
end
|
|
588
602
|
|
|
589
|
-
|
|
603
|
+
# Check if a node is NOT a `:def` or `:defs` node.
|
|
604
|
+
#
|
|
605
|
+
# @private
|
|
606
|
+
# @param [Object] node The AST node to check.
|
|
607
|
+
# @return [Boolean]
|
|
608
|
+
def not_def_node?(node)
|
|
609
|
+
!%i[def defs].include?(node.type)
|
|
590
610
|
end
|
|
591
611
|
|
|
592
|
-
#
|
|
612
|
+
# Find the visibility modifier node (`private`/`protected`/`public`) in a group of nodes.
|
|
593
613
|
#
|
|
594
|
-
# @
|
|
595
|
-
# @param
|
|
596
|
-
# @
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
# @return [void]
|
|
601
|
-
# @api private
|
|
602
|
-
def flush_visibility_section!(sections, current_visibility, current_defs, section_start, body_nodes, last_idx)
|
|
603
|
-
return if current_defs.empty?
|
|
614
|
+
# @private
|
|
615
|
+
# @param [Array<RuboCop::AST::Node>] group A group of consecutive body nodes to search for a visibility modifier.
|
|
616
|
+
# @return [RuboCop::AST::Node?]
|
|
617
|
+
def vis_node(group)
|
|
618
|
+
group.find { |n| n.send_type? && bare_visibility_send?(n) }
|
|
619
|
+
end
|
|
604
620
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
621
|
+
# Build a section hash with visibility, def nodes, and positional bounds.
|
|
622
|
+
#
|
|
623
|
+
# @private
|
|
624
|
+
# @param [RuboCop::AST::Node?] vis The visibility modifier node (or nil for default public).
|
|
625
|
+
# @param [Array<RuboCop::AST::DefNode>] defs Array of method definition nodes in this section.
|
|
626
|
+
# @return [RuboCop::Cop::SortedMethodsByCall::Waterfall::section]
|
|
627
|
+
def make_section(vis, defs)
|
|
628
|
+
{ visibility: vis, defs: defs, start_pos: defs.first.source_range.begin_pos,
|
|
629
|
+
end_pos: defs.last.source_range.end_pos }
|
|
611
630
|
end
|
|
612
631
|
|
|
613
|
-
# Check if
|
|
614
|
-
# `private`, `protected`, or `public` (with no args and no receiver).
|
|
632
|
+
# Check if a node is a bare visibility modifier send (no receiver, no args).
|
|
615
633
|
#
|
|
616
|
-
# @
|
|
634
|
+
# @private
|
|
635
|
+
# @param [Object] node The AST node to check for bare visibility send pattern.
|
|
617
636
|
# @return [Boolean]
|
|
618
|
-
# @api private
|
|
619
637
|
def bare_visibility_send?(node)
|
|
620
638
|
node.receiver.nil? &&
|
|
621
639
|
VISIBILITY_METHODS.include?(node.method_name) &&
|
|
@@ -624,34 +642,70 @@ module RuboCop
|
|
|
624
642
|
|
|
625
643
|
# Find the visibility section containing a given method name.
|
|
626
644
|
#
|
|
627
|
-
# @
|
|
628
|
-
# @param
|
|
629
|
-
# @
|
|
630
|
-
# @
|
|
645
|
+
# @private
|
|
646
|
+
# @param [Array<RuboCop::Cop::SortedMethodsByCall::Waterfall::section>] sections Visibility sections to search through.
|
|
647
|
+
# @param [Symbol] method_name The method name to locate.
|
|
648
|
+
# @return [RuboCop::Cop::SortedMethodsByCall::Waterfall::section?]
|
|
631
649
|
def section_for_method(sections, method_name)
|
|
632
650
|
sections.find { |s| s[:defs].any? { |d| d.method_name == method_name } }
|
|
633
651
|
end
|
|
634
652
|
|
|
635
|
-
#
|
|
653
|
+
# Convert a visibility section to a string label (`"public"`, `"private"`, `"protected"`).
|
|
636
654
|
#
|
|
637
|
-
# @
|
|
655
|
+
# @private
|
|
656
|
+
# @param [RuboCop::Cop::SortedMethodsByCall::Waterfall::section?] section The visibility section node (or nil for default public).
|
|
638
657
|
# @return [String]
|
|
639
|
-
# @api private
|
|
640
658
|
def visibility_label(section)
|
|
641
659
|
return 'public' unless section # default visibility
|
|
642
660
|
|
|
643
661
|
(section[:visibility]&.method_name || :public).to_s
|
|
644
662
|
end
|
|
645
663
|
|
|
646
|
-
#
|
|
664
|
+
# Topologically sort names by edges; returns nil if a cycle exists.
|
|
647
665
|
#
|
|
648
|
-
# @
|
|
649
|
-
# @param
|
|
650
|
-
# @param
|
|
651
|
-
# @
|
|
652
|
-
# @
|
|
666
|
+
# @private
|
|
667
|
+
# @param [Array<Symbol>] names Ordered array of method names to sort.
|
|
668
|
+
# @param [Array<[ ::Symbol, ::Symbol ]>] edges Edges defining the dependency ordering constraints.
|
|
669
|
+
# @param [Hash<Symbol, Integer>] idx_of Map from method names to their original position index for stable sorting.
|
|
670
|
+
# @return [Array<Symbol>?]
|
|
653
671
|
def topo_sort(names, edges, idx_of)
|
|
672
|
+
indegree, adj = graph(names, edges)
|
|
673
|
+
queue = names.select { |n| indegree[n].zero? }.sort_by { |n| idx_of[n] }
|
|
674
|
+
result = kahn_sort(indegree, adj, queue, idx_of)
|
|
675
|
+
result.size == names.size ? result : nil
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Kahn's algorithm for topological sort with stable tie-breaking by original index.
|
|
679
|
+
#
|
|
680
|
+
# @private
|
|
681
|
+
# @param [Hash<Symbol, Integer>] indegree Map from node to its indegree count.
|
|
682
|
+
# @param [Hash<Symbol, Array<Symbol>>] adj Adjacency list of the graph.
|
|
683
|
+
# @param [Array<Symbol>] queue Initial queue of nodes with zero indegree, pre-sorted by original index.
|
|
684
|
+
# @param [Hash<Symbol, Integer>] idx_of Map from method names to their original position index for stable sorting.
|
|
685
|
+
# @return [Array<Symbol>]
|
|
686
|
+
def kahn_sort(indegree, adj, queue, idx_of)
|
|
687
|
+
# @type var result: Array[Symbol]
|
|
688
|
+
result = []
|
|
689
|
+
until queue.empty?
|
|
690
|
+
result << (n = queue.shift)
|
|
691
|
+
adj[n].each do |m|
|
|
692
|
+
indegree[m] -= 1
|
|
693
|
+
queue << m if indegree[m].zero?
|
|
694
|
+
end
|
|
695
|
+
queue.sort_by! { |x| idx_of[x] }
|
|
696
|
+
end
|
|
697
|
+
result
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
# Build indegree map and adjacency list from edges for topological sort.
|
|
701
|
+
#
|
|
702
|
+
# @private
|
|
703
|
+
# @param [Array<Symbol>] names Ordered array of method names to include in the graph.
|
|
704
|
+
# @param [Array<[ ::Symbol, ::Symbol ]>] edges Edges defining the dependency relationships.
|
|
705
|
+
# @return [[ ::Hash[::Symbol, ::Integer], ::Hash[::Symbol, ::Array[::Symbol]] ]]
|
|
706
|
+
def graph(names, edges)
|
|
654
707
|
indegree = Hash.new(0)
|
|
708
|
+
# @type var adj: Hash[Symbol, Array[Symbol]]
|
|
655
709
|
adj = Hash.new { |h, k| h[k] = [] }
|
|
656
710
|
|
|
657
711
|
edges.each do |caller, callee|
|
|
@@ -660,80 +714,52 @@ module RuboCop
|
|
|
660
714
|
|
|
661
715
|
adj[caller] << callee
|
|
662
716
|
indegree[callee] += 1
|
|
663
|
-
indegree[caller] ||= 0
|
|
664
717
|
end
|
|
665
718
|
|
|
666
719
|
names.each { |n| indegree[n] ||= 0 }
|
|
667
720
|
|
|
668
|
-
|
|
669
|
-
result = []
|
|
670
|
-
|
|
671
|
-
until queue.empty?
|
|
672
|
-
n = queue.shift
|
|
673
|
-
result << n
|
|
674
|
-
|
|
675
|
-
adj[n].each do |m|
|
|
676
|
-
indegree[m] -= 1
|
|
677
|
-
queue << m if indegree[m].zero?
|
|
678
|
-
end
|
|
679
|
-
|
|
680
|
-
queue.sort_by! { |x| idx_of[x] }
|
|
681
|
-
end
|
|
682
|
-
|
|
683
|
-
return nil unless result.size == names.size
|
|
684
|
-
|
|
685
|
-
result
|
|
721
|
+
[indegree, adj]
|
|
686
722
|
end
|
|
687
723
|
|
|
688
|
-
#
|
|
689
|
-
# above the def/defs node and ends at the end of the def. This preserves
|
|
690
|
-
# doc comments when methods are moved during autocorrect.
|
|
724
|
+
# Expand a node's source range to include leading comment lines.
|
|
691
725
|
#
|
|
692
|
-
# @
|
|
726
|
+
# @private
|
|
727
|
+
# @param [RuboCop::AST::Node] node The AST node whose source range to expand.
|
|
693
728
|
# @return [Parser::Source::Range]
|
|
694
|
-
# @api private
|
|
695
729
|
def range_with_leading_comments(node)
|
|
696
730
|
buffer = processed_source.buffer
|
|
697
731
|
expr = node.source_range
|
|
698
732
|
|
|
699
|
-
start_line = expr.line
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
while lineno >= 1
|
|
703
|
-
line = buffer.source_line(lineno)
|
|
704
|
-
break unless line =~ /\A\s*#/
|
|
705
|
-
|
|
706
|
-
start_line = lineno
|
|
707
|
-
lineno -= 1
|
|
733
|
+
start_line = (1...expr.line).reverse_each.reduce(expr.line) do |line, lineno|
|
|
734
|
+
buffer.source_line(lineno) =~ /\A\s*#/ ? lineno : (break line)
|
|
708
735
|
end
|
|
709
736
|
|
|
710
|
-
|
|
711
|
-
range_between(start_pos, expr.end_pos)
|
|
737
|
+
range_between(buffer.line_range(start_line).begin_pos, expr.end_pos)
|
|
712
738
|
end
|
|
713
739
|
|
|
714
|
-
#
|
|
740
|
+
# Recursively analyze nested class/module/sclass scopes within body nodes.
|
|
715
741
|
#
|
|
716
|
-
# @
|
|
742
|
+
# @private
|
|
743
|
+
# @param [Array<RuboCop::AST::Node>] body_nodes Body nodes to scan for nested scope definitions.
|
|
717
744
|
# @return [void]
|
|
718
|
-
# @api private
|
|
719
745
|
def analyze_nested_scopes(body_nodes)
|
|
720
746
|
body_nodes.each do |n|
|
|
721
747
|
analyze_scope(n) if n.class_type? || n.module_type? || n.sclass_type?
|
|
722
748
|
end
|
|
723
749
|
end
|
|
724
750
|
|
|
725
|
-
# Read
|
|
751
|
+
# Read the `AllowedRecursion` config option (default true).
|
|
726
752
|
#
|
|
753
|
+
# @private
|
|
727
754
|
# @return [Boolean]
|
|
728
|
-
# @api private
|
|
729
755
|
def allowed_recursion?
|
|
730
756
|
cop_config.fetch('AllowedRecursion') { true }
|
|
731
757
|
end
|
|
732
758
|
|
|
733
|
-
# Read
|
|
759
|
+
# Read the `SkipCyclicSiblingEdges` config option (default false).
|
|
734
760
|
#
|
|
761
|
+
# @private
|
|
735
762
|
# @return [Boolean]
|
|
736
|
-
# @api private
|
|
737
763
|
def skip_cyclic_sibling_edges?
|
|
738
764
|
cop_config.fetch('SkipCyclicSiblingEdges') { false }
|
|
739
765
|
end
|