Class: RuboCop::Cop::SortedMethodsByCall::Waterfall
- Inherits:
-
Base
- Object
- Base
- RuboCop::Cop::SortedMethodsByCall::Waterfall
- 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).
Constant Summary collapse
- VISIBILITY_METHODS =
%i[private protected public].freeze
- MSG =
Template message for offenses where a callee appears before its caller.
'Define %<callee>s after its caller %<caller>s (waterfall order).'- SIBLING_MSG =
'Define %<callee>s after %<caller>s to match the order they are called together.'- MSG_CROSS_VISIBILITY_NOTE =
'%<base>s (Autocorrect not supported across visibility boundaries: ' \ '%<caller_visibility>s vs %<callee_visibility>s.)'
- MSG_SIBLING_CYCLE_NOTE =
'%<base>s (Possible sibling cycle detected; autocorrect may be skipped.)'
Instance Method Summary collapse
-
#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.
-
#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.
-
#allowed_recursion? ⇒ Boolean
Read the
AllowedRecursionconfig option (default true). -
#analyze_nested_scopes(body_nodes) ⇒ void
Recursively analyze nested class/module/sclass scopes within body nodes.
-
#analyze_scope(scope_node) ⇒ void
Analyze a scope node for waterfall ordering violations and recurse into nested scopes.
-
#auto_correct_data(def_nodes, edges, initial_violation) ⇒ RuboCop::Cop::SortedMethodsByCall::Waterfall::data?
Build data for autocorrection, recomputing direct edges and finding the violation.
-
#auto_correct_violation(corrector, data) ⇒ void
Delegate autocorrection to
try_autocorrectwith violation data. -
#bare_visibility_send?(node) ⇒ Boolean
Check if a node is a bare visibility modifier send (no receiver, no args).
-
#base_message_for(violation_type, caller_name, callee_name) ⇒ String
Return the base offense message template for a direct or sibling violation.
-
#bounds(ranges, defs) ⇒ Parser::Source::Range
Compute a source range spanning all given method definitions by their stored ranges.
-
#build_adj(names, edges) ⇒ Hash<Symbol, Array<Symbol>>
Build an adjacency list (caller → [callees]) from edges, restricted to known names.
-
#build_direct_edges(def_nodes, names_set) ⇒ Array<[ ::Symbol, ::Symbol ]>
Build direct call edges from each method definition to its local callees.
-
#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.
-
#build_sibling_edges(def_nodes, names_set, direct_edges, names) ⇒ Array<[ ::Symbol, ::Symbol ]>
Build sibling call-order edges for orchestration methods.
-
#correction_order(defs, data, violation) ⇒ Array<Symbol>?
Compute the corrected method order via topological sort; returns nil if already correct.
-
#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.
-
#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.
-
#filter_names(edges, names) ⇒ Array<[ ::Symbol, ::Symbol ]>
Filter edges to only those whose both endpoints are in the given name list.
-
#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).
-
#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.
-
#graph(names, edges) ⇒ [ ::Hash[::Symbol, ::Integer], ::Hash[::Symbol, ::Array[::Symbol]] ]
Build indegree map and adjacency list from edges for topological sort.
-
#kahn_sort(indegree, adj, queue, idx_of) ⇒ Array<Symbol>
Kahn's algorithm for topological sort with stable tie-breaking by original index.
-
#local_calls(def_node, names_set) ⇒ Array<Symbol>
Collect local method calls (no receiver or self) within a method body that match known names.
-
#make_section(vis, defs) ⇒ RuboCop::Cop::SortedMethodsByCall::Waterfall::section
Build a section hash with visibility, def nodes, and positional bounds.
-
#method_def_nodes(body_nodes) ⇒ Array<RuboCop::AST::DefNode>
Filter body nodes to only
:def/:defs(method definition) nodes. -
#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.
-
#not_def_node?(node) ⇒ Boolean
Check if a node is NOT a
:defor:defsnode. -
#on_begin(node) ⇒ void
Entry point for top-level
:beginscopes; delegates toanalyze_scope. -
#on_class(node) ⇒ void
Entry point for
:classscopes; delegates toanalyze_scope. -
#on_module(node) ⇒ void
Entry point for
:modulescopes; delegates toanalyze_scope. -
#on_sclass(node) ⇒ void
Entry point for singleton class (
class << self) scopes; delegates toanalyze_scope. -
#path_exists?(src, dst, adj, limit = 200) ⇒ Boolean
Check if a path exists from
srctodstin the adjacency graph (BFS with limit). -
#range_with_leading_comments(node) ⇒ Parser::Source::Range
Expand a node's source range to include leading comment lines.
-
#register_violation(data) ⇒ void
Register an offense for the given violation data.
-
#reject_reciprocal(edges) ⇒ Array<[ ::Symbol, ::Symbol ]>
Remove pairs of reciprocal edges (a→b, b→a) from the edge list.
-
#replace_sorted_section(corrector, defs, sorted_names) ⇒ void
Replace the source code of a section of method definitions with the new sorted order.
-
#scope_body_nodes(node) ⇒ Array<RuboCop::AST::Node>
Extract direct child nodes from a scope node's body.
-
#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.
-
#section_containing(sections, *method_names) ⇒ RuboCop::Cop::SortedMethodsByCall::Waterfall::section?
Find the visibility section that contains all given method names.
-
#section_for_method(sections, method_name) ⇒ RuboCop::Cop::SortedMethodsByCall::Waterfall::section?
Find the visibility section containing a given method name.
-
#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.
-
#skip_cyclic_sibling_edges? ⇒ Boolean
Read the
SkipCyclicSiblingEdgesconfig option (default false). -
#topo_sort(names, edges, idx_of) ⇒ Array<Symbol>?
Topologically sort names by edges; returns nil if a cycle exists.
-
#try_autocorrect(corrector, body_nodes, def_nodes, edges, initial_violation = nil) ⇒ void
Attempt to autocorrect a violation by reordering methods within their visibility section.
-
#vis_node(group) ⇒ RuboCop::AST::Node?
Find the visibility modifier node (
private/protected/public) in a group of nodes. -
#visibility_label(section) ⇒ String
Convert a visibility section to a string label (
"public","private","protected").
Methods included from AutoCorrector
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.
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(, 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 unless caller_vis != callee_vis format(MSG_CROSS_VISIBILITY_NOTE, base: , 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.
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(, violation_type, caller_name, callee_name, adj_all) return unless violation_type == :sibling if path_exists?(callee_name, caller_name, adj_all) format(MSG_SIBLING_CYCLE_NOTE, base: ) else end end |
#allowed_recursion? ⇒ Boolean
Read the AllowedRecursion config option (default true).
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.
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.
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.
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.
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).
637 638 639 640 641 |
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 637 def (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.
343 344 345 346 347 348 349 |
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 343 def (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.
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.
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.
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.
327 328 329 330 331 332 333 334 |
# File 'lib/rubocop/cop/sorted_methods_by_call/waterfall.rb', line 327 def (violation_type:, violation:, names:, edges_for_sort:, body_nodes:) caller_name, callee_name = violation base = (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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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: ( 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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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? && (n) } end |
#visibility_label(section) ⇒ String
Convert a visibility section to a string label ("public", "private", "protected").
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 |