mutineer 1.5.0 → 1.7.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.
@@ -28,6 +28,34 @@ module Mutineer
28
28
  # Walks an AST, maintaining a namespace stack, emitting Subjects.
29
29
  # Nested inside Project to signal its private role.
30
30
  class SubjectVisitor < Prism::Visitor
31
+ # Calls whose block is the body of the class they build: receiver name => method.
32
+ CLASS_BUILDERS = { "Data" => :define, "Struct" => :new, "Class" => :new, "Module" => :new }.freeze
33
+
34
+ # ActiveSupport::Concern calls whose block another module or class runs (see {#concern_block}).
35
+ CONCERN_BLOCKS = %i[class_methods included prepended].freeze
36
+
37
+ # A constant name, or a path of them; Ruby allows Unicode in both.
38
+ CONSTANT_PATH = /\A[[:upper:]][[:word:]]*(?:::[[:upper:]][[:word:]]*)*\z/
39
+
40
+ # A name that reads like a constant name but may not be one (#216). It is
41
+ # the singleton class name of a `class << self` opened inside a def or a
42
+ # block that does not build a class, or built on a written path or
43
+ # `#<anonymous>` (`#<Class:Foo::X>` from `class Foo::X` inside `class << self`);
44
+ # or the `X` of `module self::X` or `self::X = ...` in a def or a block. The segment carries that mark (see
45
+ # {#singleton_name}, {#with_namespace}, {#assigned_owner}), and `scope`, the
46
+ # def or block whose `self` the name is under, or a new object when unknown.
47
+ class WrittenName < String
48
+ # @return [Object] the def or block node whose `self` the name is under.
49
+ attr_reader :scope
50
+
51
+ # @param name [String] the name as built.
52
+ # @param scope [Object] the def or block node whose `self` the name is under.
53
+ def initialize(name, scope = Object.new)
54
+ super(name)
55
+ @scope = scope
56
+ end
57
+ end
58
+
31
59
  attr_reader :subjects
32
60
 
33
61
  # Builds a subject visitor.
@@ -40,22 +68,35 @@ module Mutineer
40
68
  @subjects = []
41
69
  @singleton_depth = 0
42
70
  @module_function_active = false # bareword `module_function` seen in this module body
43
- @module_function_names = [] # [namespace, name] from `module_function :a` / `module_function def` (#98)
71
+ @module_function_names = [] # [module key, name] from `module_function :a` / `module_function def` (#98)
72
+ @subject_keys = [] # the module key of each subject, by index
73
+ @body = Object.new # identifies the class/module body or builder block being visited
74
+ @block_owner = nil
75
+ @block_namespace = nil
76
+ @owner_unknown = false
77
+ @assigned = nil
78
+ @singleton_cref = nil # the singleton class name, in a builder block inside `class << self`
79
+ @namespace_unknown = false # the namespace is under a singleton class (#208) or a block's `self` (#229)
80
+ @anonymous_block = false # inside a builder block not assigned to a constant
81
+ @self_unknown = false # inside a def or a block that is not a builder, so `self` is not the namespace
82
+ @self_scope = nil # the def or block whose `self` the code runs with
83
+ @singleton_uncertain = false # the open `class << self` was inside a def or a non-builder block
84
+ @concern_modules = Set.new # the {#module_key} of each module that ran `extend ActiveSupport::Concern`
85
+ @hook_block = false # inside a concern's `included` or `prepended` block (see {#hook_block?})
44
86
  super()
45
87
  end
46
88
 
47
89
  # Promote `module_function :name` / `module_function def name` subjects to
48
90
  # singleton after the full walk — the naming call may appear before or after
49
91
  # the def, so it can't be decided at visit_def_node time (#20). Only methods
50
- # of the module that made the call are promoted (#98); namespaces compare
51
- # joined, since `module A::B` and nested `module A; module B` differ as arrays.
92
+ # of the module that made the call are promoted (#98), matched by {#module_key}.
52
93
  #
53
94
  # @return [void]
54
95
  def promote_module_functions!
55
96
  return if @module_function_names.empty?
56
97
 
57
98
  named = @module_function_names.to_set
58
- @subjects.each { |s| s.singleton = true if named.include?([s.namespace.join("::"), s.name]) }
99
+ @subjects.each_with_index { |s, i| s.singleton = true if named.include?([@subject_keys[i], s.name]) }
59
100
  end
60
101
 
61
102
  # Visits class nodes and tracks namespace nesting.
@@ -77,7 +118,15 @@ module Mutineer
77
118
  # Track `module_function` so its methods are recorded as singletons (#20) —
78
119
  # the called form is the singleton method on the module object. Bareword
79
120
  # `module_function` flips all SUBSEQUENT defs in this body; the argument
80
- # forms (`:sym`, `def`) name methods promoted after the walk.
121
+ # forms (`:sym`, `def`) name methods promoted after the walk. A builder
122
+ # block's defs belong to the class it builds; one not assigned to a
123
+ # constant has no name, so its subjects are marked `owner_unknown`, and a
124
+ # `module_function :name` in it promotes nothing. Elsewhere it promotes the
125
+ # methods of the same module, matched by {#module_key}. A module that runs
126
+ # `extend ActiveSupport::Concern` is a concern from then on in this file. A
127
+ # `class_methods` block in its body (see {#concern_block}) is visited like a
128
+ # builder block that builds the module's `ClassMethods`, so its defs belong to
129
+ # that module (#252). The defs in any other such block have their owner unknown.
81
130
  #
82
131
  # @param node [Prism::CallNode] call node.
83
132
  # @return [void]
@@ -89,16 +138,85 @@ module Mutineer
89
138
  args = node.arguments&.arguments || []
90
139
  if args.empty?
91
140
  @module_function_active = true
92
- else
93
- namespace = @namespace_stack.join("::")
141
+ elsif !@anonymous_block
94
142
  args.each do |arg|
95
- @module_function_names << [namespace, arg.value.to_sym] if arg.is_a?(Prism::SymbolNode)
96
- @module_function_names << [namespace, arg.name] if arg.is_a?(Prism::DefNode)
143
+ @module_function_names << [module_key, arg.value.to_sym] if arg.is_a?(Prism::SymbolNode)
144
+ @module_function_names << [module_key, arg.name] if arg.is_a?(Prism::DefNode)
97
145
  end
98
146
  end
99
147
  end
148
+ if node.name == :extend && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
149
+ !@self_unknown && @singleton_depth.zero?
150
+ args = node.arguments&.arguments || []
151
+ concern = args.any? { |arg| arg.slice.delete_prefix("::") == "ActiveSupport::Concern" }
152
+ @concern_modules << module_key if concern
153
+ end
154
+ case concern_block(node)
155
+ when :owner_unknown
156
+ hook = hook_block?(node)
157
+ return preserving_scope do
158
+ @hook_block ||= hook
159
+ @owner_unknown = true
160
+ super
161
+ end
162
+ when :class_methods
163
+ owner = named_owner((@block_namespace || @namespace_stack) + ["ClassMethods"])
164
+ anonymous = false
165
+ else
166
+ return super unless builds_class?(node)
167
+
168
+ anonymous = !@assigned&.first.equal?(node)
169
+ owner = anonymous ? [nil, @namespace_stack, true] : @assigned.last
170
+ end
171
+ with_block_owner(owner, anonymous) do
172
+ @self_scope = node.block
173
+ super
174
+ end
175
+ end
176
+
177
+ # A block that does not build a class may run with any `self` (`class_eval`,
178
+ # `instance_eval`, a callback), so a `class << self` in it names a singleton
179
+ # class Mutineer cannot be sure of (#216). A lambda is treated the same.
180
+ #
181
+ # @param node [Prism::BlockNode, Prism::LambdaNode] block or lambda node.
182
+ # @return [void]
183
+ def visit_block_node(node)
184
+ return super if node.equal?(@self_scope) # a builder block
185
+
186
+ saved_self = [@self_unknown, @self_scope]
187
+ @self_unknown = true
188
+ @self_scope = node
100
189
  super
190
+ @self_unknown, @self_scope = saved_self
101
191
  end
192
+ alias visit_lambda_node visit_block_node
193
+
194
+ # Names the class a builder block assigned to this constant builds (see {#builds_class?}).
195
+ #
196
+ # `=`, `||=` and `&&=` store the class, so each names it; `+=` stores what `+`
197
+ # returns, so its builder is left unnamed.
198
+ #
199
+ # @param node [Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
200
+ # Prism::ConstantAndWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode]
201
+ # constant assignment.
202
+ # @return [void]
203
+ def visit_constant_write_node(node)
204
+ call = assigned_value(node.value)
205
+ return super unless builds_class?(call)
206
+
207
+ saved = @assigned
208
+ @assigned = [call, assigned_owner(node)]
209
+ begin
210
+ super
211
+ ensure
212
+ @assigned = saved
213
+ end
214
+ end
215
+ alias visit_constant_path_write_node visit_constant_write_node
216
+ alias visit_constant_or_write_node visit_constant_write_node
217
+ alias visit_constant_and_write_node visit_constant_write_node
218
+ alias visit_constant_path_or_write_node visit_constant_write_node
219
+ alias visit_constant_path_and_write_node visit_constant_write_node
102
220
 
103
221
  # Methods inside `class << self` are class methods of the enclosing
104
222
  # namespace, but their def nodes have no receiver — track the singleton
@@ -113,8 +231,11 @@ module Mutineer
113
231
 
114
232
  @singleton_depth += 1
115
233
  saved_active = @module_function_active
234
+ saved_uncertain = @singleton_uncertain
235
+ @singleton_uncertain ||= @self_unknown # `self` here may be another object (#216)
116
236
  super
117
237
  @module_function_active = saved_active # a visibility call in here is not the module body's
238
+ @singleton_uncertain = saved_uncertain
118
239
  @singleton_depth -= 1
119
240
  end
120
241
 
@@ -125,42 +246,311 @@ module Mutineer
125
246
  def visit_def_node(node)
126
247
  @subjects << Subject.new(
127
248
  file: @file,
128
- namespace: @namespace_stack.dup,
249
+ namespace: (@block_namespace || @namespace_stack).dup,
129
250
  lexical: @lexical_stack.dup,
251
+ block_owner: @block_owner,
252
+ owner_unknown: @owner_unknown,
253
+ hook_block: @hook_block,
130
254
  name: node.name,
131
255
  singleton: !node.receiver.nil? || @singleton_depth.positive? || @module_function_active,
132
256
  def_node: node
133
257
  )
258
+ @subject_keys << module_key
134
259
  saved_active = @module_function_active
260
+ saved_self = [@self_unknown, @self_scope]
261
+ @self_unknown = true # `self` in a method body is the receiver it is called on
262
+ @self_scope = node
135
263
  super
136
264
  @module_function_active = saved_active # a visibility call in a method body runs only when it is called
265
+ @self_unknown, @self_scope = saved_self
137
266
  end
138
267
 
139
268
  private
140
269
 
270
+ # The module a `module_function` call and a def are matched by: its joined
271
+ # namespace, so a module reopened later in the file matches (#216), and
272
+ # `module A::B` matches nested `module A; module B`. An unknown owner that an
273
+ # anonymous builder block owns, or whose name has no {WrittenName} segment
274
+ # and is not built only from constant names (see {#named_segment?}), may be
275
+ # shared by another module, so it is matched only within this body (#208).
276
+ # Otherwise a name with a {WrittenName}
277
+ # segment is matched by that segment's scope and text, so openings under one
278
+ # `self` match each other, but not those under another, whether the owner is
279
+ # known or not (#229).
280
+ #
281
+ # @return [String, Object]
282
+ def module_key
283
+ namespace = @block_namespace || @namespace_stack
284
+ written = written?(namespace)
285
+ return @body if @owner_unknown && (@anonymous_block || !written && !namespace.all? { |s| named_segment?(s) })
286
+ return namespace.join("::") unless written
287
+
288
+ namespace.map { |s| s.is_a?(WrittenName) ? "#{s.scope.object_id}:#{s}" : s }.join("::")
289
+ end
290
+
291
+ # True when any segment of the namespace is a {WrittenName}.
292
+ #
293
+ # @param namespace [Array<String>] namespace segments.
294
+ # @return [Boolean]
295
+ def written?(namespace)
296
+ namespace.any?(WrittenName)
297
+ end
298
+
299
+ # True when a namespace segment is a constant name, or a singleton class
300
+ # (`#<Class:App::M>`) of constant names. A written path or `#<anonymous>` is
301
+ # not, nor is a singleton class name built on one ({WrittenName}).
302
+ #
303
+ # @param segment [String] namespace segment.
304
+ # @return [Boolean]
305
+ def named_segment?(segment)
306
+ !segment.is_a?(WrittenName) && segment.gsub(/#<Class:|>/, "").match?(CONSTANT_PATH) &&
307
+ (segment.start_with?("#<Class:") || !segment.include?("::"))
308
+ end
309
+
141
310
  # Runs the block with `path` pushed as the current namespace. A
142
311
  # root-anchored path (`module ::X` / `class ::X`) names the top-level X,
143
312
  # not X nested in the enclosing scope, so the namespace restarts there.
144
313
  # Bareword `module_function` state does not cross a class or module
145
314
  # boundary: each body starts without it, and the outer state returns after.
315
+ # A class or module `X` opened inside `class << self`, even within a builder
316
+ # block there, is a constant of the singleton class, which has no constant
317
+ # path. It and everything nested in it is named under `#<Class:...>` with
318
+ # its owner unknown (#208), and its body defines instance methods again.
319
+ # A compact `Foo::X` or a top-level `::X` there is named as written, and
320
+ # its owner is unknown too: redefine reopens the lexical chain without the
321
+ # singleton class, so constants the body looks up through it would not resolve.
322
+ # A `self::X` in any block is under the block's `self`, not the namespace
323
+ # around the block (#229). In a builder block that is the built class, so
324
+ # it is named under the builder's constant; in any other block (`class_eval`,
325
+ # a callback) `self` may be any object, so it is named under the innermost
326
+ # enclosing builder's constant, or else the enclosing namespace. Either way
327
+ # the last segment is a {WrittenName}, and it and everything nested in it
328
+ # has its owner unknown. Redefine reopens the lexical chain (`module Outer;
329
+ # module X`), which never reaches the block's `self`; reopening X through
330
+ # its constant with `class_eval`, as for `self::X = ...` in a builder block,
331
+ # would drop X from `Module.nesting`, so constants its body looks up would not resolve.
146
332
  #
147
333
  # @param path [Prism::Node] the class/module constant path.
148
334
  # @yield the class or module body visit.
149
335
  # @return [void]
150
336
  def with_namespace(path)
151
- saved_stack = @namespace_stack
152
- saved_lexical = @lexical_stack
153
- saved_active = @module_function_active
154
- name = extract_constant_name(path)
155
- root = root_anchored?(path)
156
- @namespace_stack = root ? [name] : saved_stack + [name]
157
- @lexical_stack = saved_lexical + [root ? "::#{name}" : name]
158
- @module_function_active = false
337
+ preserving_scope do
338
+ name = extract_constant_name(path)
339
+ root = root_anchored?(path)
340
+ in_singleton = !@singleton_cref.nil? || @singleton_depth.positive?
341
+ self_in_block = !in_singleton && !root && self_rooted?(path) && (!@block_namespace.nil? || @self_unknown)
342
+ @namespace_stack =
343
+ if root then [name]
344
+ elsif in_singleton then path.is_a?(Prism::ConstantPathNode) ? [path.slice] : [singleton_name, name]
345
+ elsif self_in_block then (@block_namespace || @namespace_stack) + [WrittenName.new(name, @self_scope)]
346
+ else @namespace_stack + [name]
347
+ end
348
+ @lexical_stack += [root ? "::#{name}" : name]
349
+ @module_function_active = false
350
+ @block_owner = @block_namespace = nil
351
+ # redefine reopens the lexical chain, which does not pass through the block's `self`
352
+ @namespace_unknown = @owner_unknown = in_singleton || self_in_block || @namespace_unknown
353
+ @singleton_depth = 0
354
+ @singleton_cref = nil
355
+ @anonymous_block = @self_unknown = @singleton_uncertain = false
356
+ @body = Object.new
357
+ yield
358
+ end
359
+ end
360
+
361
+ # Visitor state a class/module body or builder block sets for what it contains.
362
+ SCOPE_STATE = %i[@namespace_stack @lexical_stack @module_function_active @block_owner @block_namespace
363
+ @owner_unknown @singleton_depth @singleton_cref @namespace_unknown @anonymous_block @body
364
+ @self_unknown @self_scope @singleton_uncertain @hook_block].freeze
365
+
366
+ # Runs the block and then restores every {SCOPE_STATE} variable, so a
367
+ # nested body's state never leaks out of it.
368
+ #
369
+ # @yield the nested visit.
370
+ # @return [void]
371
+ def preserving_scope
372
+ saved = SCOPE_STATE.map { |name| instance_variable_get(name) }
159
373
  yield
160
374
  ensure
161
- @namespace_stack = saved_stack
162
- @lexical_stack = saved_lexical
163
- @module_function_active = saved_active
375
+ SCOPE_STATE.zip(saved) { |name, value| instance_variable_set(name, value) }
376
+ end
377
+
378
+ # Resolves the constant an assignment writes the way Ruby does. `X` is in the
379
+ # current namespace, `::X` and a path at the top level start from Object, and
380
+ # `self::X` is under the current class (the built class inside a builder
381
+ # block). In a block that does not build a class, `self` may be any object,
382
+ # so `self::X` there has its owner unknown (#229). Any other path is
383
+ # looked up at run time, so its owner is unknown
384
+ # and the subject is named as written. Lexically inside `class << self`,
385
+ # even within a builder block there, the constant belongs to the singleton
386
+ # class, which has no constant path, so its owner is unknown too, as is
387
+ # `X` or `self::X` in a class or module opened there (#208). A `::X` there
388
+ # is named `X`, still unknown (see {#with_namespace}).
389
+ #
390
+ # @param node [Prism::Node] constant assignment.
391
+ # @return [Array(String, Array<String>, Boolean)] owner, namespace, unknown.
392
+ def assigned_owner(node)
393
+ if @singleton_cref || @singleton_depth.positive?
394
+ written = node.respond_to?(:target) ? node.target.slice : node.name.to_s
395
+ return [nil, written.start_with?("::") ? [written.delete_prefix("::")] : [singleton_name, written], true]
396
+ end
397
+ unless node.respond_to?(:target)
398
+ namespace = @namespace_stack + [node.name.to_s]
399
+ return @namespace_unknown ? [nil, namespace, true] : named_owner(namespace)
400
+ end
401
+
402
+ names = []
403
+ path = node.target
404
+ while path.is_a?(Prism::ConstantPathNode)
405
+ names.unshift(path.name.to_s)
406
+ path = path.parent
407
+ end
408
+ # see {#with_namespace}; in a builder block too, so a later `module self::X` there matches it
409
+ if path.is_a?(Prism::SelfNode) && (@self_unknown || @block_namespace)
410
+ names[0] = WrittenName.new(names[0], @self_scope)
411
+ end
412
+ if path.is_a?(Prism::SelfNode) && (@self_unknown || @namespace_unknown && !@anonymous_block)
413
+ return [nil, (@block_namespace || @namespace_stack) + names, true]
414
+ end
415
+
416
+ base =
417
+ case path
418
+ when nil then []
419
+ when Prism::SelfNode then @block_namespace || @namespace_stack unless @owner_unknown
420
+ when Prism::ConstantReadNode then [path.name.to_s] if @namespace_stack.empty?
421
+ end
422
+ return [nil, [node.target.slice], true] unless base
423
+
424
+ @namespace_unknown ? [nil, base + names, true] : named_owner(base + names)
425
+ end
426
+
427
+ # Names the singleton class that owns the constants written here: `class << self`
428
+ # opens that of the current class (the built class inside a builder block),
429
+ # and a builder block inside `class << self` keeps the enclosing one. A block
430
+ # not assigned to a constant builds a class with no name, written `#<anonymous>`.
431
+ # Each nested `class << self` opens the singleton class of the one around it.
432
+ # A name built on a written path or `#<anonymous>`, or opened where `self` may
433
+ # be another object (in a def or a non-builder block), is a {WrittenName} (see {#module_key}).
434
+ #
435
+ # @return [String] e.g. `#<Class:App>`, or `#<Class:#<Class:App>>` two deep.
436
+ def singleton_name
437
+ return @singleton_cref unless @singleton_depth.positive?
438
+
439
+ base = @anonymous_block ? @namespace_stack + ["#<anonymous>"] : @block_namespace || @namespace_stack
440
+ name = @singleton_depth.times.reduce(base.join("::")) { |inner, _| "#<Class:#{inner}>" }
441
+ named = !@singleton_uncertain && (@owner_unknown ? base.all? { |s| named_segment?(s) } : !written?(base))
442
+ named ? name : WrittenName.new(name)
443
+ end
444
+
445
+ # The owner for a resolved namespace, root-anchored so the redefine
446
+ # wrapper loads onto that constant from any nesting.
447
+ #
448
+ # @param namespace [Array<String>] resolved namespace.
449
+ # @return [Array(String, Array<String>, Boolean)]
450
+ def named_owner(namespace)
451
+ ["::#{namespace.join("::")}", namespace, false]
452
+ end
453
+
454
+ # The value an assignment stores: the last statement inside parentheses or a
455
+ # `begin` without `rescue`, which Ruby returns from them.
456
+ #
457
+ # @param node [Prism::Node] assigned expression.
458
+ # @return [Prism::Node, nil]
459
+ def assigned_value(node)
460
+ loop do
461
+ body =
462
+ case node
463
+ when Prism::ParenthesesNode then node.body
464
+ when Prism::BeginNode then node.statements unless node.rescue_clause
465
+ end
466
+ return node unless body
467
+
468
+ node = body.is_a?(Prism::StatementsNode) ? body.body.last : body
469
+ end
470
+ end
471
+
472
+ # True when the node is `Data.define`, `Struct.new`, `Class.new` or `Module.new` with a block.
473
+ #
474
+ # @param node [Prism::Node, nil] node.
475
+ # @return [Boolean]
476
+ def builds_class?(node)
477
+ node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode) &&
478
+ CLASS_BUILDERS[node.receiver&.slice&.delete_prefix("::")] == node.name
479
+ end
480
+
481
+ # Sorts an ActiveSupport::Concern block by where redefine can load its defs.
482
+ # ActiveSupport evaluates a `class_methods` block in the module's `ClassMethods`,
483
+ # which it extends onto every includer, so the test calls `Includer.name` on
484
+ # that module's method, not on the concern (#252). In a class or module body,
485
+ # or a builder block, with a known owner, that module is `<namespace>::ClassMethods`.
486
+ # The block keeps the lexical scope, so redefine reopens the concern's chain and
487
+ # loads the mutant with `::Concern::ClassMethods.class_eval`. In a def, a block
488
+ # that does not build a class (Rails' `concerning :Name do`), `class << self`, or
489
+ # under an owner no constant names (an unassigned `Module.new`), no constant here
490
+ # names that `ClassMethods`. ActiveSupport evaluates an `included` or `prepended`
491
+ # block in each includer, which no constant names either. The call has no
492
+ # receiver or `self`, no arguments, and a block.
493
+ #
494
+ # Another library can define methods with these names and run the block in
495
+ # the module itself. So a `class_methods` block counts only in a module that ran
496
+ # `extend ActiveSupport::Concern` earlier in this file, in this body or an earlier
497
+ # one ({#module_key}). Any other block with these names is unplaceable: guessing
498
+ # its owner would load a mutant where the tests never call it.
499
+ #
500
+ # @param node [Prism::CallNode] call node.
501
+ # @return [Symbol, nil] `:class_methods` when redefine can name the module,
502
+ # `:owner_unknown` for any other Concern block, or nil for any other call.
503
+ def concern_block(node)
504
+ return unless CONCERN_BLOCKS.include?(node.name) && node.arguments.nil? && node.block.is_a?(Prism::BlockNode)
505
+ return unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
506
+ placeable = node.name == :class_methods && !@self_unknown && @singleton_depth.zero? && !@owner_unknown &&
507
+ @concern_modules.include?(module_key)
508
+ placeable ? :class_methods : :owner_unknown
509
+ end
510
+
511
+ # True for an `included` or `prepended` block of a module that ran `extend
512
+ # ActiveSupport::Concern` earlier in this file (see {#concern_block}).
513
+ # ActiveSupport runs the block only when a class first includes or prepends
514
+ # the concern, so loading the file again stores the new block but never
515
+ # runs it in a class that already has the concern (#256). Everything the
516
+ # block defines, a nested class or a builder block included, stays as the
517
+ # original wrote it under `--strategy reload`.
518
+ #
519
+ # @param node [Prism::CallNode] a call {#concern_block} sorted as a concern block.
520
+ # @return [Boolean]
521
+ def hook_block?(node)
522
+ %i[included prepended].include?(node.name) && @concern_modules.include?(module_key)
523
+ end
524
+
525
+ # Runs the block with the defs it visits owned by the class a builder block builds.
526
+ # The block body defines instance methods of that class, even inside `class << self`,
527
+ # but its constants still land where the enclosing `class << self` puts them.
528
+ #
529
+ # @param owner [Array(String, Array<String>, Boolean)] the owner as written, its namespace,
530
+ # and whether that name is unknown.
531
+ # @param anonymous [Boolean] true when the block is not assigned to a constant.
532
+ # @yield the builder call visit.
533
+ # @return [void]
534
+ def with_block_owner(owner, anonymous)
535
+ preserving_scope do
536
+ @singleton_cref = singleton_name if @singleton_depth.positive?
537
+ @block_owner, @block_namespace, @owner_unknown = owner
538
+ @anonymous_block = anonymous
539
+ @self_unknown = @singleton_uncertain = false
540
+ @module_function_active = false
541
+ @singleton_depth = 0
542
+ @body = Object.new
543
+ yield
544
+ end
545
+ end
546
+
547
+ # True when a constant path starts with `self` (e.g. `self::X`).
548
+ #
549
+ # @param node [Prism::Node] constant path node.
550
+ # @return [Boolean]
551
+ def self_rooted?(node)
552
+ node = node.parent while node.is_a?(Prism::ConstantPathNode)
553
+ node.is_a?(Prism::SelfNode)
164
554
  end
165
555
 
166
556
  # True when a constant path starts with `::` (e.g. `::X` or `::A::B`).
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutineer
4
+ # Pins the environment Rails and Spring read during a mutant run.
5
+ #
6
+ # Rails reads +PARALLEL_WORKERS+ inside +parallelize+ and uses it instead of
7
+ # the +workers:+ argument. A value of 1 keeps that run in one process.
8
+ # +--test-command+ also sets +DISABLE_SPRING+ so the Spring preloader does
9
+ # not start. {pin_process!} prints the replacement line once. The caller
10
+ # puts the process value back with {restore_process!} when the run ends.
11
+ module RailsRunEnv
12
+ # Value of +PARALLEL_WORKERS+ that keeps Rails test workers off.
13
+ PARALLEL_WORKERS = "1"
14
+
15
+ # Value of +DISABLE_SPRING+ that stops the Spring preloader.
16
+ DISABLE_SPRING = "1"
17
+
18
+ # Sets +PARALLEL_WORKERS+ to {PARALLEL_WORKERS} in this process.
19
+ # Prints one line when the previous value is set and is not already 1.
20
+ #
21
+ # @return [void]
22
+ def self.pin_process!
23
+ current = ENV["PARALLEL_WORKERS"]
24
+ warn(replacement_line(current)) if replace?(current)
25
+ ENV["PARALLEL_WORKERS"] = PARALLEL_WORKERS
26
+ nil
27
+ end
28
+
29
+ # Puts the caller's +PARALLEL_WORKERS+ back. +nil+ means it was unset.
30
+ #
31
+ # @param previous [String, nil]
32
+ # @return [void]
33
+ def self.restore_process!(previous)
34
+ if previous.nil?
35
+ ENV.delete("PARALLEL_WORKERS")
36
+ else
37
+ ENV["PARALLEL_WORKERS"] = previous
38
+ end
39
+ nil
40
+ end
41
+
42
+ # Forces +PARALLEL_WORKERS+ on a child environment.
43
+ # +spring+ also sets +DISABLE_SPRING+. The hash is the spawn delta or the
44
+ # full daemon environment. This does not change the parent process.
45
+ #
46
+ # @param env [Hash{String => String, nil}]
47
+ # @param spring [Boolean] set +DISABLE_SPRING+ when true.
48
+ # @return [void]
49
+ def self.pin_child!(env, spring: false)
50
+ env["PARALLEL_WORKERS"] = PARALLEL_WORKERS
51
+ env["DISABLE_SPRING"] = DISABLE_SPRING if spring
52
+ nil
53
+ end
54
+
55
+ # True when Mutineer must replace a +PARALLEL_WORKERS+ value the user set.
56
+ #
57
+ # @param current [String, nil]
58
+ # @return [Boolean]
59
+ def self.replace?(current)
60
+ !current.nil? && current != PARALLEL_WORKERS
61
+ end
62
+
63
+ # The one line printed when {replace?} is true.
64
+ #
65
+ # @param current [String]
66
+ # @return [String]
67
+ def self.replacement_line(current)
68
+ "[mutineer] PARALLEL_WORKERS was #{current.inspect}; using 1 so each mutant's tests run in one process. " \
69
+ "Unset it or set it to 1 to drop this line."
70
+ end
71
+ private_class_method :replace?, :replacement_line
72
+ end
73
+ end