Class: Ast::Merge::FreezeNodeBase

Inherits:
Object
  • Object
show all
Includes:
BlockDirective, 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 BlockDirective

#block_directive?, #covers_line?, #freeze_directive?, #line_range, #nocov_directive?

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



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/ast/merge/freeze_node_base.rb', line 288

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)



259
260
261
# File 'lib/ast/merge/freeze_node_base.rb', line 259

def analysis
  @analysis
end

#contentString (readonly)

Returns Content of the freeze block.

Returns:

  • (String)

    Content of the freeze block



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

def content
  @content
end

#end_lineInteger (readonly)

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

Returns:

  • (Integer)

    Line number of unfreeze marker (1-based)



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

def end_line
  @end_line
end

#end_markerString? (readonly)

Returns The freeze end marker text.

Returns:

  • (String, nil)

    The freeze end marker text



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

def end_marker
  @end_marker
end

#linesArray<String>? (readonly)

Returns Lines within the freeze block.

Returns:

  • (Array<String>, nil)

    Lines within the freeze block



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

def lines
  @lines
end

#nodesArray (readonly)

Returns AST nodes contained within the freeze block.

Returns:

  • (Array)

    AST nodes contained within the freeze block



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

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



265
266
267
# File 'lib/ast/merge/freeze_node_base.rb', line 265

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



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

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)



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

def start_line
  @start_line
end

#start_markerString? (readonly)

Returns The freeze start marker text.

Returns:

  • (String, nil)

    The freeze start marker text



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

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



161
162
163
164
165
166
# File 'lib/ast/merge/freeze_node_base.rb', line 161

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)


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

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)


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

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



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/ast/merge/freeze_node_base.rb', line 186

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


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

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



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

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



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

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

#childrenArray

Returns AST nodes contained within the freeze block.

Returns:

  • (Array)

    AST nodes contained within the freeze block



67
# File 'lib/ast/merge/freeze_node_base.rb', line 67

def children = @nodes

#freeze_node?Boolean

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

Returns:

  • (Boolean)


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

def freeze_node?
  true
end

#inspectString

String representation for debugging

Returns:

  • (String)


385
386
387
# File 'lib/ast/merge/freeze_node_base.rb', line 385

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

#kindSymbol

Returns Always :freeze for FreezeNodeBase.

Returns:

  • (Symbol)

    Always :freeze for FreezeNodeBase



64
# File 'lib/ast/merge/freeze_node_base.rb', line 64

def kind = :freeze

#locationLocation

Returns a location-like object for compatibility with AST nodes

Returns:



318
319
320
# File 'lib/ast/merge/freeze_node_base.rb', line 318

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

#merge_policySymbol

Returns Freeze blocks are user customizations; dest always wins.

Returns:

  • (Symbol)

    Freeze blocks are user customizations; dest always wins



70
# File 'lib/ast/merge/freeze_node_base.rb', line 70

def merge_policy = :destination

#merge_typeSymbol Also known as: type

Node type for merge classification

Returns:

  • (Symbol)

    :freeze_block



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

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



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/ast/merge/freeze_node_base.rb', line 335

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



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

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

#sliceString

Returns the freeze block content

Returns:

  • (String)


357
358
359
# File 'lib/ast/merge/freeze_node_base.rb', line 357

def slice
  @content
end

#to_sString

Returns:

  • (String)


390
391
392
# File 'lib/ast/merge/freeze_node_base.rb', line 390

def to_s
  inspect
end