rubocop-sorted_methods_by_call 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 635807053b2df421a854cdc73001545451f8a7017623c93c6d6eb89bf304e172
4
- data.tar.gz: cacd6163f57b4ee053b3053a950240b71bc2665b8bd93d6a4dba916d02490683
3
+ metadata.gz: 3cac98639251f1177bd34bb14fb7098b8adbda6eae96d186a202a2ce75d37f77
4
+ data.tar.gz: b198db699e4cba49214d9f4188a11ae14965c7ba57b51dfa9dc046c0332762b0
5
5
  SHA512:
6
- metadata.gz: 4bedcb4e198e0d75209bb286a15d55ba16ec3ea026cbc52769f7acb13bc8c8f118cac590d4ca7fd16bdfd82730e380c3d02782e6dbf24001fc4815f316928d2a
7
- data.tar.gz: 05d4815fcce53e4c90e8b66ac1933363ec3a4940f551d6f0464a993b63b2b3af36ba8795be3bea7a34cb462ad9aa6f39da011a87f055ac5154e8e4174319ffab
6
+ metadata.gz: 774b4c4bced8dec5236826739aa7bb90a9266fe77a5355d677abbe4a629b2c20928861689f2b00c7063f471f3811f03605d624be6d7e4607d67a624578966d56
7
+ data.tar.gz: eb0b7cca4f2d460f4ff2b8c1cd3c70756011b424041f29715e0838a8678cac4192b2ac2e717a2f0a5b4ae6e6736bf60eae1dad6201578ad20bb657fe33033065
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2025-11-06 15:18:01 UTC using RuboCop version 1.81.7.
3
+ # on 2025-11-12 14:06:36 UTC using RuboCop version 1.81.7.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -22,30 +22,30 @@ Gemspec/RequiredRubyVersion:
22
22
  # Offense count: 4
23
23
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
24
24
  Metrics/AbcSize:
25
- Max: 60
25
+ Max: 87
26
26
 
27
27
  # Offense count: 2
28
28
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
29
29
  # AllowedMethods: refine
30
30
  Metrics/BlockLength:
31
- Max: 43
31
+ Max: 36
32
32
 
33
33
  # Offense count: 5
34
34
  # Configuration parameters: AllowedMethods, AllowedPatterns.
35
35
  Metrics/CyclomaticComplexity:
36
- Max: 20
36
+ Max: 31
37
37
 
38
38
  # Offense count: 7
39
39
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
40
40
  Metrics/MethodLength:
41
- Max: 58
41
+ Max: 59
42
42
 
43
43
  # Offense count: 4
44
44
  # Configuration parameters: AllowedMethods, AllowedPatterns.
45
45
  Metrics/PerceivedComplexity:
46
- Max: 22
46
+ Max: 34
47
47
 
48
- # Offense count: 12
48
+ # Offense count: 19
49
49
  # Configuration parameters: CountAsOne.
50
50
  RSpec/ExampleLength:
51
51
  Max: 37
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rubocop-sorted_methods_by_call (1.1.0)
4
+ rubocop-sorted_methods_by_call (1.1.2)
5
5
  lint_roller
6
6
  rubocop (>= 1.72.0)
7
7
 
data/config/default.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  SortedMethodsByCall/Waterfall:
2
2
  Description: "Enforces method ordering based on call relationships."
3
- Enabled: false
4
- VersionAdded: "1.1.0"
3
+ Enabled: true
4
+ VersionAdded: "1.1.2"
5
5
  SafeAutoCorrect: false
6
6
  AllowedRecursion: true
@@ -3,89 +3,107 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module SortedMethodsByCall
6
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall+ enforces "waterfall" ordering:
7
- # define a method after any method that calls it (within the same scope).
6
+ # Enforces "waterfall" ordering: define a method after any method
7
+ # that calls it within the same scope. Produces a top-down reading flow
8
+ # where orchestration appears before implementation details.
8
9
  #
9
- # - Scopes: class/module/sclass (top-level can be enabled in config)
10
+ # - Scopes: class/module/sclass (top-level can be analyzed via on_begin)
10
11
  # - Offense: when a callee is defined above its caller
11
12
  # - Autocorrect: UNSAFE; reorders methods within a contiguous visibility section
13
+ # (does not cross other statements or nested scopes). Preserves leading
14
+ # doc comments on each method. Skips cycles and non-contiguous groups.
12
15
  #
13
- # Example (good):
14
- # def call
15
- # foo
16
- # bar
17
- # end
16
+ # Configuration
17
+ # - AllowedRecursion [Boolean] (default: true)
18
+ # If true, the cop ignores violations that are part of a mutual recursion
19
+ # cycle (callee → … → caller). If false, such cycles are reported.
20
+ # - SafeAutoCorrect [Boolean] (default: false)
21
+ # Autocorrection is unsafe and only runs under -A, never under -a.
18
22
  #
19
- # private
23
+ # @example Good (waterfall order)
24
+ # class Service
25
+ # def call
26
+ # foo
27
+ # bar
28
+ # end
20
29
  #
21
- # def bar
22
- # method123
23
- # end
30
+ # private
24
31
  #
25
- # def method123
26
- # foo
27
- # end
32
+ # def bar
33
+ # method123
34
+ # end
28
35
  #
29
- # def foo
30
- # 123
31
- # end
36
+ # def method123
37
+ # foo
38
+ # end
32
39
  #
33
- # Example (bad):
34
- # def foo
35
- # 123
40
+ # def foo
41
+ # 123
42
+ # end
36
43
  # end
37
44
  #
38
- # def call
39
- # foo
45
+ # @example Bad (violates waterfall order)
46
+ # class Service
47
+ # def call
48
+ # foo
49
+ # bar
50
+ # end
51
+ #
52
+ # private
53
+ #
54
+ # def foo
55
+ # 123
56
+ # end
57
+ #
58
+ # def bar
59
+ # method123
60
+ # end
61
+ #
62
+ # def method123
63
+ # foo
64
+ # end
40
65
  # end
41
66
  #
42
- # Autocorrect (unsafe, opt-in via SafeAutoCorrect: false): topologically sorts the contiguous
43
- # block of defs to satisfy edges (caller -> callee). Skips cycles and non-contiguous groups.
67
+ # @see #analyze_scope
68
+ # @see #try_autocorrect
44
69
  class Waterfall < ::RuboCop::Cop::Base # rubocop:disable Metrics/ClassLength
45
70
  include ::RuboCop::Cop::RangeHelp
46
71
  extend ::RuboCop::Cop::AutoCorrector
47
72
 
48
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall::MSG+ -> String
49
- #
50
- # Template message for offenses.
73
+ # Template message for offenses where a callee appears before its caller.
51
74
  MSG = 'Define %<callee>s after its caller %<caller>s (waterfall order).'
52
75
 
53
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#on_begin+ -> void
76
+ # Entry point for root :begin nodes (top-level).
54
77
  #
55
- # Entry point for root :begin nodes (top-level). Whether it is analyzed
56
- # depends on configuration (e.g., CheckTopLevel). By default, only class/module scopes are analyzed.
78
+ # Whether top-level is analyzed depends on how the code is structured;
79
+ # by default we only analyze class/module/sclass scopes, but top-level
80
+ # is supported through this hook.
57
81
  #
58
- # @param [RuboCop::AST::Node] node
82
+ # @param node [RuboCop::AST::Node] root :begin node
59
83
  # @return [void]
60
84
  def on_begin(node)
61
85
  analyze_scope(node)
62
86
  end
63
87
 
64
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#on_class+ -> void
65
- #
66
88
  # Entry point for class scopes.
67
89
  #
68
- # @param [RuboCop::AST::Node] node
90
+ # @param node [RuboCop::AST::Node] :class node
69
91
  # @return [void]
70
92
  def on_class(node)
71
93
  analyze_scope(node)
72
94
  end
73
95
 
74
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#on_module+ -> void
75
- #
76
96
  # Entry point for module scopes.
77
97
  #
78
- # @param [RuboCop::AST::Node] node
98
+ # @param node [RuboCop::AST::Node] :module node
79
99
  # @return [void]
80
100
  def on_module(node)
81
101
  analyze_scope(node)
82
102
  end
83
103
 
84
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#on_sclass+ -> void
85
- #
86
104
  # Entry point for singleton class scopes (class << self).
87
105
  #
88
- # @param [RuboCop::AST::Node] node
106
+ # @param node [RuboCop::AST::Node] :sclass node
89
107
  # @return [void]
90
108
  def on_sclass(node)
91
109
  analyze_scope(node)
@@ -93,14 +111,16 @@ module RuboCop
93
111
 
94
112
  private
95
113
 
96
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#analyze_scope+ -> void
114
+ # Collects defs in the current scope, builds caller→callee edges for local sends,
115
+ # locates the first backward edge (callee defined before caller), and registers
116
+ # an offense. If autocorrection is requested, attempts to reorder methods within
117
+ # the same visibility section.
97
118
  #
98
- # Collects defs in the current scope, builds caller->callee edges
99
- # for local sends, finds the first backward edge (callee defined before caller),
100
- # and registers an offense. If autocorrection is requested, attempts to reorder
101
- # methods within the same visibility section.
119
+ # - Direct edges are used for recursion checks (AllowedRecursion).
120
+ # - “Sibling” edges are added from orchestrator methods (not called by others)
121
+ # to reflect the order of consecutive calls (foo then bar).
102
122
  #
103
- # @param [RuboCop::AST::Node] scope_node
123
+ # @param scope_node [RuboCop::AST::Node] a :begin, :class, :module, or :sclass node
104
124
  # @return [void]
105
125
  def analyze_scope(scope_node)
106
126
  body_nodes = scope_body_nodes(scope_node)
@@ -109,44 +129,73 @@ module RuboCop
109
129
  def_nodes = body_nodes.select { |n| %i[def defs].include?(n.type) }
110
130
  return if def_nodes.size <= 1
111
131
 
112
- names = def_nodes.map(&:method_name)
132
+ names = def_nodes.map(&:method_name)
113
133
  names_set = names.to_set
114
- index_of = names.each_with_index.to_h
134
+ index_of = names.each_with_index.to_h
115
135
 
116
- # Build complete call graph - find ALL method calls in ALL methods
117
- edges = []
136
+ # Phase 1: direct call edges (caller -> callee)
137
+ direct_edges = def_nodes.flat_map do |def_node|
138
+ calls = local_calls(def_node, names_set)
139
+ calls.reject { |callee| callee == def_node.method_name }
140
+ .map { |callee| [def_node.method_name, callee] }
141
+ end
142
+
143
+ # Methods that are called by someone else in this scope
144
+ all_callees = direct_edges.to_set(&:last)
145
+
146
+ # Phase 2: sibling-order edges from orchestration methods
147
+ sibling_edges = []
118
148
  def_nodes.each do |def_node|
119
- local_calls(def_node, names_set).each do |callee|
120
- next if callee == def_node.method_name # self-recursion
149
+ next if all_callees.include?(def_node.method_name)
150
+
151
+ calls = local_calls(def_node, names_set)
152
+ calls.each_cons(2) do |a, b|
153
+ next if direct_edges.any? { |u, v| (u == a && v == b) || (u == b && v == a) }
121
154
 
122
- edges << [def_node.method_name, callee]
155
+ sibling_edges << [a, b]
123
156
  end
124
157
  end
125
158
 
126
- allow_recursion = cop_config.fetch('AllowedRecursion') { true }
127
- adj = build_adj(names, edges)
159
+ # Phase 3: combine for sorting, but only use direct edges for recursion checks
160
+ edges_for_sort = direct_edges + sibling_edges
161
+ allow_recursion = cop_config.fetch('AllowedRecursion') { true }
162
+ adj_direct = build_adj(names, direct_edges)
163
+
164
+ violation = first_backward_edge(direct_edges, index_of, adj_direct, allow_recursion)
165
+ violation_type = :direct if violation
166
+
167
+ unless violation
168
+ violation = first_backward_edge(sibling_edges, index_of, adj_direct, allow_recursion)
169
+ violation_type = :sibling if violation
170
+ end
128
171
 
129
- violation = first_backward_edge(edges, index_of, adj, allow_recursion)
130
172
  return unless violation
131
173
 
132
174
  caller_name, callee_name = violation
133
175
  callee_node = def_nodes[index_of[callee_name]]
134
176
 
135
- add_offense(callee_node,
136
- message: format(MSG, callee: "##{callee_name}", caller: "##{caller_name}")) do |corrector|
137
- try_autocorrect(corrector, body_nodes, def_nodes, edges)
177
+ message =
178
+ if violation_type == :sibling
179
+ "Define ##{callee_name} after ##{caller_name} to match the order they are called together"
180
+ else
181
+ format(MSG, callee: "##{callee_name}", caller: "##{caller_name}")
182
+ end
183
+
184
+ add_offense(callee_node, message: message) do |corrector|
185
+ try_autocorrect(corrector, body_nodes, def_nodes, edges_for_sort, violation)
138
186
  end
139
187
 
140
- # Recurse into nested scopes
141
- body_nodes.each { |n| analyze_scope(n) if n.class_type? || n.module_type? || n.sclass_type? }
188
+ # Recurse into nested scopes inside this body
189
+ body_nodes.each do |n|
190
+ analyze_scope(n) if n.class_type? || n.module_type? || n.sclass_type?
191
+ end
142
192
  end
143
193
 
144
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#scope_body_nodes+ -> Array<RuboCop::AST::Node>
145
- #
146
194
  # Normalizes a scope node to its immediate "body" items we iterate over.
147
195
  #
148
- # @param [RuboCop::AST::Node] node
149
- # @return [Array<RuboCop::AST::Node>]
196
+ # @param node [RuboCop::AST::Node]
197
+ # @return [Array<RuboCop::AST::Node>] direct children inside this scope
198
+ # @api private
150
199
  def scope_body_nodes(node)
151
200
  case node.type
152
201
  when :begin
@@ -161,119 +210,140 @@ module RuboCop
161
210
  end
162
211
  end
163
212
 
164
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#local_calls+ -> Array<Symbol>
165
- #
166
- # Returns the set of local method names (receiver is nil/self) invoked inside
167
- # a given def node whose names exist in the provided name set.
168
- #
169
- # @param [RuboCop::AST::Node] def_node
170
- # @param [Set<Symbol>] names_set
171
- # @return [Array<Symbol>]
172
- def local_calls(def_node, names_set)
173
- body = def_node.body
174
- return [] unless body
175
-
176
- res = []
177
- body.each_node(:send) do |send|
178
- recv = send.receiver
179
- next unless recv.nil? || recv&.self_type?
180
-
181
- mname = send.method_name
182
- res << mname if names_set.include?(mname)
183
- end
184
- res.uniq
185
- end
186
-
187
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#try_autocorrect+ -> void
188
- #
189
213
  # UNSAFE: Reorders method definitions inside the target visibility section only
190
214
  # (does not cross private/protected/public boundaries). Skips if defs are not
191
215
  # contiguous within the section or if a cycle prevents a consistent topo order.
192
216
  #
193
- # @param [RuboCop::Cop::Corrector] corrector
194
- # @param [Array<RuboCop::AST::Node>] body_nodes
195
- # @param [Array<RuboCop::AST::Node>] def_nodes
196
- # @param [Array<Array(Symbol, Symbol)>] edges
197
- # @return [void]
217
+ # - Uses direct call edges for recursion checks.
218
+ # - If the violation is a direct-call violation, sorts using only direct edges
219
+ # inside the section (so sibling edges cannot block the fix).
220
+ # - If the violation is a sibling-order violation, includes sibling edges.
221
+ # - Rewrites only the exact contiguous section (plus the visibility line if present).
222
+ # - Preserves leading doc comments for each method.
198
223
  #
199
- # @note Applied only when user asked for autocorrections; with SafeAutoCorrect: false, this runs under -A.
200
- # @note Also preserves contiguous leading doc comments above each method.
201
- def try_autocorrect(corrector, body_nodes, def_nodes, edges)
202
- # Group method definitions into visibility sections
224
+ # @param corrector [RuboCop::Cop::Corrector]
225
+ # @param body_nodes [Array<RuboCop::AST::Node>] raw nodes of the scope body
226
+ # @param def_nodes [Array<RuboCop::AST::Node>] all def/defs nodes in this body
227
+ # @param edges [Array<Array(Symbol, Symbol)>] direct + sibling edges for this scope
228
+ # @param initial_violation [Array<(Symbol, Symbol)>, nil] an already-found violating edge
229
+ # @return [void]
230
+ # @api private
231
+ def try_autocorrect(corrector, body_nodes, def_nodes, edges, initial_violation = nil)
203
232
  sections = extract_visibility_sections(body_nodes)
204
233
 
205
- # Find the section that contains our violating methods
206
- caller_name, callee_name = first_backward_edge(
207
- edges,
208
- def_nodes.map(&:method_name).each_with_index.to_h,
209
- build_adj(def_nodes.map(&:method_name), edges),
210
- cop_config.fetch('AllowedRecursion') { true }
211
- )
234
+ names = def_nodes.map(&:method_name)
235
+ idx_of = names.each_with_index.to_h
236
+ names_set = names.to_set
237
+
238
+ # Recompute direct edges; split edges back into direct vs sibling
239
+ direct_edges = def_nodes.flat_map do |def_node|
240
+ local_calls(def_node, names_set)
241
+ .reject { |callee| callee == def_node.method_name }
242
+ .map { |callee| [def_node.method_name, callee] }
243
+ end
244
+ sibling_edges = edges - direct_edges
245
+
246
+ # Recursion check uses only direct edges
247
+ allow_recursion = cop_config.fetch('AllowedRecursion') { true }
248
+ adj_direct = build_adj(names, direct_edges)
249
+
250
+ violation = initial_violation ||
251
+ first_backward_edge(edges, idx_of, adj_direct, allow_recursion)
252
+ return unless violation
212
253
 
213
- # No violation -> nothing to do
214
- return unless caller_name && callee_name
254
+ caller_name, callee_name = violation
215
255
 
216
- # Find a visibility section that contains both names
256
+ # Find the contiguous section containing both caller and callee
217
257
  target_section = sections.find do |section|
218
- names_in_section = section[:defs].to_set(&:method_name)
219
- names_in_section.include?(caller_name) && names_in_section.include?(callee_name)
258
+ section_names = section[:defs].map(&:method_name)
259
+ section_names.include?(caller_name) && section_names.include?(callee_name)
220
260
  end
221
-
222
- # If violation spans multiple sections, skip autocorrect
223
261
  return unless target_section
224
262
 
225
263
  defs = target_section[:defs]
226
- return unless defs.size > 1
264
+ return if defs.size <= 1
227
265
 
228
- # Apply topological sort only within this visibility section
229
- names = defs.map(&:method_name)
230
- idx_of = names.each_with_index.to_h
266
+ section_names = defs.map(&:method_name)
267
+ section_idx_of = section_names.each_with_index.to_h
231
268
 
232
- # Filter edges to only those within this section
233
- section_names = names.to_set
234
- section_edges = edges.select { |u, v| section_names.include?(u) && section_names.include?(v) }
269
+ # Is this a direct-call violation?
270
+ direct_violation = direct_edges.any? { |u, v| u == caller_name && v == callee_name }
235
271
 
236
- sorted_names = topo_sort(names, section_edges, idx_of)
237
- return unless sorted_names
272
+ # Restrict edges to this contiguous section
273
+ section_direct_edges = direct_edges.select { |u, v| section_names.include?(u) && section_names.include?(v) }
274
+ section_sibling_edges = sibling_edges.select { |u, v| section_names.include?(u) && section_names.include?(v) }
238
275
 
239
- # Capture each def with its leading contiguous comment block
276
+ # Prune mutual-recursion edges inside the section if allowed
277
+ if allow_recursion
278
+ pair_set = section_direct_edges.to_set
279
+ section_direct_edges = section_direct_edges.reject { |u, v| pair_set.include?([v, u]) }
280
+ end
281
+
282
+ # Sorting edges: direct-only for direct violation, otherwise sibling + pruned direct
283
+ section_edges_for_sort =
284
+ if direct_violation
285
+ section_direct_edges
286
+ else
287
+ section_sibling_edges + section_direct_edges
288
+ end
289
+
290
+ sorted_names = topo_sort(section_names, section_edges_for_sort, section_idx_of)
291
+ return if sorted_names.nil? || sorted_names == section_names
292
+
293
+ # Rebuild section (preserve per-method leading docs)
240
294
  ranges_by_name = defs.to_h { |d| [d.method_name, range_with_leading_comments(d)] }
241
- sorted_def_sources = sorted_names.map { |name| ranges_by_name[name].source }
295
+ sorted_def_sources = sorted_names.map { |n| ranges_by_name[n].source }
242
296
 
243
- # Reconstruct the section: keep the visibility modifier (if any) above the first def
244
- visibility_node = target_section[:visibility]
297
+ visibility_node = target_section[:visibility]
245
298
  visibility_source = visibility_node&.source.to_s
246
299
 
247
- new_content = if visibility_source.empty?
248
- sorted_def_sources.join("\n\n")
249
- else
250
- "#{visibility_source}\n\n#{sorted_def_sources.join("\n\n")}"
251
- end
300
+ new_content =
301
+ if visibility_source.empty?
302
+ sorted_def_sources.join("\n\n")
303
+ else
304
+ "#{visibility_source}\n\n#{sorted_def_sources.join("\n\n")}"
305
+ end
252
306
 
253
- # Expand the replaced region:
254
- # - if a visibility node exists, start from its begin_pos
255
- # - otherwise, start from the earliest leading doc-comment of the defs
256
307
  section_begin =
257
308
  if visibility_node
258
309
  visibility_node.source_range.begin_pos
259
310
  else
260
311
  defs.map { |d| range_with_leading_comments(d).begin_pos }.min
261
312
  end
262
-
263
- # Always end at the end of the last def
264
- section_end = defs.last.source_range.end_pos
313
+ section_end = target_section[:end_pos]
265
314
 
266
315
  region = Parser::Source::Range.new(processed_source.buffer, section_begin, section_end)
267
316
  corrector.replace(region, new_content)
268
317
  end
269
318
 
270
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#build_adj+ -> Hash{Symbol=>Array<Symbol>}
319
+ # Collects local calls (receiver is nil/self) from within a def node
320
+ # whose names are present in +names_set+.
271
321
  #
322
+ # @param def_node [RuboCop::AST::Node] :def or :defs
323
+ # @param names_set [Set<Symbol>] known local method names in this scope
324
+ # @return [Array<Symbol>] unique callee names
325
+ # @api private
326
+ def local_calls(def_node, names_set)
327
+ body = def_node.body
328
+ return [] unless body
329
+
330
+ res = []
331
+ body.each_node(:send) do |send|
332
+ recv = send.receiver
333
+ next unless recv.nil? || recv&.self_type?
334
+
335
+ mname = send.method_name
336
+ res << mname if names_set.include?(mname)
337
+ end
338
+ res.uniq
339
+ end
340
+
272
341
  # Builds an adjacency list for edges restricted to known names.
273
342
  #
274
- # @param [Array<Symbol>] names
275
- # @param [Array<Array(Symbol, Symbol)>] edges
276
- # @return [Hash{Symbol=>Array<Symbol>}]
343
+ # @param names [Array<Symbol>] method names
344
+ # @param edges [Array<Array(Symbol, Symbol)>] caller→callee pairs
345
+ # @return [Hash{Symbol=>Array<Symbol>}] adjacency list
346
+ # @api private
277
347
  def build_adj(names, edges)
278
348
  allowed = names.to_set
279
349
  adj = Hash.new { |h, k| h[k] = [] }
@@ -286,16 +356,15 @@ module RuboCop
286
356
  adj
287
357
  end
288
358
 
289
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#first_backward_edge+ -> [Symbol, Symbol], nil
290
- #
291
- # Returns the first backward edge found, optionally skipping mutual recursion
292
- # if so configured.
359
+ # Returns the first backward edge found, optionally skipping edges
360
+ # that participate in mutual recursion (when AllowedRecursion is true).
293
361
  #
294
- # @param [Array<Array(Symbol, Symbol)>] edges
295
- # @param [Hash{Symbol=>Integer}] index_of
296
- # @param [Hash{Symbol=>Array<Symbol>}] adj
297
- # @param [Boolean] allow_recursion whether to ignore cycles
298
- # @return [[Symbol, Symbol], nil]
362
+ # @param edges [Array<Array(Symbol, Symbol)>] candidate edges to check
363
+ # @param index_of [Hash{Symbol=>Integer}] current definition order (name -> index)
364
+ # @param adj [Hash{Symbol=>Array<Symbol>}] direct-call adjacency for path checks
365
+ # @param allow_recursion [Boolean] whether mutual recursion suppresses a violation
366
+ # @return [(Symbol, Symbol), nil] the violating (caller, callee) or nil
367
+ # @api private
299
368
  def first_backward_edge(edges, index_of, adj, allow_recursion)
300
369
  edges.find do |caller, callee|
301
370
  next unless index_of.key?(caller) && index_of.key?(callee)
@@ -307,15 +376,14 @@ module RuboCop
307
376
  end
308
377
  end
309
378
 
310
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#path_exists?+ -> Boolean
311
- #
312
- # Tests whether a path exists in the adjacency graph from +src+ to +dst+ (BFS).
379
+ # Breadth-first search to detect if a path exists in the direct-call graph.
313
380
  #
314
- # @param [Symbol] src
315
- # @param [Symbol] dst
316
- # @param [Hash{Symbol=>Array<Symbol>}] adj
317
- # @param [Integer] limit traversal step limit (guard)
318
- # @return [Boolean]
381
+ # @param src [Symbol] source method
382
+ # @param dst [Symbol] destination method
383
+ # @param adj [Hash{Symbol=>Array<Symbol>}] adjacency list
384
+ # @param limit [Integer] traversal safety limit
385
+ # @return [Boolean] true if a path exists
386
+ # @api private
319
387
  def path_exists?(src, dst, adj, limit = 200)
320
388
  return true if src == dst
321
389
 
@@ -337,17 +405,20 @@ module RuboCop
337
405
  false
338
406
  end
339
407
 
340
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#extract_visibility_sections+ -> Array<Hash>
408
+ # Splits the scope body into contiguous sections of def/defs grouped
409
+ # by the visibility modifier immediately preceding them (private/protected/public).
341
410
  #
342
- # Splits the body into contiguous sections of defs grouped by visibility modifier
343
- # (private/protected/public). Returns metadata for each section including:
344
- # :visibility -> visibility modifier node or nil
345
- # :defs -> array of def/defs nodes
346
- # :start_pos -> Integer (begin_pos)
347
- # :end_pos -> Integer (end_pos)
411
+ # A section is represented as a Hash with:
412
+ # - :visibility [RuboCop::AST::Node, nil] the bare visibility send, or nil
413
+ # - :defs [Array<RuboCop::AST::Node>] contiguous def/defs nodes
414
+ # - :start_pos [Integer] begin_pos of the first def in the section
415
+ # - :end_pos [Integer] end_pos of the last def in the section
348
416
  #
349
- # @param [Array<RuboCop::AST::Node>] body_nodes
350
- # @return [Array<Hash{Symbol=>untyped}>]
417
+ # Non-visibility sends, constants, and nested scopes break contiguity.
418
+ #
419
+ # @param body_nodes [Array<RuboCop::AST::Node>] raw nodes in the scope body
420
+ # @return [Array<Hash>] list of sections metadata
421
+ # @api private
351
422
  def extract_visibility_sections(body_nodes)
352
423
  sections = []
353
424
  current_visibility = nil
@@ -359,37 +430,30 @@ module RuboCop
359
430
  when :def, :defs
360
431
  current_defs << node
361
432
  section_start ||= node.source_range.begin_pos
433
+
362
434
  when :send
363
- # Check if this is a visibility modifier (private/protected/public)
435
+ # Close any running section before processing visibility/non-visibility send
436
+ unless current_defs.empty?
437
+ sections << {
438
+ visibility: current_visibility,
439
+ defs: current_defs.dup,
440
+ start_pos: section_start,
441
+ end_pos: body_nodes[idx - 1].source_range.end_pos
442
+ }
443
+ current_defs = []
444
+ section_start = nil
445
+ end
446
+
447
+ # Bare visibility modifiers: private/protected/public without args
364
448
  if node.receiver.nil? && %i[private protected public].include?(node.method_name) && node.arguments.empty?
365
- # End current section if it has defs
366
- unless current_defs.empty?
367
- sections << {
368
- visibility: current_visibility,
369
- defs: current_defs.dup,
370
- start_pos: section_start,
371
- end_pos: body_nodes[idx - 1].source_range.end_pos
372
- }
373
- current_defs = []
374
- section_start = nil
375
- end
376
449
  current_visibility = node
377
450
  else
378
- # Non-visibility send - breaks contiguity
379
- unless current_defs.empty?
380
- sections << {
381
- visibility: current_visibility,
382
- defs: current_defs.dup,
383
- start_pos: section_start,
384
- end_pos: body_nodes[idx - 1].source_range.end_pos
385
- }
386
- current_defs = []
387
- section_start = nil
388
- current_visibility = nil
389
- end
451
+ # Non-visibility send breaks contiguity and resets visibility context
452
+ current_visibility = nil
390
453
  end
454
+
391
455
  else
392
- # Any other node type breaks contiguity
456
+ # Any other node breaks contiguity and resets visibility context
393
457
  unless current_defs.empty?
394
458
  sections << {
395
459
  visibility: current_visibility,
@@ -399,12 +463,12 @@ module RuboCop
399
463
  }
400
464
  current_defs = []
401
465
  section_start = nil
402
- current_visibility = nil
403
466
  end
467
+ current_visibility = nil
404
468
  end
405
469
  end
406
470
 
407
- # Handle trailing defs
471
+ # trailing defs
408
472
  unless current_defs.empty?
409
473
  sections << {
410
474
  visibility: current_visibility,
@@ -417,14 +481,13 @@ module RuboCop
417
481
  sections
418
482
  end
419
483
 
420
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#topo_sort+ -> Array<Symbol>, nil
421
- #
422
- # Performs a stable topological sort using current order as a tie-breaker.
484
+ # Stable topological sort using the current order as a tie-breaker.
423
485
  #
424
- # @param [Array<Symbol>] names
425
- # @param [Array<Array(Symbol, Symbol)>] edges
426
- # @param [Hash{Symbol=>Integer}] idx_of
486
+ # @param names [Array<Symbol>] names to sort
487
+ # @param edges [Array<Array(Symbol, Symbol)>] caller→callee edges to respect
488
+ # @param idx_of [Hash{Symbol=>Integer}] current order (name -> index)
427
489
  # @return [Array<Symbol>, nil] sorted names or nil if a cycle prevents a full order
490
+ # @api private
428
491
  def topo_sort(names, edges, idx_of)
429
492
  indegree = Hash.new(0)
430
493
  adj = Hash.new { |h, k| h[k] = [] }
@@ -457,14 +520,13 @@ module RuboCop
457
520
  result
458
521
  end
459
522
 
460
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#range_with_leading_comments+ -> Parser::Source::Range
461
- #
462
523
  # Returns a range that starts at the first contiguous comment line immediately
463
- # above the def/defs node, and ends at the end of the def. This preserves
524
+ # above the def/defs node and ends at the end of the def. This preserves
464
525
  # YARD/RDoc doc comments when methods are moved during autocorrect.
465
526
  #
466
- # @param [RuboCop::AST::Node] node The def/defs node.
467
- # @return [Parser::Source::Range] Range covering leading comments + method body.
527
+ # @param node [RuboCop::AST::Node] :def or :defs to capture with leading comments
528
+ # @return [Parser::Source::Range]
529
+ # @api private
468
530
  def range_with_leading_comments(node)
469
531
  buffer = processed_source.buffer
470
532
  expr = node.source_range
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module SortedMethodsByCall
5
- VERSION = '1.1.0'
5
+ VERSION = '1.1.2'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-sorted_methods_by_call
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - unurgunite