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