Class: RuboCop::Cop::SortedMethodsByCall::Waterfall

Inherits:
Base
  • Object
show all
Extended by:
AutoCorrector
Includes:
RangeHelp
Defined in:
lib/rubocop/cop/sorted_methods_by_call/waterfall.rb,
sig/rubocop/cop/sorted_methods_by_call/waterfall.rbs

Overview

Enforces "waterfall" ordering: define a method after any method that calls it within the same scope. Produces a top-down reading flow where orchestration appears before implementation details.

  • Scopes: class/module/sclass (top-level can be analyzed via on_begin)
  • Offense: when a callee is defined above its caller
  • Autocorrect: UNSAFE; reorders methods within a contiguous visibility section (does not cross other statements or nested scopes). Preserves leading doc comments on each method. Skips cycles and non-contiguous groups.

Configuration

  • AllowedRecursion [Boolean] (default: true) If true, the cop ignores violations that are part of a recursion cycle detectable in the direct call graph (callee → … → caller). If false, such cycles are reported.
  • SafeAutoCorrect [Boolean] (default: false) Autocorrection is unsafe and only runs under -A, never under -a.
  • SkipCyclicSiblingEdges [Boolean] (default: false) If true, the cop will not add "called together" sibling-order edges that would introduce a cycle with existing constraints (direct edges + already accepted sibling edges).

Examples:

Good (waterfall order)

class Service
  def call
    foo
    bar
  end

  private

  def bar
    method123
  end

  def method123
    foo
  end

  def foo
    123
  end
end

Bad (violates waterfall order)

class Service
  def call
    foo
    bar
  end

  private

  def foo
    123
  end

  def bar
    method123
  end

  def method123
    foo
  end
end

See Also:

Constant Summary collapse

VISIBILITY_METHODS =

Returns:

  • (Array[Symbol])
%i[private protected public].freeze
MSG =

Template message for offenses where a callee appears before its caller.

Returns:

  • (String)
'Define %<callee>s after its caller %<caller>s (waterfall order).'
SIBLING_MSG =

Returns:

  • (String)
'Define %<callee>s after %<caller>s to match the order they are called together.'
MSG_CROSS_VISIBILITY_NOTE =

Returns:

  • (String)
'%<base>s (Autocorrect not supported across visibility boundaries: ' \
'%<caller_visibility>s vs %<callee_visibility>s.)'
MSG_SIBLING_CYCLE_NOTE =

Returns:

  • (String)
'%<base>s (Possible sibling cycle detected; autocorrect may be skipped.)'

Instance Method Summary collapse

Methods included from AutoCorrector

support_autocorrect?

Instance Method Details

#add_cross_visibility_note_if_needed(base_message, body_nodes, caller_name, callee_name) ⇒ String

Append a cross-visibility note if caller and callee are in different visibility sections.

Parameters:

  • base_message (String)

    The base offense message to potentially annotate.

  • body_nodes (Array<RuboCop::AST::Node>)

    Body nodes of the scope for visibility section extraction.

  • caller_name (Symbol)

    Name of the caller method.

  • callee_name (Symbol)

    Name of the callee method.

Returns:

  • (String)


378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 378

def add_cross_visibility_note_if_needed(base_message, body_nodes, caller_name, callee_name)
  sections = extract_visibility_sections(body_nodes)
  caller_vis = visibility_label(section_for_method(sections, caller_name))
  callee_vis = visibility_label(section_for_method(sections, callee_name))

  return base_message unless caller_vis != callee_vis

  format(MSG_CROSS_VISIBILITY_NOTE,
         base: base_message,
         caller_visibility: caller_vis,
         callee_visibility: callee_vis)
end

#add_sibling_cycle_note_if_needed(base_message, violation_type, caller_name, callee_name, adj_all) ⇒ String

Append a sibling-cycle warning note if applicable.

Parameters:

  • base_message (String)

    The base offense message to potentially annotate.

  • violation_type (Symbol)

    Either :direct or :sibling.

  • caller_name (Symbol)

    Name of the caller method.

  • callee_name (Symbol)

    Name of the callee method.

  • adj_all (Hash<Symbol, Array<Symbol>>)

    Full adjacency map for cycle detection.

Returns:

  • (String)


360
361
362
363
364
365
366
367
368
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 360

def add_sibling_cycle_note_if_needed(base_message, violation_type, caller_name, callee_name, adj_all)
  return base_message unless violation_type == :sibling

  if path_exists?(callee_name, caller_name, adj_all)
    format(MSG_SIBLING_CYCLE_NOTE, base: base_message)
  else
    base_message
  end
end

#allowed_recursion?Boolean

Read the AllowedRecursion config option (default true).

Returns:

  • (Boolean)


755
756
757
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 755

def allowed_recursion?
  cop_config.fetch('AllowedRecursion') { true }
end

#analyze_nested_scopes(body_nodes) ⇒ void

This method returns an undefined value.

Recursively analyze nested class/module/sclass scopes within body nodes.

Parameters:

  • body_nodes (Array<RuboCop::AST::Node>)

    Body nodes to scan for nested scope definitions.



745
746
747
748
749
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 745

def analyze_nested_scopes(body_nodes)
  body_nodes.each do |n|
    analyze_scope(n) if n.class_type? || n.module_type? || n.sclass_type?
  end
end

#analyze_scope(scope_node) ⇒ void

This method returns an undefined value.

Analyze a scope node for waterfall ordering violations and recurse into nested scopes.

Parameters:

  • scope_node (RuboCop::AST::Node)

    The scope node (begin/class/module/sclass) to analyze.



131
132
133
134
135
136
137
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 131

def analyze_scope(scope_node)
  data = scope_data(scope_node)
  return unless data

  register_violation(data) if data[:edge]
  analyze_nested_scopes(data[:body])
end

#auto_correct_data(def_nodes, edges, initial_violation) ⇒ RuboCop::Cop::SortedMethodsByCall::Waterfall::data?

Build data for autocorrection, recomputing direct edges and finding the violation.

Parameters:

  • def_nodes (Array<RuboCop::AST::DefNode>)

    All method definition nodes in the scope.

  • edges (Array<[ ::Symbol, ::Symbol ]>)

    All edges (direct + sibling) for the scope.

  • initial_violation ([ ::Symbol, ::Symbol ]?)

    The specific violating edge, or nil to auto-detect.

Returns:

  • (RuboCop::Cop::SortedMethodsByCall::Waterfall::data?)


433
434
435
436
437
438
439
440
441
442
443
444
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 433

def auto_correct_data(def_nodes, edges, initial_violation)
  names = def_nodes.map(&:method_name)
  name_set = names.to_set
  direct = build_direct_edges(def_nodes, name_set)
  adj = build_adj(names, direct)
  violation = initial_violation || first_backward_edge(
    edges, names.each_with_index.to_h, adj, allowed_recursion?
  )
  return unless violation

  { direct: direct, sibling: edges - direct, violation: violation }
end

#auto_correct_violation(corrector, data) ⇒ void

This method returns an undefined value.

Delegate autocorrection to try_autocorrect with violation data.

Parameters:

  • corrector (RuboCop::Cop::Corrector)

    The RuboCop corrector object used to apply corrections.

  • data (RuboCop::Cop::SortedMethodsByCall::Waterfall::data)

    The scope data hash containing edges and violation info.



181
182
183
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 181

def auto_correct_violation(corrector, data)
  try_autocorrect(corrector, data[:body], data[:defs], data[:edges], data[:edge])
end

#bare_visibility_send?(node) ⇒ Boolean

Check if a node is a bare visibility modifier send (no receiver, no args).

Parameters:

  • node (Object)

    The AST node to check for bare visibility send pattern.

Returns:

  • (Boolean)


637
638
639
640
641
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 637

def bare_visibility_send?(node)
  node.receiver.nil? &&
    VISIBILITY_METHODS.include?(node.method_name) &&
    node.arguments.empty?
end

#base_message_for(violation_type, caller_name, callee_name) ⇒ String

Return the base offense message template for a direct or sibling violation.

Parameters:

  • violation_type (Symbol)

    Either :direct or :sibling.

  • caller_name (Symbol)

    Name of the method that calls another.

  • callee_name (Symbol)

    Name of the method being called.

Returns:

  • (String)


343
344
345
346
347
348
349
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 343

def base_message_for(violation_type, caller_name, callee_name)
  if violation_type == :sibling
    format(SIBLING_MSG, callee: "##{callee_name}", caller: "##{caller_name}")
  else
    format(MSG, callee: "##{callee_name}", caller: "##{caller_name}")
  end
end

#bounds(ranges, defs) ⇒ Parser::Source::Range

Compute a source range spanning all given method definitions by their stored ranges.

Parameters:

  • ranges (Hash<Symbol, Object>)

    Hash mapping method names to their source ranges (including leading comments).

  • defs (Array<RuboCop::AST::DefNode>)

    Method definition nodes whose span to compute.

Returns:

  • (Parser::Source::Range)


513
514
515
516
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 513

def bounds(ranges, defs)
  range_between(defs.map { |d| ranges.fetch(d.method_name).begin_pos }.min,
                defs.map { |d| ranges.fetch(d.method_name).end_pos }.max)
end

#build_adj(names, edges) ⇒ Hash<Symbol, Array<Symbol>>

Build an adjacency list (caller → [callees]) from edges, restricted to known names.

Parameters:

  • names (Array<Symbol>)

    Ordered array of method names to restrict the adjacency to.

  • edges (Array<[ ::Symbol, ::Symbol ]>)

    Edges to build the adjacency list from.

Returns:

  • (Hash<Symbol, Array<Symbol>>)


547
548
549
550
551
552
553
554
555
556
557
558
559
560
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 547

def build_adj(names, edges)
  allowed = names.to_set
  # @type var adj: Hash[Symbol, Array[Symbol]]
  adj = Hash.new { |h, k| h[k] = [] }

  edges.each do |u, v|
    next unless allowed.include?(u) && allowed.include?(v)
    next if u == v

    adj[u] << v
  end

  adj
end

#build_direct_edges(def_nodes, names_set) ⇒ Array<[ ::Symbol, ::Symbol ]>

Build direct call edges from each method definition to its local callees.

Parameters:

  • def_nodes (Array<RuboCop::AST::DefNode>)

    Array of method definition nodes to analyze.

  • names_set (Set<Symbol>)

    Set of known method names within the current scope.

Returns:

  • (Array<[ ::Symbol, ::Symbol ]>)


231
232
233
234
235
236
237
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 231

def build_direct_edges(def_nodes, names_set)
  def_nodes.flat_map do |def_node|
    local_calls(def_node, names_set)
      .reject { |callee| callee == def_node.method_name }
      .map { |callee| [def_node.method_name, callee] }
  end
end

#build_offense_message(violation_type:, violation:, names:, edges_for_sort:, body_nodes:) ⇒ String

Build a full offense message with optional sibling-cycle and cross-visibility notes.

Parameters:

  • violation_type (Symbol)

    Either :direct or :sibling indicating the violation kind.

  • violation ([ ::Symbol, ::Symbol ])

    The violating edge as a [caller, callee] pair.

  • names (Array<Symbol>)

    Ordered array of method names for adjacency construction.

  • edges_for_sort (Array<[ ::Symbol, ::Symbol ]>)

    All edges in the scope used for building the full adjacency graph.

  • body_nodes (Array<RuboCop::AST::Node>)

    Body nodes of the scope for cross-visibility detection.

  • violation_type: (Symbol)
  • violation: ([Symbol, Symbol])
  • names: (Array[Symbol])
  • edges_for_sort: (Array[ [Symbol, Symbol] ])
  • body_nodes: (Array[::RuboCop::AST::Node])

Returns:

  • (String)


327
328
329
330
331
332
333
334
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 327

def build_offense_message(violation_type:, violation:, names:, edges_for_sort:, body_nodes:)
  caller_name, callee_name = violation

  base = base_message_for(violation_type, caller_name, callee_name)
  adj_all = build_adj(names, edges_for_sort)
  base = add_sibling_cycle_note_if_needed(base, violation_type, caller_name, callee_name, adj_all)
  add_cross_visibility_note_if_needed(base, body_nodes, caller_name, callee_name)
end

#build_sibling_edges(def_nodes, names_set, direct_edges, names) ⇒ Array<[ ::Symbol, ::Symbol ]>

Build sibling call-order edges for orchestration methods.

Parameters:

  • def_nodes (Array<RuboCop::AST::DefNode>)

    Array of method definition nodes to analyze.

  • names_set (Set<Symbol>)

    Set of known method names within the current scope.

  • direct_edges (Array<[ ::Symbol, ::Symbol ]>)

    Previously computed direct call edges.

  • names (Array<Symbol>)

    Ordered array of method names in the current scope.

Returns:

  • (Array<[ ::Symbol, ::Symbol ]>)


247
248
249
250
251
252
253
254
255
256
257
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 247

def build_sibling_edges(def_nodes, names_set, direct_edges, names)
  all_callees = direct_edges.to_set(&:last)
  direct_pair_set = direct_edges.to_set
  adj_for_siblings = build_adj(names, direct_edges)

  def_nodes.each_with_object([]) do |def_node, sibling_edges|
    next if all_callees.include?(def_node.method_name)

    sibling_edges.concat(sibling_edges_for_method(def_node, names_set, direct_pair_set, adj_for_siblings))
  end
end

#correction_order(defs, data, violation) ⇒ Array<Symbol>?

Compute the corrected method order via topological sort; returns nil if already correct.

Parameters:

  • defs (Array<RuboCop::AST::DefNode>)

    Method definition nodes in the target visibility section.

  • data (RuboCop::Cop::SortedMethodsByCall::Waterfall::data)

    Autocorrect data hash with direct, sibling edges, and violation.

  • violation ([ ::Symbol, ::Symbol ])

    The violating [caller, callee] pair to resolve.

Returns:

  • (Array<Symbol>?)


418
419
420
421
422
423
424
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 418

def correction_order(defs, data, violation)
  names = defs.map(&:method_name)
  caller_name, callee_name = violation
  result = topo_sort(names, edges_for_section(data, names, caller_name, callee_name),
                     names.each_with_index.to_h)
  result == names ? nil : result
end

#edges_for_section(data, section_names, caller_name, callee_name) ⇒ Array<[ ::Symbol, ::Symbol ]>

Filter edges to those relevant to a given visibility section and violation.

Parameters:

  • data (RuboCop::Cop::SortedMethodsByCall::Waterfall::data)

    Autocorrect data hash with direct and sibling edge lists.

  • section_names (Array<Symbol>)

    Method names in the target visibility section.

  • caller_name (Symbol)

    Name of the caller method in the violation.

  • callee_name (Symbol)

    Name of the callee method in the violation.

Returns:

  • (Array<[ ::Symbol, ::Symbol ]>)


454
455
456
457
458
459
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 454

def edges_for_section(data, section_names, caller_name, callee_name)
  direct = filter_names(data[:direct], section_names)
  sibling = filter_names(data[:sibling], section_names)
  direct = reject_reciprocal(direct) if allowed_recursion?
  data[:direct].any? { |u, v| u == caller_name && v == callee_name } ? direct : sibling + direct
end

#extract_visibility_sections(body_nodes) ⇒ Array<RuboCop::Cop::SortedMethodsByCall::Waterfall::section>

Split body nodes into contiguous groups separated by non-def nodes, each with a visibility.

Parameters:

  • body_nodes (Array<RuboCop::AST::Node>)

    Body nodes of the scope to partition into visibility sections.

Returns:

  • (Array<RuboCop::Cop::SortedMethodsByCall::Waterfall::section>)


591
592
593
594
595
596
597
598
599
600
601
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 591

def extract_visibility_sections(body_nodes)
  vis = nil
  body_nodes.slice_when { |_, b| not_def_node?(b) }.filter_map do |group|
    # @type var defs: Array[::RuboCop::AST::DefNode]
    defs = group.reject { |n| not_def_node?(n) }
    next if defs.empty?

    vis = vis_node(group) || vis
    make_section(vis, defs)
  end
end

#filter_names(edges, names) ⇒ Array<[ ::Symbol, ::Symbol ]>

Filter edges to only those whose both endpoints are in the given name list.

Parameters:

  • edges (Array<[ ::Symbol, ::Symbol ]>)

    Edges to filter.

  • names (Array<Symbol>)

    Allowed method names; only edges between these names are kept.

Returns:

  • (Array<[ ::Symbol, ::Symbol ]>)


467
468
469
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 467

def filter_names(edges, names)
  edges.select { |u, v| names.include?(u) && names.include?(v) }
end

#find_violation(direct_edges, sibling_edges, index_of, adj_direct) ⇒ [ ::Symbol, [ ::Symbol, ::Symbol ]? ]?

Find the first backward edge in direct or sibling edges (waterfall order violation).

Parameters:

  • direct_edges (Array<[ ::Symbol, ::Symbol ]>)

    Direct call edges to check for violations.

  • sibling_edges (Array<[ ::Symbol, ::Symbol ]>)

    Sibling call-order edges to check for violations.

  • index_of (Hash<Symbol, Integer>)

    Map from method names to their definition position index.

  • adj_direct (Hash<Symbol, Array<Symbol>>)

    Adjacency map of direct edges for recursion cycle detection.

Returns:

  • ([ ::Symbol, [ ::Symbol, ::Symbol ]? ]?)


289
290
291
292
293
294
295
296
297
298
299
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 289

def find_violation(direct_edges, sibling_edges, index_of, adj_direct)
  allow_recursion = allowed_recursion?

  violation = first_backward_edge(direct_edges, index_of, adj_direct, allow_recursion)
  return [:direct, violation] if violation

  violation = first_backward_edge(sibling_edges, index_of, adj_direct, allow_recursion)
  return [:sibling, violation] if violation

  nil
end

#first_backward_edge(edges, index_of, adj_direct, allow_recursion) ⇒ [ ::Symbol, ::Symbol ]?

Find the first edge where callee is defined before caller, optionally skipping recursive cycles.

Parameters:

  • edges (Array<[ ::Symbol, ::Symbol ]>)

    Edges to search for backward ordering.

  • index_of (Hash<Symbol, Integer>)

    Map from method names to their definition position index.

  • adj_direct (Hash<Symbol, Array<Symbol>>)

    Adjacency map for recursion cycle detection.

  • allow_recursion (Boolean)

    Whether to skip edges that are part of a recursive call cycle.

Returns:

  • ([ ::Symbol, ::Symbol ]?)


309
310
311
312
313
314
315
316
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 309

def first_backward_edge(edges, index_of, adj_direct, allow_recursion)
  edges.find do |caller, callee|
    next unless index_of.key?(caller) && index_of.key?(callee)
    next if allow_recursion && path_exists?(callee, caller, adj_direct)

    index_of[callee] < index_of[caller]
  end
end

#graph(names, edges) ⇒ [ ::Hash[::Symbol, ::Integer], ::Hash[::Symbol, ::Array[::Symbol]] ]

Build indegree map and adjacency list from edges for topological sort.

Parameters:

  • names (Array<Symbol>)

    Ordered array of method names to include in the graph.

  • edges (Array<[ ::Symbol, ::Symbol ]>)

    Edges defining the dependency relationships.

Returns:

  • ([ ::Hash[::Symbol, ::Integer], ::Hash[::Symbol, ::Array[::Symbol]] ])


706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 706

def graph(names, edges)
  indegree = Hash.new(0)
  # @type var adj: Hash[Symbol, Array[Symbol]]
  adj = Hash.new { |h, k| h[k] = [] }

  edges.each do |caller, callee|
    next unless names.include?(caller) && names.include?(callee)
    next if caller == callee

    adj[caller] << callee
    indegree[callee] += 1
  end

  names.each { |n| indegree[n] ||= 0 }

  [indegree, adj]
end

#kahn_sort(indegree, adj, queue, idx_of) ⇒ Array<Symbol>

Kahn's algorithm for topological sort with stable tie-breaking by original index.

Parameters:

  • indegree (Hash<Symbol, Integer>)

    Map from node to its indegree count.

  • adj (Hash<Symbol, Array<Symbol>>)

    Adjacency list of the graph.

  • queue (Array<Symbol>)

    Initial queue of nodes with zero indegree, pre-sorted by original index.

  • idx_of (Hash<Symbol, Integer>)

    Map from method names to their original position index for stable sorting.

Returns:

  • (Array<Symbol>)


686
687
688
689
690
691
692
693
694
695
696
697
698
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 686

def kahn_sort(indegree, adj, queue, idx_of)
  # @type var result: Array[Symbol]
  result = []
  until queue.empty?
    result << (n = queue.shift)
    adj[n].each do |m|
      indegree[m] -= 1
      queue << m if indegree[m].zero?
    end
    queue.sort_by! { |x| idx_of[x] }
  end
  result
end

#local_calls(def_node, names_set) ⇒ Array<Symbol>

Collect local method calls (no receiver or self) within a method body that match known names.

Parameters:

  • def_node (RuboCop::AST::DefNode)

    The method definition node whose body to scan.

  • names_set (Set<Symbol>)

    Set of known method names to match against.

Returns:

  • (Array<Symbol>)


524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 524

def local_calls(def_node, names_set)
  body = def_node.body
  return [] unless body

  # @type var res: Array[Symbol]
  res = []
  body.each_node(:send) do |send|
    # @type var send: ::RuboCop::AST::SendNode
    recv = send.receiver
    next unless recv.nil? || recv&.self_type?

    mname = send.method_name
    res << mname if names_set.include?(mname)
  end
  res.uniq
end

#make_section(vis, defs) ⇒ RuboCop::Cop::SortedMethodsByCall::Waterfall::section

Build a section hash with visibility, def nodes, and positional bounds.

Parameters:

  • vis (RuboCop::AST::Node?)

    The visibility modifier node (or nil for default public).

  • defs (Array<RuboCop::AST::DefNode>)

    Array of method definition nodes in this section.

Returns:

  • (RuboCop::Cop::SortedMethodsByCall::Waterfall::section)


627
628
629
630
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 627

def make_section(vis, defs)
  { visibility: vis, defs: defs, start_pos: defs.first.source_range.begin_pos,
    end_pos: defs.last.source_range.end_pos }
end

#method_def_nodes(body_nodes) ⇒ Array<RuboCop::AST::DefNode>

Filter body nodes to only :def/:defs (method definition) nodes.

Parameters:

  • body_nodes (Array<RuboCop::AST::Node>)

    Array of child nodes from a scope body.

Returns:

  • (Array<RuboCop::AST::DefNode>)


209
210
211
212
213
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 209

def method_def_nodes(body_nodes)
  # rubocop:disable Layout/LeadingCommentSpace
  body_nodes.select { |n| %i[def defs].include?(n.type) } #: Array[::RuboCop::AST::DefNode]
  # rubocop:enable Layout/LeadingCommentSpace
end

#method_name_index(def_nodes) ⇒ [ ::Array[::Symbol], ::Set[::Symbol], ::Hash[::Symbol, ::Integer] ]

Build index structures: array of names, set of names, and name-to-position hash.

Parameters:

  • def_nodes (Array<RuboCop::AST::DefNode>)

    Array of method definition nodes to index.

Returns:

  • ([ ::Array[::Symbol], ::Set[::Symbol], ::Hash[::Symbol, ::Integer] ])


220
221
222
223
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 220

def method_name_index(def_nodes)
  names = def_nodes.map(&:method_name)
  [names, names.to_set, names.each_with_index.to_h]
end

#not_def_node?(node) ⇒ Boolean

Check if a node is NOT a :def or :defs node.

Parameters:

  • node (Object)

    The AST node to check.

Returns:

  • (Boolean)


608
609
610
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 608

def not_def_node?(node)
  !%i[def defs].include?(node.type)
end

#on_begin(node) ⇒ void

This method returns an undefined value.

Entry point for top-level :begin scopes; delegates to analyze_scope.

Parameters:

  • node (RuboCop::AST::Node)

    The :begin AST node representing the top-level scope.



96
97
98
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 96

def on_begin(node)
  analyze_scope(node)
end

#on_class(node) ⇒ void

This method returns an undefined value.

Entry point for :class scopes; delegates to analyze_scope.

Parameters:

  • node (RuboCop::AST::Node)

    The :class AST node to analyze.



104
105
106
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 104

def on_class(node)
  analyze_scope(node)
end

#on_module(node) ⇒ void

This method returns an undefined value.

Entry point for :module scopes; delegates to analyze_scope.

Parameters:

  • node (RuboCop::AST::Node)

    The :module AST node to analyze.



112
113
114
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 112

def on_module(node)
  analyze_scope(node)
end

#on_sclass(node) ⇒ void

This method returns an undefined value.

Entry point for singleton class (class << self) scopes; delegates to analyze_scope.

Parameters:

  • node (RuboCop::AST::Node)

    The :sclass AST node to analyze.



120
121
122
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 120

def on_sclass(node)
  analyze_scope(node)
end

#path_exists?(src, dst, adj, limit = 200) ⇒ Boolean

Check if a path exists from src to dst in the adjacency graph (BFS with limit).

Parameters:

  • src (Symbol)

    The starting node for the path search.

  • dst (Symbol)

    The target node to find a path to.

  • adj (Hash<Symbol, Array<Symbol>>)

    Adjacency map of the graph to search.

  • limit (Integer) (defaults to: 200)

    Maximum number of iterations (nodes visited) for the BFS.

Returns:

  • (Boolean)


570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 570

def path_exists?(src, dst, adj, limit = 200)
  # @type var visited: Hash[Symbol, bool]
  visited = {}
  queue = [src]
  limit.times do
    break if queue.empty?

    u = queue.shift
    visited[u] ? next : (visited[u] = true)
    return true if u == dst

    adj[u].each { |v| queue << v unless visited[v] }
  end
  false
end

#range_with_leading_comments(node) ⇒ Parser::Source::Range

Expand a node's source range to include leading comment lines.

Parameters:

  • node (RuboCop::AST::Node)

    The AST node whose source range to expand.

Returns:

  • (Parser::Source::Range)


729
730
731
732
733
734
735
736
737
738
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 729

def range_with_leading_comments(node)
  buffer = processed_source.buffer
  expr = node.source_range

  start_line = (1...expr.line).reverse_each.reduce(expr.line) do |line, lineno|
    buffer.source_line(lineno) =~ /\A\s*#/ ? lineno : (break line)
  end

  range_between(buffer.line_range(start_line).begin_pos, expr.end_pos)
end

#register_violation(data) ⇒ void

This method returns an undefined value.

Register an offense for the given violation data.

Parameters:

  • data (RuboCop::Cop::SortedMethodsByCall::Waterfall::data)

    The scope data hash containing violation and method information.



165
166
167
168
169
170
171
172
173
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 165

def register_violation(data)
  _, callee = data[:edge]
  add_offense(data[:defs][data[:idx].fetch(callee)], message: build_offense_message(
    violation_type: data[:type], violation: data[:edge], names: data[:names],
    edges_for_sort: data[:edges], body_nodes: data[:body]
  )) do |corrector|
    auto_correct_violation(corrector, data)
  end
end

#reject_reciprocal(edges) ⇒ Array<[ ::Symbol, ::Symbol ]>

Remove pairs of reciprocal edges (a→b, b→a) from the edge list.

Parameters:

  • edges (Array<[ ::Symbol, ::Symbol ]>)

    Edges to filter reciprocal pairs from.

Returns:

  • (Array<[ ::Symbol, ::Symbol ]>)


476
477
478
479
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 476

def reject_reciprocal(edges)
  pair_set = edges.to_set
  edges.reject { |u, v| pair_set.include?([v, u]) }
end

#replace_sorted_section(corrector, defs, sorted_names) ⇒ void

This method returns an undefined value.

Replace the source code of a section of method definitions with the new sorted order.

Parameters:

  • corrector (RuboCop::Cop::Corrector)

    The RuboCop corrector object used to apply corrections.

  • defs (Array<RuboCop::AST::DefNode>)

    Method definition nodes in the section being reordered.

  • sorted_names (Array<Symbol>)

    Method names in their corrected order.



501
502
503
504
505
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 501

def replace_sorted_section(corrector, defs, sorted_names)
  ranges = defs.to_h { |d| [d.method_name, range_with_leading_comments(d)] }
  content = sorted_names.map { |n| ranges.fetch(n).source }.join("\n\n")
  corrector.replace(bounds(ranges, defs), content)
end

#scope_body_nodes(node) ⇒ Array<RuboCop::AST::Node>

Extract direct child nodes from a scope node's body.

Parameters:

  • node (Object)

    The scope node whose body children to extract.

Returns:

  • (Array<RuboCop::AST::Node>)


190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 190

def scope_body_nodes(node)
  case node.type
  when :begin
    node.children
  when :class, :module, :sclass
    body = node.body
    return [] unless body

    body.begin_type? ? body.children : [body]
  else
    []
  end
end

#scope_data(scope_node) ⇒ RuboCop::Cop::SortedMethodsByCall::Waterfall::data?

Build a data hash with method definitions, edges, and the first violation (if any) for a scope.

Parameters:

  • scope_node (RuboCop::AST::Node)

    The scope node to extract data from.

Returns:

  • (RuboCop::Cop::SortedMethodsByCall::Waterfall::data?)


144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 144

def scope_data(scope_node)
  body = scope_body_nodes(scope_node)
  defs = method_def_nodes(body) if body.any?
  return unless defs && defs.size > 1

  names, name_set, idx = method_name_index(defs)
  direct = build_direct_edges(defs, name_set)
  sibling = build_sibling_edges(defs, name_set, direct, names)
  adj = build_adj(names, direct)

  type, edge = find_violation(direct, sibling, idx, adj)

  { body: body, idx: idx, defs: defs, names: names, edges: direct + sibling,
    type: type, edge: edge }
end

#section_containing(sections, *method_names) ⇒ RuboCop::Cop::SortedMethodsByCall::Waterfall::section?

Find the visibility section that contains all given method names.

Parameters:

  • sections (Array<RuboCop::Cop::SortedMethodsByCall::Waterfall::section>)

    Visibility sections to search through.

  • method_names (Array<Symbol>)

    Method names to locate within a single section.

Returns:

  • (RuboCop::Cop::SortedMethodsByCall::Waterfall::section?)


487
488
489
490
491
492
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 487

def section_containing(sections, *method_names)
  sections.find do |section|
    section_names = section[:defs].map(&:method_name)
    method_names.all? { |name| section_names.include?(name) }
  end
end

#section_for_method(sections, method_name) ⇒ RuboCop::Cop::SortedMethodsByCall::Waterfall::section?

Find the visibility section containing a given method name.

Parameters:

  • sections (Array<RuboCop::Cop::SortedMethodsByCall::Waterfall::section>)

    Visibility sections to search through.

  • method_name (Symbol)

    The method name to locate.

Returns:

  • (RuboCop::Cop::SortedMethodsByCall::Waterfall::section?)


649
650
651
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 649

def section_for_method(sections, method_name)
  sections.find { |s| s[:defs].any? { |d| d.method_name == method_name } }
end

#sibling_edges_for_method(def_node, names_set, direct_pair_set, adj_for_siblings) ⇒ Array<[ ::Symbol, ::Symbol ]>

Generate sibling edges for consecutive calls within a single method body.

Parameters:

  • def_node (RuboCop::AST::DefNode)

    The method definition node whose body to scan for consecutive calls.

  • names_set (Set<Symbol>)

    Set of known method names within the current scope.

  • direct_pair_set (Set<[ ::Symbol, ::Symbol ]>)

    Set of existing direct call pairs to avoid duplicating.

  • adj_for_siblings (Hash<Symbol, Array<Symbol>>)

    Adjacency map of existing edges for cycle detection.

Returns:

  • (Array<[ ::Symbol, ::Symbol ]>)


267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 267

def sibling_edges_for_method(def_node, names_set, direct_pair_set, adj_for_siblings)
  calls = local_calls(def_node, names_set)
  calls.each_cons(2).filter_map do |a, b|
    # @type var a: Symbol
    # @type var b: Symbol

    next if direct_pair_set.include?([a, b]) || direct_pair_set.include?([b, a])
    next if skip_cyclic_sibling_edges? && path_exists?(b, a, adj_for_siblings)

    adj_for_siblings[a] << b unless adj_for_siblings[a].include?(b)
    [a, b]
  end
end

#skip_cyclic_sibling_edges?Boolean

Read the SkipCyclicSiblingEdges config option (default false).

Returns:

  • (Boolean)


763
764
765
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 763

def skip_cyclic_sibling_edges?
  cop_config.fetch('SkipCyclicSiblingEdges') { false }
end

#topo_sort(names, edges, idx_of) ⇒ Array<Symbol>?

Topologically sort names by edges; returns nil if a cycle exists.

Parameters:

  • names (Array<Symbol>)

    Ordered array of method names to sort.

  • edges (Array<[ ::Symbol, ::Symbol ]>)

    Edges defining the dependency ordering constraints.

  • idx_of (Hash<Symbol, Integer>)

    Map from method names to their original position index for stable sorting.

Returns:

  • (Array<Symbol>?)


671
672
673
674
675
676
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 671

def topo_sort(names, edges, idx_of)
  indegree, adj = graph(names, edges)
  queue = names.select { |n| indegree[n].zero? }.sort_by { |n| idx_of[n] }
  result = kahn_sort(indegree, adj, queue, idx_of)
  result.size == names.size ? result : nil
end

#try_autocorrect(corrector, body_nodes, def_nodes, edges, initial_violation = nil) ⇒ void

This method returns an undefined value.

Attempt to autocorrect a violation by reordering methods within their visibility section.

Parameters:

  • corrector (RuboCop::Cop::Corrector)

    The RuboCop corrector object used to apply corrections.

  • body_nodes (Array<RuboCop::AST::Node>)

    Body nodes of the scope for visibility section extraction.

  • def_nodes (Array<RuboCop::AST::DefNode>)

    All method definition nodes in the scope.

  • edges (Array<[ ::Symbol, ::Symbol ]>)

    All edges (direct + sibling) for the scope.

  • initial_violation ([ ::Symbol, ::Symbol ]?) (defaults to: nil)

    The specific violating edge to autocorrect, or nil to auto-detect.



400
401
402
403
404
405
406
407
408
409
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 400

def try_autocorrect(corrector, body_nodes, def_nodes, edges, initial_violation = nil)
  data = auto_correct_data(def_nodes, edges, initial_violation) or return
  target = section_containing(extract_visibility_sections(body_nodes), *data[:violation])
  return unless target && target[:defs].size > 1

  sorted = correction_order(target[:defs], data, data[:violation])
  return unless sorted

  replace_sorted_section(corrector, target[:defs], sorted)
end

#vis_node(group) ⇒ RuboCop::AST::Node?

Find the visibility modifier node (private/protected/public) in a group of nodes.

Parameters:

  • group (Array<RuboCop::AST::Node>)

    A group of consecutive body nodes to search for a visibility modifier.

Returns:

  • (RuboCop::AST::Node?)


617
618
619
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 617

def vis_node(group)
  group.find { |n| n.send_type? && bare_visibility_send?(n) }
end

#visibility_label(section) ⇒ String

Convert a visibility section to a string label ("public", "private", "protected").

Parameters:

  • section (RuboCop::Cop::SortedMethodsByCall::Waterfall::section?)

    The visibility section node (or nil for default public).

Returns:

  • (String)


658
659
660
661
662
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 658

def visibility_label(section)
  return 'public' unless section # default visibility

  (section[:visibility]&.method_name || :public).to_s
end