Class: ActiveMutator::SubjectFinder

Inherits:
Prism::Visitor
  • Object
show all
Defined in:
lib/active_mutator/subject_finder.rb

Constant Summary collapse

SKIP_MARKER =
/#\s*active_mutator:\s*skip\b/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(file, skip_lines: Set.new, class_level: true) ⇒ SubjectFinder

Returns a new instance of SubjectFinder.



29
30
31
32
33
34
35
36
37
# File 'lib/active_mutator/subject_finder.rb', line 29

def initialize(file, skip_lines: Set.new, class_level: true)
  @file = file
  @skip_lines = skip_lines
  @class_level = class_level
  @stack = []
  @subjects = []
  @sclass_depth = 0
  super()
end

Instance Attribute Details

#subjectsObject (readonly)

Returns the value of attribute subjects.



27
28
29
# File 'lib/active_mutator/subject_finder.rb', line 27

def subjects
  @subjects
end

Class Method Details

.call(file) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
# File 'lib/active_mutator/subject_finder.rb', line 7

def self.call(file)
  result = Prism.parse(File.read(file))
  return [] unless result.success?

  skip_lines = result.comments
    .select { |c| c.slice.match?(SKIP_MARKER) }
    .to_set { |c| c.location.start_line }
  finder = new(file, skip_lines: skip_lines,
               class_level: zeitwerk_shaped?(result.value))
  finder.visit(result.value)
  finder.subjects
end

.zeitwerk_shaped?(program) ⇒ Boolean

Class-body subjects only for Zeitwerk-shaped files: exactly one top-level constant. Multi-constant files and core-class reopens have no safe remove_const + re-eval story (issue #32). Shared with ClosureReload.

Returns:

  • (Boolean)


23
24
25
# File 'lib/active_mutator/subject_finder.rb', line 23

def self.zeitwerk_shaped?(program)
  ClassShape.single_top_level_constant?(program)
end

Instance Method Details

#visit_block_node(node) ⇒ Object

Defs inside blocks (Data.define do ... end, class_eval do ... end) do not live on the enclosing constant scope, so Inserter would redefine them on the wrong constant and every mutant would falsely survive. Same v1 limit as class << self: not visited. Note this also hides classes/modules defined inside blocks (accepted v1 limit).



79
# File 'lib/active_mutator/subject_finder.rb', line 79

def visit_block_node(node); end

#visit_class_node(node) ⇒ Object

Classes/modules declared inside class << self hang their constant on the SINGLETON class, so a lexically-joined scope like "Foo::Bar" is not reachable via Object.const_get — Inserter would crash. Skipped entirely.



42
43
44
45
46
47
48
49
# File 'lib/active_mutator/subject_finder.rb', line 42

def visit_class_node(node)
  return if @sclass_depth.positive?

  with_scope(node.constant_path.slice) do
    add_class_body_subject(node)
    super
  end
end

#visit_def_node(node) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/active_mutator/subject_finder.rb', line 81

def visit_def_node(node)
  return if @skip_lines.include?(node.location.start_line - 1)

  sclass = @sclass_depth.positive?
  singleton = sclass || node.receiver.is_a?(Prism::SelfNode)
  scope = @stack.empty? ? nil : @stack.join("::")
  loc = node.location
  @subjects << Subject.new(
    name: "#{scope || "Object"}#{singleton ? "." : "#"}#{node.name}",
    file: @file,
    byte_range: loc.start_offset...loc.end_offset,
    line_range: loc.start_line..loc.end_line,
    constant_scope: scope,
    kind: singleton ? :singleton : :instance,
    sclass: sclass
  )
  # No `super`: nested defs get no subject of their own -- their bodies
  # are mutated via the OUTER def (Engine#walk descends into them).
end

#visit_module_node(node) ⇒ Object



51
52
53
54
55
56
57
58
# File 'lib/active_mutator/subject_finder.rb', line 51

def visit_module_node(node)
  return if @sclass_depth.positive?

  with_scope(node.constant_path.slice) do
    add_class_body_subject(node)
    super
  end
end

#visit_singleton_class_node(node) ⇒ Object

class << self inside a constant scope: defs there are singleton methods of the enclosing constant. class << obj and a top-level class << self (no constant to hang the method on) stay skipped.



63
64
65
66
67
68
69
70
71
72
# File 'lib/active_mutator/subject_finder.rb', line 63

def visit_singleton_class_node(node)
  return unless node.expression.is_a?(Prism::SelfNode) && !@stack.empty?

  @sclass_depth += 1
  begin
    super
  ensure
    @sclass_depth -= 1
  end
end