rubocop-sorted_methods_by_call 1.2.1 → 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/config/default.yml +1 -1
- data/docscribe.yml +18 -0
- data/lib/rubocop/cop/sorted_methods_by_call/waterfall.rb +394 -377
- 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,137 +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
|
-
#
|
|
389
|
-
#
|
|
390
|
-
# This method intentionally does NOT reorder across:
|
|
391
|
-
# - `private/protected/public` boundaries
|
|
392
|
-
# - nested scopes
|
|
393
|
-
# - non-visibility statements that break contiguity
|
|
391
|
+
# Attempt to autocorrect a violation by reordering methods within their visibility section.
|
|
394
392
|
#
|
|
395
|
-
# @
|
|
396
|
-
# @param
|
|
397
|
-
# @param
|
|
398
|
-
# @param
|
|
399
|
-
# @param
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
names_set = names.to_set
|
|
407
|
-
idx_of = names.each_with_index.to_h
|
|
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
|
|
408
404
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
sibling_edges = edges - direct_edges
|
|
412
|
-
|
|
413
|
-
allow_recursion = allowed_recursion?
|
|
414
|
-
adj_direct = build_adj(names, direct_edges)
|
|
405
|
+
sorted = correction_order(target[:defs], data, data[:violation])
|
|
406
|
+
return unless sorted
|
|
415
407
|
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
visibility_node = target_section[:visibility]
|
|
457
|
-
visibility_source = visibility_node&.source.to_s
|
|
458
|
-
|
|
459
|
-
new_content =
|
|
460
|
-
if visibility_source.empty?
|
|
461
|
-
sorted_def_sources.join("\n\n")
|
|
462
|
-
else
|
|
463
|
-
"#{visibility_source}\n\n#{sorted_def_sources.join("\n\n")}"
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
section_begin =
|
|
467
|
-
if visibility_node
|
|
468
|
-
visibility_node.source_range.begin_pos
|
|
469
|
-
else
|
|
470
|
-
defs.map { |d| range_with_leading_comments(d).begin_pos }.min
|
|
471
|
-
end
|
|
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
|
|
472
506
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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)
|
|
476
516
|
end
|
|
477
517
|
|
|
478
|
-
# Collect local method calls (receiver
|
|
479
|
-
# 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.
|
|
480
519
|
#
|
|
481
|
-
# @
|
|
482
|
-
# @param
|
|
483
|
-
# @
|
|
484
|
-
# @
|
|
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>]
|
|
485
524
|
def local_calls(def_node, names_set)
|
|
486
525
|
body = def_node.body
|
|
487
526
|
return [] unless body
|
|
488
527
|
|
|
528
|
+
# @type var res: Array[Symbol]
|
|
489
529
|
res = []
|
|
490
530
|
body.each_node(:send) do |send|
|
|
531
|
+
# @type var send: ::RuboCop::AST::SendNode
|
|
491
532
|
recv = send.receiver
|
|
492
533
|
next unless recv.nil? || recv&.self_type?
|
|
493
534
|
|
|
@@ -497,14 +538,15 @@ module RuboCop
|
|
|
497
538
|
res.uniq
|
|
498
539
|
end
|
|
499
540
|
|
|
500
|
-
# Build an adjacency list
|
|
541
|
+
# Build an adjacency list (caller → [callees]) from edges, restricted to known names.
|
|
501
542
|
#
|
|
502
|
-
# @
|
|
503
|
-
# @param
|
|
504
|
-
# @
|
|
505
|
-
# @
|
|
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>>]
|
|
506
547
|
def build_adj(names, edges)
|
|
507
548
|
allowed = names.to_set
|
|
549
|
+
# @type var adj: Hash[Symbol, Array[Symbol]]
|
|
508
550
|
adj = Hash.new { |h, k| h[k] = [] }
|
|
509
551
|
|
|
510
552
|
edges.each do |u, v|
|
|
@@ -517,114 +559,81 @@ module RuboCop
|
|
|
517
559
|
adj
|
|
518
560
|
end
|
|
519
561
|
|
|
520
|
-
#
|
|
562
|
+
# Check if a path exists from `src` to `dst` in the adjacency graph (BFS with limit).
|
|
521
563
|
#
|
|
522
|
-
# @
|
|
523
|
-
# @param
|
|
524
|
-
# @param
|
|
525
|
-
# @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.
|
|
526
569
|
# @return [Boolean]
|
|
527
|
-
# @api private
|
|
528
570
|
def path_exists?(src, dst, adj, limit = 200)
|
|
529
|
-
|
|
530
|
-
|
|
571
|
+
# @type var visited: Hash[Symbol, bool]
|
|
531
572
|
visited = {}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
while i < q.length
|
|
537
|
-
steps += 1
|
|
538
|
-
return false if steps > limit
|
|
539
|
-
|
|
540
|
-
u = q[i]
|
|
541
|
-
i += 1
|
|
542
|
-
next if visited[u]
|
|
573
|
+
queue = [src]
|
|
574
|
+
limit.times do
|
|
575
|
+
break if queue.empty?
|
|
543
576
|
|
|
544
|
-
|
|
577
|
+
u = queue.shift
|
|
578
|
+
visited[u] ? next : (visited[u] = true)
|
|
545
579
|
return true if u == dst
|
|
546
580
|
|
|
547
|
-
adj[u].each { |v|
|
|
581
|
+
adj[u].each { |v| queue << v unless visited[v] }
|
|
548
582
|
end
|
|
549
|
-
|
|
550
583
|
false
|
|
551
584
|
end
|
|
552
585
|
|
|
553
|
-
# Split
|
|
554
|
-
# by the visibility modifier immediately preceding them (private/protected/public).
|
|
555
|
-
#
|
|
556
|
-
# A section is represented as a Hash with:
|
|
557
|
-
# - :visibility [RuboCop::AST::Node, nil] the bare visibility send, or nil
|
|
558
|
-
# - :defs [Array<RuboCop::AST::Node>] contiguous def/defs nodes
|
|
559
|
-
# - :start_pos [Integer]
|
|
560
|
-
# - :end_pos [Integer]
|
|
586
|
+
# Split body nodes into contiguous groups separated by non-def nodes, each with a visibility.
|
|
561
587
|
#
|
|
562
|
-
# @
|
|
563
|
-
# @
|
|
564
|
-
# @
|
|
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>]
|
|
565
591
|
def extract_visibility_sections(body_nodes)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
current_defs << node
|
|
575
|
-
section_start ||= node.source_range.begin_pos
|
|
576
|
-
when :send
|
|
577
|
-
flush_visibility_section!(sections, current_visibility, current_defs, section_start, body_nodes, idx - 1)
|
|
578
|
-
current_defs = []
|
|
579
|
-
section_start = nil
|
|
580
|
-
current_visibility = node if bare_visibility_send?(node)
|
|
581
|
-
else
|
|
582
|
-
flush_visibility_section!(sections, current_visibility, current_defs, section_start, body_nodes, idx - 1)
|
|
583
|
-
current_defs = []
|
|
584
|
-
section_start = nil
|
|
585
|
-
current_visibility = nil
|
|
586
|
-
end
|
|
587
|
-
end
|
|
588
|
-
|
|
589
|
-
unless current_defs.empty?
|
|
590
|
-
sections << {
|
|
591
|
-
visibility: current_visibility,
|
|
592
|
-
defs: current_defs,
|
|
593
|
-
start_pos: section_start,
|
|
594
|
-
end_pos: current_defs.last.source_range.end_pos
|
|
595
|
-
}
|
|
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)
|
|
596
600
|
end
|
|
601
|
+
end
|
|
597
602
|
|
|
598
|
-
|
|
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)
|
|
599
610
|
end
|
|
600
611
|
|
|
601
|
-
#
|
|
612
|
+
# Find the visibility modifier node (`private`/`protected`/`public`) in a group of nodes.
|
|
602
613
|
#
|
|
603
|
-
# @
|
|
604
|
-
# @param
|
|
605
|
-
# @
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
# @return [void]
|
|
610
|
-
# @api private
|
|
611
|
-
def flush_visibility_section!(sections, current_visibility, current_defs, section_start, body_nodes, last_idx)
|
|
612
|
-
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
|
|
613
620
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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 }
|
|
620
630
|
end
|
|
621
631
|
|
|
622
|
-
# Check if
|
|
623
|
-
# `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).
|
|
624
633
|
#
|
|
625
|
-
# @
|
|
634
|
+
# @private
|
|
635
|
+
# @param [Object] node The AST node to check for bare visibility send pattern.
|
|
626
636
|
# @return [Boolean]
|
|
627
|
-
# @api private
|
|
628
637
|
def bare_visibility_send?(node)
|
|
629
638
|
node.receiver.nil? &&
|
|
630
639
|
VISIBILITY_METHODS.include?(node.method_name) &&
|
|
@@ -633,34 +642,70 @@ module RuboCop
|
|
|
633
642
|
|
|
634
643
|
# Find the visibility section containing a given method name.
|
|
635
644
|
#
|
|
636
|
-
# @
|
|
637
|
-
# @param
|
|
638
|
-
# @
|
|
639
|
-
# @
|
|
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?]
|
|
640
649
|
def section_for_method(sections, method_name)
|
|
641
650
|
sections.find { |s| s[:defs].any? { |d| d.method_name == method_name } }
|
|
642
651
|
end
|
|
643
652
|
|
|
644
|
-
#
|
|
653
|
+
# Convert a visibility section to a string label (`"public"`, `"private"`, `"protected"`).
|
|
645
654
|
#
|
|
646
|
-
# @
|
|
655
|
+
# @private
|
|
656
|
+
# @param [RuboCop::Cop::SortedMethodsByCall::Waterfall::section?] section The visibility section node (or nil for default public).
|
|
647
657
|
# @return [String]
|
|
648
|
-
# @api private
|
|
649
658
|
def visibility_label(section)
|
|
650
659
|
return 'public' unless section # default visibility
|
|
651
660
|
|
|
652
661
|
(section[:visibility]&.method_name || :public).to_s
|
|
653
662
|
end
|
|
654
663
|
|
|
655
|
-
#
|
|
664
|
+
# Topologically sort names by edges; returns nil if a cycle exists.
|
|
656
665
|
#
|
|
657
|
-
# @
|
|
658
|
-
# @param
|
|
659
|
-
# @param
|
|
660
|
-
# @
|
|
661
|
-
# @
|
|
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>?]
|
|
662
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)
|
|
663
707
|
indegree = Hash.new(0)
|
|
708
|
+
# @type var adj: Hash[Symbol, Array[Symbol]]
|
|
664
709
|
adj = Hash.new { |h, k| h[k] = [] }
|
|
665
710
|
|
|
666
711
|
edges.each do |caller, callee|
|
|
@@ -669,80 +714,52 @@ module RuboCop
|
|
|
669
714
|
|
|
670
715
|
adj[caller] << callee
|
|
671
716
|
indegree[callee] += 1
|
|
672
|
-
indegree[caller] ||= 0
|
|
673
717
|
end
|
|
674
718
|
|
|
675
719
|
names.each { |n| indegree[n] ||= 0 }
|
|
676
720
|
|
|
677
|
-
|
|
678
|
-
result = []
|
|
679
|
-
|
|
680
|
-
until queue.empty?
|
|
681
|
-
n = queue.shift
|
|
682
|
-
result << n
|
|
683
|
-
|
|
684
|
-
adj[n].each do |m|
|
|
685
|
-
indegree[m] -= 1
|
|
686
|
-
queue << m if indegree[m].zero?
|
|
687
|
-
end
|
|
688
|
-
|
|
689
|
-
queue.sort_by! { |x| idx_of[x] }
|
|
690
|
-
end
|
|
691
|
-
|
|
692
|
-
return nil unless result.size == names.size
|
|
693
|
-
|
|
694
|
-
result
|
|
721
|
+
[indegree, adj]
|
|
695
722
|
end
|
|
696
723
|
|
|
697
|
-
#
|
|
698
|
-
# above the def/defs node and ends at the end of the def. This preserves
|
|
699
|
-
# doc comments when methods are moved during autocorrect.
|
|
724
|
+
# Expand a node's source range to include leading comment lines.
|
|
700
725
|
#
|
|
701
|
-
# @
|
|
726
|
+
# @private
|
|
727
|
+
# @param [RuboCop::AST::Node] node The AST node whose source range to expand.
|
|
702
728
|
# @return [Parser::Source::Range]
|
|
703
|
-
# @api private
|
|
704
729
|
def range_with_leading_comments(node)
|
|
705
730
|
buffer = processed_source.buffer
|
|
706
731
|
expr = node.source_range
|
|
707
732
|
|
|
708
|
-
start_line = expr.line
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
while lineno >= 1
|
|
712
|
-
line = buffer.source_line(lineno)
|
|
713
|
-
break unless line =~ /\A\s*#/
|
|
714
|
-
|
|
715
|
-
start_line = lineno
|
|
716
|
-
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)
|
|
717
735
|
end
|
|
718
736
|
|
|
719
|
-
|
|
720
|
-
range_between(start_pos, expr.end_pos)
|
|
737
|
+
range_between(buffer.line_range(start_line).begin_pos, expr.end_pos)
|
|
721
738
|
end
|
|
722
739
|
|
|
723
|
-
#
|
|
740
|
+
# Recursively analyze nested class/module/sclass scopes within body nodes.
|
|
724
741
|
#
|
|
725
|
-
# @
|
|
742
|
+
# @private
|
|
743
|
+
# @param [Array<RuboCop::AST::Node>] body_nodes Body nodes to scan for nested scope definitions.
|
|
726
744
|
# @return [void]
|
|
727
|
-
# @api private
|
|
728
745
|
def analyze_nested_scopes(body_nodes)
|
|
729
746
|
body_nodes.each do |n|
|
|
730
747
|
analyze_scope(n) if n.class_type? || n.module_type? || n.sclass_type?
|
|
731
748
|
end
|
|
732
749
|
end
|
|
733
750
|
|
|
734
|
-
# Read
|
|
751
|
+
# Read the `AllowedRecursion` config option (default true).
|
|
735
752
|
#
|
|
753
|
+
# @private
|
|
736
754
|
# @return [Boolean]
|
|
737
|
-
# @api private
|
|
738
755
|
def allowed_recursion?
|
|
739
756
|
cop_config.fetch('AllowedRecursion') { true }
|
|
740
757
|
end
|
|
741
758
|
|
|
742
|
-
# Read
|
|
759
|
+
# Read the `SkipCyclicSiblingEdges` config option (default false).
|
|
743
760
|
#
|
|
761
|
+
# @private
|
|
744
762
|
# @return [Boolean]
|
|
745
|
-
# @api private
|
|
746
763
|
def skip_cyclic_sibling_edges?
|
|
747
764
|
cop_config.fetch('SkipCyclicSiblingEdges') { false }
|
|
748
765
|
end
|