rubocop-sorted_methods_by_call 1.1.1 → 1.2.0

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: 02e794a25b73c4b96c7ec77721e5b5e4c71a3a23e0137011f3d272d27e81121e
4
+ data.tar.gz: 75db5c1df55a2d40a9fdf596b654bafa76eecbb163e16c419b7c0366ef4cd772
5
5
  SHA512:
6
- metadata.gz: 91901d4d06ad36e83396752d9c7b47b3c838f61bb42686351b43f382281136aa219dcbeaaf1d5f0e2007db56728dd9e69dd7ba307f1758b9acc26cadf7b7416a
7
- data.tar.gz: a06c24a47cf38ea1e06021d54f49a04ed26fb47174f2c4dd1b6491cb768243acbda01fe25933b55895387e58d8f00d7e163c24d08525394f1891953f9a8a6017
6
+ metadata.gz: 7c3e6192757164ad8c877337a9bf60e78ccb2c4b8d8abd2a3f8c15a865cfe7ea4b1440f6c443dd26ca9836951884b229927a718081ab521b64926cc25896bab4
7
+ data.tar.gz: 14f8232209323631fb6a993282e6133907d3937ecf1b639c9e186b79fb835f531a750a4ebdb6e972c3f558921507c7fbb3a954c6c26e7510d19a3ddc3e3cfd7d
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 2026-01-13 11:42:57 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
@@ -19,37 +19,47 @@ Gemspec/RequiredRubyVersion:
19
19
  Exclude:
20
20
  - 'rubocop-sorted_methods_by_call.gemspec'
21
21
 
22
- # Offense count: 4
22
+ # Offense count: 5
23
23
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
24
24
  Metrics/AbcSize:
25
- Max: 61
25
+ Max: 78
26
26
 
27
- # Offense count: 2
27
+ # Offense count: 1
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
- # Offense count: 5
33
+ # Offense count: 4
34
34
  # Configuration parameters: AllowedMethods, AllowedPatterns.
35
35
  Metrics/CyclomaticComplexity:
36
- Max: 26
36
+ Max: 35
37
37
 
38
- # Offense count: 7
38
+ # Offense count: 9
39
39
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
40
40
  Metrics/MethodLength:
41
- Max: 68
41
+ Max: 70
42
42
 
43
- # Offense count: 4
43
+ # Offense count: 2
44
+ # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
45
+ Metrics/ParameterLists:
46
+ Max: 6
47
+
48
+ # Offense count: 3
44
49
  # Configuration parameters: AllowedMethods, AllowedPatterns.
45
50
  Metrics/PerceivedComplexity:
46
- Max: 27
51
+ Max: 37
47
52
 
48
- # Offense count: 16
53
+ # Offense count: 21
49
54
  # Configuration parameters: CountAsOne.
50
55
  RSpec/ExampleLength:
51
56
  Max: 37
52
57
 
58
+ # Offense count: 1
59
+ # Configuration parameters: AllowedGroups.
60
+ RSpec/NestedGroups:
61
+ Max: 4
62
+
53
63
  # Offense count: 2
54
64
  # This cop supports safe autocorrection (--autocorrect).
55
65
  # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
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.2.0)
5
5
  lint_roller
6
6
  rubocop (>= 1.72.0)
7
7
 
data/README.md CHANGED
@@ -15,6 +15,7 @@
15
15
  * [Usage Examples](#usage-examples)
16
16
  * [Good Code (waterfall order)](#good-code-waterfall-order)
17
17
  * [Bad Code (violates waterfall order)](#bad-code-violates-waterfall-order)
18
+ * [Sibling ordering and cycles (why autocorrect can be skipped)](#sibling-ordering-and-cycles-why-autocorrect-can-be-skipped)
18
19
  * [Autocorrection](#autocorrection)
19
20
  * [Testing](#testing)
20
21
  * [Development](#development)
@@ -76,6 +77,12 @@ SortedMethodsByCall/Waterfall:
76
77
  Enabled: true
77
78
  SafeAutoCorrect: false # Autocorrection requires -A flag
78
79
  AllowedRecursion: true # Allow mutual recursion (default: true)
80
+ # If true, the cop will NOT add "called together" sibling-order edges
81
+ # that would introduce a cycle with existing constraints. This reduces
82
+ # impossible-to-fix sibling offenses and makes autocorrect more reliable.
83
+ #
84
+ # Default: false
85
+ SkipCyclicSiblingEdges: false
79
86
  ```
80
87
 
81
88
  ## Usage Examples
@@ -135,6 +142,46 @@ class Service
135
142
  end
136
143
  ```
137
144
 
145
+ ### Sibling ordering and cycles (why autocorrect can be skipped)
146
+
147
+ `SortedMethodsByCall/Waterfall` enforces two kinds of ordering constraints:
148
+
149
+ 1. **Direct call edges**: if `caller` calls `callee`, then `caller` must be defined **before** `callee`.
150
+ 2. **Sibling ("called together") edges**: in orchestration methods (methods not called by others in the same scope),
151
+ consecutive calls imply an intended order (e.g., `a` then `b`), so `a` should be defined before `b`.
152
+
153
+ Sometimes these constraints can conflict and create a **cycle**, which means there is no valid ordering that satisfies
154
+ all constraints. In this situation, autocorrect may be skipped.
155
+
156
+ Example:
157
+
158
+ ```ruby
159
+ class SiblingCycleExample
160
+ def call
161
+ a
162
+ b
163
+ end
164
+
165
+ private
166
+
167
+ def b
168
+ c
169
+ end
170
+
171
+ def c
172
+ a
173
+ end
174
+
175
+ def a; end
176
+ end
177
+ ```
178
+
179
+ Here, the direct dependencies imply `b -> c -> a`, but the orchestration method implies `a -> b`,
180
+ which forms the cycle `a -> b -> c -> a`.
181
+
182
+ If you prefer to keep the warning (to encourage refactoring), leave `SkipCyclicSiblingEdges: false`.
183
+ If you prefer the cop to avoid enforcing sibling edges that create cycles, set `SkipCyclicSiblingEdges: true`.
184
+
138
185
  ### Autocorrection
139
186
 
140
187
  Run with unsafe autocorrection to automatically fix violations:
data/config/default.yml CHANGED
@@ -1,6 +1,7 @@
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.2.0"
5
5
  SafeAutoCorrect: false
6
6
  AllowedRecursion: true
7
+ SkipCyclicSiblingEdges: false
@@ -3,89 +3,123 @@
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 recursion cycle
19
+ # detectable in the direct call graph (callee → … → caller). If false,
20
+ # such cycles are reported.
21
+ # - SafeAutoCorrect [Boolean] (default: false)
22
+ # Autocorrection is unsafe and only runs under -A, never under -a.
23
+ # - SkipCyclicSiblingEdges [Boolean] (default: false)
24
+ # If true, the cop will not add "called together" sibling-order edges
25
+ # that would introduce a cycle with existing constraints (direct edges +
26
+ # already accepted sibling edges).
18
27
  #
19
- # private
28
+ # @example Good (waterfall order)
29
+ # class Service
30
+ # def call
31
+ # foo
32
+ # bar
33
+ # end
20
34
  #
21
- # def bar
22
- # method123
23
- # end
35
+ # private
24
36
  #
25
- # def method123
26
- # foo
27
- # end
37
+ # def bar
38
+ # method123
39
+ # end
28
40
  #
29
- # def foo
30
- # 123
31
- # end
41
+ # def method123
42
+ # foo
43
+ # end
32
44
  #
33
- # Example (bad):
34
- # def foo
35
- # 123
45
+ # def foo
46
+ # 123
47
+ # end
36
48
  # end
37
49
  #
38
- # def call
39
- # foo
50
+ # @example Bad (violates waterfall order)
51
+ # class Service
52
+ # def call
53
+ # foo
54
+ # bar
55
+ # end
56
+ #
57
+ # private
58
+ #
59
+ # def foo
60
+ # 123
61
+ # end
62
+ #
63
+ # def bar
64
+ # method123
65
+ # end
66
+ #
67
+ # def method123
68
+ # foo
69
+ # end
40
70
  # end
41
71
  #
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.
72
+ # @see #analyze_scope
73
+ # @see #try_autocorrect
44
74
  class Waterfall < ::RuboCop::Cop::Base # rubocop:disable Metrics/ClassLength
45
75
  include ::RuboCop::Cop::RangeHelp
46
76
  extend ::RuboCop::Cop::AutoCorrector
47
77
 
48
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall::MSG+ -> String
49
- #
50
- # Template message for offenses.
78
+ VISIBILITY_METHODS = %i[private protected public].freeze
79
+
80
+ # Template message for offenses where a callee appears before its caller.
51
81
  MSG = 'Define %<callee>s after its caller %<caller>s (waterfall order).'
52
82
 
53
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#on_begin+ -> void
83
+ SIBLING_MSG = 'Define %<callee>s after %<caller>s to match the order they are called together.'
84
+
85
+ MSG_CROSS_VISIBILITY_NOTE =
86
+ '%<base>s (Autocorrect not supported across visibility boundaries: ' \
87
+ '%<caller_visibility>s vs %<callee_visibility>s.)'
88
+
89
+ MSG_SIBLING_CYCLE_NOTE =
90
+ '%<base>s (Possible sibling cycle detected; autocorrect may be skipped.)'
91
+
92
+ # Entry point for root :begin nodes (top-level).
54
93
  #
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.
94
+ # Whether top-level is analyzed depends on how the code is structured;
95
+ # by default we only analyze class/module/sclass scopes, but top-level
96
+ # is supported through this hook.
57
97
  #
58
- # @param [RuboCop::AST::Node] node
98
+ # @param node [RuboCop::AST::Node] root :begin node
59
99
  # @return [void]
60
100
  def on_begin(node)
61
101
  analyze_scope(node)
62
102
  end
63
103
 
64
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#on_class+ -> void
65
- #
66
104
  # Entry point for class scopes.
67
105
  #
68
- # @param [RuboCop::AST::Node] node
106
+ # @param node [RuboCop::AST::Node] :class node
69
107
  # @return [void]
70
108
  def on_class(node)
71
109
  analyze_scope(node)
72
110
  end
73
111
 
74
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#on_module+ -> void
75
- #
76
112
  # Entry point for module scopes.
77
113
  #
78
- # @param [RuboCop::AST::Node] node
114
+ # @param node [RuboCop::AST::Node] :module node
79
115
  # @return [void]
80
116
  def on_module(node)
81
117
  analyze_scope(node)
82
118
  end
83
119
 
84
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#on_sclass+ -> void
85
- #
86
120
  # Entry point for singleton class scopes (class << self).
87
121
  #
88
- # @param [RuboCop::AST::Node] node
122
+ # @param node [RuboCop::AST::Node] :sclass node
89
123
  # @return [void]
90
124
  def on_sclass(node)
91
125
  analyze_scope(node)
@@ -93,156 +127,297 @@ module RuboCop
93
127
 
94
128
  private
95
129
 
96
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#analyze_scope+ -> void
97
- #
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.
130
+ # Analyze a single scope node (:begin, :class, :module, :sclass):
131
+ # - Collect method defs in the scope body
132
+ # - Build direct call edges (caller callee)
133
+ # - Optionally build sibling-order edges ("called together")
134
+ # - Find the first ordering violation and register an offense
135
+ # - Attempt autocorrect (under -A) within a contiguous visibility section
136
+ # - Recurse into nested scopes inside the body
102
137
  #
103
- # @param [RuboCop::AST::Node] scope_node
138
+ # @param scope_node [RuboCop::AST::Node] a :begin, :class, :module, or :sclass node
104
139
  # @return [void]
140
+ # @api private
105
141
  def analyze_scope(scope_node)
106
142
  body_nodes = scope_body_nodes(scope_node)
107
143
  return if body_nodes.empty?
108
144
 
109
- def_nodes = body_nodes.select { |n| %i[def defs].include?(n.type) }
145
+ def_nodes = method_def_nodes(body_nodes)
110
146
  return if def_nodes.size <= 1
111
147
 
112
- names = def_nodes.map(&:method_name)
113
- names_set = names.to_set
114
- index_of = names.each_with_index.to_h
148
+ names, names_set, index_of = method_name_index(def_nodes)
115
149
 
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] }
150
+ direct_edges = build_direct_edges(def_nodes, names_set)
151
+ sibling_edges = build_sibling_edges(def_nodes, names_set, direct_edges, names)
152
+
153
+ edges_for_sort = direct_edges + sibling_edges
154
+ adj_direct = build_adj(names, direct_edges)
155
+
156
+ violation_type, violation = find_violation(direct_edges, sibling_edges, index_of, adj_direct)
157
+ if violation
158
+ _, callee_name = violation
159
+ callee_node = def_nodes[index_of.fetch(callee_name)]
160
+
161
+ message = build_offense_message(
162
+ violation_type: violation_type,
163
+ violation: violation,
164
+ names: names,
165
+ edges_for_sort: edges_for_sort,
166
+ body_nodes: body_nodes
167
+ )
168
+
169
+ add_offense(callee_node, message: message) do |corrector|
170
+ try_autocorrect(corrector, body_nodes, def_nodes, edges_for_sort, violation)
171
+ end
123
172
  end
124
173
 
174
+ analyze_nested_scopes(body_nodes)
175
+ end
176
+
177
+ # Return the direct "body statements" inside a scope node.
178
+ #
179
+ # @param node [RuboCop::AST::Node]
180
+ # @return [Array<RuboCop::AST::Node>] direct children inside the scope body
181
+ # @api private
182
+ def scope_body_nodes(node)
183
+ case node.type
184
+ when :begin
185
+ node.children
186
+ when :class, :module, :sclass
187
+ body = node.body
188
+ return [] unless body
189
+
190
+ body.begin_type? ? body.children : [body]
191
+ else
192
+ []
193
+ end
194
+ end
195
+
196
+ # Select only method definition nodes from a scope body.
197
+ #
198
+ # @param body_nodes [Array<RuboCop::AST::Node>]
199
+ # @return [Array<RuboCop::AST::Node>] :def/:defs nodes
200
+ # @api private
201
+ def method_def_nodes(body_nodes)
202
+ body_nodes.select { |n| %i[def defs].include?(n.type) }
203
+ end
204
+
205
+ # Compute helper structures for method names in this scope.
206
+ #
207
+ # @param def_nodes [Array<RuboCop::AST::Node>]
208
+ # @return [Array<(Array<Symbol>, Set<Symbol>, Hash{Symbol=>Integer})>]
209
+ # @api private
210
+ def method_name_index(def_nodes)
211
+ names = def_nodes.map(&:method_name)
212
+ [names, names.to_set, names.each_with_index.to_h]
213
+ end
214
+
215
+ # Build direct call edges (caller -> callee) for local calls within each method body.
216
+ #
217
+ # @param def_nodes [Array<RuboCop::AST::Node>]
218
+ # @param names_set [Set<Symbol>]
219
+ # @return [Array<Array(Symbol, Symbol)>]
220
+ # @api private
221
+ def build_direct_edges(def_nodes, names_set)
222
+ def_nodes.flat_map do |def_node|
223
+ local_calls(def_node, names_set)
224
+ .reject { |callee| callee == def_node.method_name }
225
+ .map { |callee| [def_node.method_name, callee] }
226
+ end
227
+ end
228
+
229
+ # Build sibling-order edges (a -> b) for consecutive calls inside orchestration methods.
230
+ #
231
+ # Orchestration methods are those not called by any other method in this scope.
232
+ #
233
+ # @param def_nodes [Array<RuboCop::AST::Node>]
234
+ # @param names_set [Set<Symbol>]
235
+ # @param direct_edges [Array<Array(Symbol, Symbol)>]
236
+ # @param names [Array<Symbol>]
237
+ # @return [Array<Array(Symbol, Symbol)>]
238
+ # @api private
239
+ def build_sibling_edges(def_nodes, names_set, direct_edges, names)
125
240
  all_callees = direct_edges.to_set(&:last)
241
+ direct_pair_set = direct_edges.to_set
242
+
243
+ skip_cyclic_siblings = skip_cyclic_sibling_edges?
244
+ adj_for_siblings = build_adj(names, direct_edges)
126
245
 
127
- # -----------------------------------------------------------
128
- # Phase 2 Add sibling‑order edges for orchestration methods
129
- # -----------------------------------------------------------
130
246
  sibling_edges = []
247
+
131
248
  def_nodes.each do |def_node|
132
249
  next if all_callees.include?(def_node.method_name)
133
250
 
134
251
  calls = local_calls(def_node, names_set)
135
252
  calls.each_cons(2) do |a, b|
136
- next if direct_edges.any? { |u, v| (u == a && v == b) || (u == b && v == a) }
253
+ # If there is already a direct relationship between a and b (either direction),
254
+ # do not add a sibling-order edge.
255
+ next if direct_pair_set.include?([a, b]) || direct_pair_set.include?([b, a])
256
+
257
+ # Optional: do not add a sibling edge that would introduce a cycle.
258
+ next if skip_cyclic_siblings && path_exists?(b, a, adj_for_siblings)
137
259
 
138
260
  sibling_edges << [a, b]
261
+ adj_for_siblings[a] << b unless adj_for_siblings[a].include?(b)
139
262
  end
140
263
  end
141
264
 
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 }
265
+ sibling_edges
266
+ end
147
267
 
148
- # Build adjacency only from *direct* calls for recursion checks
149
- adj_direct = build_adj(names, direct_edges)
268
+ # Find the first ordering violation. Checks direct edges first, then sibling edges.
269
+ #
270
+ # @param direct_edges [Array<Array(Symbol, Symbol)>]
271
+ # @param sibling_edges [Array<Array(Symbol, Symbol)>]
272
+ # @param index_of [Hash{Symbol=>Integer}]
273
+ # @param adj_direct [Hash{Symbol=>Array<Symbol>}] adjacency list for direct edges
274
+ # @return [Array<(Symbol, Array(Symbol, Symbol))>, Array<(nil, nil)>]
275
+ # @api private
276
+ def find_violation(direct_edges, sibling_edges, index_of, adj_direct)
277
+ allow_recursion = allowed_recursion?
150
278
 
151
- # Check for violations with edge type tracking
152
279
  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
280
+ return [:direct, violation] if violation
159
281
 
160
- return unless violation
282
+ violation = first_backward_edge(sibling_edges, index_of, adj_direct, allow_recursion)
283
+ return [:sibling, violation] if violation
161
284
 
162
- caller_name, callee_name = violation
163
- callee_node = def_nodes[index_of[callee_name]]
164
-
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
285
+ [nil, nil]
286
+ end
171
287
 
172
- add_offense(callee_node, message: message) do |corrector|
173
- try_autocorrect(corrector, body_nodes, def_nodes, edges_for_sort, violation)
174
- end
288
+ # Return the first backward edge found, optionally skipping edges that participate
289
+ # in recursion/cycles detectable in the direct-call graph (AllowedRecursion).
290
+ #
291
+ # @param edges [Array<Array(Symbol, Symbol)>]
292
+ # @param index_of [Hash{Symbol=>Integer}]
293
+ # @param adj_direct [Hash{Symbol=>Array<Symbol>}] direct-call adjacency for path checks
294
+ # @param allow_recursion [Boolean]
295
+ # @return [Array(Symbol, Symbol), nil]
296
+ # @api private
297
+ def first_backward_edge(edges, index_of, adj_direct, allow_recursion)
298
+ edges.find do |caller, callee|
299
+ next unless index_of.key?(caller) && index_of.key?(callee)
300
+ next if allow_recursion && path_exists?(callee, caller, adj_direct)
175
301
 
176
- # Recurse into nested scopes
177
- body_nodes.each do |n|
178
- analyze_scope(n) if n.class_type? || n.module_type? || n.sclass_type?
302
+ index_of[callee] < index_of[caller]
179
303
  end
180
304
  end
181
305
 
182
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#scope_body_nodes+ -> Array<RuboCop::AST::Node>
183
- #
184
- # Normalizes a scope node to its immediate "body" items we iterate over.
306
+ # Construct the final offense message, including optional notes:
307
+ # - sibling cycle note (for sibling violations)
308
+ # - cross-visibility note (public/private/protected boundary)
185
309
  #
186
- # @param [RuboCop::AST::Node] node
187
- # @return [Array<RuboCop::AST::Node>]
188
- def scope_body_nodes(node)
189
- case node.type
190
- when :begin
191
- node.children
192
- when :class, :module, :sclass
193
- body = node.body
194
- return [] unless body
310
+ # @param violation_type [Symbol] :direct or :sibling
311
+ # @param violation [Array(Symbol, Symbol)] (caller_name, callee_name)
312
+ # @param names [Array<Symbol>]
313
+ # @param edges_for_sort [Array<Array(Symbol, Symbol)>]
314
+ # @param body_nodes [Array<RuboCop::AST::Node>]
315
+ # @return [String]
316
+ # @api private
317
+ def build_offense_message(violation_type:, violation:, names:, edges_for_sort:, body_nodes:)
318
+ caller_name, callee_name = violation
195
319
 
196
- body.begin_type? ? body.children : [body]
320
+ base = base_message_for(violation_type, caller_name, callee_name)
321
+ base = add_sibling_cycle_note_if_needed(base, violation_type, caller_name, callee_name, names, edges_for_sort)
322
+ add_cross_visibility_note_if_needed(base, body_nodes, caller_name, callee_name)
323
+ end
324
+
325
+ # @param violation_type [Symbol]
326
+ # @param caller_name [Symbol]
327
+ # @param callee_name [Symbol]
328
+ # @return [String]
329
+ # @api private
330
+ def base_message_for(violation_type, caller_name, callee_name)
331
+ if violation_type == :sibling
332
+ format(SIBLING_MSG, callee: "##{callee_name}", caller: "##{caller_name}")
197
333
  else
198
- []
334
+ format(MSG, callee: "##{callee_name}", caller: "##{caller_name}")
199
335
  end
200
336
  end
201
337
 
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.
338
+ # Add a note when a sibling-order edge is part of a cycle in the combined graph.
206
339
  #
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?
340
+ # @param base_message [String]
341
+ # @param violation_type [Symbol]
342
+ # @param caller_name [Symbol]
343
+ # @param callee_name [Symbol]
344
+ # @param names [Array<Symbol>]
345
+ # @param edges_for_sort [Array<Array(Symbol, Symbol)>]
346
+ # @return [String]
347
+ # @api private
348
+ def add_sibling_cycle_note_if_needed(base_message, violation_type, caller_name, callee_name, names,
349
+ edges_for_sort)
350
+ return base_message unless violation_type == :sibling
351
+
352
+ adj_all = build_adj(names, edges_for_sort)
353
+ if path_exists?(callee_name, caller_name, adj_all)
354
+ format(MSG_SIBLING_CYCLE_NOTE, base: base_message)
355
+ else
356
+ base_message
357
+ end
358
+ end
218
359
 
219
- mname = send.method_name
220
- res << mname if names_set.include?(mname)
360
+ # Add a note when the violation crosses visibility boundaries.
361
+ #
362
+ # @param base_message [String]
363
+ # @param body_nodes [Array<RuboCop::AST::Node>]
364
+ # @param caller_name [Symbol]
365
+ # @param callee_name [Symbol]
366
+ # @return [String]
367
+ # @api private
368
+ def add_cross_visibility_note_if_needed(base_message, body_nodes, caller_name, callee_name)
369
+ sections = extract_visibility_sections(body_nodes)
370
+ caller_section = section_for_method(sections, caller_name)
371
+ callee_section = section_for_method(sections, callee_name)
372
+
373
+ caller_vis = visibility_label(caller_section)
374
+ callee_vis = visibility_label(callee_section)
375
+
376
+ if caller_section && callee_section && caller_vis != callee_vis
377
+ format(
378
+ MSG_CROSS_VISIBILITY_NOTE,
379
+ base: base_message,
380
+ caller_visibility: caller_vis,
381
+ callee_visibility: callee_vis
382
+ )
383
+ else
384
+ base_message
221
385
  end
222
- res.uniq
223
386
  end
224
387
 
225
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#try_autocorrect+ -> void
388
+ # UNSAFE autocorrect: reorder method definitions inside one contiguous visibility section only.
226
389
  #
227
- # UNSAFE: Reorders method definitions inside the target visibility section only
228
- # (does not cross private/protected/public boundaries). Skips if defs are not
229
- # contiguous within the section or if a cycle prevents a consistent topo order.
390
+ # This method intentionally does NOT reorder across:
391
+ # - `private/protected/public` boundaries
392
+ # - nested scopes
393
+ # - non-visibility statements that break contiguity
230
394
  #
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)
395
+ # @param corrector [RuboCop::Cop::Corrector]
396
+ # @param body_nodes [Array<RuboCop::AST::Node>]
397
+ # @param def_nodes [Array<RuboCop::AST::Node>]
398
+ # @param edges [Array<Array(Symbol, Symbol)>] direct + sibling edges for this scope
399
+ # @param initial_violation [Array(Symbol, Symbol), nil]
236
400
  # @return [void]
237
- #
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)
401
+ # @api private
402
+ def try_autocorrect(corrector, body_nodes, def_nodes, edges, initial_violation = nil)
241
403
  sections = extract_visibility_sections(body_nodes)
242
404
 
405
+ names = def_nodes.map(&:method_name)
406
+ names_set = names.to_set
407
+ idx_of = names.each_with_index.to_h
408
+
409
+ # Recompute direct edges; split edges back into direct vs sibling
410
+ direct_edges = build_direct_edges(def_nodes, names_set)
411
+ sibling_edges = edges - direct_edges
412
+
413
+ allow_recursion = allowed_recursion?
414
+ adj_direct = build_adj(names, direct_edges)
415
+
416
+ violation = initial_violation || first_backward_edge(edges, idx_of, adj_direct, allow_recursion)
417
+ return unless violation
418
+
243
419
  caller_name, callee_name = violation
244
420
 
245
- # find target section containing both defs
246
421
  target_section = sections.find do |section|
247
422
  section_names = section[:defs].map(&:method_name)
248
423
  section_names.include?(caller_name) && section_names.include?(callee_name)
@@ -252,27 +427,31 @@ module RuboCop
252
427
  defs = target_section[:defs]
253
428
  return if defs.size <= 1
254
429
 
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
430
+ section_names = defs.map(&:method_name)
431
+ section_idx_of = section_names.each_with_index.to_h
258
432
 
259
- # sort within section or minimally fix if graph is cyclic
260
- sorted_names = topo_sort(section_names, section_edges, section_idx_of)
433
+ direct_violation = direct_edges.any? { |u, v| u == caller_name && v == callee_name }
261
434
 
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
435
+ section_direct_edges = direct_edges.select { |u, v| section_names.include?(u) && section_names.include?(v) }
436
+ section_sibling_edges = sibling_edges.select { |u, v| section_names.include?(u) && section_names.include?(v) }
270
437
 
271
- # reconstruct source
272
- ranges_by_name = defs.to_h do |d|
273
- [d.method_name, range_with_leading_comments(d)]
438
+ if allow_recursion
439
+ pair_set = section_direct_edges.to_set
440
+ section_direct_edges = section_direct_edges.reject { |u, v| pair_set.include?([v, u]) }
274
441
  end
275
- sorted_def_sources = sorted_names.map { |n| ranges_by_name[n].source }
442
+
443
+ section_edges_for_sort =
444
+ if direct_violation
445
+ section_direct_edges
446
+ else
447
+ section_sibling_edges + section_direct_edges
448
+ end
449
+
450
+ sorted_names = topo_sort(section_names, section_edges_for_sort, section_idx_of)
451
+ return if sorted_names.nil? || sorted_names == section_names
452
+
453
+ ranges_by_name = defs.to_h { |d| [d.method_name, range_with_leading_comments(d)] }
454
+ sorted_def_sources = sorted_names.map { |n| ranges_by_name.fetch(n).source }
276
455
 
277
456
  visibility_node = target_section[:visibility]
278
457
  visibility_source = visibility_node&.source.to_s
@@ -290,72 +469,76 @@ module RuboCop
290
469
  else
291
470
  defs.map { |d| range_with_leading_comments(d).begin_pos }.min
292
471
  end
293
- section_end = defs.last.source_range.end_pos
294
472
 
295
- region = Parser::Source::Range.new(processed_source.buffer, section_begin, section_end)
473
+ section_end = target_section[:end_pos]
474
+ region = range_between(section_begin, section_end)
296
475
  corrector.replace(region, new_content)
297
476
  end
298
477
 
299
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#build_adj+ -> Hash{Symbol=>Array<Symbol>}
478
+ # Collect local method calls (receiver is nil/self) from within a def node,
479
+ # restricted to known method names in this scope.
300
480
  #
301
- # Builds an adjacency list for edges restricted to known names.
481
+ # @param def_node [RuboCop::AST::Node] :def or :defs
482
+ # @param names_set [Set<Symbol>] known local method names in this scope
483
+ # @return [Array<Symbol>] unique callee names
484
+ # @api private
485
+ def local_calls(def_node, names_set)
486
+ body = def_node.body
487
+ return [] unless body
488
+
489
+ res = []
490
+ body.each_node(:send) do |send|
491
+ recv = send.receiver
492
+ next unless recv.nil? || recv&.self_type?
493
+
494
+ mname = send.method_name
495
+ res << mname if names_set.include?(mname)
496
+ end
497
+ res.uniq
498
+ end
499
+
500
+ # Build an adjacency list for a set of edges restricted to known names.
302
501
  #
303
- # @param [Array<Symbol>] names
304
- # @param [Array<Array(Symbol, Symbol)>] edges
305
- # @return [Hash{Symbol=>Array<Symbol>}]
502
+ # @param names [Array<Symbol>]
503
+ # @param edges [Array<Array(Symbol, Symbol)>]
504
+ # @return [Hash{Symbol=>Array<Symbol>}] adjacency list
505
+ # @api private
306
506
  def build_adj(names, edges)
307
507
  allowed = names.to_set
308
508
  adj = Hash.new { |h, k| h[k] = [] }
509
+
309
510
  edges.each do |u, v|
310
511
  next unless allowed.include?(u) && allowed.include?(v)
311
512
  next if u == v
312
513
 
313
514
  adj[u] << v
314
515
  end
315
- adj
316
- end
317
516
 
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.
322
- #
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]
328
- def first_backward_edge(edges, index_of, adj, allow_recursion)
329
- edges.find do |caller, callee|
330
- next unless index_of.key?(caller) && index_of.key?(callee)
331
- # If mutual recursion allowed and there is a path callee -> caller, skip
332
- next if allow_recursion && path_exists?(callee, caller, adj)
333
-
334
- # Violation: callee is defined BEFORE caller (waterfall order)
335
- index_of[callee] < index_of[caller]
336
- end
517
+ adj
337
518
  end
338
519
 
339
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#path_exists?+ -> Boolean
520
+ # Breadth-first search to detect whether a path exists from +src+ to +dst+.
340
521
  #
341
- # Tests whether a path exists in the adjacency graph from +src+ to +dst+ (BFS).
342
- #
343
- # @param [Symbol] src
344
- # @param [Symbol] dst
345
- # @param [Hash{Symbol=>Array<Symbol>}] adj
346
- # @param [Integer] limit traversal step limit (guard)
522
+ # @param src [Symbol]
523
+ # @param dst [Symbol]
524
+ # @param adj [Hash{Symbol=>Array<Symbol>}] adjacency list
525
+ # @param limit [Integer] traversal safety limit
347
526
  # @return [Boolean]
527
+ # @api private
348
528
  def path_exists?(src, dst, adj, limit = 200)
349
529
  return true if src == dst
350
530
 
351
531
  visited = {}
352
532
  q = [src]
533
+ i = 0
353
534
  steps = 0
354
- until q.empty?
535
+
536
+ while i < q.length
355
537
  steps += 1
356
538
  return false if steps > limit
357
539
 
358
- u = q.shift
540
+ u = q[i]
541
+ i += 1
359
542
  next if visited[u]
360
543
 
361
544
  visited[u] = true
@@ -363,20 +546,22 @@ module RuboCop
363
546
 
364
547
  adj[u].each { |v| q << v unless visited[v] }
365
548
  end
549
+
366
550
  false
367
551
  end
368
552
 
369
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#extract_visibility_sections+ -> Array<Hash>
553
+ # Split the scope body into contiguous sections of def/defs grouped
554
+ # by the visibility modifier immediately preceding them (private/protected/public).
370
555
  #
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)
556
+ # A section is represented as a Hash with:
557
+ # - :visibility [RuboCop::AST::Node, nil] the bare visibility send, or nil
558
+ # - :defs [Array<RuboCop::AST::Node>] contiguous def/defs nodes
559
+ # - :start_pos [Integer]
560
+ # - :end_pos [Integer]
377
561
  #
378
- # @param [Array<RuboCop::AST::Node>] body_nodes
379
- # @return [Array<Hash{Symbol=>untyped}>]
562
+ # @param body_nodes [Array<RuboCop::AST::Node>]
563
+ # @return [Array<Hash>]
564
+ # @api private
380
565
  def extract_visibility_sections(body_nodes)
381
566
  sections = []
382
567
  current_visibility = nil
@@ -389,49 +574,18 @@ module RuboCop
389
574
  current_defs << node
390
575
  section_start ||= node.source_range.begin_pos
391
576
  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
406
- current_visibility = node
407
- 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
419
- end
577
+ flush_visibility_section!(sections, current_visibility, current_defs, section_start, body_nodes, idx - 1)
578
+ current_defs = []
579
+ section_start = nil
580
+ current_visibility = node if bare_visibility_send?(node)
420
581
  else
421
- unless current_defs.empty?
422
- sections << {
423
- visibility: current_visibility,
424
- defs: current_defs.dup,
425
- start_pos: section_start,
426
- end_pos: body_nodes[idx - 1].source_range.end_pos
427
- }
428
- current_defs = []
429
- section_start = nil
430
- end
582
+ flush_visibility_section!(sections, current_visibility, current_defs, section_start, body_nodes, idx - 1)
583
+ current_defs = []
584
+ section_start = nil
585
+ current_visibility = nil
431
586
  end
432
587
  end
433
588
 
434
- # trailing defs
435
589
  unless current_defs.empty?
436
590
  sections << {
437
591
  visibility: current_visibility,
@@ -441,30 +595,70 @@ module RuboCop
441
595
  }
442
596
  end
443
597
 
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
598
+ sections
599
+ end
600
+
601
+ # Flush a currently-collected contiguous def/defs group into +sections+.
602
+ #
603
+ # @param sections [Array<Hash>]
604
+ # @param current_visibility [RuboCop::AST::Node, nil]
605
+ # @param current_defs [Array<RuboCop::AST::Node>]
606
+ # @param section_start [Integer, nil]
607
+ # @param body_nodes [Array<RuboCop::AST::Node>]
608
+ # @param last_idx [Integer]
609
+ # @return [void]
610
+ # @api private
611
+ def flush_visibility_section!(sections, current_visibility, current_defs, section_start, body_nodes, last_idx)
612
+ return if current_defs.empty?
613
+
614
+ sections << {
615
+ visibility: current_visibility,
616
+ defs: current_defs.dup,
617
+ start_pos: section_start,
618
+ end_pos: body_nodes[last_idx].source_range.end_pos
619
+ }
620
+ end
621
+
622
+ # Check if +node+ is a bare visibility modifier send:
623
+ # `private`, `protected`, or `public` (with no args and no receiver).
624
+ #
625
+ # @param node [RuboCop::AST::Node]
626
+ # @return [Boolean]
627
+ # @api private
628
+ def bare_visibility_send?(node)
629
+ node.receiver.nil? &&
630
+ VISIBILITY_METHODS.include?(node.method_name) &&
631
+ node.arguments.empty?
632
+ end
633
+
634
+ # Find the visibility section containing a given method name.
635
+ #
636
+ # @param sections [Array<Hash>]
637
+ # @param method_name [Symbol]
638
+ # @return [Hash, nil]
639
+ # @api private
640
+ def section_for_method(sections, method_name)
641
+ sections.find { |s| s[:defs].any? { |d| d.method_name == method_name } }
458
642
  end
459
643
 
460
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#topo_sort+ -> Array<Symbol>, nil
644
+ # Normalize a section to a string visibility label ("public", "private", "protected").
461
645
  #
462
- # Performs a stable topological sort using current order as a tie-breaker.
646
+ # @param section [Hash, nil]
647
+ # @return [String]
648
+ # @api private
649
+ def visibility_label(section)
650
+ return 'public' unless section # default visibility
651
+
652
+ (section[:visibility]&.method_name || :public).to_s
653
+ end
654
+
655
+ # Stable topological sort using the current definition order as a tie-breaker.
463
656
  #
464
- # @param [Array<Symbol>] names
465
- # @param [Array<Array(Symbol, Symbol)>] edges
466
- # @param [Hash{Symbol=>Integer}] idx_of
467
- # @return [Array<Symbol>, nil] sorted names or nil if a cycle prevents a full order
657
+ # @param names [Array<Symbol>]
658
+ # @param edges [Array<Array(Symbol, Symbol)>]
659
+ # @param idx_of [Hash{Symbol=>Integer}]
660
+ # @return [Array<Symbol>, nil] sorted list, or nil if cycle prevents a full order
661
+ # @api private
468
662
  def topo_sort(names, edges, idx_of)
469
663
  indegree = Hash.new(0)
470
664
  adj = Hash.new { |h, k| h[k] = [] }
@@ -477,6 +671,7 @@ module RuboCop
477
671
  indegree[callee] += 1
478
672
  indegree[caller] ||= 0
479
673
  end
674
+
480
675
  names.each { |n| indegree[n] ||= 0 }
481
676
 
482
677
  queue = names.select { |n| indegree[n].zero? }.sort_by { |n| idx_of[n] }
@@ -485,10 +680,12 @@ module RuboCop
485
680
  until queue.empty?
486
681
  n = queue.shift
487
682
  result << n
683
+
488
684
  adj[n].each do |m|
489
685
  indegree[m] -= 1
490
686
  queue << m if indegree[m].zero?
491
687
  end
688
+
492
689
  queue.sort_by! { |x| idx_of[x] }
493
690
  end
494
691
 
@@ -497,20 +694,20 @@ module RuboCop
497
694
  result
498
695
  end
499
696
 
500
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#range_with_leading_comments+ -> Parser::Source::Range
697
+ # Return a range that starts at the first contiguous comment line immediately
698
+ # above the def/defs node and ends at the end of the def. This preserves
699
+ # doc comments when methods are moved during autocorrect.
501
700
  #
502
- # 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
504
- # YARD/RDoc doc comments when methods are moved during autocorrect.
505
- #
506
- # @param [RuboCop::AST::Node] node The def/defs node.
507
- # @return [Parser::Source::Range] Range covering leading comments + method body.
701
+ # @param node [RuboCop::AST::Node] :def or :defs
702
+ # @return [Parser::Source::Range]
703
+ # @api private
508
704
  def range_with_leading_comments(node)
509
705
  buffer = processed_source.buffer
510
706
  expr = node.source_range
511
707
 
512
708
  start_line = expr.line
513
709
  lineno = start_line - 1
710
+
514
711
  while lineno >= 1
515
712
  line = buffer.source_line(lineno)
516
713
  break unless line =~ /\A\s*#/
@@ -520,7 +717,34 @@ module RuboCop
520
717
  end
521
718
 
522
719
  start_pos = buffer.line_range(start_line).begin_pos
523
- Parser::Source::Range.new(buffer, start_pos, expr.end_pos)
720
+ range_between(start_pos, expr.end_pos)
721
+ end
722
+
723
+ # Recurse into nested scopes inside the current scope body.
724
+ #
725
+ # @param body_nodes [Array<RuboCop::AST::Node>]
726
+ # @return [void]
727
+ # @api private
728
+ def analyze_nested_scopes(body_nodes)
729
+ body_nodes.each do |n|
730
+ analyze_scope(n) if n.class_type? || n.module_type? || n.sclass_type?
731
+ end
732
+ end
733
+
734
+ # Read config: AllowedRecursion (default true).
735
+ #
736
+ # @return [Boolean]
737
+ # @api private
738
+ def allowed_recursion?
739
+ cop_config.fetch('AllowedRecursion') { true }
740
+ end
741
+
742
+ # Read config: SkipCyclicSiblingEdges (default false).
743
+ #
744
+ # @return [Boolean]
745
+ # @api private
746
+ def skip_cyclic_sibling_edges?
747
+ cop_config.fetch('SkipCyclicSiblingEdges') { false }
524
748
  end
525
749
  end
526
750
  end
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module SortedMethodsByCall
5
5
  # +RuboCop::SortedMethodsByCall::Compare+ provides helpers to compare
6
- # definition orders and call orders using ordered subsequence semantics.
6
+ # definition orders and call orders using "ordered subsequence" semantics.
7
7
  # It’s used by the cop to check that called methods appear in the same
8
8
  # relative order as they are defined (not necessarily contiguously).
9
9
  module Compare
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module SortedMethodsByCall
5
- VERSION = '1.1.1'
5
+ VERSION = '1.2.0'
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.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - unurgunite
@@ -186,7 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
186
  - !ruby/object:Gem::Version
187
187
  version: '0'
188
188
  requirements: []
189
- rubygems_version: 3.7.2
189
+ rubygems_version: 4.0.2
190
190
  specification_version: 4
191
191
  summary: RuboCop extension for method sorting in AST by stack trace.
192
192
  test_files: []