Class: Mutineer::Project::SubjectVisitor
- Inherits:
-
Prism::Visitor
- Object
- Prism::Visitor
- Mutineer::Project::SubjectVisitor
- Defined in:
- lib/mutineer/project.rb
Overview
Walks an AST, maintaining a namespace stack, emitting Subjects. Nested inside Project to signal its private role.
Instance Attribute Summary collapse
-
#subjects ⇒ Object
readonly
Returns the value of attribute subjects.
Instance Method Summary collapse
-
#extract_constant_name(node) ⇒ String?
private
private
Extracts a constant name from a Prism constant node.
-
#initialize(file) ⇒ SubjectVisitor
constructor
Builds a subject visitor.
-
#promote_module_functions! ⇒ void
Promote
module_function :name/module_function def namesubjects to singleton after the full walk — the naming call may appear before or after the def, so it can't be decided at visit_def_node time (#20). -
#root_anchored?(node) ⇒ Boolean
private
True when a constant path starts with
::(e.g.::Xor::A::B). -
#visit_call_node(node) ⇒ void
Track
module_functionso its methods are recorded as singletons (#20) — the called form is the singleton method on the module object. -
#visit_class_node(node) ⇒ void
Visits class nodes and tracks namespace nesting.
-
#visit_def_node(node) ⇒ void
Records a discovered method definition.
-
#visit_module_node(node) ⇒ void
Visits module nodes and tracks namespace nesting.
-
#visit_singleton_class_node(node) ⇒ void
Methods inside
class << selfare class methods of the enclosing namespace, but their def nodes have no receiver — track the singleton context so they're recorded as singleton (so redefine targets the singleton_class, not instances). -
#with_namespace(path) { ... } ⇒ void
private
Runs the block with
pathpushed as the current namespace.
Constructor Details
#initialize(file) ⇒ SubjectVisitor
Builds a subject visitor.
36 37 38 39 40 41 42 43 44 45 |
# File 'lib/mutineer/project.rb', line 36 def initialize(file) @file = file @namespace_stack = [] @lexical_stack = [] # class/module names as written, `::X` kept (#145) @subjects = [] @singleton_depth = 0 @module_function_active = false # bareword `module_function` seen in this module body @module_function_names = [] # [namespace, name] from `module_function :a` / `module_function def` (#98) super() end |
Instance Attribute Details
#subjects ⇒ Object (readonly)
Returns the value of attribute subjects.
31 32 33 |
# File 'lib/mutineer/project.rb', line 31 def subjects @subjects end |
Instance Method Details
#extract_constant_name(node) ⇒ String? (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Extracts a constant name from a Prism constant node.
180 181 182 183 184 185 186 187 |
# File 'lib/mutineer/project.rb', line 180 def extract_constant_name(node) case node when Prism::ConstantReadNode node.name.to_s when Prism::ConstantPathNode [extract_constant_name(node.parent), node.name.to_s].compact.join("::") end end |
#promote_module_functions! ⇒ void
This method returns an undefined value.
Promote module_function :name / module_function def name subjects to
singleton after the full walk — the naming call may appear before or after
the def, so it can't be decided at visit_def_node time (#20). Only methods
of the module that made the call are promoted (#98); namespaces compare
joined, since module A::B and nested module A; module B differ as arrays.
54 55 56 57 58 59 |
# File 'lib/mutineer/project.rb', line 54 def promote_module_functions! return if @module_function_names.empty? named = @module_function_names.to_set @subjects.each { |s| s.singleton = true if named.include?([s.namespace.join("::"), s.name]) } end |
#root_anchored?(node) ⇒ Boolean (private)
True when a constant path starts with :: (e.g. ::X or ::A::B).
170 171 172 173 |
# File 'lib/mutineer/project.rb', line 170 def root_anchored?(node) node = node.parent while node.is_a?(Prism::ConstantPathNode) && node.parent node.is_a?(Prism::ConstantPathNode) end |
#visit_call_node(node) ⇒ void
This method returns an undefined value.
Track module_function so its methods are recorded as singletons (#20) —
the called form is the singleton method on the module object. Bareword
module_function flips all SUBSEQUENT defs in this body; the argument
forms (:sym, def) name methods promoted after the walk.
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/mutineer/project.rb', line 84 def visit_call_node(node) if %i[public private protected].include?(node.name) && node.receiver.nil? && node.arguments.nil? @module_function_active = false # without arguments, Ruby goes back to instance methods end if node.name == :module_function && node.receiver.nil? args = node.arguments&.arguments || [] if args.empty? @module_function_active = true else namespace = @namespace_stack.join("::") args.each do |arg| @module_function_names << [namespace, arg.value.to_sym] if arg.is_a?(Prism::SymbolNode) @module_function_names << [namespace, arg.name] if arg.is_a?(Prism::DefNode) end end end super end |
#visit_class_node(node) ⇒ void
This method returns an undefined value.
Visits class nodes and tracks namespace nesting.
65 66 67 |
# File 'lib/mutineer/project.rb', line 65 def visit_class_node(node) with_namespace(node.constant_path) { super } end |
#visit_def_node(node) ⇒ void
This method returns an undefined value.
Records a discovered method definition.
125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/mutineer/project.rb', line 125 def visit_def_node(node) @subjects << Subject.new( file: @file, namespace: @namespace_stack.dup, lexical: @lexical_stack.dup, name: node.name, singleton: !node.receiver.nil? || @singleton_depth.positive? || @module_function_active, def_node: node ) saved_active = @module_function_active super @module_function_active = saved_active # a visibility call in a method body runs only when it is called end |
#visit_module_node(node) ⇒ void
This method returns an undefined value.
Visits module nodes and tracks namespace nesting.
73 74 75 |
# File 'lib/mutineer/project.rb', line 73 def visit_module_node(node) with_namespace(node.constant_path) { super } end |
#visit_singleton_class_node(node) ⇒ void
This method returns an undefined value.
Methods inside class << self are class methods of the enclosing
namespace, but their def nodes have no receiver — track the singleton
context so they're recorded as singleton (so redefine targets the
singleton_class, not instances). class << some_other_obj can't be
represented against the namespace, so its defs are skipped (not recursed).
111 112 113 114 115 116 117 118 119 |
# File 'lib/mutineer/project.rb', line 111 def visit_singleton_class_node(node) return unless node.expression.is_a?(Prism::SelfNode) @singleton_depth += 1 saved_active = @module_function_active super @module_function_active = saved_active # a visibility call in here is not the module body's @singleton_depth -= 1 end |
#with_namespace(path) { ... } ⇒ void (private)
This method returns an undefined value.
Runs the block with path pushed as the current namespace. A
root-anchored path (module ::X / class ::X) names the top-level X,
not X nested in the enclosing scope, so the namespace restarts there.
Bareword module_function state does not cross a class or module
boundary: each body starts without it, and the outer state returns after.
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/mutineer/project.rb', line 150 def with_namespace(path) saved_stack = @namespace_stack saved_lexical = @lexical_stack saved_active = @module_function_active name = extract_constant_name(path) root = root_anchored?(path) @namespace_stack = root ? [name] : saved_stack + [name] @lexical_stack = saved_lexical + [root ? "::#{name}" : name] @module_function_active = false yield ensure @namespace_stack = saved_stack @lexical_stack = saved_lexical @module_function_active = saved_active end |