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.
@@ -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