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