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



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
305
306
# File 'lib/ast/merge/freeze_node_base.rb', line 280

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)



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

def analysis
  @analysis
end

#contentString (readonly)

Returns Content of the freeze block.

Returns:

  • (String)

    Content of the freeze block



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

def content
  @content
end

#end_lineInteger (readonly)

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

Returns:

  • (Integer)

    Line number of unfreeze marker (1-based)



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

def end_line
  @end_line
end

#end_markerString? (readonly)

Returns The freeze end marker text.

Returns:

  • (String, nil)

    The freeze end marker text



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

def end_marker
  @end_marker
end

#linesArray<String>? (readonly)

Returns Lines within the freeze block.

Returns:

  • (Array<String>, nil)

    Lines within the freeze block



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

def lines
  @lines
end

#nodesArray (readonly)

Returns AST nodes contained within the freeze block.

Returns:

  • (Array)

    AST nodes contained within the freeze block



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

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



257
258
259
# File 'lib/ast/merge/freeze_node_base.rb', line 257

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



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

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)



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

def start_line
  @start_line
end

#start_markerString? (readonly)

Returns The freeze start marker text.

Returns:

  • (String, nil)

    The freeze start marker text



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

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



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

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)


216
217
218
219
220
# File 'lib/ast/merge/freeze_node_base.rb', line 216

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)


206
207
208
209
210
# File 'lib/ast/merge/freeze_node_base.rb', line 206

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



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

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


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

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



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

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



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

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)


355
356
357
# File 'lib/ast/merge/freeze_node_base.rb', line 355

def freeze_node?
  true
end

#inspectString

String representation for debugging

Returns:

  • (String)


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

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:



310
311
312
# File 'lib/ast/merge/freeze_node_base.rb', line 310

def location
  @location ||= Location.new(@start_line, @end_line)
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



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

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



362
363
364
# File 'lib/ast/merge/freeze_node_base.rb', line 362

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

#sliceString

Returns the freeze block content

Returns:

  • (String)


349
350
351
# File 'lib/ast/merge/freeze_node_base.rb', line 349

def slice
  @content
end

#to_sString

Returns:

  • (String)


373
374
375
# File 'lib/ast/merge/freeze_node_base.rb', line 373

def to_s
  inspect
end