Class: ActiveMutator::SubjectFinder
- Inherits:
-
Prism::Visitor
- Object
- Prism::Visitor
- ActiveMutator::SubjectFinder
- Defined in:
- lib/active_mutator/subject_finder.rb
Constant Summary collapse
- SKIP_MARKER =
/#\s*active_mutator:\s*skip\b/
Instance Attribute Summary collapse
-
#subjects ⇒ Object
readonly
Returns the value of attribute subjects.
Class Method Summary collapse
- .call(file) ⇒ Object
-
.zeitwerk_shaped?(program) ⇒ Boolean
Class-body subjects only for Zeitwerk-shaped files: exactly one top-level constant.
Instance Method Summary collapse
-
#initialize(file, skip_lines: Set.new, class_level: true) ⇒ SubjectFinder
constructor
A new instance of SubjectFinder.
-
#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. -
#visit_class_node(node) ⇒ Object
Classes/modules declared inside
class << selfhang their constant on the SINGLETON class, so a lexically-joined scope like "Foo::Bar" is not reachable via Object.const_get — Inserter would crash. - #visit_def_node(node) ⇒ Object
- #visit_module_node(node) ⇒ Object
-
#visit_singleton_class_node(node) ⇒ Object
class << selfinside a constant scope: defs there are singleton methods of the enclosing constant.
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
#subjects ⇒ Object (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.
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 |