-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmethod_order.rb
257 lines (211 loc) · 8.15 KB
/
method_order.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# frozen_string_literal: true
module RuboCop
module Cop
module Obsession
# This cop checks for private/protected methods that are not ordered
# correctly. It supports autocorrect.
#
# Code should read from top to bottom: methods should be defined in the
# same order as the order when they are first mentioned.
# Private/protected methods should follow that rule.
#
# Note 1: public methods do not have to follow that rule, and can be
# defined in any order the developer wants, like by order of importance.
# This is because they are usually called outside of the class and often
# not called within the class at all. If possible though, developers
# should still try to order their public methods from top to bottom when
# it makes sense.
#
# Note 2: method order cannot be computed for methods called by `send`,
# metaprogramming, private methods called by superclasses or modules,
# etc. This cop's suggestions and autocorrections may be slightly off for
# these kinds of edge cases.
#
# Note 3: for simplicity, protected methods do not have to follow that
# rule if there are both a protected section and a private section.
#
# Note 4: for more information on this style of method ordering, see
# Robert C. Martin's "Clean Code" book > "Chapter 3: Functions" > "One
# level of abstraction per function" > "Reading Code from Top to Bottom:
# The Stepdown Rule" chapter.
#
# @example
#
# # bad
# def perform
# return if method_a?
# method_b
# method_c
# end
#
# private
#
# def method_c; ...; end
# def method_b; ...; end
# def method_a?; ...; end
#
# # good
# def perform
# return if method_a?
# method_b
# method_c
# end
#
# private
#
# def method_a?; ...; end
# def method_b; ...; end
# def method_c; ...; end
class MethodOrder < Base
include Helpers
include CommentsHelp
include VisibilityHelp
extend AutoCorrector
MSG = 'Method `%<after>s` should appear below `%<previous>s`.'
def_node_search :private_nodes, <<~PATTERN
(send nil? {:private :protected})
PATTERN
def_node_matcher :on_callback, <<~PATTERN
(send nil? $_ (sym $_) ...)
PATTERN
def_node_search :method_calls, <<~PATTERN
(send nil? $_ ...)
PATTERN
class Node
attr_accessor :value, :children
def initialize(value:, children: [])
@value = value
@children = children
end
end
def on_class(class_node)
@class_node = class_node
find_private_node || return
build_methods || return
build_callback_methods
build_method_call_tree
build_ordered_private_methods
build_private_methods
verify_private_methods_order
end
private
def find_private_node
private_nodes = private_nodes(@class_node).to_a
return nil if private_nodes.empty?
visibilities = private_nodes.map(&:method_name)
@ignore_protected = visibilities.include?(:protected) && visibilities.include?(:private)
@private_node = private_nodes.find { |node| !ignore_visibility?(node.method_name) }
end
def ignore_visibility?(visibility)
case visibility
when :public
true
when :protected
@ignore_protected
when :private
false
end
end
def build_methods
@methods = {}
return false if @class_node&.body&.type != :begin
@class_node.body.children.each do |child|
@methods[child.method_name] = child if child.type == :def
end
@methods.any?
end
def build_callback_methods
@callback_methods = []
@class_node.body.children.each do |node|
on_callback(node) do |callback, method_name|
if rails_callback?(callback.to_s) && @methods[method_name]
@callback_methods << @methods[method_name]
end
end
end
end
def build_method_call_tree
methods = (@callback_methods + @methods.values).uniq
@method_call_tree =
Node.new(value: nil, children: methods.map { |method| method_call_tree(method) })
end
def method_call_tree(method_node, seen_method_calls = Set.new)
method_name = method_node.method_name
return nil if seen_method_calls.include?(method_name)
called_methods = find_called_methods(method_node)
return Node.new(value: method_node, children: []) if called_methods.empty?
children =
called_methods.filter_map do |called_method|
method_call_tree(called_method, seen_method_calls + [method_name])
end
Node.new(value: method_node, children: children)
end
def find_called_methods(method_node)
called_methods =
method_calls(method_node).filter_map { |method_call| @methods[method_call] }
@called_methods ||= Set.new(@callback_methods)
@called_methods += called_methods
called_methods
end
def build_ordered_private_methods
@ordered_private_methods = ordered_private_methods(@method_call_tree)
end
def ordered_private_methods(node)
ast_node = node.value
method_name = should_ignore?(ast_node) ? nil : ast_node.method_name
next_names = node.children.flat_map { |child| ordered_private_methods(child) }
([method_name] + next_names).compact.uniq
end
def should_ignore?(ast_node)
ast_node.nil? || ignore_visibility?(node_visibility(ast_node)) ||
!@called_methods.include?(ast_node)
end
def build_private_methods
@private_methods = @methods.keys.intersection(@ordered_private_methods)
end
def verify_private_methods_order
@ordered_private_methods.each_with_index do |ordered_method, ordered_index|
index = @private_methods.index(ordered_method)
add_method_offense(ordered_method, ordered_index) && return if index != ordered_index
end
end
def add_method_offense(method_name, method_index)
method = @methods[method_name]
previous_method =
if method_index > 0
previous_method_name = @ordered_private_methods[method_index - 1]
@methods[previous_method_name]
else
@private_node
end
message = format(MSG, previous: previous_method.method_name, after: method_name)
add_offense(method, message: message) do |corrector|
autocorrect(corrector, method, previous_method)
end
end
def autocorrect(corrector, method, previous_method)
previous_method_range = source_range_with_comment(previous_method)
if buffer.source[previous_method_range.end_pos + 1] == "\n"
previous_method_range = previous_method_range.adjust(end_pos: 1)
end
method_range = source_range_with_signature(method)
if buffer.source[method_range.begin_pos - 1] == "\n"
method_range = method_range.adjust(end_pos: 1)
end
corrector.insert_after(previous_method_range, method_range.source)
corrector.remove(method_range)
end
def source_range_with_signature(method)
previous_node = method.left_sibling
begin_node = sorbet_signature?(previous_node) ? previous_node : method
begin_pos = begin_pos_with_comment(begin_node)
end_pos = end_position_for(method)
Parser::Source::Range.new(buffer, begin_pos, end_pos)
end
def sorbet_signature?(node)
node && node.method_name == :sig && node.type == :block
end
end
end
end
end