Class: Ast::Merge::NodeWrapperBase Abstract

Inherits:
Object
  • Object
show all
Defined in:
lib/ast/merge/node_wrapper_base.rb

Overview

This class is abstract.

Subclass and implement #compute_signature

Base class for format-specific node wrappers used in *-merge gems.

This provides common functionality for wrapping TreeHaver nodes with:

  • Source context (lines, source string)
  • Line information (start_line, end_line)
  • Comment associations (leading_comments, inline_comment)
  • Content extraction (text, content)
  • Signature generation (abstract)

Relationship to NodeTyping::Wrapper

This class is DIFFERENT from Ast::Merge::NodeTyping::Wrapper:

  • NodeWrapperBase: Provides format-specific functionality (line info, signatures, comments, type predicates). Used to wrap raw TreeHaver nodes with rich context needed for merging.

  • NodeTyping::Wrapper: Adds a custom merge_type attribute for merge classification. Used by SmartMergerBase to apply custom typing rules.

A node CAN be wrapped by both:

NodeTyping::Wrapper(Toml::Merge::NodeWrapper(tree_sitter_node))

The NodeTyping.unwrap method handles unwrapping NodeTyping::Wrapper, while NodeWrapperBase#node provides access to the underlying TreeHaver node.

Subclass Responsibilities

Subclasses MUST implement:

  • #compute_signature(node) - Generate a signature for node matching

Subclasses SHOULD implement format-specific type predicates:

  • TOML: #table?, #pair?, #array_of_tables?, etc.
  • JSON: #object?, #array?, #pair?, etc.
  • Bash: #function_definition?, #variable_assignment?, etc.

Examples:

Creating a format-specific wrapper

class NodeWrapper < Ast::Merge::NodeWrapperBase
  def table?
    type == :table
  end

  private

  def compute_signature(node)
    case node.type.to_sym
    when :table
      [:table, table_name]
    else
      [node.type.to_sym]
    end
  end
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(node, lines:, source: nil, leading_comments: [], inline_comment: nil, **options) ⇒ NodeWrapperBase

Initialize the node wrapper with source context.

Parameters:

  • node (Object)

    TreeHaver node to wrap

  • lines (Array<String>)

    Source lines for content extraction

  • source (String, nil) (defaults to: nil)

    Original source string for byte-based text extraction

  • leading_comments (Array<Hash>) (defaults to: [])

    Comments before this node

  • inline_comment (Hash, nil) (defaults to: nil)

    Inline comment on the node's line

  • options (Hash)

    Additional options for subclasses (forward compatibility)



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/ast/merge/node_wrapper_base.rb', line 92

def initialize(node, lines:, source: nil, leading_comments: [], inline_comment: nil, **options)
  @node = node
  @lines = lines
  @source = source || lines.join("\n")
  @leading_comments = leading_comments
  @inline_comment = inline_comment

  # Store additional options for subclasses to use
  process_additional_options(options)

  # Extract line information from the node (0-indexed to 1-indexed)
  extract_line_info(node)

  # Handle edge case where end_line might be before start_line
  @end_line = @start_line if @start_line && @end_line && @end_line < @start_line
end

Instance Attribute Details

#end_lineInteger? (readonly)

Returns End line (1-based).

Returns:

  • (Integer, nil)

    End line (1-based)



82
83
84
# File 'lib/ast/merge/node_wrapper_base.rb', line 82

def end_line
  @end_line
end

#inline_commentHash? (readonly)

Returns Inline/trailing comment on the same line.

Returns:

  • (Hash, nil)

    Inline/trailing comment on the same line



76
77
78
# File 'lib/ast/merge/node_wrapper_base.rb', line 76

def inline_comment
  @inline_comment
end

#leading_commentsArray<Hash> (readonly)

Returns Leading comments associated with this node.

Returns:

  • (Array<Hash>)

    Leading comments associated with this node



73
74
75
# File 'lib/ast/merge/node_wrapper_base.rb', line 73

def leading_comments
  @leading_comments
end

#linesArray<String> (readonly)

Returns Source lines for content extraction.

Returns:

  • (Array<String>)

    Source lines for content extraction



67
68
69
# File 'lib/ast/merge/node_wrapper_base.rb', line 67

def lines
  @lines
end

#nodeObject (readonly)

Returns The wrapped TreeHaver node.

Returns:

  • (Object)

    The wrapped TreeHaver node



64
65
66
# File 'lib/ast/merge/node_wrapper_base.rb', line 64

def node
  @node
end

#sourceString (readonly)

Returns The original source string.

Returns:

  • (String)

    The original source string



70
71
72
# File 'lib/ast/merge/node_wrapper_base.rb', line 70

def source
  @source
end

#start_lineInteger? (readonly)

Returns Start line (1-based).

Returns:

  • (Integer, nil)

    Start line (1-based)



79
80
81
# File 'lib/ast/merge/node_wrapper_base.rb', line 79

def start_line
  @start_line
end

Instance Method Details

#childrenArray<NodeWrapperBase>

Get children wrapped as NodeWrappers. Override in subclasses to return wrapped children.

Returns:



251
252
253
254
255
256
257
258
259
# File 'lib/ast/merge/node_wrapper_base.rb', line 251

def children
  return [] unless @node.respond_to?(:each)

  result = []
  @node.each do |child|
    result << wrap_child(child)
  end
  result
end

#comment_attachment(style: nil, **metadata) ⇒ Comment::Attachment

Build a shared attachment from the wrapper's raw comment hashes.

This provides an incremental bridge from existing leading_comments / inline_comment flows to the normalized merge-facing comment model.

Parameters:

  • style (Comment::Style, Symbol, nil) (defaults to: nil)

    line-comment style to use for conversion

  • metadata (Hash)

    extra attachment metadata

Returns:



202
203
204
205
206
207
208
209
210
211
# File 'lib/ast/merge/node_wrapper_base.rb', line 202

def comment_attachment(style: nil, **)
  Comment::Attachment.new(
    owner: self,
    leading_region: leading_comment_region(style: style, **),
    inline_region: inline_comment_region(style: style, **),
    metadata: {
      source: :node_wrapper_base
    }.merge()
  )
end

#container?Boolean

Check if this node is a container (has children for merging). Override in subclasses to define container types.

Returns:

  • (Boolean)


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

def container?
  false
end

#contentString

Get the content for this node from source lines.

Returns:

  • (String)


170
171
172
173
174
# File 'lib/ast/merge/node_wrapper_base.rb', line 170

def content
  return '' unless @start_line && @end_line

  (@start_line..@end_line).map { |ln| @lines[ln - 1] }.compact.join("\n")
end

#freeze_node?Boolean

Check if this is a freeze node. Override in subclasses if freeze node detection differs.

Returns:

  • (Boolean)


139
140
141
# File 'lib/ast/merge/node_wrapper_base.rb', line 139

def freeze_node?
  false
end

#inline_comment_region(style: nil, **metadata) ⇒ Comment::Region?

Convert the wrapper's raw inline comment hash into a shared region.

Parameters:

  • style (Comment::Style, Symbol, nil) (defaults to: nil)

    line-comment style to use for conversion

  • metadata (Hash)

    extra region metadata

Returns:



190
191
192
# File 'lib/ast/merge/node_wrapper_base.rb', line 190

def inline_comment_region(style: nil, **)
  comments_to_region(:inline, inline_comment ? [inline_comment] : [], style: style, **)
end

#inspectString

String representation for debugging.

Returns:

  • (String)


270
271
272
# File 'lib/ast/merge/node_wrapper_base.rb', line 270

def inspect
  "#<#{self.class.name} type=#{@node.type} lines=#{@start_line}..#{@end_line}>"
end

#leading_comment_freeze?(freeze_token, style: nil, **metadata) ⇒ Boolean

Check whether the wrapper's leading comment region contains a freeze directive.

Parameters:

  • freeze_token (String)

    Freeze token to detect

  • style (Comment::Style, Symbol, nil) (defaults to: nil)

    line-comment style to use for conversion

  • metadata (Hash)

    extra region metadata

Returns:

  • (Boolean)


219
220
221
222
# File 'lib/ast/merge/node_wrapper_base.rb', line 219

def leading_comment_freeze?(freeze_token, style: nil, **)
  region = leading_comment_region(style: style, **)
  region.respond_to?(:freeze?) && region.freeze?(freeze_token)
end

#leading_comment_region(style: nil, **metadata) ⇒ Comment::Region?

Convert the wrapper's raw leading comment hashes into a shared region.

Parameters:

  • style (Comment::Style, Symbol, nil) (defaults to: nil)

    line-comment style to use for conversion

  • metadata (Hash)

    extra region metadata

Returns:



181
182
183
# File 'lib/ast/merge/node_wrapper_base.rb', line 181

def leading_comment_region(style: nil, **)
  comments_to_region(:leading, leading_comments, style: style, **)
end

#leading_comment_unfreeze?(freeze_token, style: nil, **metadata) ⇒ Boolean

Check whether the wrapper's leading comment region contains an unfreeze directive.

Parameters:

  • freeze_token (String)

    Freeze token to detect

  • style (Comment::Style, Symbol, nil) (defaults to: nil)

    line-comment style to use for conversion

  • metadata (Hash)

    extra region metadata

Returns:

  • (Boolean)


230
231
232
233
# File 'lib/ast/merge/node_wrapper_base.rb', line 230

def leading_comment_unfreeze?(freeze_token, style: nil, **)
  region = leading_comment_region(style: style, **)
  region.respond_to?(:unfreeze?) && region.unfreeze?(freeze_token)
end

#leaf?Boolean

Check if this node is a leaf (no mergeable children).

Returns:

  • (Boolean)


244
245
246
# File 'lib/ast/merge/node_wrapper_base.rb', line 244

def leaf?
  !container?
end

#mergeable_childrenArray<NodeWrapperBase>

Get mergeable children - the semantically meaningful children for tree merging. Override in subclasses to return format-specific mergeable children.

Returns:



264
265
266
# File 'lib/ast/merge/node_wrapper_base.rb', line 264

def mergeable_children
  children
end

#node_text(ts_node) ⇒ String

Extract text from a node using byte positions. Uses byteslice for correct multi-byte character handling (e.g. emoji).

Parameters:

  • ts_node (Object)

    The TreeHaver node

Returns:

  • (String)


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

def node_text(ts_node)
  return '' unless ts_node.respond_to?(:start_byte) && ts_node.respond_to?(:end_byte)

  length = ts_node.end_byte - ts_node.start_byte
  text = @source.byteslice(ts_node.start_byte, length)
  return text if text&.valid_encoding?

  # Some parser adapters expose character offsets through byte-named
  # methods. Fall back before scrubbing so Unicode source is preserved.
  text = @source[ts_node.start_byte, length]
  return text if text&.valid_encoding?

  text.to_s.scrub
end

#node_wrapper?Boolean

Returns true to indicate this is a node wrapper. Used to distinguish from NodeTyping::Wrapper.

Returns:

  • (Boolean)


277
278
279
# File 'lib/ast/merge/node_wrapper_base.rb', line 277

def node_wrapper?
  true
end

#process_additional_options(options) ⇒ Object

Process additional options. Override in subclasses to handle format-specific options.

Parameters:

  • options (Hash)

    Additional options



111
112
113
# File 'lib/ast/merge/node_wrapper_base.rb', line 111

def process_additional_options(options)
  # Default: no-op. Subclasses can override to process options like :backend, :document_root
end

#signatureArray?

Generate a signature for this node for matching purposes. Signatures are used to identify corresponding nodes between template and destination.

Returns:

  • (Array, nil)

    Signature array or nil if not signaturable



119
120
121
# File 'lib/ast/merge/node_wrapper_base.rb', line 119

def signature
  compute_signature(@node)
end

#textString

Get the text content for this node by extracting from source using byte positions.

Returns:

  • (String)


145
146
147
# File 'lib/ast/merge/node_wrapper_base.rb', line 145

def text
  node_text(@node)
end

#typeSymbol

Get the node type as a symbol.

Returns:

  • (Symbol)


125
126
127
# File 'lib/ast/merge/node_wrapper_base.rb', line 125

def type
  @node.type.to_sym
end

#type?(type_name) ⇒ Boolean

Check if this node has a specific type.

Parameters:

  • type_name (Symbol, String)

    Type to check

Returns:

  • (Boolean)


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

def type?(type_name)
  @node.type.to_s == type_name.to_s
end

#underlying_nodeObject

Get the underlying TreeHaver node. Note: This is NOT the same as NodeTyping::Wrapper#unwrap which removes the typing wrapper. This method provides access to the raw parser node.

Returns:

  • (Object)

    The underlying TreeHaver node



285
286
287
# File 'lib/ast/merge/node_wrapper_base.rb', line 285

def underlying_node
  @node
end