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.
@@ -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 root :begin nodes (top-level).
92
+ # Entry point for top-level `:begin` scopes; delegates to `analyze_scope`.
93
93
  #
94
- # Whether top-level is analyzed depends on how the code is structured;
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 node [RuboCop::AST::Node] :class node
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 node [RuboCop::AST::Node] :module node
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 scopes (class << self).
116
+ # Entry point for singleton class (`class << self`) scopes; delegates to `analyze_scope`.
121
117
  #
122
- # @param node [RuboCop::AST::Node] :sclass node
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 single scope node (:begin, :class, :module, :sclass):
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
- # @param scope_node [RuboCop::AST::Node] a :begin, :class, :module, or :sclass node
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
- body_nodes = scope_body_nodes(scope_node)
143
- return if body_nodes.empty?
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
- names, names_set, index_of = method_name_index(def_nodes)
135
+ register_violation(data) if data[:edge]
136
+ analyze_nested_scopes(data[:body])
137
+ end
149
138
 
150
- direct_edges = build_direct_edges(def_nodes, names_set)
151
- sibling_edges = build_sibling_edges(def_nodes, names_set, direct_edges, names)
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
- edges_for_sort = direct_edges + sibling_edges
154
- adj_direct = build_adj(names, direct_edges)
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
- violation_type, violation = find_violation(direct_edges, sibling_edges, index_of, adj_direct)
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
- message = build_offense_message(
162
- violation_type: violation_type,
163
- violation: violation,
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
- add_offense(callee_node, message: message) do |corrector|
170
- try_autocorrect(corrector, body_nodes, def_nodes, edges_for_sort, violation)
171
- end
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
- analyze_nested_scopes(body_nodes)
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
- # Return the direct "body statements" inside a scope node.
185
+ # Extract direct child nodes from a scope node's body.
178
186
  #
179
- # @param node [RuboCop::AST::Node]
180
- # @return [Array<RuboCop::AST::Node>] direct children inside the scope body
181
- # @api private
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
- # Select only method definition nodes from a scope body.
204
+ # Filter body nodes to only `:def`/`:defs` (method definition) nodes.
197
205
  #
198
- # @param body_nodes [Array<RuboCop::AST::Node>]
199
- # @return [Array<RuboCop::AST::Node>] :def/:defs nodes
200
- # @api private
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
- body_nodes.select { |n| %i[def defs].include?(n.type) }
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
- # Compute helper structures for method names in this scope.
215
+ # Build index structures: array of names, set of names, and name-to-position hash.
206
216
  #
207
- # @param def_nodes [Array<RuboCop::AST::Node>]
208
- # @return [Array<(Array<Symbol>, Set<Symbol>, Hash{Symbol=>Integer})>]
209
- # @api private
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 (caller -> callee) for local calls within each method body.
225
+ # Build direct call edges from each method definition to its local callees.
216
226
  #
217
- # @param def_nodes [Array<RuboCop::AST::Node>]
218
- # @param names_set [Set<Symbol>]
219
- # @return [Array<Array(Symbol, Symbol)>]
220
- # @api private
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 (a -> b) for consecutive calls inside orchestration methods.
239
+ # Build sibling call-order edges for orchestration methods.
230
240
  #
231
- # Orchestration methods are those not called by any other method in this scope.
232
- #
233
- # @param def_nodes [Array<RuboCop::AST::Node>]
234
- # @param names_set [Set<Symbol>]
235
- # @param direct_edges [Array<Array(Symbol, Symbol)>]
236
- # @param names [Array<Symbol>]
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
- sibling_edges = []
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
- calls = local_calls(def_node, names_set)
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
- sibling_edges
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 ordering violation. Checks direct edges first, then sibling edges.
281
+ # Find the first backward edge in direct or sibling edges (waterfall order violation).
269
282
  #
270
- # @param direct_edges [Array<Array(Symbol, Symbol)>]
271
- # @param sibling_edges [Array<Array(Symbol, Symbol)>]
272
- # @param index_of [Hash{Symbol=>Integer}]
273
- # @param adj_direct [Hash{Symbol=>Array<Symbol>}] adjacency list for direct edges
274
- # @return [Array<(Symbol, Array(Symbol, Symbol))>, Array<(nil, nil)>]
275
- # @api private
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
- [nil, nil]
298
+ nil
286
299
  end
287
300
 
288
- # Return the first backward edge found, optionally skipping edges that participate
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
- # @param edges [Array<Array(Symbol, Symbol)>]
292
- # @param index_of [Hash{Symbol=>Integer}]
293
- # @param adj_direct [Hash{Symbol=>Array<Symbol>}] direct-call adjacency for path checks
294
- # @param allow_recursion [Boolean]
295
- # @return [Array(Symbol, Symbol), nil]
296
- # @api private
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
- # Construct the final offense message, including optional notes:
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
- # @param violation_type [Symbol] :direct or :sibling
311
- # @param violation [Array(Symbol, Symbol)] (caller_name, callee_name)
312
- # @param names [Array<Symbol>]
313
- # @param edges_for_sort [Array<Array(Symbol, Symbol)>]
314
- # @param body_nodes [Array<RuboCop::AST::Node>]
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
- base = add_sibling_cycle_note_if_needed(base, violation_type, caller_name, callee_name, names, edges_for_sort)
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
- # @param violation_type [Symbol]
326
- # @param caller_name [Symbol]
327
- # @param callee_name [Symbol]
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
- # Add a note when a sibling-order edge is part of a cycle in the combined graph.
351
+ # Append a sibling-cycle warning note if applicable.
339
352
  #
340
- # @param base_message [String]
341
- # @param violation_type [Symbol]
342
- # @param caller_name [Symbol]
343
- # @param callee_name [Symbol]
344
- # @param names [Array<Symbol>]
345
- # @param edges_for_sort [Array<Array(Symbol, Symbol)>]
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
- # @api private
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
- # Add a note when the violation crosses visibility boundaries.
370
+ # Append a cross-visibility note if caller and callee are in different visibility sections.
361
371
  #
362
- # @param base_message [String]
363
- # @param body_nodes [Array<RuboCop::AST::Node>]
364
- # @param caller_name [Symbol]
365
- # @param callee_name [Symbol]
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
- caller_section = section_for_method(sections, caller_name)
371
- callee_section = section_for_method(sections, callee_name)
372
-
373
- caller_vis = visibility_label(caller_section)
374
- callee_vis = visibility_label(callee_section)
375
-
376
- if caller_section && callee_section && caller_vis != callee_vis
377
- format(
378
- MSG_CROSS_VISIBILITY_NOTE,
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
- # UNSAFE autocorrect: reorder method definitions inside one contiguous visibility section only.
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
- # @param corrector [RuboCop::Cop::Corrector]
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
- sections = extract_visibility_sections(body_nodes)
404
-
405
- names = def_nodes.map(&:method_name)
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
- # Recompute direct edges; split edges back into direct vs sibling
410
- direct_edges = build_direct_edges(def_nodes, names_set)
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
- 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
- target_section = sections.find do |section|
422
- section_names = section[:defs].map(&:method_name)
423
- section_names.include?(caller_name) && section_names.include?(callee_name)
424
- end
425
- return unless target_section
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
- defs = target_section[:defs]
428
- return if defs.size <= 1
443
+ { direct: direct, sibling: edges - direct, violation: violation }
444
+ end
429
445
 
430
- section_names = defs.map(&:method_name)
431
- section_idx_of = section_names.each_with_index.to_h
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
- direct_violation = direct_edges.any? { |u, v| u == caller_name && v == callee_name }
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
- section_direct_edges = direct_edges.select { |u, v| section_names.include?(u) && section_names.include?(v) }
436
- section_sibling_edges = sibling_edges.select { |u, v| section_names.include?(u) && section_names.include?(v) }
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
- if allow_recursion
439
- pair_set = section_direct_edges.to_set
440
- section_direct_edges = section_direct_edges.reject { |u, v| pair_set.include?([v, u]) }
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
- section_edges_for_sort =
444
- if direct_violation
445
- section_direct_edges
446
- else
447
- section_sibling_edges + section_direct_edges
448
- end
449
-
450
- sorted_names = topo_sort(section_names, section_edges_for_sort, section_idx_of)
451
- return if sorted_names.nil? || sorted_names == section_names
452
-
453
- ranges_by_name = defs.to_h { |d| [d.method_name, range_with_leading_comments(d)] }
454
- sorted_def_sources = sorted_names.map { |n| ranges_by_name.fetch(n).source }
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
- section_end = target_section[:end_pos]
474
- region = range_between(section_begin, section_end)
475
- corrector.replace(region, new_content)
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 is nil/self) from within a def node,
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
- # @param def_node [RuboCop::AST::Node] :def or :defs
482
- # @param names_set [Set<Symbol>] known local method names in this scope
483
- # @return [Array<Symbol>] unique callee names
484
- # @api private
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 for a set of edges restricted to known names.
541
+ # Build an adjacency list (caller [callees]) from edges, restricted to known names.
501
542
  #
502
- # @param names [Array<Symbol>]
503
- # @param edges [Array<Array(Symbol, Symbol)>]
504
- # @return [Hash{Symbol=>Array<Symbol>}] adjacency list
505
- # @api private
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
- # Breadth-first search to detect whether a path exists from +src+ to +dst+.
562
+ # Check if a path exists from `src` to `dst` in the adjacency graph (BFS with limit).
521
563
  #
522
- # @param src [Symbol]
523
- # @param dst [Symbol]
524
- # @param adj [Hash{Symbol=>Array<Symbol>}] adjacency list
525
- # @param limit [Integer] traversal safety limit
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
- return true if src == dst
530
-
571
+ # @type var visited: Hash[Symbol, bool]
531
572
  visited = {}
532
- q = [src]
533
- i = 0
534
- steps = 0
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
- visited[u] = true
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| q << v unless visited[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 the scope body into contiguous sections of def/defs grouped
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
- # @param body_nodes [Array<RuboCop::AST::Node>]
563
- # @return [Array<Hash>]
564
- # @api private
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
- sections = []
567
- current_visibility = nil
568
- current_defs = []
569
- section_start = nil
570
-
571
- body_nodes.each_with_index do |node, idx|
572
- case node.type
573
- when :def, :defs
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
- sections
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
- # Flush a currently-collected contiguous def/defs group into +sections+.
612
+ # Find the visibility modifier node (`private`/`protected`/`public`) in a group of nodes.
602
613
  #
603
- # @param sections [Array<Hash>]
604
- # @param current_visibility [RuboCop::AST::Node, nil]
605
- # @param current_defs [Array<RuboCop::AST::Node>]
606
- # @param section_start [Integer, nil]
607
- # @param body_nodes [Array<RuboCop::AST::Node>]
608
- # @param last_idx [Integer]
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
- sections << {
615
- visibility: current_visibility,
616
- defs: current_defs.dup,
617
- start_pos: section_start,
618
- end_pos: body_nodes[last_idx].source_range.end_pos
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 +node+ is a bare visibility modifier send:
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
- # @param node [RuboCop::AST::Node]
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
- # @param sections [Array<Hash>]
637
- # @param method_name [Symbol]
638
- # @return [Hash, nil]
639
- # @api private
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
- # Normalize a section to a string visibility label ("public", "private", "protected").
653
+ # Convert a visibility section to a string label (`"public"`, `"private"`, `"protected"`).
645
654
  #
646
- # @param section [Hash, nil]
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
- # Stable topological sort using the current definition order as a tie-breaker.
664
+ # Topologically sort names by edges; returns nil if a cycle exists.
656
665
  #
657
- # @param names [Array<Symbol>]
658
- # @param edges [Array<Array(Symbol, Symbol)>]
659
- # @param idx_of [Hash{Symbol=>Integer}]
660
- # @return [Array<Symbol>, nil] sorted list, or nil if cycle prevents a full order
661
- # @api private
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
- queue = names.select { |n| indegree[n].zero? }.sort_by { |n| idx_of[n] }
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
- # Return a range that starts at the first contiguous comment line immediately
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
- # @param node [RuboCop::AST::Node] :def or :defs
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
- lineno = start_line - 1
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
- start_pos = buffer.line_range(start_line).begin_pos
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
- # Recurse into nested scopes inside the current scope body.
740
+ # Recursively analyze nested class/module/sclass scopes within body nodes.
724
741
  #
725
- # @param body_nodes [Array<RuboCop::AST::Node>]
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 config: AllowedRecursion (default true).
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 config: SkipCyclicSiblingEdges (default false).
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