rubocop-sorted_methods_by_call 1.1.0 → 1.1.1

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: 1d62de644bc87dd509f3407cc4db3c9e0b4c46991e6a4e682344a7024eae1217
4
+ data.tar.gz: 94858039c6204ae841a595b4e5c13db9cf3a0775cf615fdd4696951d68181303
5
5
  SHA512:
6
- metadata.gz: 4bedcb4e198e0d75209bb286a15d55ba16ec3ea026cbc52769f7acb13bc8c8f118cac590d4ca7fd16bdfd82730e380c3d02782e6dbf24001fc4815f316928d2a
7
- data.tar.gz: 05d4815fcce53e4c90e8b66ac1933363ec3a4940f551d6f0464a993b63b2b3af36ba8795be3bea7a34cb462ad9aa6f39da011a87f055ac5154e8e4174319ffab
6
+ metadata.gz: 91901d4d06ad36e83396752d9c7b47b3c838f61bb42686351b43f382281136aa219dcbeaaf1d5f0e2007db56728dd9e69dd7ba307f1758b9acc26cadf7b7416a
7
+ data.tar.gz: a06c24a47cf38ea1e06021d54f49a04ed26fb47174f2c4dd1b6491cb768243acbda01fe25933b55895387e58d8f00d7e163c24d08525394f1891953f9a8a6017
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-06 17:19:21 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,7 +22,7 @@ Gemspec/RequiredRubyVersion:
22
22
  # Offense count: 4
23
23
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
24
24
  Metrics/AbcSize:
25
- Max: 60
25
+ Max: 61
26
26
 
27
27
  # Offense count: 2
28
28
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
@@ -33,19 +33,19 @@ Metrics/BlockLength:
33
33
  # Offense count: 5
34
34
  # Configuration parameters: AllowedMethods, AllowedPatterns.
35
35
  Metrics/CyclomaticComplexity:
36
- Max: 20
36
+ Max: 26
37
37
 
38
38
  # Offense count: 7
39
39
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
40
40
  Metrics/MethodLength:
41
- Max: 58
41
+ Max: 68
42
42
 
43
43
  # Offense count: 4
44
44
  # Configuration parameters: AllowedMethods, AllowedPatterns.
45
45
  Metrics/PerceivedComplexity:
46
- Max: 22
46
+ Max: 27
47
47
 
48
- # Offense count: 12
48
+ # Offense count: 16
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.1)
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
3
  Enabled: false
4
- VersionAdded: "1.1.0"
4
+ VersionAdded: "1.1.1"
5
5
  SafeAutoCorrect: false
6
6
  AllowedRecursion: true
@@ -109,36 +109,74 @@ module RuboCop
109
109
  def_nodes = body_nodes.select { |n| %i[def defs].include?(n.type) }
110
110
  return if def_nodes.size <= 1
111
111
 
112
- names = def_nodes.map(&:method_name)
112
+ names = def_nodes.map(&:method_name)
113
113
  names_set = names.to_set
114
- index_of = names.each_with_index.to_h
114
+ index_of = names.each_with_index.to_h
115
+
116
+ # -----------------------------------------------------------
117
+ # Phase 1 Collect direct call edges (caller → callee)
118
+ # -----------------------------------------------------------
119
+ direct_edges = def_nodes.flat_map do |def_node|
120
+ calls = local_calls(def_node, names_set)
121
+ calls.reject { |callee| callee == def_node.method_name }
122
+ .map { |callee| [def_node.method_name, callee] }
123
+ end
124
+
125
+ all_callees = direct_edges.to_set(&:last)
115
126
 
116
- # Build complete call graph - find ALL method calls in ALL methods
117
- edges = []
127
+ # -----------------------------------------------------------
128
+ # Phase 2 Add sibling‑order edges for orchestration methods
129
+ # -----------------------------------------------------------
130
+ sibling_edges = []
118
131
  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
132
+ next if all_callees.include?(def_node.method_name)
133
+
134
+ calls = local_calls(def_node, names_set)
135
+ calls.each_cons(2) do |a, b|
136
+ next if direct_edges.any? { |u, v| (u == a && v == b) || (u == b && v == a) }
121
137
 
122
- edges << [def_node.method_name, callee]
138
+ sibling_edges << [a, b]
123
139
  end
124
140
  end
125
141
 
142
+ # -----------------------------------------------------------
143
+ # Phase 3 Combine for sorting, but keep direct set for recursion
144
+ # -----------------------------------------------------------
145
+ edges_for_sort = direct_edges + sibling_edges
126
146
  allow_recursion = cop_config.fetch('AllowedRecursion') { true }
127
- adj = build_adj(names, edges)
128
147
 
129
- violation = first_backward_edge(edges, index_of, adj, allow_recursion)
148
+ # Build adjacency only from *direct* calls for recursion checks
149
+ adj_direct = build_adj(names, direct_edges)
150
+
151
+ # Check for violations with edge type tracking
152
+ violation = first_backward_edge(direct_edges, index_of, adj_direct, allow_recursion)
153
+ violation_type = :direct if violation
154
+
155
+ unless violation
156
+ violation = first_backward_edge(sibling_edges, index_of, adj_direct, allow_recursion)
157
+ violation_type = :sibling if violation
158
+ end
159
+
130
160
  return unless violation
131
161
 
132
162
  caller_name, callee_name = violation
133
163
  callee_node = def_nodes[index_of[callee_name]]
134
164
 
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)
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
171
+
172
+ add_offense(callee_node, message: message) do |corrector|
173
+ try_autocorrect(corrector, body_nodes, def_nodes, edges_for_sort, violation)
138
174
  end
139
175
 
140
176
  # Recurse into nested scopes
141
- body_nodes.each { |n| analyze_scope(n) if n.class_type? || n.module_type? || n.sclass_type? }
177
+ body_nodes.each do |n|
178
+ analyze_scope(n) if n.class_type? || n.module_type? || n.sclass_type?
179
+ end
142
180
  end
143
181
 
144
182
  # +RuboCop::Cop::SortedMethodsByCall::Waterfall#scope_body_nodes+ -> Array<RuboCop::AST::Node>
@@ -194,73 +232,64 @@ module RuboCop
194
232
  # @param [Array<RuboCop::AST::Node>] body_nodes
195
233
  # @param [Array<RuboCop::AST::Node>] def_nodes
196
234
  # @param [Array<Array(Symbol, Symbol)>] edges
235
+ # @param [Array(Symbol, Symbol)] violation The found violation (caller, callee)
197
236
  # @return [void]
198
237
  #
199
238
  # @note Applied only when user asked for autocorrections; with SafeAutoCorrect: false, this runs under -A.
200
239
  # @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
240
+ def try_autocorrect(corrector, body_nodes, _def_nodes, edges, violation)
203
241
  sections = extract_visibility_sections(body_nodes)
204
242
 
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
- )
212
-
213
- # No violation -> nothing to do
214
- return unless caller_name && callee_name
243
+ caller_name, callee_name = violation
215
244
 
216
- # Find a visibility section that contains both names
245
+ # find target section containing both defs
217
246
  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)
247
+ section_names = section[:defs].map(&:method_name)
248
+ section_names.include?(caller_name) && section_names.include?(callee_name)
220
249
  end
221
-
222
- # If violation spans multiple sections, skip autocorrect
223
250
  return unless target_section
224
251
 
225
252
  defs = target_section[:defs]
226
- return unless defs.size > 1
227
-
228
- # Apply topological sort only within this visibility section
229
- names = defs.map(&:method_name)
230
- idx_of = names.each_with_index.to_h
231
-
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) }
235
-
236
- sorted_names = topo_sort(names, section_edges, idx_of)
237
- return unless sorted_names
253
+ return if defs.size <= 1
254
+
255
+ section_names = defs.map(&:method_name)
256
+ section_edges = edges.select { |u, v| section_names.include?(u) && section_names.include?(v) }
257
+ section_idx_of = section_names.each_with_index.to_h
258
+
259
+ # sort within section or minimally fix if graph is cyclic
260
+ sorted_names = topo_sort(section_names, section_edges, section_idx_of)
261
+
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
238
270
 
239
- # Capture each def with its leading contiguous comment block
240
- 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 }
271
+ # reconstruct source
272
+ ranges_by_name = defs.to_h do |d|
273
+ [d.method_name, range_with_leading_comments(d)]
274
+ end
275
+ sorted_def_sources = sorted_names.map { |n| ranges_by_name[n].source }
242
276
 
243
- # Reconstruct the section: keep the visibility modifier (if any) above the first def
244
- visibility_node = target_section[:visibility]
277
+ visibility_node = target_section[:visibility]
245
278
  visibility_source = visibility_node&.source.to_s
246
279
 
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
280
+ new_content =
281
+ if visibility_source.empty?
282
+ sorted_def_sources.join("\n\n")
283
+ else
284
+ "#{visibility_source}\n\n#{sorted_def_sources.join("\n\n")}"
285
+ end
252
286
 
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
287
  section_begin =
257
288
  if visibility_node
258
289
  visibility_node.source_range.begin_pos
259
290
  else
260
291
  defs.map { |d| range_with_leading_comments(d).begin_pos }.min
261
292
  end
262
-
263
- # Always end at the end of the last def
264
293
  section_end = defs.last.source_range.end_pos
265
294
 
266
295
  region = Parser::Source::Range.new(processed_source.buffer, section_begin, section_end)
@@ -360,9 +389,10 @@ module RuboCop
360
389
  current_defs << node
361
390
  section_start ||= node.source_range.begin_pos
362
391
  when :send
363
- # Check if this is a visibility modifier (private/protected/public)
364
- if node.receiver.nil? && %i[private protected public].include?(node.method_name) && node.arguments.empty?
365
- # End current section if it has defs
392
+ # visibility modifier?
393
+ if node.receiver.nil? &&
394
+ %i[private protected public].include?(node.method_name) &&
395
+ node.arguments.empty?
366
396
  unless current_defs.empty?
367
397
  sections << {
368
398
  visibility: current_visibility,
@@ -375,7 +405,7 @@ module RuboCop
375
405
  end
376
406
  current_visibility = node
377
407
  else
378
- # Non-visibility send - breaks contiguity
408
+ # anything else breaks contiguous run
379
409
  unless current_defs.empty?
380
410
  sections << {
381
411
  visibility: current_visibility,
@@ -385,11 +415,9 @@ module RuboCop
385
415
  }
386
416
  current_defs = []
387
417
  section_start = nil
388
- current_visibility = nil
389
418
  end
390
419
  end
391
420
  else
392
- # Any other node type breaks contiguity
393
421
  unless current_defs.empty?
394
422
  sections << {
395
423
  visibility: current_visibility,
@@ -399,12 +427,11 @@ module RuboCop
399
427
  }
400
428
  current_defs = []
401
429
  section_start = nil
402
- current_visibility = nil
403
430
  end
404
431
  end
405
432
  end
406
433
 
407
- # Handle trailing defs
434
+ # trailing defs
408
435
  unless current_defs.empty?
409
436
  sections << {
410
437
  visibility: current_visibility,
@@ -414,7 +441,20 @@ module RuboCop
414
441
  }
415
442
  end
416
443
 
417
- sections
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
418
458
  end
419
459
 
420
460
  # +RuboCop::Cop::SortedMethodsByCall::Waterfall#topo_sort+ -> Array<Symbol>, nil
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module SortedMethodsByCall
5
- VERSION = '1.1.0'
5
+ VERSION = '1.1.1'
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - unurgunite