rubocop-sorted_methods_by_call 1.1.1 → 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: 1d62de644bc87dd509f3407cc4db3c9e0b4c46991e6a4e682344a7024eae1217
4
- data.tar.gz: 94858039c6204ae841a595b4e5c13db9cf3a0775cf615fdd4696951d68181303
3
+ metadata.gz: 3cac98639251f1177bd34bb14fb7098b8adbda6eae96d186a202a2ce75d37f77
4
+ data.tar.gz: b198db699e4cba49214d9f4188a11ae14965c7ba57b51dfa9dc046c0332762b0
5
5
  SHA512:
6
- metadata.gz: 91901d4d06ad36e83396752d9c7b47b3c838f61bb42686351b43f382281136aa219dcbeaaf1d5f0e2007db56728dd9e69dd7ba307f1758b9acc26cadf7b7416a
7
- data.tar.gz: a06c24a47cf38ea1e06021d54f49a04ed26fb47174f2c4dd1b6491cb768243acbda01fe25933b55895387e58d8f00d7e163c24d08525394f1891953f9a8a6017
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 17:19:21 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: 61
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: 26
36
+ Max: 31
37
37
 
38
38
  # Offense count: 7
39
39
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
40
40
  Metrics/MethodLength:
41
- Max: 68
41
+ Max: 59
42
42
 
43
43
  # Offense count: 4
44
44
  # Configuration parameters: AllowedMethods, AllowedPatterns.
45
45
  Metrics/PerceivedComplexity:
46
- Max: 27
46
+ Max: 34
47
47
 
48
- # Offense count: 16
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.1)
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.1"
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)
@@ -113,20 +133,17 @@ module RuboCop
113
133
  names_set = names.to_set
114
134
  index_of = names.each_with_index.to_h
115
135
 
116
- # -----------------------------------------------------------
117
- # Phase 1 Collect direct call edges (caller → callee)
118
- # -----------------------------------------------------------
136
+ # Phase 1: direct call edges (caller -> callee)
119
137
  direct_edges = def_nodes.flat_map do |def_node|
120
138
  calls = local_calls(def_node, names_set)
121
139
  calls.reject { |callee| callee == def_node.method_name }
122
140
  .map { |callee| [def_node.method_name, callee] }
123
141
  end
124
142
 
143
+ # Methods that are called by someone else in this scope
125
144
  all_callees = direct_edges.to_set(&:last)
126
145
 
127
- # -----------------------------------------------------------
128
- # Phase 2 Add sibling‑order edges for orchestration methods
129
- # -----------------------------------------------------------
146
+ # Phase 2: sibling-order edges from orchestration methods
130
147
  sibling_edges = []
131
148
  def_nodes.each do |def_node|
132
149
  next if all_callees.include?(def_node.method_name)
@@ -139,16 +156,11 @@ module RuboCop
139
156
  end
140
157
  end
141
158
 
142
- # -----------------------------------------------------------
143
- # Phase 3 Combine for sorting, but keep direct set for recursion
144
- # -----------------------------------------------------------
145
- edges_for_sort = direct_edges + sibling_edges
146
- allow_recursion = cop_config.fetch('AllowedRecursion') { true }
147
-
148
- # Build adjacency only from *direct* calls for recursion checks
149
- adj_direct = build_adj(names, direct_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)
150
163
 
151
- # Check for violations with edge type tracking
152
164
  violation = first_backward_edge(direct_edges, index_of, adj_direct, allow_recursion)
153
165
  violation_type = :direct if violation
154
166
 
@@ -162,29 +174,28 @@ module RuboCop
162
174
  caller_name, callee_name = violation
163
175
  callee_node = def_nodes[index_of[callee_name]]
164
176
 
165
- # Choose message based on violation type
166
- message = if violation_type == :sibling
167
- "Define ##{callee_name} after ##{caller_name} to match the order they are called together"
168
- else
169
- format(MSG, callee: "##{callee_name}", caller: "##{caller_name}")
170
- end
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
171
183
 
172
184
  add_offense(callee_node, message: message) do |corrector|
173
185
  try_autocorrect(corrector, body_nodes, def_nodes, edges_for_sort, violation)
174
186
  end
175
187
 
176
- # Recurse into nested scopes
188
+ # Recurse into nested scopes inside this body
177
189
  body_nodes.each do |n|
178
190
  analyze_scope(n) if n.class_type? || n.module_type? || n.sclass_type?
179
191
  end
180
192
  end
181
193
 
182
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#scope_body_nodes+ -> Array<RuboCop::AST::Node>
183
- #
184
194
  # Normalizes a scope node to its immediate "body" items we iterate over.
185
195
  #
186
- # @param [RuboCop::AST::Node] node
187
- # @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
188
199
  def scope_body_nodes(node)
189
200
  case node.type
190
201
  when :begin
@@ -199,50 +210,50 @@ module RuboCop
199
210
  end
200
211
  end
201
212
 
202
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#local_calls+ -> Array<Symbol>
203
- #
204
- # Returns the set of local method names (receiver is nil/self) invoked inside
205
- # a given def node whose names exist in the provided name set.
206
- #
207
- # @param [RuboCop::AST::Node] def_node
208
- # @param [Set<Symbol>] names_set
209
- # @return [Array<Symbol>]
210
- def local_calls(def_node, names_set)
211
- body = def_node.body
212
- return [] unless body
213
-
214
- res = []
215
- body.each_node(:send) do |send|
216
- recv = send.receiver
217
- next unless recv.nil? || recv&.self_type?
218
-
219
- mname = send.method_name
220
- res << mname if names_set.include?(mname)
221
- end
222
- res.uniq
223
- end
224
-
225
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#try_autocorrect+ -> void
226
- #
227
213
  # UNSAFE: Reorders method definitions inside the target visibility section only
228
214
  # (does not cross private/protected/public boundaries). Skips if defs are not
229
215
  # contiguous within the section or if a cycle prevents a consistent topo order.
230
216
  #
231
- # @param [RuboCop::Cop::Corrector] corrector
232
- # @param [Array<RuboCop::AST::Node>] body_nodes
233
- # @param [Array<RuboCop::AST::Node>] def_nodes
234
- # @param [Array<Array(Symbol, Symbol)>] edges
235
- # @param [Array(Symbol, Symbol)] violation The found violation (caller, callee)
236
- # @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.
237
223
  #
238
- # @note Applied only when user asked for autocorrections; with SafeAutoCorrect: false, this runs under -A.
239
- # @note Also preserves contiguous leading doc comments above each method.
240
- def try_autocorrect(corrector, body_nodes, _def_nodes, edges, violation)
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)
241
232
  sections = extract_visibility_sections(body_nodes)
242
233
 
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
253
+
243
254
  caller_name, callee_name = violation
244
255
 
245
- # find target section containing both defs
256
+ # Find the contiguous section containing both caller and callee
246
257
  target_section = sections.find do |section|
247
258
  section_names = section[:defs].map(&:method_name)
248
259
  section_names.include?(caller_name) && section_names.include?(callee_name)
@@ -253,25 +264,34 @@ module RuboCop
253
264
  return if defs.size <= 1
254
265
 
255
266
  section_names = defs.map(&:method_name)
256
- section_edges = edges.select { |u, v| section_names.include?(u) && section_names.include?(v) }
257
267
  section_idx_of = section_names.each_with_index.to_h
258
268
 
259
- # sort within section or minimally fix if graph is cyclic
260
- sorted_names = topo_sort(section_names, section_edges, section_idx_of)
269
+ # Is this a direct-call violation?
270
+ direct_violation = direct_edges.any? { |u, v| u == caller_name && v == callee_name }
261
271
 
262
- # forcibly fix when topo_sort failed or produced same order
263
- if sorted_names.nil? || sorted_names == section_names
264
- sorted_names = section_names.dup
265
- # remove callee and reinsert it after its caller
266
- sorted_names.delete(callee_name)
267
- caller_index = sorted_names.index(caller_name) || -1
268
- sorted_names.insert(caller_index + 1, callee_name)
269
- end
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) }
270
275
 
271
- # reconstruct source
272
- ranges_by_name = defs.to_h do |d|
273
- [d.method_name, range_with_leading_comments(d)]
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]) }
274
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)
294
+ ranges_by_name = defs.to_h { |d| [d.method_name, range_with_leading_comments(d)] }
275
295
  sorted_def_sources = sorted_names.map { |n| ranges_by_name[n].source }
276
296
 
277
297
  visibility_node = target_section[:visibility]
@@ -290,19 +310,40 @@ module RuboCop
290
310
  else
291
311
  defs.map { |d| range_with_leading_comments(d).begin_pos }.min
292
312
  end
293
- section_end = defs.last.source_range.end_pos
313
+ section_end = target_section[:end_pos]
294
314
 
295
315
  region = Parser::Source::Range.new(processed_source.buffer, section_begin, section_end)
296
316
  corrector.replace(region, new_content)
297
317
  end
298
318
 
299
- # +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+.
300
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
+
301
341
  # Builds an adjacency list for edges restricted to known names.
302
342
  #
303
- # @param [Array<Symbol>] names
304
- # @param [Array<Array(Symbol, Symbol)>] edges
305
- # @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
306
347
  def build_adj(names, edges)
307
348
  allowed = names.to_set
308
349
  adj = Hash.new { |h, k| h[k] = [] }
@@ -315,16 +356,15 @@ module RuboCop
315
356
  adj
316
357
  end
317
358
 
318
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#first_backward_edge+ -> [Symbol, Symbol], nil
319
- #
320
- # Returns the first backward edge found, optionally skipping mutual recursion
321
- # if so configured.
359
+ # Returns the first backward edge found, optionally skipping edges
360
+ # that participate in mutual recursion (when AllowedRecursion is true).
322
361
  #
323
- # @param [Array<Array(Symbol, Symbol)>] edges
324
- # @param [Hash{Symbol=>Integer}] index_of
325
- # @param [Hash{Symbol=>Array<Symbol>}] adj
326
- # @param [Boolean] allow_recursion whether to ignore cycles
327
- # @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
328
368
  def first_backward_edge(edges, index_of, adj, allow_recursion)
329
369
  edges.find do |caller, callee|
330
370
  next unless index_of.key?(caller) && index_of.key?(callee)
@@ -336,15 +376,14 @@ module RuboCop
336
376
  end
337
377
  end
338
378
 
339
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#path_exists?+ -> Boolean
340
- #
341
- # 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.
342
380
  #
343
- # @param [Symbol] src
344
- # @param [Symbol] dst
345
- # @param [Hash{Symbol=>Array<Symbol>}] adj
346
- # @param [Integer] limit traversal step limit (guard)
347
- # @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
348
387
  def path_exists?(src, dst, adj, limit = 200)
349
388
  return true if src == dst
350
389
 
@@ -366,17 +405,20 @@ module RuboCop
366
405
  false
367
406
  end
368
407
 
369
- # +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).
370
410
  #
371
- # Splits the body into contiguous sections of defs grouped by visibility modifier
372
- # (private/protected/public). Returns metadata for each section including:
373
- # :visibility -> visibility modifier node or nil
374
- # :defs -> array of def/defs nodes
375
- # :start_pos -> Integer (begin_pos)
376
- # :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
377
416
  #
378
- # @param [Array<RuboCop::AST::Node>] body_nodes
379
- # @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
380
422
  def extract_visibility_sections(body_nodes)
381
423
  sections = []
382
424
  current_visibility = nil
@@ -388,36 +430,30 @@ module RuboCop
388
430
  when :def, :defs
389
431
  current_defs << node
390
432
  section_start ||= node.source_range.begin_pos
433
+
391
434
  when :send
392
- # visibility modifier?
393
- if node.receiver.nil? &&
394
- %i[private protected public].include?(node.method_name) &&
395
- node.arguments.empty?
396
- unless current_defs.empty?
397
- sections << {
398
- visibility: current_visibility,
399
- defs: current_defs.dup,
400
- start_pos: section_start,
401
- end_pos: body_nodes[idx - 1].source_range.end_pos
402
- }
403
- current_defs = []
404
- section_start = nil
405
- end
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
448
+ if node.receiver.nil? && %i[private protected public].include?(node.method_name) && node.arguments.empty?
406
449
  current_visibility = node
407
450
  else
408
- # anything else breaks contiguous run
409
- unless current_defs.empty?
410
- sections << {
411
- visibility: current_visibility,
412
- defs: current_defs.dup,
413
- start_pos: section_start,
414
- end_pos: body_nodes[idx - 1].source_range.end_pos
415
- }
416
- current_defs = []
417
- section_start = nil
418
- end
451
+ # Non-visibility send breaks contiguity and resets visibility context
452
+ current_visibility = nil
419
453
  end
454
+
420
455
  else
456
+ # Any other node breaks contiguity and resets visibility context
421
457
  unless current_defs.empty?
422
458
  sections << {
423
459
  visibility: current_visibility,
@@ -428,6 +464,7 @@ module RuboCop
428
464
  current_defs = []
429
465
  section_start = nil
430
466
  end
467
+ current_visibility = nil
431
468
  end
432
469
  end
433
470
 
@@ -441,30 +478,16 @@ module RuboCop
441
478
  }
442
479
  end
443
480
 
444
- # -----------------------------------------------
445
- # merge consecutive groups with identical visibility
446
- # -----------------------------------------------
447
- merged = []
448
- sections.each do |s|
449
- if !merged.empty? &&
450
- merged.last[:visibility]&.source == s[:visibility]&.source
451
- merged.last[:defs].concat(s[:defs])
452
- merged.last[:end_pos] = s[:end_pos]
453
- else
454
- merged << s
455
- end
456
- end
457
- merged
481
+ sections
458
482
  end
459
483
 
460
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#topo_sort+ -> Array<Symbol>, nil
484
+ # Stable topological sort using the current order as a tie-breaker.
461
485
  #
462
- # Performs a stable topological sort using current order as a tie-breaker.
463
- #
464
- # @param [Array<Symbol>] names
465
- # @param [Array<Array(Symbol, Symbol)>] edges
466
- # @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)
467
489
  # @return [Array<Symbol>, nil] sorted names or nil if a cycle prevents a full order
490
+ # @api private
468
491
  def topo_sort(names, edges, idx_of)
469
492
  indegree = Hash.new(0)
470
493
  adj = Hash.new { |h, k| h[k] = [] }
@@ -497,14 +520,13 @@ module RuboCop
497
520
  result
498
521
  end
499
522
 
500
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#range_with_leading_comments+ -> Parser::Source::Range
501
- #
502
523
  # Returns a range that starts at the first contiguous comment line immediately
503
- # 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
504
525
  # YARD/RDoc doc comments when methods are moved during autocorrect.
505
526
  #
506
- # @param [RuboCop::AST::Node] node The def/defs node.
507
- # @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
508
530
  def range_with_leading_comments(node)
509
531
  buffer = processed_source.buffer
510
532
  expr = node.source_range
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module SortedMethodsByCall
5
- VERSION = '1.1.1'
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.1
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - unurgunite