Class: Mutineer::Project::SubjectVisitor

Inherits:
Prism::Visitor
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initialize(file) ⇒ SubjectVisitor

Builds a subject visitor.

Parameters:

  • file (String) —

    source file path being visited.



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.

Parameters:

  • node (Prism::Node) —

    constant node.

Returns:

  • (String, nil) —

    constant name.



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).

Parameters:

  • node (Prism::Node) —

    constant path node.

Returns:

  • (Boolean)


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.

Parameters:

  • node (Prism::CallNode) —

    call node.



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.

Parameters:

  • node (Prism::ClassNode) —

    class node.



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.

Parameters:

  • node (Prism::DefNode) —

    method definition node.



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.

Parameters:

  • node (Prism::ModuleNode) —

    module node.



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).

Parameters:

  • node (Prism::SingletonClassNode) —

    singleton-class node.



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.

Parameters:

  • path (Prism::Node) —

    the class/module constant path.

Yields:

  • the class or module body visit.



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