Class: Ast::Merge::FreezeNodeBase

Inherits:
Object
  • Object
show all
Includes:
Freezable
Defined in:
lib/ast/merge/freeze_node_base.rb

Overview

Base class for freeze block nodes in AST merge libraries.

A freeze block is a section marked with freeze/unfreeze comment markers that
should be preserved from the destination during merges. The entire content
between the markers is treated as opaque and matched by content identity.

Key Distinction from FrozenWrapper

FreezeNodeBase represents explicit freeze blocks with clear boundaries:

  • Starts with # token:freeze (or equivalent in other comment styles)
  • Ends with # token:unfreeze
  • The content between markers is opaque and preserved verbatim
  • Matched by CONTENT identity via freeze_signature

In contrast, NodeTyping::FrozenWrapper represents AST nodes with freeze markers
in their leading comments
:

  • The marker appears in the node’s leading comments, not as a block boundary
  • The node is still a structural AST element (e.g., a gem call)
  • Matched by the underlying node’s STRUCTURAL identity

Signature Generation Behavior

When FileAnalyzable#generate_signature encounters a FreezeNodeBase, it uses
the freeze_signature method directly, which returns [:FreezeNode, content].
This ensures that explicit freeze blocks are matched by their exact content.

This class provides shared functionality for file-type-specific implementations
(e.g., Prism::Merge::FreezeNode, Psych::Merge::FreezeNode).

Supports multiple comment syntax styles via configurable marker patterns:

  • :hash_comment - Ruby/Python/YAML style (# freeze-begin / # freeze-end)
  • :html_comment - HTML/Markdown style (<!-- freeze-begin --> / <!-- freeze-end -->)
  • :c_style_line - C/JavaScript line comments (// freeze-begin / // freeze-end)
  • :c_style_block - C/JavaScript block comments (/* freeze-begin */ / /* freeze-end */)

Examples:

Freeze block with hash comments (Ruby/YAML)

# <token>:freeze
content to preserve...
# <token>:unfreeze

Freeze block with HTML comments (Markdown)

<!-- <token>:freeze -->
content to preserve...
<!-- <token>:unfreeze -->

Creating a custom pattern

FreezeNodeBase.register_pattern(:custom,
  start: /^--\s*freeze-begin/i,
  end_pattern: /^--\s*freeze-end/i
)

See Also:

Defined Under Namespace

Classes: InvalidStructureError, Location

Constant Summary collapse

MARKER_PATTERNS =

Pattern configuration for freeze block markers.
Mutable to allow runtime registration of custom patterns.

Returns:

  • (Hash{Symbol => Hash{Symbol => Regexp}})

    Registered marker patterns

{
  hash_comment: {
    start: /^\s*#\s*[\w-]+:freeze\b/i,
    end: /^\s*#\s*[\w-]+:unfreeze\b/i,
  },
  html_comment: {
    start: /^\s*<!--\s*[\w-]+:freeze\b.*-->/i,
    end: /^\s*<!--\s*[\w-]+:unfreeze\b.*-->/i,
  },
  c_style_line: {
    start: %r{^\s*//\s*[\w-]+:freeze\b}i,
    end: %r{^\s*//\s*[\w-]+:unfreeze\b}i,
  },
  c_style_block: {
    start: %r{^\s*/\*\s*[\w-]+:freeze\b.*\*/}i,
    end: %r{^\s*/\*\s*[\w-]+:unfreeze\b.*\*/}i,
  },
}
DEFAULT_PATTERN =

Default pattern when none specified

Returns:

  • (Symbol)
:hash_comment

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Freezable

#freeze_signature

Constructor Details

#initialize(start_line:, end_line:, lines: nil, analysis: nil, content: nil, nodes: [], overlapping_nodes: nil, start_marker: nil, end_marker: nil, pattern_type: DEFAULT_PATTERN, reason: nil) ⇒ FreezeNodeBase

Initialize a freeze node.

This unified constructor accepts all parameters that any *-merge gem might need.
Subclasses should call super with the parameters they use.

Content can be provided via:

  • lines: - Direct array of line strings
  • analysis: - FileAnalysis reference (lines extracted via analysis.lines)
  • content: - Direct content string (will be split into lines)

Parameters:

  • start_line (Integer)

    Line number of freeze marker (1-based)

  • end_line (Integer)

    Line number of unfreeze marker (1-based)

  • lines (Array<String>, nil) (defaults to: nil)

    Direct array of source lines

  • analysis (Object, nil) (defaults to: nil)

    FileAnalysis reference for content access

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

    Direct content string

  • nodes (Array) (defaults to: [])

    AST nodes contained within the freeze block

  • overlapping_nodes (Array, nil) (defaults to: nil)

    Nodes that overlap block boundaries

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

    The freeze start marker text

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

    The freeze end marker text

  • pattern_type (Symbol) (defaults to: DEFAULT_PATTERN)

    Pattern type for marker matching

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

    Optional reason extracted from freeze marker



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/ast/merge/freeze_node_base.rb', line 278

def initialize(
  start_line:,
  end_line:,
  lines: nil,
  analysis: nil,
  content: nil,
  nodes: [],
  overlapping_nodes: nil,
  start_marker: nil,
  end_marker: nil,
  pattern_type: DEFAULT_PATTERN,
  reason: nil
)
  @start_line = start_line
  @end_line = end_line
  @start_marker = start_marker
  @end_marker = end_marker
  @pattern_type = pattern_type
  @explicit_reason = reason
  @nodes = nodes
  @overlapping_nodes = overlapping_nodes
  @analysis = analysis

  # Handle content from various sources
  @lines = resolve_lines(lines, analysis, content)
  @content = resolve_content(@lines, content)
end

Instance Attribute Details

#analysisObject? (readonly)

Returns Reference to FileAnalysis (for subclasses that need it).

Returns:

  • (Object, nil)

    Reference to FileAnalysis (for subclasses that need it)



249
250
251
# File 'lib/ast/merge/freeze_node_base.rb', line 249

def analysis
  @analysis
end

#contentString (readonly)

Returns Content of the freeze block.

Returns:

  • (String)

    Content of the freeze block



234
235
236
# File 'lib/ast/merge/freeze_node_base.rb', line 234

def content
  @content
end

#end_lineInteger (readonly)

Returns Line number of unfreeze marker (1-based).

Returns:

  • (Integer)

    Line number of unfreeze marker (1-based)



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

def end_line
  @end_line
end

#end_markerString? (readonly)

Returns The freeze end marker text.

Returns:

  • (String, nil)

    The freeze end marker text



240
241
242
# File 'lib/ast/merge/freeze_node_base.rb', line 240

def end_marker
  @end_marker
end

#linesArray<String>? (readonly)

Returns Lines within the freeze block.

Returns:

  • (Array<String>, nil)

    Lines within the freeze block



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

def lines
  @lines
end

#nodesArray (readonly)

Returns AST nodes contained within the freeze block.

Returns:

  • (Array)

    AST nodes contained within the freeze block



252
253
254
# File 'lib/ast/merge/freeze_node_base.rb', line 252

def nodes
  @nodes
end

#overlapping_nodesArray? (readonly)

Returns Nodes that overlap with the freeze block boundaries.

Returns:

  • (Array, nil)

    Nodes that overlap with the freeze block boundaries



255
256
257
# File 'lib/ast/merge/freeze_node_base.rb', line 255

def overlapping_nodes
  @overlapping_nodes
end

#pattern_typeSymbol (readonly)

Returns The pattern type used for this freeze node.

Returns:

  • (Symbol)

    The pattern type used for this freeze node



243
244
245
# File 'lib/ast/merge/freeze_node_base.rb', line 243

def pattern_type
  @pattern_type
end

#start_lineInteger (readonly)

Returns Line number of freeze marker (1-based).

Returns:

  • (Integer)

    Line number of freeze marker (1-based)



228
229
230
# File 'lib/ast/merge/freeze_node_base.rb', line 228

def start_line
  @start_line
end

#start_markerString? (readonly)

Returns The freeze start marker text.

Returns:

  • (String, nil)

    The freeze start marker text



237
238
239
# File 'lib/ast/merge/freeze_node_base.rb', line 237

def start_marker
  @start_marker
end

Class Method Details

.end_pattern(pattern_type = DEFAULT_PATTERN) ⇒ Regexp

Get end marker pattern for a given pattern type

Parameters:

  • pattern_type (Symbol) (defaults to: DEFAULT_PATTERN)

    Pattern type name (defaults to DEFAULT_PATTERN)

Returns:

  • (Regexp)

    End marker regex

Raises:

  • (ArgumentError)

    if pattern type not found



151
152
153
154
155
156
# File 'lib/ast/merge/freeze_node_base.rb', line 151

def end_pattern(pattern_type = DEFAULT_PATTERN)
  patterns = MARKER_PATTERNS[pattern_type]
  raise ArgumentError, "Unknown pattern type: #{pattern_type}" unless patterns

  patterns[:end]
end

.freeze_end?(line, pattern_type = DEFAULT_PATTERN) ⇒ Boolean

Check if a line matches a freeze end marker

Parameters:

  • line (String)

    Line content to check

  • pattern_type (Symbol) (defaults to: DEFAULT_PATTERN)

    Pattern type to use (defaults to DEFAULT_PATTERN)

Returns:

  • (Boolean)


214
215
216
217
218
# File 'lib/ast/merge/freeze_node_base.rb', line 214

def freeze_end?(line, pattern_type = DEFAULT_PATTERN)
  return false if line.nil?

  end_pattern(pattern_type).match?(line)
end

.freeze_start?(line, pattern_type = DEFAULT_PATTERN) ⇒ Boolean

Check if a line matches a freeze start marker

Parameters:

  • line (String)

    Line content to check

  • pattern_type (Symbol) (defaults to: DEFAULT_PATTERN)

    Pattern type to use (defaults to DEFAULT_PATTERN)

Returns:

  • (Boolean)


204
205
206
207
208
# File 'lib/ast/merge/freeze_node_base.rb', line 204

def freeze_start?(line, pattern_type = DEFAULT_PATTERN)
  return false if line.nil?

  start_pattern(pattern_type).match?(line)
end

.pattern_for(pattern_type = DEFAULT_PATTERN, token = nil) ⇒ Hash{Symbol => Regexp}, Regexp

Get both start and end patterns for a given pattern type
When token is provided, returns a combined pattern with capture groups
for marker type (freeze/unfreeze) and optional reason.

Examples:

Without token (returns hash of patterns)

FreezeNode.pattern_for(:hash_comment)
# => { start: /.../, end: /.../ }

With token (returns combined pattern with capture groups)

FreezeNode.pattern_for(:hash_comment, "my-merge")
# => /^\s*#\s*my-merge:(freeze|unfreeze)\b\s*(.*)?$/i
# Capture group 1: "freeze" or "unfreeze"
# Capture group 2: optional reason text

Parameters:

  • pattern_type (Symbol) (defaults to: DEFAULT_PATTERN)

    Pattern type name (defaults to DEFAULT_PATTERN)

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

    Optional freeze token to build specific pattern

Returns:

  • (Hash{Symbol => Regexp}, Regexp)

    Hash with :start/:end keys, or combined Regexp if token provided

Raises:

  • (ArgumentError)

    if pattern type not found



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

def pattern_for(pattern_type = DEFAULT_PATTERN, token = nil)
  raise ArgumentError, "Unknown pattern type: #{pattern_type}" unless MARKER_PATTERNS.key?(pattern_type)

  # If no token provided, return the static patterns hash
  return MARKER_PATTERNS[pattern_type] unless token

  # Build a combined pattern with capture groups for the specific token
  escaped_token = Regexp.escape(token)

  case pattern_type
  when :hash_comment
    /^\s*#\s*#{escaped_token}:(freeze|unfreeze)\b\s*(.*)?$/i
  when :html_comment
    /^\s*<!--\s*#{escaped_token}:(freeze|unfreeze)(?:\s+(.+?))?\s*-->/i
  when :c_style_line
    %r{^\s*//\s*#{escaped_token}:(freeze|unfreeze)\b\s*(.*)?$}i
  when :c_style_block
    %r{^\s*/\*\s*#{escaped_token}:(freeze|unfreeze)\b\s*(.*)? *\*/}i
  else
    # Fallback for custom registered patterns - can't build token-specific
    raise ArgumentError, "Cannot build token-specific pattern for custom type: #{pattern_type}"
  end
end

.pattern_typesArray<Symbol>

Available pattern types

Returns:

  • (Array<Symbol>)


222
223
224
# File 'lib/ast/merge/freeze_node_base.rb', line 222

def pattern_types
  MARKER_PATTERNS.keys
end

.register_pattern(name, start:, end_pattern:) ⇒ Hash{Symbol => Regexp}

Register a custom marker pattern

Parameters:

  • name (Symbol)

    Pattern name

  • start (Regexp)

    Regex to match freeze start marker

  • end_pattern (Regexp)

    Regex to match freeze end marker

Returns:

  • (Hash{Symbol => Regexp})

    The registered pattern

Raises:

  • (ArgumentError)

    if name already exists or patterns invalid



128
129
130
131
132
133
134
# File 'lib/ast/merge/freeze_node_base.rb', line 128

def register_pattern(name, start:, end_pattern:)
  raise ArgumentError, "Pattern :#{name} already registered" if MARKER_PATTERNS.key?(name)
  raise ArgumentError, "Start pattern must be a Regexp" unless start.is_a?(Regexp)
  raise ArgumentError, "End pattern must be a Regexp" unless end_pattern.is_a?(Regexp)

  MARKER_PATTERNS[name] = {start: start, end: end_pattern}
end

.start_pattern(pattern_type = DEFAULT_PATTERN) ⇒ Regexp

Get start marker pattern for a given pattern type

Parameters:

  • pattern_type (Symbol) (defaults to: DEFAULT_PATTERN)

    Pattern type name (defaults to DEFAULT_PATTERN)

Returns:

  • (Regexp)

    Start marker regex

Raises:

  • (ArgumentError)

    if pattern type not found



140
141
142
143
144
145
# File 'lib/ast/merge/freeze_node_base.rb', line 140

def start_pattern(pattern_type = DEFAULT_PATTERN)
  patterns = MARKER_PATTERNS[pattern_type]
  raise ArgumentError, "Unknown pattern type: #{pattern_type}" unless patterns

  patterns[:start]
end

Instance Method Details

#freeze_node?Boolean

Check if this is a freeze node (always true for FreezeNode)

Returns:

  • (Boolean)


353
354
355
# File 'lib/ast/merge/freeze_node_base.rb', line 353

def freeze_node?
  true
end

#inspectString

String representation for debugging

Returns:

  • (String)


375
376
377
# File 'lib/ast/merge/freeze_node_base.rb', line 375

def inspect
  "#<#{self.class.name} lines=#{start_line}..#{end_line} pattern=#{pattern_type}>"
end

#locationLocation

Returns a location-like object for compatibility with AST nodes

Returns:



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

def location
  @location ||= Location.new(@start_line, @end_line)
end

#merge_typeSymbol Also known as: type

Node type for merge classification

Returns:

  • (Symbol)

    :freeze_block



359
360
361
# File 'lib/ast/merge/freeze_node_base.rb', line 359

def merge_type
  :freeze_block
end

#reasonString?

Extract the reason/comment from the freeze start marker.
The reason is any text after the freeze directive.
If an explicit reason was provided at initialization, that takes precedence.

Examples:

With reason

# rbs-merge:freeze Custom reason here
=> "Custom reason here"

Without reason

# rbs-merge:freeze
=> nil

Returns:

  • (String, nil)

    The reason text, or nil if not present



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/ast/merge/freeze_node_base.rb', line 325

def reason
  # Return explicit reason if provided at initialization
  return @explicit_reason if @explicit_reason

  return unless @start_marker

  # Use the canonical pattern which has capture group 2 for reason
  # We need to extract the token from the marker first
  token = extract_token_from_marker
  return unless token

  pattern = self.class.pattern_for(@pattern_type, token)
  match = @start_marker.match(pattern)
  return unless match

  # Capture group 2 is the reason text
  reason_text = match[2]&.strip
  reason_text&.empty? ? nil : reason_text
end

#signatureArray

Returns a stable signature for this freeze block.
Override in subclasses for file-type-specific normalization.

Returns:

  • (Array)

    Signature array



369
370
371
# File 'lib/ast/merge/freeze_node_base.rb', line 369

def signature
  [:FreezeNode, @content&.strip]
end

#sliceString

Returns the freeze block content

Returns:

  • (String)


347
348
349
# File 'lib/ast/merge/freeze_node_base.rb', line 347

def slice
  @content
end

#to_sString

Returns:

  • (String)


380
381
382
# File 'lib/ast/merge/freeze_node_base.rb', line 380

def to_s
  inspect
end