Class: Ast::Merge::Navigable::Statement

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

Overview

Wraps any node (parser-backed or synthetic) with uniform navigation.

Provides two levels of navigation:

  1. Flat list navigation: prev_statement, next_statement, index
    • Works for ALL nodes (synthetic and parser-backed)
    • Represents position in the flattened statement list
  2. Tree navigation: tree_parent, tree_next, tree_previous, tree_children
    • Only available for parser-backed nodes
    • Delegates to inner_node’s tree methods

This allows code to work with the flat list for simple merging,
while still accessing tree structure for section-aware operations.

Examples:

Basic usage

statements = Statement.build_list(raw_statements)
stmt = statements[0]

# Flat navigation (always works)
stmt.next           # => next statement in flat list
stmt.previous       # => previous statement in flat list
stmt.index          # => position in array

# Tree navigation (when available)
stmt.tree_parent    # => parent in original AST (or nil)
stmt.tree_next      # => next sibling in original AST (or nil)
stmt.tree_children  # => children in original AST (or [])

Section grouping

# Group statements into sections by heading level
sections = Statement.group_by_heading(statements, level: 3)
sections.each do |section|
  puts "Section: #{section.heading.text}"
  section.statements.each { |s| puts "  - #{s.type}" }
end

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(node, index:) ⇒ Statement

Initialize a Statement wrapper.

Parameters:

  • node (Object)

    The node to wrap

  • index (Integer)

    Position in the statement list



62
63
64
65
66
67
68
# File 'lib/ast/merge/navigable/statement.rb', line 62

def initialize(node, index:)
  @node = node
  @index = index
  @prev_statement = nil
  @next_statement = nil
  @context = nil
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object

Delegate unknown methods to the wrapped node.



366
367
368
369
370
371
372
# File 'lib/ast/merge/navigable/statement.rb', line 366

def method_missing(method, *args, &block)
  if node.respond_to?(method)
    node.send(method, *args, &block)
  else
    super
  end
end

Instance Attribute Details

#contextObject?

Returns Optional context/metadata for this statement.

Returns:

  • (Object, nil)

    Optional context/metadata for this statement



56
57
58
# File 'lib/ast/merge/navigable/statement.rb', line 56

def context
  @context
end

#indexInteger (readonly)

Returns Index in the flattened statement list.

Returns:

  • (Integer)

    Index in the flattened statement list



47
48
49
# File 'lib/ast/merge/navigable/statement.rb', line 47

def index
  @index
end

#next_statementStatement?

Returns Next statement in flat list.

Returns:

  • (Statement, nil)

    Next statement in flat list



53
54
55
# File 'lib/ast/merge/navigable/statement.rb', line 53

def next_statement
  @next_statement
end

#nodeObject (readonly)

Returns The wrapped node (parser-backed or synthetic).

Returns:

  • (Object)

    The wrapped node (parser-backed or synthetic)



44
45
46
# File 'lib/ast/merge/navigable/statement.rb', line 44

def node
  @node
end

#prev_statementStatement?

Returns Previous statement in flat list.

Returns:

  • (Statement, nil)

    Previous statement in flat list



50
51
52
# File 'lib/ast/merge/navigable/statement.rb', line 50

def prev_statement
  @prev_statement
end

Class Method Details

.build_list(raw_statements) ⇒ Array<Statement>

Build a linked list of Statements from raw statements.

Parameters:

  • raw_statements (Array<Object>)

    Raw statement nodes

Returns:

  • (Array<Statement>)

    Linked statement list



75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/ast/merge/navigable/statement.rb', line 75

def build_list(raw_statements)
  statements = raw_statements.each_with_index.map do |node, i|
    new(node, index: i)
  end

  # Link siblings in flat list
  statements.each_cons(2) do |prev_stmt, next_stmt|
    prev_stmt.next_statement = next_stmt
    next_stmt.prev_statement = prev_stmt
  end

  statements
end

.find_first(statements, type: nil, text: nil) {|Statement| ... } ⇒ Statement?

Find the first statement matching criteria.

Parameters:

  • statements (Array<Statement>)

    Statement list

  • type (Symbol, String, nil) (defaults to: nil)

    Node type to match

  • text (String, Regexp, nil) (defaults to: nil)

    Text pattern to match

Yields:

  • (Statement)

    Optional block for custom matching

Returns:

  • (Statement, nil)

    First matching statement



116
117
118
# File 'lib/ast/merge/navigable/statement.rb', line 116

def find_first(statements, type: nil, text: nil, &block)
  find_matching(statements, type: type, text: text, &block).first
end

.find_matching(statements, type: nil, text: nil) {|Statement| ... } ⇒ Array<Statement>

Find statements matching a query.

Parameters:

  • statements (Array<Statement>)

    Statement list

  • type (Symbol, String, nil) (defaults to: nil)

    Node type to match (nil = any)

  • text (String, Regexp, nil) (defaults to: nil)

    Text pattern to match

Yields:

  • (Statement)

    Optional block for custom matching

Returns:



96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/ast/merge/navigable/statement.rb', line 96

def find_matching(statements, type: nil, text: nil, &block)
  # If no criteria specified, return empty array (nothing to match)
  return [] if type.nil? && text.nil? && !block_given?

  statements.select do |stmt|
    matches = true
    matches &&= stmt.type.to_s == type.to_s if type
    matches &&= text.is_a?(Regexp) ? stmt.text.match?(text) : stmt.text.include?(text.to_s) if text
    matches &&= yield(stmt) if block_given?
    matches
  end
end

Instance Method Details

#each_following {|Statement| ... } ⇒ Enumerator?

Iterate from this statement to the end (or until block returns false).

Yields:

Returns:

  • (Enumerator, nil)


149
150
151
152
153
154
155
156
157
# File 'lib/ast/merge/navigable/statement.rb', line 149

def each_following(&block)
  return to_enum(:each_following) unless block_given?

  current = self.next
  while current
    break unless yield(current)
    current = current.next
  end
end

#end_lineInteger?

Returns End line number.

Returns:

  • (Integer, nil)

    End line number



295
296
297
298
# File 'lib/ast/merge/navigable/statement.rb', line 295

def end_line
  pos = source_position
  pos[:end_line] if pos
end

#first?Boolean

Returns true if this is the first statement.

Returns:

  • (Boolean)

    true if this is the first statement



136
137
138
# File 'lib/ast/merge/navigable/statement.rb', line 136

def first?
  prev_statement.nil?
end

#has_tree_navigation?Boolean

Returns true if tree navigation is available.

Returns:

  • (Boolean)

    true if tree navigation is available



220
221
222
223
# File 'lib/ast/merge/navigable/statement.rb', line 220

def has_tree_navigation?
  inner = unwrapped_node
  inner.respond_to?(:parent) || inner.respond_to?(:next)
end

#inspectString

Returns Human-readable representation.

Returns:

  • (String)

    Human-readable representation



356
357
358
# File 'lib/ast/merge/navigable/statement.rb', line 356

def inspect
  "#<Navigable::Statement[#{index}] type=#{type} tree=#{has_tree_navigation?}>"
end

#last?Boolean

Returns true if this is the last statement.

Returns:

  • (Boolean)

    true if this is the last statement



141
142
143
# File 'lib/ast/merge/navigable/statement.rb', line 141

def last?
  next_statement.nil?
end

#nextStatement?

Returns Next statement in flat list.

Returns:

  • (Statement, nil)

    Next statement in flat list



126
127
128
# File 'lib/ast/merge/navigable/statement.rb', line 126

def next
  next_statement
end

#node_attribute(name, *aliases) ⇒ Object?

Get an attribute from the underlying node.

Tries multiple method names to support different parser APIs.

Parameters:

  • name (Symbol, String)

    Attribute name

  • aliases (Array<Symbol>)

    Alternative method names

Returns:

  • (Object, nil)

    Attribute value



332
333
334
335
336
337
338
# File 'lib/ast/merge/navigable/statement.rb', line 332

def node_attribute(name, *aliases)
  inner = unwrapped_node
  [name, *aliases].each do |method_name|
    return inner.send(method_name) if inner.respond_to?(method_name)
  end
  nil
end

#previousStatement?

Returns Previous statement in flat list.

Returns:

  • (Statement, nil)

    Previous statement in flat list



131
132
133
# File 'lib/ast/merge/navigable/statement.rb', line 131

def previous
  prev_statement
end

#respond_to_missing?(method, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


374
375
376
# File 'lib/ast/merge/navigable/statement.rb', line 374

def respond_to_missing?(method, include_private = false)
  node.respond_to?(method, include_private) || super
end

#same_or_shallower_than?(other) ⇒ Boolean

Check if this node is at same or shallower depth than another.
Useful for determining section boundaries.

Parameters:

  • other (Statement, Integer)

    Other statement or depth value

Returns:

  • (Boolean)

    true if this node is at same or shallower depth



253
254
255
256
# File 'lib/ast/merge/navigable/statement.rb', line 253

def same_or_shallower_than?(other)
  other_depth = other.is_a?(Integer) ? other : other.tree_depth
  tree_depth <= other_depth
end

#signatureArray, ...

Returns Node signature for matching.

Returns:

  • (Array, Object, nil)

    Node signature for matching



272
273
274
# File 'lib/ast/merge/navigable/statement.rb', line 272

def signature
  node.signature if node.respond_to?(:signature)
end

#source_positionHash?

Returns Source position info.

Returns:

  • (Hash, nil)

    Source position info



284
285
286
# File 'lib/ast/merge/navigable/statement.rb', line 284

def source_position
  node.source_position if node.respond_to?(:source_position)
end

#start_lineInteger?

Returns Start line number.

Returns:

  • (Integer, nil)

    Start line number



289
290
291
292
# File 'lib/ast/merge/navigable/statement.rb', line 289

def start_line
  pos = source_position
  pos[:start_line] if pos
end

#synthetic?Boolean

Returns true if this is a synthetic node (no tree navigation).

Returns:

  • (Boolean)

    true if this is a synthetic node (no tree navigation)



226
227
228
# File 'lib/ast/merge/navigable/statement.rb', line 226

def synthetic?
  !has_tree_navigation?
end

#take_until {|Statement| ... } ⇒ Array<Statement>

Collect statements until a condition is met.

Yields:

Returns:

  • (Array<Statement>)

    Statements until condition



163
164
165
166
167
168
169
170
171
# File 'lib/ast/merge/navigable/statement.rb', line 163

def take_until(&block)
  result = []
  each_following do |stmt|
    break if yield(stmt)
    result << stmt
    true
  end
  result
end

#textString

Returns Node text content.

Returns:

  • (String)

    Node text content



277
278
279
280
281
# File 'lib/ast/merge/navigable/statement.rb', line 277

def text
  # TreeHaver nodes (and any node conforming to the unified API) provide #text.
  # No conditional fallbacks - nodes must conform to the API.
  node.text.to_s
end

#text_matches?(pattern) ⇒ Boolean

Check if this node’s text matches a pattern.

Parameters:

  • pattern (String, Regexp)

    Pattern to match

Returns:

  • (Boolean)


316
317
318
319
320
321
322
323
# File 'lib/ast/merge/navigable/statement.rb', line 316

def text_matches?(pattern)
  case pattern
  when Regexp
    text.match?(pattern)
  else
    text.include?(pattern.to_s)
  end
end

#to_sString

Returns String representation.

Returns:

  • (String)

    String representation



361
362
363
# File 'lib/ast/merge/navigable/statement.rb', line 361

def to_s
  text.to_s.strip[0, 50]
end

#tree_childrenArray<Object>

Returns Children in original AST.

Returns:

  • (Array<Object>)

    Children in original AST



196
197
198
199
200
201
202
203
204
205
# File 'lib/ast/merge/navigable/statement.rb', line 196

def tree_children
  inner = unwrapped_node
  if inner.respond_to?(:each)
    inner.to_a
  elsif inner.respond_to?(:children)
    inner.children
  else
    []
  end
end

#tree_depthInteger

Calculate the tree depth (distance from root).

Returns:

  • (Integer)

    Depth in tree (0 = root level)



233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/ast/merge/navigable/statement.rb', line 233

def tree_depth
  depth = 0
  current = tree_parent
  while current
    depth += 1
    # Navigate up through parents
    if current.respond_to?(:parent)
      current = current.parent
    else
      break
    end
  end
  depth
end

#tree_first_childObject?

Returns First child in original AST.

Returns:

  • (Object, nil)

    First child in original AST



208
209
210
211
# File 'lib/ast/merge/navigable/statement.rb', line 208

def tree_first_child
  inner = unwrapped_node
  inner.first_child if inner.respond_to?(:first_child)
end

#tree_last_childObject?

Returns Last child in original AST.

Returns:

  • (Object, nil)

    Last child in original AST



214
215
216
217
# File 'lib/ast/merge/navigable/statement.rb', line 214

def tree_last_child
  inner = unwrapped_node
  inner.last_child if inner.respond_to?(:last_child)
end

#tree_nextObject?

Returns Next sibling in original AST.

Returns:

  • (Object, nil)

    Next sibling in original AST



184
185
186
187
# File 'lib/ast/merge/navigable/statement.rb', line 184

def tree_next
  inner = unwrapped_node
  inner.next if inner.respond_to?(:next)
end

#tree_parentObject?

Returns Parent node in original AST.

Returns:

  • (Object, nil)

    Parent node in original AST



178
179
180
181
# File 'lib/ast/merge/navigable/statement.rb', line 178

def tree_parent
  inner = unwrapped_node
  inner.parent if inner.respond_to?(:parent)
end

#tree_previousObject?

Returns Previous sibling in original AST.

Returns:

  • (Object, nil)

    Previous sibling in original AST



190
191
192
193
# File 'lib/ast/merge/navigable/statement.rb', line 190

def tree_previous
  inner = unwrapped_node
  inner.previous if inner.respond_to?(:previous)
end

#typeSymbol, String

Returns Node type.

Returns:

  • (Symbol, String)

    Node type



263
264
265
266
267
268
269
# File 'lib/ast/merge/navigable/statement.rb', line 263

def type
  return node.type if node.respond_to?(:type)

  # Fallback: derive type from class name (handle anonymous classes)
  class_name = node.class.name
  class_name ? class_name.split("::").last : "Anonymous"
end

#type?(expected_type) ⇒ Boolean

Check if this node matches a type.

Parameters:

  • expected_type (Symbol, String)

    Type to check

Returns:

  • (Boolean)


308
309
310
# File 'lib/ast/merge/navigable/statement.rb', line 308

def type?(expected_type)
  type.to_s == expected_type.to_s
end

#unwrapped_nodeObject

Get the unwrapped inner node.

Returns:

  • (Object)

    The innermost node



347
348
349
350
351
352
353
# File 'lib/ast/merge/navigable/statement.rb', line 347

def unwrapped_node
  current = node
  while current.respond_to?(:inner_node) && current.inner_node != current
    current = current.inner_node
  end
  current
end