Module: Ast::Merge::NodeTyping

Defined in:
lib/ast/merge/node_typing.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.

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

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:



238
239
240
# File 'lib/ast/merge/node_typing.rb', line 238

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



246
247
248
# File 'lib/ast/merge/node_typing.rb', line 246

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



262
263
264
# File 'lib/ast/merge/node_typing.rb', line 262

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



291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/ast/merge/node_typing.rb', line 291

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



254
255
256
# File 'lib/ast/merge/node_typing.rb', line 254

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



271
272
273
# File 'lib/ast/merge/node_typing.rb', line 271

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



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/ast/merge/node_typing.rb', line 311

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:



224
225
226
# File 'lib/ast/merge/node_typing.rb', line 224

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