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) ⇒ SubjectFinder

Returns a new instance of SubjectFinder.



21
22
23
24
25
26
27
28
# File 'lib/active_mutator/subject_finder.rb', line 21

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

Instance Attribute Details

#subjectsObject (readonly)

Returns the value of attribute subjects.



19
20
21
# File 'lib/active_mutator/subject_finder.rb', line 19

def subjects
  @subjects
end

Class Method Details

.call(file) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
# 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)
  finder.visit(result.value)
  finder.subjects
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).



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

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.



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

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

  with_scope(node.constant_path.slice) { super }
end

#visit_def_node(node) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/active_mutator/subject_finder.rb', line 66

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



39
40
41
42
43
# File 'lib/active_mutator/subject_finder.rb', line 39

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

  with_scope(node.constant_path.slice) { super }
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.



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

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