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 +4 -4
- data/.rubocop_todo.yml +22 -12
- data/Gemfile.lock +1 -1
- data/README.md +47 -0
- data/config/default.yml +3 -2
- data/lib/rubocop/cop/sorted_methods_by_call/waterfall.rb +493 -269
- data/lib/rubocop/sorted_methods_by_call/compare.rb +1 -1
- data/lib/rubocop/sorted_methods_by_call/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02e794a25b73c4b96c7ec77721e5b5e4c71a3a23e0137011f3d272d27e81121e
|
|
4
|
+
data.tar.gz: 75db5c1df55a2d40a9fdf596b654bafa76eecbb163e16c419b7c0366ef4cd772
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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:
|
|
22
|
+
# Offense count: 5
|
|
23
23
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
|
24
24
|
Metrics/AbcSize:
|
|
25
|
-
Max:
|
|
25
|
+
Max: 78
|
|
26
26
|
|
|
27
|
-
# Offense count:
|
|
27
|
+
# Offense count: 1
|
|
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
|
-
# Offense count:
|
|
33
|
+
# Offense count: 4
|
|
34
34
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
35
35
|
Metrics/CyclomaticComplexity:
|
|
36
|
-
Max:
|
|
36
|
+
Max: 35
|
|
37
37
|
|
|
38
|
-
# Offense count:
|
|
38
|
+
# Offense count: 9
|
|
39
39
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
40
40
|
Metrics/MethodLength:
|
|
41
|
-
Max:
|
|
41
|
+
Max: 70
|
|
42
42
|
|
|
43
|
-
# Offense count:
|
|
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:
|
|
51
|
+
Max: 37
|
|
47
52
|
|
|
48
|
-
# Offense count:
|
|
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
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:
|
|
4
|
-
VersionAdded: "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
|
-
#
|
|
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 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
|
-
#
|
|
28
|
+
# @example Good (waterfall order)
|
|
29
|
+
# class Service
|
|
30
|
+
# def call
|
|
31
|
+
# foo
|
|
32
|
+
# bar
|
|
33
|
+
# end
|
|
20
34
|
#
|
|
21
|
-
#
|
|
22
|
-
# method123
|
|
23
|
-
# end
|
|
35
|
+
# private
|
|
24
36
|
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
37
|
+
# def bar
|
|
38
|
+
# method123
|
|
39
|
+
# end
|
|
28
40
|
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
41
|
+
# def method123
|
|
42
|
+
# foo
|
|
43
|
+
# end
|
|
32
44
|
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
45
|
+
# def foo
|
|
46
|
+
# 123
|
|
47
|
+
# end
|
|
36
48
|
# end
|
|
37
49
|
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
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
|
-
#
|
|
43
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
56
|
-
#
|
|
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
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
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]
|
|
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
|
|
145
|
+
def_nodes = method_def_nodes(body_nodes)
|
|
110
146
|
return if def_nodes.size <= 1
|
|
111
147
|
|
|
112
|
-
names
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
+
violation = first_backward_edge(sibling_edges, index_of, adj_direct, allow_recursion)
|
|
283
|
+
return [:sibling, violation] if violation
|
|
161
284
|
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
183
|
-
#
|
|
184
|
-
#
|
|
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 [
|
|
187
|
-
# @
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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 [
|
|
208
|
-
# @param [
|
|
209
|
-
# @
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
#
|
|
388
|
+
# UNSAFE autocorrect: reorder method definitions inside one contiguous visibility section only.
|
|
226
389
|
#
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
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]
|
|
232
|
-
# @param [Array<RuboCop::AST::Node>]
|
|
233
|
-
# @param [Array<RuboCop::AST::Node>]
|
|
234
|
-
# @param [Array<Array(Symbol, Symbol)>] edges
|
|
235
|
-
# @param [Array(Symbol, Symbol)
|
|
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
|
-
|
|
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
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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>]
|
|
304
|
-
# @param [Array<Array(Symbol, Symbol)>]
|
|
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
|
-
|
|
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
|
-
# +
|
|
520
|
+
# Breadth-first search to detect whether a path exists from +src+ to +dst+.
|
|
340
521
|
#
|
|
341
|
-
#
|
|
342
|
-
#
|
|
343
|
-
# @param [Symbol]
|
|
344
|
-
# @param [
|
|
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
|
-
|
|
535
|
+
|
|
536
|
+
while i < q.length
|
|
355
537
|
steps += 1
|
|
356
538
|
return false if steps > limit
|
|
357
539
|
|
|
358
|
-
u = q
|
|
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
|
-
#
|
|
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
|
-
#
|
|
372
|
-
#
|
|
373
|
-
#
|
|
374
|
-
#
|
|
375
|
-
#
|
|
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>]
|
|
379
|
-
# @return [Array<Hash
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
#
|
|
644
|
+
# Normalize a section to a string visibility label ("public", "private", "protected").
|
|
461
645
|
#
|
|
462
|
-
#
|
|
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>]
|
|
465
|
-
# @param [Array<Array(Symbol, Symbol)>]
|
|
466
|
-
# @param [Hash{Symbol=>Integer}]
|
|
467
|
-
# @return [Array<Symbol>, nil] sorted
|
|
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
|
-
#
|
|
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
|
-
#
|
|
503
|
-
#
|
|
504
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
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.
|
|
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:
|
|
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: []
|