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.
@@ -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,128 +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.
391
+ # Attempt to autocorrect a violation by reordering methods within their visibility section.
389
392
  #
390
- # This method intentionally does NOT reorder across:
391
- # - `private/protected/public` boundaries
392
- # - nested scopes
393
- # - non-visibility statements that break contiguity
394
- #
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)
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
- names = def_nodes.map(&:method_name)
406
- names_set = names.to_set
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
- # 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)
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
- 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
- # 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
- region = range_between(section_begin, section_end)
466
- 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)
467
516
  end
468
517
 
469
- # Collect local method calls (receiver is nil/self) from within a def node,
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
- # @param def_node [RuboCop::AST::Node] :def or :defs
473
- # @param names_set [Set<Symbol>] known local method names in this scope
474
- # @return [Array<Symbol>] unique callee names
475
- # @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>]
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 for a set of edges restricted to known names.
541
+ # Build an adjacency list (caller [callees]) from edges, restricted to known names.
492
542
  #
493
- # @param names [Array<Symbol>]
494
- # @param edges [Array<Array(Symbol, Symbol)>]
495
- # @return [Hash{Symbol=>Array<Symbol>}] adjacency list
496
- # @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>>]
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
- # 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).
512
563
  #
513
- # @param src [Symbol]
514
- # @param dst [Symbol]
515
- # @param adj [Hash{Symbol=>Array<Symbol>}] adjacency list
516
- # @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.
517
569
  # @return [Boolean]
518
- # @api private
519
570
  def path_exists?(src, dst, adj, limit = 200)
520
- return true if src == dst
521
-
571
+ # @type var visited: Hash[Symbol, bool]
522
572
  visited = {}
523
- q = [src]
524
- i = 0
525
- steps = 0
573
+ queue = [src]
574
+ limit.times do
575
+ break if queue.empty?
526
576
 
527
- while i < q.length
528
- steps += 1
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| q << v unless visited[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 the scope body into contiguous sections of def/defs grouped
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
- # @param body_nodes [Array<RuboCop::AST::Node>]
554
- # @return [Array<Hash>]
555
- # @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>]
556
591
  def extract_visibility_sections(body_nodes)
557
- sections = []
558
- current_visibility = nil
559
- current_defs = []
560
- section_start = nil
561
-
562
- body_nodes.each_with_index do |node, idx|
563
- case node.type
564
- when :def, :defs
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
- 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)
590
610
  end
591
611
 
592
- # Flush a currently-collected contiguous def/defs group into +sections+.
612
+ # Find the visibility modifier node (`private`/`protected`/`public`) in a group of nodes.
593
613
  #
594
- # @param sections [Array<Hash>]
595
- # @param current_visibility [RuboCop::AST::Node, nil]
596
- # @param current_defs [Array<RuboCop::AST::Node>]
597
- # @param section_start [Integer, nil]
598
- # @param body_nodes [Array<RuboCop::AST::Node>]
599
- # @param last_idx [Integer]
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
- sections << {
606
- visibility: current_visibility,
607
- defs: current_defs.dup,
608
- start_pos: section_start,
609
- end_pos: body_nodes[last_idx].source_range.end_pos
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 +node+ is a bare visibility modifier send:
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
- # @param node [RuboCop::AST::Node]
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
- # @param sections [Array<Hash>]
628
- # @param method_name [Symbol]
629
- # @return [Hash, nil]
630
- # @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?]
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
- # Normalize a section to a string visibility label ("public", "private", "protected").
653
+ # Convert a visibility section to a string label (`"public"`, `"private"`, `"protected"`).
636
654
  #
637
- # @param section [Hash, nil]
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
- # Stable topological sort using the current definition order as a tie-breaker.
664
+ # Topologically sort names by edges; returns nil if a cycle exists.
647
665
  #
648
- # @param names [Array<Symbol>]
649
- # @param edges [Array<Array(Symbol, Symbol)>]
650
- # @param idx_of [Hash{Symbol=>Integer}]
651
- # @return [Array<Symbol>, nil] sorted list, or nil if cycle prevents a full order
652
- # @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>?]
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
- queue = names.select { |n| indegree[n].zero? }.sort_by { |n| idx_of[n] }
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
- # Return a range that starts at the first contiguous comment line immediately
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
- # @param node [RuboCop::AST::Node] :def or :defs
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
- lineno = start_line - 1
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
- start_pos = buffer.line_range(start_line).begin_pos
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
- # Recurse into nested scopes inside the current scope body.
740
+ # Recursively analyze nested class/module/sclass scopes within body nodes.
715
741
  #
716
- # @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.
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 config: AllowedRecursion (default true).
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 config: SkipCyclicSiblingEdges (default false).
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