rubocop-sorted_methods_by_call 1.0.1 → 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: fb5b1320e0c36ea11d3931c04aad10d03a3c909cf16added71b537bb540e9851
4
- data.tar.gz: c2e121be2bfa362e5e6b8f7acaae7378d82158fbecdcb0c6b562e9eb4f4f1b0b
3
+ metadata.gz: 1d62de644bc87dd509f3407cc4db3c9e0b4c46991e6a4e682344a7024eae1217
4
+ data.tar.gz: 94858039c6204ae841a595b4e5c13db9cf3a0775cf615fdd4696951d68181303
5
5
  SHA512:
6
- metadata.gz: 45b61992be6ad19b714eaa67929b0782c0cfb6b237cfb0a9770aca073f5672b817586305b8588052d636556e73afcd54826112d19ec67cb3d13df9af554a6e2b
7
- data.tar.gz: 2983a3c4e99c5c58cb4361b2ac93b588230df3e318e79c1f4ec8b0b0f16d49ac69ff0e9f2fd443ecf5ac3ef8be3d673f3116f18854498590f129d23093079da8
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-05 21:07:30 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
@@ -24,7 +24,7 @@ Gemspec/RequiredRubyVersion:
24
24
  Metrics/AbcSize:
25
25
  Max: 61
26
26
 
27
- # Offense count: 1
27
+ # Offense count: 2
28
28
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
29
29
  # AllowedMethods: refine
30
30
  Metrics/BlockLength:
@@ -33,22 +33,22 @@ 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: 11
48
+ # Offense count: 16
49
49
  # Configuration parameters: CountAsOne.
50
50
  RSpec/ExampleLength:
51
- Max: 26
51
+ Max: 37
52
52
 
53
53
  # Offense count: 2
54
54
  # This cop supports safe autocorrection (--autocorrect).
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rubocop-sorted_methods_by_call (1.0.1)
4
+ rubocop-sorted_methods_by_call (1.1.1)
5
5
  lint_roller
6
6
  rubocop (>= 1.72.0)
7
7
 
data/LICENSE.txt CHANGED
@@ -1,11 +1,21 @@
1
- Copyright 2022 unurgunite
1
+ MIT License
2
2
 
3
- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
3
+ Copyright (c) 2025 unurgunite
4
4
 
5
- 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
6
11
 
7
- 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
8
14
 
9
- 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
-
11
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
  [![CI](https://github.com/unurgunite/rubocop-sorted_methods_by_call/actions/workflows/ci.yml/badge.svg)](https://github.com/unurgunite/rubocop-sorted_methods_by_call/actions)
5
5
  [![Gem Version](https://badge.fury.io/rb/rubocop-sorted_methods_by_call.svg)](https://rubygems.org/gems/rubocop-sorted_methods_by_call)
6
6
 
7
+ **Enforces "waterfall" method ordering**: define methods *after* any method that calls them within the same scope.
8
+
7
9
  * [RuboCop::SortedMethodsByCall](#rubocopsortedmethodsbycall)
8
10
  * [Features](#features)
9
11
  * [Installation](#installation)
@@ -24,16 +26,14 @@
24
26
  * [License](#license)
25
27
  * [Code of Conduct](#code-of-conduct)
26
28
 
27
- **Enforces "waterfall" method ordering**: define methods *after* any method that calls them within the same scope.
28
-
29
29
  ## Features
30
30
 
31
- - **Waterfall ordering enforcement**: Caller methods must be defined before their callees
32
- - **Smart visibility handling**: Respects `private`/`protected`/`public` sections
33
- - **Safe mutual recursion**: Handles recursive method calls gracefully
34
- - **Autocorrection support**: Automatically reorders methods (opt-in with `-A`)
35
- - **Full RuboCop integration**: Works seamlessly with modern RuboCop plugin system
36
- - **Comprehensive scope support**: Classes, modules, singleton classes, and top-level
31
+ - **Waterfall ordering enforcement**: Caller methods must be defined before their callees;
32
+ - **Smart visibility handling**: Respects `private`/`protected`/`public` sections;
33
+ - **Safe mutual recursion**: Handles recursive method calls gracefully;
34
+ - **Autocorrection support**: Automatically reorders methods (opt-in with `-A`);
35
+ - **Full RuboCop integration**: Works seamlessly with modern RuboCop plugin system;
36
+ - **Comprehensive scope support**: Classes, modules, singleton classes, and top-level;
37
37
 
38
38
  ## Installation
39
39
 
@@ -82,19 +82,28 @@ SortedMethodsByCall/Waterfall:
82
82
 
83
83
  ### Good Code (waterfall order)
84
84
 
85
+ In waterfall ordering, **callers come before callees**. This creates a top-down reading flow where main logic appears
86
+ before implementation details.
87
+
85
88
  ```ruby
89
+
86
90
  class Service
87
91
  def call
88
- do_smth
92
+ foo
93
+ bar
89
94
  end
90
95
 
91
96
  private
92
97
 
93
- def do_smth
94
- well
98
+ def bar
99
+ method123
95
100
  end
96
101
 
97
- def well
102
+ def method123
103
+ foo
104
+ end
105
+
106
+ def foo
98
107
  123
99
108
  end
100
109
  end
@@ -103,19 +112,25 @@ end
103
112
  ### Bad Code (violates waterfall order)
104
113
 
105
114
  ```ruby
115
+
106
116
  class Service
107
117
  def call
108
- do_smth
118
+ foo
119
+ bar
109
120
  end
110
121
 
111
122
  private
112
123
 
113
- def well # ❌ Offense: Define #well after its caller #do_smth
124
+ def foo # ❌ Offense: Define #foo after its caller #method123
114
125
  123
115
126
  end
116
127
 
117
- def do_smth
118
- well
128
+ def bar
129
+ method123
130
+ end
131
+
132
+ def method123
133
+ foo
119
134
  end
120
135
  end
121
136
  ```
@@ -128,25 +143,7 @@ Run with unsafe autocorrection to automatically fix violations:
128
143
  bundle exec rubocop -A
129
144
  ```
130
145
 
131
- This will reorder the methods while preserving comments and visibility modifiers:
132
-
133
- ```ruby
134
- class Service
135
- def call
136
- do_smth
137
- end
138
-
139
- private
140
-
141
- def do_smth
142
- well
143
- end
144
-
145
- def well
146
- 123
147
- end
148
- end
149
- ```
146
+ This will reorder the methods while preserving comments and visibility modifiers.
150
147
 
151
148
  ## Testing
152
149
 
@@ -160,7 +157,7 @@ Run RuboCop on the gem itself:
160
157
 
161
158
  ```bash
162
159
  bundle exec rubocop
163
- bundle exec rubocop --config .rubocop.test.yml lib/ -A
160
+ bundle exec rubocop --config test_project/.rubocop.test.yml lib/ -A
164
161
  ```
165
162
 
166
163
  ## Development
@@ -212,8 +209,7 @@ at https://unurgunite.github.io/rubocop-sorted_methods_by_call_docs/
212
209
 
213
210
  ## License
214
211
 
215
- The gem is available as open source under the terms of
216
- the [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause).
212
+ The gem is available as open source under the terms of MIT License.
217
213
 
218
214
  ## Code of Conduct
219
215
 
@@ -221,5 +217,6 @@ Everyone interacting with this project is expected to follow the [Code of Conduc
221
217
 
222
218
  ---
223
219
 
224
- > **Note**: This gem is now stable and ready for production use! The "waterfall" ordering pattern helps create more
225
- > readable code by ensuring that methods are defined in the order they're conceptually needed.
220
+ > **Note**: This gem implements **true waterfall ordering** that considers the complete call graph across all methods in
221
+ > a scope. Methods are ordered so that every callee appears after all of its callers, creating a natural top-down
222
+ > reading flow.
data/config/default.yml CHANGED
@@ -1,7 +1,6 @@
1
- # Default configuration for rubocop-sorted_methods_by_call
2
1
  SortedMethodsByCall/Waterfall:
3
- Description: "Enforces waterfall ordering: define methods after the methods that call them."
2
+ Description: "Enforces method ordering based on call relationships."
4
3
  Enabled: false
5
- VersionAdded: "1.0.1"
4
+ VersionAdded: "1.1.1"
6
5
  SafeAutoCorrect: false
7
6
  AllowedRecursion: true
@@ -11,21 +11,32 @@ module RuboCop
11
11
  # - Autocorrect: UNSAFE; reorders methods within a contiguous visibility section
12
12
  #
13
13
  # Example (good):
14
- # def foo
14
+ # def call
15
+ # foo
15
16
  # bar
16
17
  # end
17
18
  #
19
+ # private
20
+ #
18
21
  # def bar
22
+ # method123
23
+ # end
24
+ #
25
+ # def method123
26
+ # foo
27
+ # end
28
+ #
29
+ # def foo
19
30
  # 123
20
31
  # end
21
32
  #
22
33
  # Example (bad):
23
- # def bar
34
+ # def foo
24
35
  # 123
25
36
  # end
26
37
  #
27
- # def foo
28
- # bar
38
+ # def call
39
+ # foo
29
40
  # end
30
41
  #
31
42
  # Autocorrect (unsafe, opt-in via SafeAutoCorrect: false): topologically sorts the contiguous
@@ -98,35 +109,74 @@ module RuboCop
98
109
  def_nodes = body_nodes.select { |n| %i[def defs].include?(n.type) }
99
110
  return if def_nodes.size <= 1
100
111
 
101
- names = def_nodes.map(&:method_name)
112
+ names = def_nodes.map(&:method_name)
102
113
  names_set = names.to_set
103
- 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)
104
126
 
105
- edges = []
127
+ # -----------------------------------------------------------
128
+ # Phase 2 Add sibling‑order edges for orchestration methods
129
+ # -----------------------------------------------------------
130
+ sibling_edges = []
106
131
  def_nodes.each do |def_node|
107
- local_calls(def_node, names_set).each do |callee|
108
- 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) }
109
137
 
110
- edges << [def_node.method_name, callee]
138
+ sibling_edges << [a, b]
111
139
  end
112
140
  end
113
141
 
142
+ # -----------------------------------------------------------
143
+ # Phase 3 Combine for sorting, but keep direct set for recursion
144
+ # -----------------------------------------------------------
145
+ edges_for_sort = direct_edges + sibling_edges
114
146
  allow_recursion = cop_config.fetch('AllowedRecursion') { true }
115
- adj = build_adj(names, edges)
116
147
 
117
- 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
+
118
160
  return unless violation
119
161
 
120
162
  caller_name, callee_name = violation
121
163
  callee_node = def_nodes[index_of[callee_name]]
122
164
 
123
- add_offense(callee_node,
124
- message: format(MSG, callee: "##{callee_name}", caller: "##{caller_name}")) do |corrector|
125
- 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)
126
174
  end
127
175
 
128
176
  # Recurse into nested scopes
129
- 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
130
180
  end
131
181
 
132
182
  # +RuboCop::Cop::SortedMethodsByCall::Waterfall#scope_body_nodes+ -> Array<RuboCop::AST::Node>
@@ -172,20 +222,6 @@ module RuboCop
172
222
  res.uniq
173
223
  end
174
224
 
175
- # +RuboCop::Cop::SortedMethodsByCall::Waterfall#find_violation+ -> [Symbol, Symbol], nil
176
- #
177
- # Finds the first backward edge (caller->callee where callee is defined above caller)
178
- # using the provided index map.
179
- #
180
- # @param [Array<Array(Symbol, Symbol)>] edges
181
- # @param [Hash{Symbol=>Integer}] index_of
182
- # @return [[Symbol, Symbol], nil] tuple [caller, callee] or nil if none found
183
- def find_violation(edges, index_of)
184
- edges.find do |caller, callee|
185
- index_of.key?(caller) && index_of.key?(callee) && index_of[callee] < index_of[caller]
186
- end
187
- end
188
-
189
225
  # +RuboCop::Cop::SortedMethodsByCall::Waterfall#try_autocorrect+ -> void
190
226
  #
191
227
  # UNSAFE: Reorders method definitions inside the target visibility section only
@@ -196,75 +232,64 @@ module RuboCop
196
232
  # @param [Array<RuboCop::AST::Node>] body_nodes
197
233
  # @param [Array<RuboCop::AST::Node>] def_nodes
198
234
  # @param [Array<Array(Symbol, Symbol)>] edges
235
+ # @param [Array(Symbol, Symbol)] violation The found violation (caller, callee)
199
236
  # @return [void]
200
237
  #
201
238
  # @note Applied only when user asked for autocorrections; with SafeAutoCorrect: false, this runs under -A.
202
239
  # @note Also preserves contiguous leading doc comments above each method.
203
- def try_autocorrect(corrector, body_nodes, def_nodes, edges)
204
- # Group method definitions into visibility sections
240
+ def try_autocorrect(corrector, body_nodes, _def_nodes, edges, violation)
205
241
  sections = extract_visibility_sections(body_nodes)
206
242
 
207
- # Find the section that contains our violating methods
208
- caller_name, callee_name = first_backward_edge(
209
- edges,
210
- def_nodes.map(&:method_name).each_with_index.to_h,
211
- build_adj(def_nodes.map(&:method_name), edges),
212
- cop_config.fetch('AllowedRecursion') { true }
213
- )
214
-
215
- # No violation -> nothing to do
216
- return unless caller_name && callee_name
243
+ caller_name, callee_name = violation
217
244
 
218
- # Find a visibility section that contains both names
245
+ # find target section containing both defs
219
246
  target_section = sections.find do |section|
220
- names_in_section = section[:defs].to_set(&:method_name)
221
- 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)
222
249
  end
223
-
224
- # If violation spans multiple sections, skip autocorrect
225
250
  return unless target_section
226
251
 
227
252
  defs = target_section[:defs]
228
- return unless defs.size > 1
229
-
230
- # Apply topological sort only within this visibility section
231
- defs = target_section[:defs]
232
- names = defs.map(&:method_name)
233
- idx_of = names.each_with_index.to_h
234
-
235
- # Filter edges to only those within this section
236
- # Filter edges to only those within this section
237
- section_names = names.to_set
238
- section_edges = edges.select { |u, v| section_names.include?(u) && section_names.include?(v) }
239
-
240
- sorted_names = topo_sort(names, section_edges, idx_of)
241
- 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
242
270
 
243
- # Capture each def with its leading contiguous comment block
244
- ranges_by_name = defs.to_h { |d| [d.method_name, range_with_leading_comments(d)] }
245
- 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 }
246
276
 
247
- # Reconstruct the section: keep the visibility modifier (if any) above the first def
248
277
  visibility_node = target_section[:visibility]
249
278
  visibility_source = visibility_node&.source.to_s
250
279
 
251
- new_content = if visibility_source.empty?
252
- sorted_def_sources.join("\n\n")
253
- else
254
- "#{visibility_source}\n\n#{sorted_def_sources.join("\n\n")}"
255
- 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
256
286
 
257
- # Expand the replaced region:
258
- # - if a visibility node exists, start from its begin_pos (so we replace it)
259
- # - otherwise, start from the earliest leading doc-comment of the defs
260
287
  section_begin =
261
288
  if visibility_node
262
289
  visibility_node.source_range.begin_pos
263
290
  else
264
291
  defs.map { |d| range_with_leading_comments(d).begin_pos }.min
265
292
  end
266
-
267
- # Always end at the end of the last def
268
293
  section_end = defs.last.source_range.end_pos
269
294
 
270
295
  region = Parser::Source::Range.new(processed_source.buffer, section_begin, section_end)
@@ -306,6 +331,7 @@ module RuboCop
306
331
  # If mutual recursion allowed and there is a path callee -> caller, skip
307
332
  next if allow_recursion && path_exists?(callee, caller, adj)
308
333
 
334
+ # Violation: callee is defined BEFORE caller (waterfall order)
309
335
  index_of[callee] < index_of[caller]
310
336
  end
311
337
  end
@@ -363,9 +389,10 @@ module RuboCop
363
389
  current_defs << node
364
390
  section_start ||= node.source_range.begin_pos
365
391
  when :send
366
- # Check if this is a visibility modifier (private/protected/public)
367
- if node.receiver.nil? && %i[private protected public].include?(node.method_name) && node.arguments.empty?
368
- # 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?
369
396
  unless current_defs.empty?
370
397
  sections << {
371
398
  visibility: current_visibility,
@@ -378,7 +405,7 @@ module RuboCop
378
405
  end
379
406
  current_visibility = node
380
407
  else
381
- # Non-visibility send - breaks contiguity
408
+ # anything else breaks contiguous run
382
409
  unless current_defs.empty?
383
410
  sections << {
384
411
  visibility: current_visibility,
@@ -388,11 +415,9 @@ module RuboCop
388
415
  }
389
416
  current_defs = []
390
417
  section_start = nil
391
- current_visibility = nil
392
418
  end
393
419
  end
394
420
  else
395
- # Any other node type breaks contiguity
396
421
  unless current_defs.empty?
397
422
  sections << {
398
423
  visibility: current_visibility,
@@ -402,12 +427,11 @@ module RuboCop
402
427
  }
403
428
  current_defs = []
404
429
  section_start = nil
405
- current_visibility = nil
406
430
  end
407
431
  end
408
432
  end
409
433
 
410
- # Handle trailing defs
434
+ # trailing defs
411
435
  unless current_defs.empty?
412
436
  sections << {
413
437
  visibility: current_visibility,
@@ -417,7 +441,20 @@ module RuboCop
417
441
  }
418
442
  end
419
443
 
420
- 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
421
458
  end
422
459
 
423
460
  # +RuboCop::Cop::SortedMethodsByCall::Waterfall#topo_sort+ -> Array<Symbol>, nil
@@ -470,7 +507,7 @@ module RuboCop
470
507
  # @return [Parser::Source::Range] Range covering leading comments + method body.
471
508
  def range_with_leading_comments(node)
472
509
  buffer = processed_source.buffer
473
- expr = node.source_range
510
+ expr = node.source_range
474
511
 
475
512
  start_line = expr.line
476
513
  lineno = start_line - 1
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module SortedMethodsByCall
5
- VERSION = '1.0.1'
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.0.1
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - unurgunite