rubocop-dynamic_methods 0.1.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 +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +21 -0
- data/README.ja.md +147 -0
- data/README.md +124 -0
- data/config/default.yml +24 -0
- data/lib/rubocop/cop/dynamic_methods/define_method.rb +86 -0
- data/lib/rubocop/cop/dynamic_methods/delegation.rb +99 -0
- data/lib/rubocop/cop/dynamic_methods/method_missing.rb +29 -0
- data/lib/rubocop/cop/dynamic_methods/reporting.rb +57 -0
- data/lib/rubocop/cop/dynamic_methods/string_eval.rb +110 -0
- data/lib/rubocop/dynamic_methods/plugin.rb +32 -0
- data/lib/rubocop/dynamic_methods/rewrite.rb +822 -0
- data/lib/rubocop/dynamic_methods/version.rb +7 -0
- data/lib/rubocop/dynamic_methods.rb +13 -0
- data/lib/rubocop-dynamic_methods.rb +4 -0
- metadata +92 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module DynamicMethods
|
|
5
|
+
# Writes one defining statement, or the loop it sits in, as plain defs.
|
|
6
|
+
# A rewrite is exact or it raises Refusal with the reason: a def that only
|
|
7
|
+
# looks the same would teach a reader the wrong method. The rewrite only
|
|
8
|
+
# moves code inside one class or module body, so each def keeps the class,
|
|
9
|
+
# the constant scope, and the default visibility the original had.
|
|
10
|
+
class Rewrite
|
|
11
|
+
Refusal = Class.new(StandardError)
|
|
12
|
+
|
|
13
|
+
# The texts that replace ranges of the file, as [range, text] pairs, and
|
|
14
|
+
# the names they define.
|
|
15
|
+
Plan = Data.define(:edits, :names) do
|
|
16
|
+
def range = edits.first.first
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# One literal that a loop variable holds during one run of the loop.
|
|
20
|
+
Value = Data.define(:object, :code)
|
|
21
|
+
|
|
22
|
+
SCOPES = %i[class module sclass].freeze
|
|
23
|
+
BLOCKS = %i[block numblock itblock].freeze
|
|
24
|
+
# Nodes that start their own `next` target or their own method body.
|
|
25
|
+
JUMP_TARGETS = %i[block numblock itblock while until while_post until_post for def defs].freeze
|
|
26
|
+
SPELLS = %i[define_method define_singleton_method].freeze
|
|
27
|
+
EVALS = %i[class_eval module_eval].freeze
|
|
28
|
+
# Calls that see the locals of a block but not of a def.
|
|
29
|
+
LOCAL_READERS = %i[binding local_variables block_given? iterator? eval].freeze
|
|
30
|
+
VISIBILITY = %i[private protected public module_function].freeze
|
|
31
|
+
# Blocks read as a loop over values; a block of any other method is not.
|
|
32
|
+
ITERATORS = %i[each each_pair each_with_index each_with_object each_key each_value map flat_map times].freeze
|
|
33
|
+
# Methods that take method names and read a String as its Symbol.
|
|
34
|
+
NAME_TAKERS = %i[alias_method attr_reader attr_writer attr_accessor private public protected module_function
|
|
35
|
+
private_class_method public_class_method ruby2_keywords undef_method remove_method].freeze
|
|
36
|
+
# Of those, the ones that answer the same for a String and its Symbol:
|
|
37
|
+
# attr_* answer Symbols, and alias_method the new name's Symbol.
|
|
38
|
+
SAME_RESULT = %i[alias_method attr_reader attr_writer attr_accessor].freeze
|
|
39
|
+
LITERALS = %i[array hash pair sym str int float nil true false].freeze
|
|
40
|
+
CORE_PARENTS = %i[Object StandardError RuntimeError ArgumentError TypeError].freeze
|
|
41
|
+
NOT_IN_BODY = "it is not called directly in a class or module body, or a block that opens one"
|
|
42
|
+
# A fixnum is the same object on every read; a larger Integer is not.
|
|
43
|
+
FIXNUM = (-(2**62))...(2**62)
|
|
44
|
+
|
|
45
|
+
def initialize(processed_source, ruby_version:, frozen_strings:)
|
|
46
|
+
@source = processed_source
|
|
47
|
+
@ruby_version = ruby_version
|
|
48
|
+
@frozen_strings = frozen_strings
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The statement that defines: a block node for define_method, a send
|
|
52
|
+
# node for class_eval.
|
|
53
|
+
def self.statement?(node)
|
|
54
|
+
if BLOCKS.include?(node.type)
|
|
55
|
+
SPELLS.include?(node.method_name) && implicit?(node.send_node)
|
|
56
|
+
else
|
|
57
|
+
node.send_type? && EVALS.include?(node.method_name) && implicit?(node)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.implicit?(send)
|
|
62
|
+
send.receiver.nil? || send.receiver.self_type?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def plan(statement)
|
|
66
|
+
container, body = lift(*enclosing(statement))
|
|
67
|
+
if scope?(container)
|
|
68
|
+
scope_body = scope_body(container)
|
|
69
|
+
outer = assigned_locals(scope_body, except: statement) | visible_locals(container)
|
|
70
|
+
run = {}
|
|
71
|
+
text, names = write(statement, run, outer, moved: 0, scope_body:)
|
|
72
|
+
Plan.new(edits: [[full_range(statement), text]], names:)
|
|
73
|
+
elsif BLOCKS.include?(container.type) && container.body.equal?(body) && ITERATORS.include?(container.method_name)
|
|
74
|
+
plan_loop(container)
|
|
75
|
+
elsif BLOCKS.include?(container.type)
|
|
76
|
+
raise Refusal, "it is inside `#{container.method_name} do`, and only the code that runs the block knows " \
|
|
77
|
+
"which module it defines on"
|
|
78
|
+
elsif %i[def defs].include?(container.type)
|
|
79
|
+
plan_macro(container)
|
|
80
|
+
else
|
|
81
|
+
raise Refusal, NOT_IN_BODY
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def enclosing(node)
|
|
88
|
+
parent = node.parent
|
|
89
|
+
raise Refusal, NOT_IN_BODY if parent.nil?
|
|
90
|
+
return [parent.parent, parent] if parent.begin_type? && parent.parent
|
|
91
|
+
|
|
92
|
+
[parent, node]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# A branch of an `if` runs in the frame around it, so a def there defines
|
|
96
|
+
# what the define_method did. The condition itself is not a branch.
|
|
97
|
+
def lift(container, body)
|
|
98
|
+
while container&.if_type? && !container.condition.equal?(body)
|
|
99
|
+
if container.each_node(:send).any? { _1.receiver.nil? && !_1.arguments? && VISIBILITY.include?(_1.method_name) }
|
|
100
|
+
raise Refusal, "the `if` around it calls a bare visibility method"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
container, body = enclosing(container)
|
|
104
|
+
end
|
|
105
|
+
[container, body]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def scope_body(scope)
|
|
109
|
+
scope.body
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# A class or module body, or a block that Ruby runs as one: a def and a
|
|
113
|
+
# define_method there define on the same module, with the same default
|
|
114
|
+
# visibility and the same constant lookup. `included do` and
|
|
115
|
+
# `class_methods do` are not here: what runs them is the library's.
|
|
116
|
+
def scope?(node)
|
|
117
|
+
return true if SCOPES.include?(node.type)
|
|
118
|
+
return false unless BLOCKS.include?(node.type)
|
|
119
|
+
|
|
120
|
+
send = node.send_node
|
|
121
|
+
case send.method_name
|
|
122
|
+
when :class_eval, :module_eval, :class_exec, :module_exec then true
|
|
123
|
+
when :new then core_constant?(send.receiver, %i[Class Module Struct])
|
|
124
|
+
when :define then core_constant?(send.receiver, %i[Data])
|
|
125
|
+
else false
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def core_constant?(node, names)
|
|
130
|
+
node&.const_type? && (node.namespace.nil? || node.namespace.cbase_type?) && names.include?(node.short_name)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Locals a block can read from the code around it, up to the nearest
|
|
134
|
+
# def, class, or module, which starts a scope of its own.
|
|
135
|
+
def visible_locals(scope)
|
|
136
|
+
return [] if SCOPES.include?(scope.type)
|
|
137
|
+
|
|
138
|
+
names = scope.arguments.each_descendant(:arg, :optarg, :restarg, :kwarg, :kwoptarg, :kwrestarg, :blockarg).map(&:name)
|
|
139
|
+
scope.each_ancestor do |ancestor|
|
|
140
|
+
if BLOCKS.include?(ancestor.type) && ancestor.respond_to?(:arguments)
|
|
141
|
+
names.concat(ancestor.arguments.each_descendant(:arg, :optarg, :restarg, :kwarg, :kwoptarg, :kwrestarg, :blockarg).map(&:name))
|
|
142
|
+
names.concat(assigned_locals(ancestor.body, except: scope))
|
|
143
|
+
elsif %i[def defs].include?(ancestor.type)
|
|
144
|
+
names.concat(ancestor.arguments.each_descendant(:arg, :optarg, :restarg, :kwarg, :kwoptarg, :kwrestarg, :blockarg).map(&:name))
|
|
145
|
+
names.concat(assigned_locals(ancestor.body, except: scope))
|
|
146
|
+
return names.uniq
|
|
147
|
+
elsif SCOPES.include?(ancestor.type)
|
|
148
|
+
return (names + assigned_locals(ancestor.body, except: scope)).uniq
|
|
149
|
+
elsif ancestor.parent.nil?
|
|
150
|
+
names.concat(assigned_locals(ancestor, except: scope))
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
names.uniq
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def plan_loop(loop)
|
|
157
|
+
scope, = lift(*enclosing(loop))
|
|
158
|
+
raise Refusal, "the loop is not called directly in a class or module body, or a block that opens one" unless scope?(scope)
|
|
159
|
+
|
|
160
|
+
scope_body = scope.body
|
|
161
|
+
runs = runs_of(loop, scope_body)
|
|
162
|
+
statements = loop.body.begin_type? ? loop.body.children : [loop.body]
|
|
163
|
+
|
|
164
|
+
outer = assigned_locals(scope_body, except: loop) | loop.arguments.children.map(&:name) | visible_locals(scope)
|
|
165
|
+
indent = " " * loop.loc.column
|
|
166
|
+
names = []
|
|
167
|
+
# [text, whether it defines] for each statement of each run.
|
|
168
|
+
pieces = runs.flat_map do |run|
|
|
169
|
+
statements.map do |statement|
|
|
170
|
+
if statement.first_line == loop.first_line && statement.multiline?
|
|
171
|
+
raise Refusal, "the loop starts a definition on its own line"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
moved = statement.loc.column - loop.loc.column
|
|
175
|
+
next [copy(statement, run, moved:, loop:), false] unless self.class.statement?(statement)
|
|
176
|
+
|
|
177
|
+
text, defined = write(statement, run, outer, moved:, scope_body:, loop:)
|
|
178
|
+
names.concat(defined)
|
|
179
|
+
[text, true]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
notes = loose_comments(loop, statements).map { "#{_1}\n#{indent}" }.join
|
|
183
|
+
Plan.new(edits: [[full_range(loop), notes + join_pieces(pieces, indent)]], names:)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# A class method that defines methods, written out at each call in the
|
|
187
|
+
# same class body that gives it literal arguments. Such a call runs the
|
|
188
|
+
# macro's own method: `def self.name` (or `def name` in `class << self`)
|
|
189
|
+
# sits on the class's singleton class, which comes first in the lookup
|
|
190
|
+
# unless a module is prepended to it. The macro stays for other callers.
|
|
191
|
+
def plan_macro(macro)
|
|
192
|
+
name = macro.method_name
|
|
193
|
+
reason = "it runs inside the method `#{name}`, and only its callers know which module and names it defines"
|
|
194
|
+
holder = macro.parent&.begin_type? ? macro.parent.parent : macro.parent
|
|
195
|
+
singleton = holder&.sclass_type? && holder.children.first.self_type?
|
|
196
|
+
scope = singleton ? enclosing(holder).first : holder
|
|
197
|
+
raise Refusal, reason unless (macro.defs_type? ? macro.receiver.self_type? : singleton) && %i[class module].include?(scope&.type)
|
|
198
|
+
|
|
199
|
+
scope_body = scope.body
|
|
200
|
+
calls = macro_calls(macro, scope_body)
|
|
201
|
+
raise Refusal, "#{reason}; no call in this class body gives it literal arguments" if calls.empty?
|
|
202
|
+
|
|
203
|
+
check_macro(macro, scope_body, singleton)
|
|
204
|
+
names = []
|
|
205
|
+
edits = calls.map do |call, run|
|
|
206
|
+
check_visibility(scope_body, call, "a method such as `#{name}`")
|
|
207
|
+
pieces = statements_of(macro.body).map do |statement|
|
|
208
|
+
moved = statement.loc.column - call.loc.column
|
|
209
|
+
raise Refusal, "the method `#{name}` is not indented under its callers" unless moved.positive?
|
|
210
|
+
next [copy(statement, run, moved:, loop: macro), false] unless self.class.statement?(statement)
|
|
211
|
+
|
|
212
|
+
text, defined = write(statement, run, run.keys, moved:, scope_body:, loop: macro, anchor: call)
|
|
213
|
+
names.concat(defined)
|
|
214
|
+
[text, true]
|
|
215
|
+
end
|
|
216
|
+
[full_range(call), join_pieces(pieces, " " * call.loc.column)]
|
|
217
|
+
end
|
|
218
|
+
Plan.new(edits:, names:)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# The calls of the macro after its def, each a statement of the class
|
|
222
|
+
# body with one literal for each parameter, as [call, run] pairs.
|
|
223
|
+
def macro_calls(macro, scope_body)
|
|
224
|
+
params = macro.arguments.children
|
|
225
|
+
raise Refusal, "the method `#{macro.method_name}` takes parameters other than plain names" unless params.all?(&:arg_type?)
|
|
226
|
+
|
|
227
|
+
statements_of(scope_body).filter_map do |call|
|
|
228
|
+
next unless call.send_type? && call.receiver.nil? && call.method_name == macro.method_name
|
|
229
|
+
next unless call.source_range.begin_pos > macro.source_range.end_pos && call.arguments.size == params.size
|
|
230
|
+
|
|
231
|
+
values = call.arguments.map { value_of(_1) }
|
|
232
|
+
[call, params.map(&:name).zip(values).to_h]
|
|
233
|
+
rescue Refusal
|
|
234
|
+
nil
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def check_macro(macro, scope_body, singleton)
|
|
239
|
+
defs = scope_body.each_node(:def, :defs).count do |node|
|
|
240
|
+
node.method_name == macro.method_name && (node.defs_type? || node.parent&.sclass_type? || node.parent&.parent&.sclass_type?)
|
|
241
|
+
end
|
|
242
|
+
raise Refusal, "the class defines `#{macro.method_name}` more than once" unless defs == 1
|
|
243
|
+
# Code in a def runs later, when it is called; the class body runs now.
|
|
244
|
+
if runs_in_body(scope_body).any? { _1.send_type? && (_1.method?(:prepend) || _1.method?(:singleton_class)) }
|
|
245
|
+
raise Refusal, "the class prepends a module or opens its singleton class, which could take the call"
|
|
246
|
+
end
|
|
247
|
+
# A block in `class << self` looks constants up there first; a def in
|
|
248
|
+
# the class body does not.
|
|
249
|
+
return unless singleton && macro.body&.each_node(:const)&.any? { _1.namespace.nil? }
|
|
250
|
+
|
|
251
|
+
raise Refusal, "the method `#{macro.method_name}` sits in `class << self` and names a constant, " \
|
|
252
|
+
"which a def in the class body would look up elsewhere"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# The nodes of a body that run with it, leaving out the bodies of defs.
|
|
256
|
+
def runs_in_body(node, found = [])
|
|
257
|
+
return found unless node.is_a?(RuboCop::AST::Node) && !%i[def defs].include?(node.type)
|
|
258
|
+
|
|
259
|
+
found << node
|
|
260
|
+
node.children.each { runs_in_body(_1, found) }
|
|
261
|
+
found
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# A blank line sets each def apart; calls such as `alias_method` that
|
|
265
|
+
# follow one another stay together. Pieces are [text, whether it defines].
|
|
266
|
+
def join_pieces(pieces, indent)
|
|
267
|
+
rest = pieces.each_cons(2).map { |(_, a), (b_text, b)| (a || b ? "\n\n" : "\n") + indent + b_text }
|
|
268
|
+
pieces.first.first + rest.join
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# A statement of the loop that defines nothing, such as `alias_method` or
|
|
272
|
+
# `private`, copied once per run with the loop variables written as
|
|
273
|
+
# their literals. It moves from the block into the class body, so
|
|
274
|
+
# anything that tells the two apart is refused.
|
|
275
|
+
def copy(statement, run, moved:, loop:)
|
|
276
|
+
edits = []
|
|
277
|
+
walk = lambda do |node|
|
|
278
|
+
next unless node.is_a?(RuboCop::AST::Node)
|
|
279
|
+
|
|
280
|
+
done = copy_edits(node, statement, run, loop, edits)
|
|
281
|
+
check_moved_node(node)
|
|
282
|
+
node.children.each { walk.call(_1) unless done.include?(_1) }
|
|
283
|
+
end
|
|
284
|
+
walk.call(statement)
|
|
285
|
+
range = full_range(statement)
|
|
286
|
+
reindent(slice(range.begin_pos, range.end_pos, edits), moved)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Adds the edits for one node, and returns the children it wrote whole.
|
|
290
|
+
def copy_edits(node, statement, run, loop, edits)
|
|
291
|
+
case node.type
|
|
292
|
+
when :lvar
|
|
293
|
+
edits << loop_read(node, run.fetch(node.children.first)) if run.key?(node.children.first)
|
|
294
|
+
when :lvasgn
|
|
295
|
+
raise Refusal, "#{where(loop)} assigns the local `#{node.children.first}`, which each run starts afresh"
|
|
296
|
+
when :arg, :optarg, :restarg, :kwarg, :kwoptarg, :kwrestarg, :blockarg, :shadowarg
|
|
297
|
+
raise Refusal, "a block in #{where(loop)} reuses the name `#{node.name}`" if run.key?(node.name)
|
|
298
|
+
when :def, :defs
|
|
299
|
+
raise Refusal, "#{where(loop)} holds a def, which cannot see its locals"
|
|
300
|
+
when :class, :module, :sclass
|
|
301
|
+
raise Refusal, "#{where(loop)} opens a class or module, which cannot see its locals"
|
|
302
|
+
when :casgn
|
|
303
|
+
raise Refusal, "#{where(loop)} assigns a constant"
|
|
304
|
+
when :next, :break, :redo, :return
|
|
305
|
+
raise Refusal, "#{where(loop)} uses `#{node.type}`" if own_level?(node, loop)
|
|
306
|
+
when :yield, :super, :zsuper
|
|
307
|
+
# A method has a block and a super method of its own; a class body has neither.
|
|
308
|
+
raise Refusal, "the method uses `#{node.type == :yield ? "yield" : "super"}`" if %i[def defs].include?(loop.type)
|
|
309
|
+
when :send
|
|
310
|
+
return copy_send(node, statement, run, edits, loop)
|
|
311
|
+
end
|
|
312
|
+
[]
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def copy_send(send, statement, run, edits, loop)
|
|
316
|
+
return [] unless self.class.implicit?(send)
|
|
317
|
+
|
|
318
|
+
name = send.method_name
|
|
319
|
+
raise Refusal, "#{where(loop)} calls `#{name}`, which sees its locals" if LOCAL_READERS.include?(name)
|
|
320
|
+
if VISIBILITY.include?(name) && !send.arguments?
|
|
321
|
+
raise Refusal, "#{where(loop)} calls a bare `#{name}`, which a class body scopes differently"
|
|
322
|
+
end
|
|
323
|
+
if name == :__method__ && %i[def defs].include?(loop.type)
|
|
324
|
+
raise Refusal, "#{where(loop)} reads `__method__`, which is nil in a class body"
|
|
325
|
+
end
|
|
326
|
+
if (SPELLS + EVALS).include?(name)
|
|
327
|
+
raise Refusal, "#{where(loop)} calls `#{name}` inside `#{statement.source.lines.first.strip}`"
|
|
328
|
+
end
|
|
329
|
+
return [] unless NAME_TAKERS.include?(name) && (SAME_RESULT.include?(name) || discarded?(send, statement))
|
|
330
|
+
|
|
331
|
+
# `alias_method "#{name}?", name` reads better as `alias_method :alpha?, :alpha`.
|
|
332
|
+
send.arguments.select do |argument|
|
|
333
|
+
symbol = folded_name(argument, run)
|
|
334
|
+
edits << [argument.source_range, symbol.inspect] if symbol
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def where(loop)
|
|
339
|
+
%i[def defs].include?(loop.type) ? "the method `#{loop.method_name}`" : "the loop"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# A call whose value nothing reads: the statement, or a branch of an
|
|
343
|
+
# `if` that is the statement.
|
|
344
|
+
def discarded?(send, statement)
|
|
345
|
+
return true if send.equal?(statement)
|
|
346
|
+
|
|
347
|
+
parent = send.parent
|
|
348
|
+
parent.equal?(statement) && parent.if_type? && !send.equal?(parent.condition)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# The Symbol a name argument comes to, when it is built from the loop
|
|
352
|
+
# variables alone; nil otherwise.
|
|
353
|
+
def folded_name(node, run)
|
|
354
|
+
case node.type
|
|
355
|
+
when :lvar
|
|
356
|
+
value = run[node.children.first]&.object
|
|
357
|
+
value.to_sym if value.is_a?(String) || value.is_a?(Symbol)
|
|
358
|
+
when :dsym, :dstr
|
|
359
|
+
return if node.respond_to?(:heredoc?) && node.heredoc?
|
|
360
|
+
return unless node.each_descendant(:lvar).any? { run.key?(_1.children.first) }
|
|
361
|
+
|
|
362
|
+
name_of(node, run).to_sym
|
|
363
|
+
end
|
|
364
|
+
rescue Refusal
|
|
365
|
+
nil
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Each run of the loop as variable name => Value.
|
|
369
|
+
def runs_of(loop, scope_body)
|
|
370
|
+
send = loop.send_node
|
|
371
|
+
raise Refusal, "the loop is `#{send.method_name}`, not `each`" unless %i[each each_pair].include?(send.method_name)
|
|
372
|
+
raise Refusal, "the loop passes arguments to `#{send.method_name}`" if send.arguments?
|
|
373
|
+
raise Refusal, "the loop uses numbered parameters" unless loop.block_type?
|
|
374
|
+
|
|
375
|
+
params = loop.arguments.children
|
|
376
|
+
unless params.all?(&:arg_type?) && !loop.arguments.source.to_s.match?(/,\s*\|\z/)
|
|
377
|
+
raise Refusal, "the loop takes parameters other than plain names"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
list = list_of(send.receiver, scope_body, loop)
|
|
381
|
+
raise Refusal, "the list is empty" if list.empty?
|
|
382
|
+
|
|
383
|
+
if list.first.is_a?(Array)
|
|
384
|
+
raise Refusal, "the loop over a hash does not take a key and a value" unless params.size == 2
|
|
385
|
+
|
|
386
|
+
list.map { |key, value| { params[0].name => key, params[1].name => value } }
|
|
387
|
+
else
|
|
388
|
+
raise Refusal, "the loop does not take one value" unless params.size == 1 && send.method_name == :each
|
|
389
|
+
|
|
390
|
+
list.map { { params[0].name => _1 } }
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# The literals a loop runs over: Values, or [Value, Value] pairs.
|
|
395
|
+
def list_of(node, scope_body, loop)
|
|
396
|
+
return hash_part(node, scope_body, loop) if node&.send_type? && %i[keys values].include?(node.method_name) && !node.arguments?
|
|
397
|
+
|
|
398
|
+
node = constant_value(node, scope_body, loop) if node&.const_type?
|
|
399
|
+
case node&.type
|
|
400
|
+
when :array
|
|
401
|
+
node.children.map { value_of(_1) }
|
|
402
|
+
when :hash
|
|
403
|
+
node.children.map do |pair|
|
|
404
|
+
raise Refusal, "the hash holds more than literal pairs" unless pair.pair_type?
|
|
405
|
+
|
|
406
|
+
[value_of(pair.key), value_of(pair.value)]
|
|
407
|
+
end
|
|
408
|
+
else
|
|
409
|
+
raise Refusal, "the loop runs over `#{node&.source || "nothing"}`, which is not a literal list"
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# `LIST.keys` or `LIST.values` of a literal Hash, as the Values of one side.
|
|
414
|
+
def hash_part(node, scope_body, loop)
|
|
415
|
+
pairs = list_of(node.receiver, scope_body, loop)
|
|
416
|
+
raise Refusal, "the loop runs over `#{node.source}`, which is not the keys or values of a literal Hash" unless pairs.first.is_a?(Array)
|
|
417
|
+
|
|
418
|
+
pairs.map { node.method?(:keys) ? _1.first : _1.last }
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# A constant set once in this body, before the loop, to a literal list.
|
|
422
|
+
# A bare constant could also come from an outer module or an ancestor,
|
|
423
|
+
# which another file can set or reopen, so only this body counts.
|
|
424
|
+
def constant_value(node, scope_body, loop)
|
|
425
|
+
unless node.namespace.nil?
|
|
426
|
+
raise Refusal, "the loop runs over `#{node.source}`, a constant of another module, which another file can change"
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
sets = statements_of(scope_body).select { _1.casgn_type? && _1.namespace.nil? && _1.name == node.short_name }
|
|
430
|
+
unless sets.size == 1 && sets.first.source_range.end_pos <= loop.source_range.begin_pos
|
|
431
|
+
raise Refusal, "the loop runs over `#{node.source}`, which this class body does not set once before the loop"
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
value = sets.first.expression
|
|
435
|
+
if value&.send_type? && value.method?(:freeze) && !value.arguments? && %i[array hash].include?(value.receiver&.type)
|
|
436
|
+
return value.receiver
|
|
437
|
+
end
|
|
438
|
+
raise Refusal, "`#{node.source}` is set to `#{value&.source}`, which is not a literal list" unless %i[array hash].include?(value&.type)
|
|
439
|
+
|
|
440
|
+
check_untouched(node.short_name, sets.first, loop, scope_body)
|
|
441
|
+
value
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# An unfrozen list stays as written only when no code between its
|
|
445
|
+
# assignment and the loop could change it: the statements there may
|
|
446
|
+
# only define, or call a method of NAME_TAKERS with literal names.
|
|
447
|
+
def check_untouched(name, set, loop, scope_body)
|
|
448
|
+
between = statements_of(scope_body).select do
|
|
449
|
+
_1.source_range.begin_pos >= set.source_range.end_pos && _1.source_range.end_pos <= loop.source_range.begin_pos
|
|
450
|
+
end
|
|
451
|
+
busy = between.find { !inert?(_1) || names_now?(_1, name) }
|
|
452
|
+
if busy
|
|
453
|
+
raise Refusal, "`#{name}` is not frozen, and `#{busy.source.lines.first.strip}` runs between its assignment and the loop"
|
|
454
|
+
end
|
|
455
|
+
return unless loop.body&.each_node(:const)&.any? { _1.short_name == name }
|
|
456
|
+
|
|
457
|
+
raise Refusal, "`#{name}` is not frozen, and the loop names it, so it could grow while the loop runs"
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Whether the code reads the constant as it runs; a def's body reads it
|
|
461
|
+
# only when the method is called, after the loop.
|
|
462
|
+
def names_now?(node, name)
|
|
463
|
+
return false if %i[def defs].include?(node.type)
|
|
464
|
+
return true if node.const_type? && node.short_name == name
|
|
465
|
+
|
|
466
|
+
node.each_child_node.any? { names_now?(_1, name) }
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Code that defines and runs nothing else, so it cannot reach a list.
|
|
470
|
+
# Reading a constant can run code too, through autoload, and a parent
|
|
471
|
+
# class can run code through `inherited`, so only core parents count.
|
|
472
|
+
def inert?(node)
|
|
473
|
+
case node.type
|
|
474
|
+
when :def, :defs, :str, :sym, :int, :nil, :true, :false then true
|
|
475
|
+
when :class, :module
|
|
476
|
+
node.identifier.namespace.nil? &&
|
|
477
|
+
(node.type == :module || node.parent_class.nil? || core_constant?(node.parent_class, CORE_PARENTS)) &&
|
|
478
|
+
statements_of(node.body).all? { inert?(_1) }
|
|
479
|
+
when :casgn
|
|
480
|
+
node.namespace.nil? && node.expression && literal_tree?(node.expression)
|
|
481
|
+
when :send
|
|
482
|
+
self.class.implicit?(node) && (NAME_TAKERS + %i[private_constant public_constant]).include?(node.method_name) &&
|
|
483
|
+
node.arguments.all? { %i[sym str].include?(_1.type) }
|
|
484
|
+
else false
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# A literal made of literals, frozen or not.
|
|
489
|
+
def literal_tree?(node)
|
|
490
|
+
node = node.receiver if node.send_type? && node.method?(:freeze) && !node.arguments? && node.receiver
|
|
491
|
+
node.each_node.all? { LITERALS.include?(_1.type) }
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def value_of(node)
|
|
495
|
+
case node.type
|
|
496
|
+
when :sym
|
|
497
|
+
Value.new(object: node.value, code: node.value.inspect)
|
|
498
|
+
when :str
|
|
499
|
+
Value.new(object: node.value, code: node.value.inspect)
|
|
500
|
+
when :int
|
|
501
|
+
raise Refusal, "`#{node.source}` is too large to be the same object on every read" unless FIXNUM.cover?(node.value)
|
|
502
|
+
|
|
503
|
+
Value.new(object: node.value, code: node.value.negative? ? "(#{node.value})" : node.value.to_s)
|
|
504
|
+
when :nil, :true, :false
|
|
505
|
+
Value.new(object: { nil: nil, true: true, false: false }.fetch(node.type), code: node.type.to_s)
|
|
506
|
+
else
|
|
507
|
+
raise Refusal, "the list holds `#{node.source}`, which is not a symbol, string, small integer, nil, true, or false"
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def statements_of(body)
|
|
512
|
+
return [] if body.nil?
|
|
513
|
+
|
|
514
|
+
body.begin_type? ? body.children : [body]
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# Locals the code around a statement assigns, which a block shares and a
|
|
518
|
+
# def does not.
|
|
519
|
+
def assigned_locals(body, except:)
|
|
520
|
+
names = []
|
|
521
|
+
walk = lambda do |node|
|
|
522
|
+
next unless node.is_a?(RuboCop::AST::Node)
|
|
523
|
+
next if node.equal?(except)
|
|
524
|
+
|
|
525
|
+
names << node.children.first if node.lvasgn_type?
|
|
526
|
+
next if BLOCKS.include?(node.type) || %i[def defs class module sclass].include?(node.type)
|
|
527
|
+
|
|
528
|
+
node.children.each(&walk)
|
|
529
|
+
end
|
|
530
|
+
walk.call(body)
|
|
531
|
+
names.uniq
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def write(statement, run, outer, moved:, scope_body:, loop: nil, anchor: loop || statement)
|
|
535
|
+
if BLOCKS.include?(statement.type)
|
|
536
|
+
write_block(statement, run, outer, moved:, loop:, column: anchor.loc.column)
|
|
537
|
+
else
|
|
538
|
+
write_string(statement, run, scope_body:, anchor:)
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def write_block(block, run, outer, moved:, loop:, column:)
|
|
543
|
+
send = block.send_node
|
|
544
|
+
raise Refusal, "the block uses numbered parameters" if block.numblock_type?
|
|
545
|
+
raise Refusal, "the block uses `it`" if block.type == :itblock
|
|
546
|
+
raise Refusal, "`#{send.method_name}` takes more than a name and a block" unless send.arguments.size == 1
|
|
547
|
+
|
|
548
|
+
name = name_of(send.first_argument, run)
|
|
549
|
+
prefix = send.method?(:define_singleton_method) ? "self." : ""
|
|
550
|
+
check_name(name, prefix)
|
|
551
|
+
params = block.arguments
|
|
552
|
+
raise Refusal, "the block declares block-local variables" if params.each_descendant(:shadowarg).any?
|
|
553
|
+
raise Refusal, "the block's parameters end with a comma" if params.source.to_s.match?(/,\s*\|\z/)
|
|
554
|
+
|
|
555
|
+
edits = block_edits(block, run, outer, moving: loop || block.single_line?)
|
|
556
|
+
header = "def #{prefix}#{name}"
|
|
557
|
+
unless params.children.empty?
|
|
558
|
+
inner = slice(params.children.first.source_range.begin_pos, params.children.last.source_range.end_pos, edits)
|
|
559
|
+
header += "(#{inner})"
|
|
560
|
+
end
|
|
561
|
+
opener = params.children.empty? ? block.loc.begin : params.loc.end
|
|
562
|
+
body = slice(opener.end_pos, block.loc.end.begin_pos, edits)
|
|
563
|
+
indent = " " * column
|
|
564
|
+
# A one-line block becomes a def of three lines, the shape a reader
|
|
565
|
+
# expects; the body moves to its own line.
|
|
566
|
+
return ["#{header}\n#{indent}#{body.strip.empty? ? "" : " #{body.strip}\n#{indent}"}end", ["#{prefix}#{name}"]] if block.single_line?
|
|
567
|
+
|
|
568
|
+
body = ";#{body}" unless body.match?(/\A[ \t]*(#|\n|\z)/)
|
|
569
|
+
[reindent("#{header}#{body}end", moved), ["#{prefix}#{name}"]]
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# Edits inside a block: loop variables become their literal, and a `next`
|
|
573
|
+
# that leaves the method becomes `return`.
|
|
574
|
+
def block_edits(block, run, outer, moving:)
|
|
575
|
+
edits = []
|
|
576
|
+
block.each_descendant do |node|
|
|
577
|
+
case node.type
|
|
578
|
+
when :lvar, :lvasgn
|
|
579
|
+
local = node.children.first
|
|
580
|
+
next unless outer.include?(local)
|
|
581
|
+
raise Refusal, "the block shares the local `#{local}` with the code around it" unless node.lvar_type? && run.key?(local)
|
|
582
|
+
|
|
583
|
+
edits << loop_read(node, run.fetch(local))
|
|
584
|
+
when :arg, :optarg, :restarg, :kwarg, :kwoptarg, :kwrestarg, :blockarg, :shadowarg
|
|
585
|
+
raise Refusal, "the block reuses the loop's name `#{node.name}`" if run.key?(node.name)
|
|
586
|
+
when :zsuper
|
|
587
|
+
raise Refusal, "the block calls `super` with no arguments, which raises in a block given to define_method"
|
|
588
|
+
when :yield
|
|
589
|
+
raise Refusal, "the block uses `yield`"
|
|
590
|
+
when :send
|
|
591
|
+
if node.receiver.nil? && LOCAL_READERS.include?(node.method_name)
|
|
592
|
+
raise Refusal, "the block calls `#{node.method_name}`, which sees the locals around the block"
|
|
593
|
+
end
|
|
594
|
+
when :casgn
|
|
595
|
+
raise Refusal, "the block assigns a constant, which a def cannot"
|
|
596
|
+
when :class, :module, :sclass
|
|
597
|
+
raise Refusal, "the block opens a class or module, which a def cannot"
|
|
598
|
+
when :def, :defs
|
|
599
|
+
raise Refusal, "the block holds a def"
|
|
600
|
+
when :next
|
|
601
|
+
edits << [node.loc.keyword, "return"] if own_level?(node, block)
|
|
602
|
+
when :break, :redo
|
|
603
|
+
raise Refusal, "the block uses `#{node.type}` outside a loop" if own_level?(node, block)
|
|
604
|
+
end
|
|
605
|
+
check_moved_node(node) if moving
|
|
606
|
+
end
|
|
607
|
+
edits
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# A read inside a symbol's `#{}` writes the text in place. A string keeps
|
|
611
|
+
# `#{}`, because a string with no interpolation left would be a literal,
|
|
612
|
+
# which `# frozen_string_literal: true` freezes. A string read elsewhere
|
|
613
|
+
# is one shared object, and only a frozen literal is the same object on
|
|
614
|
+
# every call.
|
|
615
|
+
def loop_read(node, value)
|
|
616
|
+
holder = node.parent
|
|
617
|
+
raise Refusal, "the block asks `defined?` of the loop variable" if holder&.defined_type?
|
|
618
|
+
if holder&.begin_type? && holder.children.size == 1 && holder.parent&.dsym_type? &&
|
|
619
|
+
holder.parent.source.start_with?(':"') && value.object.to_s.match?(/\A[\w?!= .-]*\z/)
|
|
620
|
+
return [holder.source_range, value.object.to_s]
|
|
621
|
+
end
|
|
622
|
+
return [node.source_range, value.code] if holder&.begin_type? && holder.parent&.dstr_type?
|
|
623
|
+
# `{ v: }` names the local and the key at once.
|
|
624
|
+
return [holder.source_range, "#{holder.key.value}: #{value.code}"] if holder&.pair_type? && holder.value_omission?
|
|
625
|
+
if value.object.is_a?(String) && !@frozen_strings
|
|
626
|
+
raise Refusal, "the block reads the string #{value.code}, which is one shared object only under `# frozen_string_literal: true`"
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
[node.source_range, value.code]
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def check_moved_node(node)
|
|
633
|
+
raise Refusal, "the block reads `__LINE__`, which the rewrite moves" if node.int_type? && node.source == "__LINE__"
|
|
634
|
+
if %i[str dstr xstr dsym].include?(node.type) && (node.respond_to?(:heredoc?) && node.heredoc? || node.multiline?)
|
|
635
|
+
raise Refusal, "the block holds a string that spans lines, which moving the code would change"
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def own_level?(node, block)
|
|
640
|
+
node.each_ancestor.each do |ancestor|
|
|
641
|
+
return true if ancestor.equal?(block)
|
|
642
|
+
return false if JUMP_TARGETS.include?(ancestor.type)
|
|
643
|
+
end
|
|
644
|
+
false
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def write_string(send, run, scope_body:, anchor:)
|
|
648
|
+
unless (1..3).cover?(send.arguments.size) && %i[str dstr].include?(send.first_argument.type) && !send.block_node
|
|
649
|
+
raise Refusal, "`#{send.method_name}` is not given a string literal"
|
|
650
|
+
end
|
|
651
|
+
check_visibility(scope_body, anchor)
|
|
652
|
+
code = string_value(send.first_argument, run)
|
|
653
|
+
if code.match?(/\b__(LINE|FILE|dir)__\b|\bModule\.nesting\b/)
|
|
654
|
+
raise Refusal, "the string reads `__LINE__`, `__FILE__`, `__dir__`, or `Module.nesting`, which name the eval"
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
names = defs_in(code)
|
|
658
|
+
lines = code.chomp.lines(chomp: true)
|
|
659
|
+
margin = lines.reject { _1.strip.empty? }.map { _1[/\A */].size }.min || 0
|
|
660
|
+
indent = " " * anchor.loc.column
|
|
661
|
+
text = lines.map.with_index do |line, index|
|
|
662
|
+
stripped = line.strip.empty? ? "" : line[margin..]
|
|
663
|
+
index.zero? || stripped.empty? ? stripped : indent + stripped
|
|
664
|
+
end.join("\n")
|
|
665
|
+
[text, names]
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# A def a string eval writes is public even after a bare `private`.
|
|
669
|
+
def check_visibility(scope_body, anchor, maker = "a string eval")
|
|
670
|
+
last = statements_of(scope_body)
|
|
671
|
+
.take_while { _1.source_range.begin_pos < anchor.source_range.begin_pos }
|
|
672
|
+
.select { _1.send_type? && _1.receiver.nil? && !_1.arguments? && VISIBILITY.include?(_1.method_name) }
|
|
673
|
+
.last
|
|
674
|
+
return if last.nil? || last.method?(:public)
|
|
675
|
+
|
|
676
|
+
raise Refusal, "it follows a bare `#{last.method_name}`, and a def from #{maker} stays public"
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def string_value(node, run)
|
|
680
|
+
case node.type
|
|
681
|
+
when :str
|
|
682
|
+
node.value
|
|
683
|
+
when :dstr
|
|
684
|
+
node.children.map { string_value(_1, run) }.join
|
|
685
|
+
when :begin
|
|
686
|
+
inner = node.children
|
|
687
|
+
if inner.size == 1 && inner.first.lvar_type? && run.key?(inner.first.children.first)
|
|
688
|
+
run.fetch(inner.first.children.first).object.to_s
|
|
689
|
+
elsif inner.size == 1 && %i[sym str int].include?(inner.first.type)
|
|
690
|
+
inner.first.value.to_s
|
|
691
|
+
else
|
|
692
|
+
raise Refusal, "the string interpolates `#{node.source}`, which is not a loop variable over literals"
|
|
693
|
+
end
|
|
694
|
+
else
|
|
695
|
+
raise Refusal, "the string holds `#{node.source}`"
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def defs_in(code)
|
|
700
|
+
parsed = RuboCop::ProcessedSource.new(code, @ruby_version, "(eval)")
|
|
701
|
+
raise Refusal, "the string is not valid Ruby on its own" unless parsed.valid_syntax? && parsed.ast
|
|
702
|
+
|
|
703
|
+
top = parsed.ast.begin_type? ? parsed.ast.children : [parsed.ast]
|
|
704
|
+
raise Refusal, "the string does more than define methods" unless top.all? { %i[def defs].include?(_1.type) }
|
|
705
|
+
|
|
706
|
+
check_string_literals(parsed)
|
|
707
|
+
top.map { _1.defs_type? ? "self.#{_1.method_name}" : _1.method_name.to_s }
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# The file's `# frozen_string_literal:` comment does not reach code a
|
|
711
|
+
# string eval compiles, so a string literal there is frozen or not by
|
|
712
|
+
# its own comment. Moved into the file, it would take the file's.
|
|
713
|
+
def check_string_literals(parsed)
|
|
714
|
+
return if self.class.frozen_setting(parsed) == self.class.frozen_setting(@source)
|
|
715
|
+
|
|
716
|
+
literal = parsed.ast.each_node(:str, :dstr).find do |node|
|
|
717
|
+
next false if %i[dstr dsym xstr regexp].include?(node.parent&.type)
|
|
718
|
+
|
|
719
|
+
node.str_type? || node.children.all?(&:str_type?)
|
|
720
|
+
end
|
|
721
|
+
return unless literal
|
|
722
|
+
|
|
723
|
+
raise Refusal, "the string's code holds the string literal #{literal.source}, " \
|
|
724
|
+
"which `# frozen_string_literal:` would treat differently in this file"
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# true, false, or nil when the source has no magic comment.
|
|
728
|
+
def self.frozen_setting(processed_source)
|
|
729
|
+
first_code = processed_source.tokens.find { !_1.comment? }&.line || Float::INFINITY
|
|
730
|
+
processed_source.comments.take_while { _1.loc.line < first_code }.each do |comment|
|
|
731
|
+
magic = RuboCop::MagicComment.parse(comment.text)
|
|
732
|
+
return magic.frozen_string_literal if magic.frozen_string_literal_specified?
|
|
733
|
+
end
|
|
734
|
+
nil
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def name_of(node, run)
|
|
738
|
+
case node&.type
|
|
739
|
+
when :sym, :str
|
|
740
|
+
node.value.to_s
|
|
741
|
+
when :dsym, :dstr
|
|
742
|
+
node.children.map do |part|
|
|
743
|
+
if part.str_type?
|
|
744
|
+
part.value
|
|
745
|
+
elsif part.begin_type? && part.children.size == 1 && part.children.first.lvar_type? && run.key?(part.children.first.children.first)
|
|
746
|
+
run.fetch(part.children.first.children.first).object.to_s
|
|
747
|
+
else
|
|
748
|
+
raise Refusal, "its name interpolates `#{part.source}`, which is not a loop variable over literals"
|
|
749
|
+
end
|
|
750
|
+
end.join
|
|
751
|
+
when :lvar
|
|
752
|
+
local = node.children.first
|
|
753
|
+
raise Refusal, "its name comes from `#{node.source}`" unless run.key?(local)
|
|
754
|
+
|
|
755
|
+
run.fetch(local).object.to_s
|
|
756
|
+
else
|
|
757
|
+
raise Refusal, "its name comes from `#{node&.source}`"
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def check_name(name, prefix)
|
|
762
|
+
parsed = RuboCop::ProcessedSource.new("def #{prefix}#{name}; end", @ruby_version, "(name)")
|
|
763
|
+
ast = parsed.ast
|
|
764
|
+
return if parsed.valid_syntax? && ast && %i[def defs].include?(ast.type) && ast.method_name.to_s == name
|
|
765
|
+
|
|
766
|
+
raise Refusal, "`#{name}` cannot be written after `def`"
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
def slice(from, to, edits)
|
|
770
|
+
text = +""
|
|
771
|
+
position = from
|
|
772
|
+
edits.select { _1[0].begin_pos >= from && _1[0].end_pos <= to }.sort_by { _1[0].begin_pos }.each do |range, replacement|
|
|
773
|
+
text << buffer_source[position...range.begin_pos] << replacement
|
|
774
|
+
position = range.end_pos
|
|
775
|
+
end
|
|
776
|
+
text << buffer_source[position...to]
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def reindent(text, moved)
|
|
780
|
+
return text if moved.zero?
|
|
781
|
+
|
|
782
|
+
first, *rest = text.split("\n", -1)
|
|
783
|
+
rest = rest.map do |line|
|
|
784
|
+
next line if line.strip.empty?
|
|
785
|
+
raise Refusal, "the definition is not indented under the loop" unless line.start_with?(" " * moved)
|
|
786
|
+
|
|
787
|
+
line[moved..]
|
|
788
|
+
end
|
|
789
|
+
[first, *rest].join("\n")
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
# Comments in a loop that no definition holds, kept above the defs.
|
|
793
|
+
def loose_comments(loop, statements)
|
|
794
|
+
loop_range = full_range(loop)
|
|
795
|
+
@source.comments.select do |comment|
|
|
796
|
+
position = comment.source_range.begin_pos
|
|
797
|
+
position.between?(loop_range.begin_pos, loop_range.end_pos) &&
|
|
798
|
+
statements.none? { full_range(_1).begin_pos <= position && position < full_range(_1).end_pos }
|
|
799
|
+
end.map(&:text)
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# A node's range, widened to the end of any heredoc it starts.
|
|
803
|
+
def full_range(node)
|
|
804
|
+
finish = node.source_range.end_pos
|
|
805
|
+
node.each_node(:str, :dstr, :xstr) do |string|
|
|
806
|
+
next unless string.respond_to?(:heredoc?) && string.heredoc?
|
|
807
|
+
|
|
808
|
+
finish = [finish, string.loc.heredoc_end.end_pos].max
|
|
809
|
+
end
|
|
810
|
+
if finish > node.source_range.end_pos
|
|
811
|
+
gap = buffer_source[node.source_range.end_pos...node.each_node(:str, :dstr, :xstr).select { _1.respond_to?(:heredoc?) && _1.heredoc? }.map { _1.loc.heredoc_body.begin_pos }.min]
|
|
812
|
+
raise Refusal, "code follows the heredoc's opening line" unless gap.match?(/\A[ \t]*\n\z/)
|
|
813
|
+
end
|
|
814
|
+
node.source_range.with(end_pos: finish)
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
def buffer_source
|
|
818
|
+
@source.buffer.source
|
|
819
|
+
end
|
|
820
|
+
end
|
|
821
|
+
end
|
|
822
|
+
end
|