Module: Ast::Merge::NodeTyping

Defined in:
lib/ast/merge/node_typing.rb,
lib/ast/merge/node_typing/wrapper.rb,
lib/ast/merge/node_typing/normalizer.rb,
lib/ast/merge/node_typing/frozen_wrapper.rb

Overview

Provides node type wrapping support for SmartMerger implementations.

NodeTyping allows custom callable objects to be associated with specific
node types. When a node is processed, the corresponding callable can:

  • Return the node unchanged (passthrough)
  • Return a modified node with a custom merge_type attribute
  • Return nil to indicate the node should be skipped

The merge_type attribute can then be used by other merge tools like
signature_generator, match_refiner, and per-node-type preference settings.

Important: Two Uses of merge_type

The merge_type method serves two complementary purposes in the codebase:

1. NodeTyping-specific (gated by typed_node?)

Wrapped nodes (Wrapper/FrozenWrapper) with custom type tagging for:

  • Per-node-type preferences (e.g., :lint_gem:template)
  • Match refinement based on custom categories
  • Only applies when typed_node? returns true
  • Accessed via NodeTyping.merge_type_for(node)

2. General node classification (any node)

Any node can implement merge_type for category identification:

  • FreezeNodeBase has merge_type:freeze_block
  • GapLineNode has merge_type:gap_line
  • Used by systems like MarkdownStructure for structural spacing rules
  • These nodes are NOT “typed nodes” (typed_node? returns false)

The key distinction: typed_node? is the gate for NodeTyping wrapper
semantics. A node can have merge_type without being a NodeTyping wrapper.

Examples:

Basic node typing for different gem types

node_typing = {
  CallNode: ->(node) {
    return node unless node.name == :gem
    first_arg = node.arguments&.arguments&.first
    return node unless first_arg.is_a?(StringNode)

    gem_name = first_arg.unescaped
    if gem_name.start_with?("rubocop")
      NodeTyping.with_merge_type(node, :lint_gem)
    elsif gem_name.start_with?("rspec")
      NodeTyping.with_merge_type(node, :test_gem)
    else
      node
    end
  }
}

Using with per-node-type preference

merger = SmartMerger.new(
  template,
  destination,
  node_typing: node_typing,
  preference: {
    default: :destination,
    lint_gem: :template,  # Use template versions for lint gems
    test_gem: :destination  # Keep destination versions for test gems
  }
)

See Also:

Defined Under Namespace

Modules: Normalizer Classes: FrozenWrapper, Wrapper

Class Method Summary collapse

Class Method Details

.frozen(node, merge_type = :frozen) ⇒ FrozenWrapper

Wrap a node as frozen with the Freezable behavior.

Examples:

frozen_node = NodeTyping.frozen(call_node)
frozen_node.freeze_node?  # => true
frozen_node.is_a?(Ast::Merge::Freezable)  # => true

Parameters:

  • node (Object)

    The node to wrap as frozen

  • merge_type (Symbol) (defaults to: :frozen)

    The merge type (defaults to :frozen)

Returns:



99
100
101
# File 'lib/ast/merge/node_typing.rb', line 99

def frozen(node, merge_type = :frozen)
  FrozenWrapper.new(node, merge_type)
end

.frozen_node?(node) ⇒ Boolean

Check if a node is a frozen wrapper.

Parameters:

  • node (Object)

    The node to check

Returns:

  • (Boolean)

    true if the node is a FrozenWrapper or includes Freezable



107
108
109
# File 'lib/ast/merge/node_typing.rb', line 107

def frozen_node?(node)
  node.is_a?(Freezable)
end

.merge_type_for(node) ⇒ Symbol?

Get the merge_type from a node, returning nil if it’s not a typed node.

Parameters:

  • node (Object)

    The node to get merge_type from

Returns:

  • (Symbol, nil)

    The merge_type or nil



123
124
125
# File 'lib/ast/merge/node_typing.rb', line 123

def merge_type_for(node)
  typed_node?(node) ? node.merge_type : nil
end

.process(node, typing_config) ⇒ Object?

Process a node through a typing configuration.

Examples:

config = {
  CallNode: ->(node) {
    NodeTyping.with_merge_type(node, :special_call)
  }
}
result = NodeTyping.process(call_node, config)

Parameters:

  • node (Object)

    The node to process

  • typing_config (Hash{Symbol,String => #call}, nil)

    Hash mapping node type names
    to callables. Keys can be symbols or strings representing node class names
    (e.g., :CallNode, “DefNode”, :Prism_CallNode for fully qualified names)

Returns:

  • (Object, nil)

    The processed node (possibly wrapped with merge_type),
    or nil if the node should be skipped



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/ast/merge/node_typing.rb', line 152

def process(node, typing_config)
  return node unless typing_config
  return node if typing_config.empty?

  # Get the node type name for lookup
  type_key = node_type_key(node)

  # Try to find a matching typing callable
  callable = find_typing_callable(typing_config, type_key, node)
  return node unless callable

  # Call the typing callable with the node.
  # NOTE: For TreeHaver-based backends, the node already has a unified API
  # with #text, #type, #source_position methods. For other backends, they
  # must conform to the same API (either via TreeHaver or equivalent adapter).
  callable.call(node)
end

.typed_node?(node) ⇒ Boolean

Check if a node is a node type wrapper.

Parameters:

  • node (Object)

    The node to check

Returns:

  • (Boolean)

    true if the node is a Wrapper



115
116
117
# File 'lib/ast/merge/node_typing.rb', line 115

def typed_node?(node)
  node.respond_to?(:typed_node?) && node.typed_node?
end

.unwrap(node) ⇒ Object

Unwrap a typed node to get the original node.
Returns the node unchanged if it’s not wrapped.

Parameters:

  • node (Object)

    The node to unwrap

Returns:

  • (Object)

    The unwrapped node



132
133
134
# File 'lib/ast/merge/node_typing.rb', line 132

def unwrap(node)
  typed_node?(node) ? node.unwrap : node
end

.validate!(typing_config) ⇒ void

This method returns an undefined value.

Validate a typing configuration hash.

Parameters:

  • typing_config (Hash, nil)

    The configuration to validate

Raises:

  • (ArgumentError)

    If the configuration is invalid



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/ast/merge/node_typing.rb', line 175

def validate!(typing_config)
  return if typing_config.nil?

  unless typing_config.is_a?(Hash)
    raise ArgumentError, "node_typing must be a Hash, got #{typing_config.class}"
  end

  typing_config.each do |key, value|
    unless key.is_a?(Symbol) || key.is_a?(String)
      raise ArgumentError,
        "node_typing keys must be Symbol or String, got #{key.class} for #{key.inspect}"
    end

    unless value.respond_to?(:call)
      raise ArgumentError,
        "node_typing values must be callable (respond to #call), " \
          "got #{value.class} for key #{key.inspect}"
    end
  end
end

.with_merge_type(node, merge_type) ⇒ Wrapper

Wrap a node with a custom merge_type.

Examples:

typed_node = NodeTyping.with_merge_type(call_node, :config_call)
typed_node.merge_type  # => :config_call
typed_node.name        # => delegates to call_node.name

Parameters:

  • node (Object)

    The node to wrap

  • merge_type (Symbol)

    The merge type to assign

Returns:



85
86
87
# File 'lib/ast/merge/node_typing.rb', line 85

def with_merge_type(node, merge_type)
  Wrapper.new(node, merge_type)
end