Module: Ast::Merge::FileAnalyzable Abstract

Included in:
Text::FileAnalysis
Defined in:
lib/ast/merge/file_analyzable.rb

Overview

This module is abstract.

Include this module and implement #compute_node_signature and optionally #fallthrough_node?

Mixin module for file analysis classes across all *-merge gems.

This module provides common functionality for analyzing source files, including freeze block detection, line access, and signature generation. Include this module in your FileAnalysis class and implement the required abstract methods.

Examples:

Including in a FileAnalysis class

class FileAnalysis
  include Ast::Merge::FileAnalyzable

  def initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil)
    @source = source
    @lines = source.split("\n", -1)
    @freeze_token = freeze_token
    @signature_generator = signature_generator
    @statements = parse_and_extract_statements
  end

  # Required: implement this method for parser-specific signature logic
  def compute_node_signature(node)
    # Return signature array or nil
  end

  # Required: implement if using generate_signature with custom node type detection
  def fallthrough_node?(node)
    node.is_a?(MyParser::Node) || node.is_a?(FreezeNodeBase)
  end
end

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ void

This method returns an undefined value.

Install the shared attr_reader interface on including analyses.

Parameters:

  • base (Class)

    analysis class receiving the shared readers



54
55
56
57
58
# File 'lib/ast/merge/file_analyzable.rb', line 54

def included(base)
  base.class_eval do
    attr_reader(:source, :lines, :freeze_token, :signature_generator)
  end
end

Instance Method Details

#comment_attachment_for(owner, **options) ⇒ Comment::Attachment

Return a passive shared attachment for a structural owner.

Analyses with comment ownership data should override this to return meaningful leading/inline/trailing regions. The default implementation returns an empty attachment so callers can migrate incrementally.

Parameters:

  • owner (Object)

    structural owner

  • options (Hash)

    attachment metadata

Returns:



309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/ast/merge/file_analyzable.rb', line 309

def comment_attachment_for(owner, **options)
  layout_attachment = layout_owner_supported?(owner) ? layout_attachment_for(owner, **options) : nil

  Comment::Attachment.new(
    owner: owner,
    leading_gap: layout_attachment&.leading_gap,
    trailing_gap: layout_attachment&.trailing_gap,
    metadata: {
      source: :file_analyzable_default
    }.merge(options)
  )
end

#comment_attachment_strategySymbol

Describe the shared attachment strategy used by this analysis.

Analyses that rely on one of the common tracked/augmented attachment shapes can override this and delegate comment_attachment_for through #shared_comment_attachment_for instead of open-coding the merge path.

Returns:

  • (Symbol)


329
330
331
# File 'lib/ast/merge/file_analyzable.rb', line 329

def comment_attachment_strategy
  :layout_only
end

#comment_augmenter(owners: nil, **options) ⇒ Comment::Augmenter

Build a passive shared comment augmenter for the current analysis.

Format-specific analyses should override this when they can provide native or tracked comment data. The default implementation preserves the shared augmenter interface while reporting the analysis capability.

Parameters:

  • owners (Array<#start_line,#end_line>, nil) (defaults to: nil)

    owners for attachment inference

  • options (Hash)

    augmenter metadata

Returns:



461
462
463
464
465
466
467
468
469
# File 'lib/ast/merge/file_analyzable.rb', line 461

def comment_augmenter(owners: nil, **options)
  Comment::Augmenter.new(
    lines: lines,
    comments: [],
    owners: owners || comment_augmenter_default_owners,
    capability: comment_capability,
    **options
  )
end

#comment_capabilityComment::Capability

Describe the level of comment support available from this analysis.

Analyses with native or augmented comment support should override this. The default implementation advertises no comment support while still providing the shared hook surface used by merge gems.

Returns:



130
131
132
# File 'lib/ast/merge/file_analyzable.rb', line 130

def comment_capability
  Comment::Capability.none(source: :file_analyzable_default)
end

#comment_node_at(_line_num) ⇒ Ast::Merge::Comment::Line?

Return the shared comment node at a specific line.

Parameters:

  • _line_num (Integer)

    1-based line number

Returns:



275
276
277
# File 'lib/ast/merge/file_analyzable.rb', line 275

def comment_node_at(_line_num)
  nil
end

#comment_nodesArray<Ast::Merge::Comment::Line>

Return all shared comment nodes known to this analysis.

Returns:



137
138
139
# File 'lib/ast/merge/file_analyzable.rb', line 137

def comment_nodes
  []
end

#comment_region_for_range(range, kind:, **options) ⇒ Comment::Region

Return a shared comment region spanning a requested line range.

Analyses with comment support should override this to return attached or tracked comment content. The default implementation returns an empty region of the requested kind so callers can rely on the hook surface.

Parameters:

  • range (Range)

    1-based line range

  • kind (Symbol)

    region ownership kind

  • options (Hash)

    region metadata

Returns:



289
290
291
292
293
294
295
296
297
298
# File 'lib/ast/merge/file_analyzable.rb', line 289

def comment_region_for_range(range, kind:, **options)
  Comment::Region.new(
    kind: kind,
    nodes: [],
    metadata: {
      source: :file_analyzable_default,
      range: range
    }.merge(options)
  )
end

#comment_support_styleComment::SupportStyle

Describe how the merge pipeline will own and emit comments for this analysis.

Analyses with comment support should override this to advertise their intended read/write model independently from raw parser capability.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/ast/merge/file_analyzable.rb', line 148

def comment_support_style
  capability = comment_capability
  details = {
    source: :file_analyzable_default,
    capability: capability.level
  }

  if capability.none?
    Comment::SupportStyle.unavailable(**details)
  elsif capability.source_augmented?
    Ruleset::SupportStyleResolver.call(
      read: :source_augmented_portable_write,
      source: details[:source],
      capability: details[:capability],
      style: details[:style]
    )
  else
    Ruleset::SupportStyleResolver.call(
      read: :native_read_portable_write,
      source: details[:source],
      capability: details[:capability],
      style: details[:style]
    )
  end
end

#compute_node_signature(node) ⇒ Array?

This method is abstract.

Compute default signature for a node. This method must be implemented by including classes.

Parameters:

  • node (Object)

    The node to compute signature for

Returns:

  • (Array, nil)

    Signature array or nil

Raises:

  • (NotImplementedError)


744
745
746
# File 'lib/ast/merge/file_analyzable.rb', line 744

def compute_node_signature(node)
  raise NotImplementedError, "#{self.class} must implement #compute_node_signature"
end

#fallthrough_node?(value) ⇒ Boolean

Check if a value represents a fallthrough node that should be used for default signature computation.

When a signature_generator returns a non-Array/nil value, we check if it's a "fallthrough" node that should be passed to compute_node_signature. This includes:

  • AstNode instances (custom AST nodes like Comment::Line)
  • Freezable nodes (frozen wrappers)
  • FreezeNodeBase instances
  • NodeTyping::Wrapper instances (unwrapped to get the underlying node)

Override this method to add custom node type detection for your parser.

Parameters:

  • value (Object)

    The value to check

Returns:

  • (Boolean)

    true if this is a fallthrough node



730
731
732
733
734
735
736
# File 'lib/ast/merge/file_analyzable.rb', line 730

def fallthrough_node?(value)
  value.is_a?(AstNode) ||
    value.is_a?(Freezable) ||
    value.is_a?(FreezeNodeBase) ||
    value.is_a?(NodeTyping::Wrapper) ||
    value.is_a?(BlockDirective)
end

#feature_profileRuleset::FeatureProfile

Describe the current analysis using spec-aligned merge feature terms.

Analyses can override the individual ruleset_* hooks below to make this surface more specific without replacing the whole profile object.



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/ast/merge/file_analyzable.rb', line 202

def feature_profile
  Ruleset::FeatureProfile.new(
    owner_selector: ruleset_owner_selector,
    match_key: ruleset_match_key,
    read_strategy: ruleset_read_strategy,
    attachment_strategy: ruleset_attachment_strategy,
    comment_style: ruleset_comment_style,
    render_family: ruleset_render_family,
    comment_capability: comment_capability,
    support_style: comment_support_style,
    capabilities: ruleset_capabilities,
    logical_owners: ruleset_logical_owners,
    repair_policies: ruleset_repair_policies,
    surfaces: ruleset_surfaces,
    delegation_policies: ruleset_delegation_policies,
    metadata: { source: :file_analyzable_default }
  )
end

#freeze_block_at(line_num) ⇒ FreezeNodeBase?

Get the freeze block containing the given line, if any.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:



90
91
92
# File 'lib/ast/merge/file_analyzable.rb', line 90

def freeze_block_at(line_num)
  freeze_blocks.find { |fb| fb.location.cover?(line_num) }
end

#freeze_blocksArray<Freezable>

Get all freeze blocks/nodes from statements. Includes both traditional FreezeNodeBase blocks and Freezable-wrapped nodes.

Returns:



74
75
76
# File 'lib/ast/merge/file_analyzable.rb', line 74

def freeze_blocks
  statements.select { |node| node.is_a?(Freezable) }
end

#generate_signature(node) ⇒ Array?

Generate signature for a node.

Signatures are used to match nodes between template and destination files. Two nodes with the same signature are considered "the same" for merge purposes, allowing the merger to decide which version to keep based on preference settings.

Signature Generation Flow

  1. FreezeNodeBase (explicit freeze blocks like # token:freeze ... # token:unfreeze): Uses content-based signature via freeze_signature. This ensures explicit freeze blocks match between files based on their actual content.

  2. FrozenWrapper (AST nodes with freeze markers in leading comments): The wrapper is unwrapped first to get the underlying AST node. The signature is then generated from the underlying node, NOT the wrapper. This is critical because the freeze marker only affects merge preference (destination wins), not matching. Two nodes should match by their structural identity even if their content differs slightly.

  3. Custom signature_generator: If provided, receives the unwrapped node and can:

    • Return an Array signature (e.g., [:gem, "foo"]) - used directly
    • Return nil - node gets no signature, won't be matched
    • Return the node (fallthrough) - default signature computation is used
  4. Default computation: Falls through to compute_node_signature for parser-specific default signature generation.

Why FrozenWrapper Must Be Unwrapped

Consider a gemspec with a frozen gem_version variable:

Template:                         Destination:
# kettle-dev:freeze               # kettle-dev:freeze
# Comment                         # Comment
# kettle-dev:unfreeze             # More comments
gem_version = "1.0"               # kettle-dev:unfreeze
                                gem_version = "1.0"

Both have a gem_version assignment with a freeze marker in leading comments. The assignments are wrapped in FrozenWrapper, but their CONTENT differs (template has fewer comments in the freeze block).

If we generated signatures from the wrapper (which delegates slice to the full node content), they would NOT match and both would be output - duplicating the freeze block!

By unwrapping first, we generate signatures from the underlying LocalVariableWriteNode, which matches by variable name (gem_version), ensuring only ONE version is output (the destination version, since it's frozen).

Examples:

Custom generator with fallthrough

signature_generator = ->(node) {
  case node
  when MyParser::SpecialNode
    [:special, node.name]
  else
    node  # Return original node for default signature computation
  end
}

Parameters:

  • node (Object)

    Node to generate signature for (may be wrapped)

Returns:

  • (Array, nil)

    Signature array or nil

See Also:



614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
# File 'lib/ast/merge/file_analyzable.rb', line 614

def generate_signature(node)
  # ==========================================================================
  # CASE 1: FreezeNodeBase (explicit freeze blocks)
  # ==========================================================================
  # FreezeNodeBase represents an explicit freeze block delimited by markers:
  #   # token:freeze
  #   ... content ...
  #   # token:unfreeze
  #
  # These are standalone structural elements (not attached to AST nodes).
  # They use content-based signatures so identical freeze blocks match.
  # This is different from FrozenWrapper which wraps AST nodes.
  return node.freeze_signature if node.is_a?(FreezeNodeBase)

  # ==========================================================================
  # CASE 2: Unwrap FrozenWrapper (and other wrappers)
  # ==========================================================================
  # FrozenWrapper wraps AST nodes that have freeze markers in their leading
  # comments. The wrapper marks the node as "frozen" (prefer destination),
  # but for MATCHING purposes, we need the underlying node's identity.
  #
  # Example: A `gem_version = ...` assignment wrapped in FrozenWrapper should
  # match another `gem_version = ...` assignment by variable name, not by
  # the full content of the assignment (which may differ).
  #
  # CRITICAL: We must unwrap BEFORE calling the signature_generator so it
  # receives the actual AST node type (e.g., Prism::LocalVariableWriteNode)
  # rather than the wrapper (FrozenWrapper). Otherwise, type-based signature
  # generators (like kettle-jem's gemspec generator) won't recognize the node
  # and will fall through to default handling incorrectly.
  actual_node = node.respond_to?(:unwrap) ? node.unwrap : node

  result = if signature_generator
             # ==========================================================================
             # CASE 3: Custom signature generator
             # ==========================================================================
             # Pass the UNWRAPPED node to the custom generator. This ensures:
             # - Type checks work (e.g., `node.is_a?(Prism::CallNode)`)
             # - The generator sees the real AST structure
             # - Frozen nodes match by their underlying identity
             #
             # NOTE: For TreeHaver-based backends, the node already has a unified API
             # with #text, #type, #source_position methods. For other backends, they
             # must conform to the same API (either via TreeHaver or equivalent adapter).
             custom_result = signature_generator.call(actual_node)
             case custom_result
             when Array, nil
               # Generator returned a final signature or nil - use as-is
               custom_result
             else
               # Generator returned a node (fallthrough) - compute default signature.
               #
               # Two conditions indicate the generator is deferring to default handling:
               # 1. Identity equality: the generator returned the exact same object it
               #    received (classic "I don't handle this type" passthrough pattern).
               # 2. Known node type: the result is a recognised wrapper/node class.
               #
               # If neither applies, treat the return value as a final custom signature
               # (e.g. a String, Symbol, or other non-node key).
               if custom_result.equal?(actual_node) || fallthrough_node?(custom_result)
                 # Special case: if fallthrough result is Freezable, use freeze_signature
                 # This handles cases where the generator wraps a node in Freezable
                 if custom_result.is_a?(Freezable)
                   custom_result.freeze_signature
                 else
                   # Unwrap any wrapper and compute default signature
                   unwrapped = custom_result.respond_to?(:unwrap) ? custom_result.unwrap : custom_result
                   compute_node_signature(unwrapped)
                 end
               else
                 # Non-node return value - pass through (allows arbitrary signature types)
                 custom_result
               end
             end
           else
             # ==========================================================================
             # CASE 4: No custom generator - use default computation
             # ==========================================================================
             # Pass the UNWRAPPED node to compute_node_signature. This is critical
             # because compute_node_signature uses type checking (e.g., case statements
             # matching Prism::DefNode, Prism::CallNode, etc.). If we pass a
             # FrozenWrapper, it won't match any of those types and will fall through
             # to a generic handler, producing incorrect signatures.
             #
             # For FrozenWrapper nodes, the underlying AST node determines the signature
             # (e.g., method name for DefNode, gem name for CallNode). The wrapper only
             # affects merge preference (destination wins), not matching.
             compute_node_signature(actual_node)
           end

  if result
    DebugLogger.debug('Generated signature', {
                        node_type: node.class.name.split('::').last,
                        signature: result,
                        generator: signature_generator ? 'custom' : 'default'
                      })
  end

  result
end

#in_freeze_block?(line_num) ⇒ Boolean

Check if a line is within a freeze block.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:

  • (Boolean)

    true if line is inside a freeze block



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

def in_freeze_block?(line_num)
  freeze_blocks.any? { |fb| fb.location.cover?(line_num) }
end

#layout_attachment_for(owner, **options) ⇒ Layout::Attachment

Return a passive shared layout attachment for a structural owner.

Analyses with explicit blank-line ownership data should override this to return meaningful leading/trailing gap attachments. The default implementation returns an empty attachment so callers can migrate incrementally.

Parameters:

  • owner (Object)

    structural owner

  • options (Hash)

    attachment metadata

Returns:



481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/ast/merge/file_analyzable.rb', line 481

def layout_attachment_for(owner, **options)
  owners = layout_augmenter_default_owners
  augmenter = if owners.any? { |candidate| candidate.equal?(owner) }
                layout_augmenter(**options)
              else
                layout_augmenter(owners: [owner], **options)
              end

  inferred_attachment = augmenter.attachment_for(owner)

  Layout::Attachment.new(
    owner: owner,
    leading_gap: inferred_attachment&.leading_gap,
    trailing_gap: inferred_attachment&.trailing_gap,
    metadata: {
      source: :file_analyzable_default
    }.merge(options)
  )
end

#layout_augmenter(owners: nil, **options) ⇒ Layout::Augmenter

Build a passive shared layout augmenter for the current analysis.

Format-specific analyses should override this when they can provide a stronger ownership model for blank-line gaps. The default implementation preserves the shared hook surface while inferring only directly adjacent blank-line runs from source lines and owner ranges.

Parameters:

  • owners (Array<#start_line,#end_line>, nil) (defaults to: nil)

    owners for gap inference

  • options (Hash)

    augmenter metadata

Returns:



511
512
513
514
515
516
517
# File 'lib/ast/merge/file_analyzable.rb', line 511

def layout_augmenter(owners: nil, **options)
  Layout::Augmenter.new(
    lines: lines,
    owners: owners || layout_augmenter_default_owners,
    **options
  )
end

#layout_owner_supported?(owner) ⇒ Boolean

Returns:

  • (Boolean)


519
520
521
522
523
524
# File 'lib/ast/merge/file_analyzable.rb', line 519

def layout_owner_supported?(owner)
  owner.respond_to?(:start_line) &&
    owner.respond_to?(:end_line) &&
    !owner.start_line.nil? &&
    !owner.end_line.nil?
end

#line_at(line_num) ⇒ String?

Get a specific line (1-indexed).

Parameters:

  • line_num (Integer)

    Line number (1-indexed)

Returns:

  • (String, nil)

    The line content or nil if out of bounds



108
109
110
111
112
# File 'lib/ast/merge/file_analyzable.rb', line 108

def line_at(line_num)
  return if line_num < 1

  lines[line_num - 1]
end

#merge_augmented_comment_attachment_with_layout(owner, tracker_attachment:, **options) ⇒ Comment::Attachment

Prefer an augmenter-built attachment when available, then merge the result with shared layout ownership data.

This lets analyses use richer augmenter ownership (for example floating leading regions) while preserving tracker-based fallback behavior.

Parameters:

  • owner (Object)

    structural owner

  • tracker_attachment (Comment::Attachment, nil)

    tracker-built fallback

  • options (Hash)

    attachment metadata

Returns:



394
395
396
397
398
399
400
401
402
# File 'lib/ast/merge/file_analyzable.rb', line 394

def merge_augmented_comment_attachment_with_layout(owner, tracker_attachment:, **options)
  augmenter_attachment = comment_augmenter(owners: [owner]).attachment_for(owner)

  merge_comment_attachment_with_layout(
    owner,
    augmenter_attachment || tracker_attachment,
    **options
  )
end

#merge_comment_attachment_with_layout(owner, comment_attachment, **options) ⇒ Comment::Attachment

Merge a format-specific comment attachment with shared inferred layout gaps.

This lets analyses keep custom comment-region logic while still exposing the shared gap ownership model through the returned comment attachment.

Parameters:

  • owner (Object)

    structural owner

  • comment_attachment (Comment::Attachment, nil)

    existing comment attachment

  • options (Hash)

    attachment metadata

Returns:



369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/ast/merge/file_analyzable.rb', line 369

def merge_comment_attachment_with_layout(owner, comment_attachment, **options)
  layout_attachment = layout_owner_supported?(owner) ? layout_attachment_for(owner, **options) : nil

  Comment::Attachment.new(
    owner: comment_attachment&.owner || owner,
    leading_region: comment_attachment&.leading_region,
    inline_region: comment_attachment&.inline_region,
    trailing_region: comment_attachment&.trailing_region,
    orphan_regions: comment_attachment&.orphan_regions || [],
    leading_gap: layout_attachment&.leading_gap,
    trailing_gap: layout_attachment&.trailing_gap,
    metadata: (comment_attachment&. || {}).merge(options)
  )
end

#normalize_layout_owned_comment_attachment(attachment) ⇒ Comment::Attachment?

Mark a leading region as floating when shared layout already proves it is gap-owned, without changing the underlying region content.

Some format analyzers still build attachments from tracker-level comment regions that do not set floating: true even though the shared layout augmenter has already established a leading gap. This helper preserves the attachment shape while normalizing that metadata in one place.

Parameters:

Returns:



431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/ast/merge/file_analyzable.rb', line 431

def normalize_layout_owned_comment_attachment(attachment)
  return attachment unless attachment&.leading_region
  return attachment if attachment.leading_region.floating?
  return attachment unless attachment.leading_gap

  Comment::Attachment.new(
    owner: attachment.owner,
    leading_region: Comment::Region.new(
      kind: attachment.leading_region.kind,
      nodes: attachment.leading_region.nodes,
      metadata: attachment.leading_region..merge(floating: true)
    ),
    inline_region: attachment.inline_region,
    trailing_region: attachment.trailing_region,
    orphan_regions: attachment.orphan_regions,
    leading_gap: attachment.leading_gap,
    trailing_gap: attachment.trailing_gap,
    metadata: attachment.
  )
end

#normalize_tracked_comment_attachment_with_layout(owner, tracker_attachment:, **options) ⇒ Comment::Attachment

Merge a tracker-built attachment with shared layout ownership and then normalize any layout-owned leading region metadata.

Parameters:

  • owner (Object)

    structural owner

  • tracker_attachment (Comment::Attachment, nil)

    tracker-built attachment

  • options (Hash)

    attachment metadata

Returns:



411
412
413
414
415
416
417
418
419
# File 'lib/ast/merge/file_analyzable.rb', line 411

def normalize_tracked_comment_attachment_with_layout(owner, tracker_attachment:, **options)
  normalize_layout_owned_comment_attachment(
    merge_comment_attachment_with_layout(
      owner,
      tracker_attachment,
      **options
    )
  )
end

#normalized_line(line_num) ⇒ String?

Get a normalized line (whitespace-trimmed, for comparison).

Parameters:

  • line_num (Integer)

    Line number (1-indexed)

Returns:

  • (String, nil)

    Normalized line content or nil if out of bounds



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

def normalized_line(line_num)
  line = line_at(line_num)
  line&.strip
end

#owner_leading_comment_freeze?(owner, freeze_token: self.freeze_token, **options) ⇒ Boolean

Check whether an owner's leading normalized comment attachment contains a freeze directive.

Parameters:

  • owner (Object)

    structural owner

  • freeze_token (String, nil) (defaults to: self.freeze_token)

    token to detect (defaults to this analysis token)

  • options (Hash)

    attachment metadata

Returns:

  • (Boolean)


532
533
534
535
# File 'lib/ast/merge/file_analyzable.rb', line 532

def owner_leading_comment_freeze?(owner, freeze_token: self.freeze_token, **options)
  attachment = comment_attachment_for(owner, **options)
  attachment.respond_to?(:leading_freeze?) && attachment.leading_freeze?(freeze_token)
end

#owner_leading_comment_unfreeze?(owner, freeze_token: self.freeze_token, **options) ⇒ Boolean

Check whether an owner's leading normalized comment attachment contains an unfreeze directive.

Parameters:

  • owner (Object)

    structural owner

  • freeze_token (String, nil) (defaults to: self.freeze_token)

    token to detect (defaults to this analysis token)

  • options (Hash)

    attachment metadata

Returns:

  • (Boolean)


543
544
545
546
# File 'lib/ast/merge/file_analyzable.rb', line 543

def owner_leading_comment_unfreeze?(owner, freeze_token: self.freeze_token, **options)
  attachment = comment_attachment_for(owner, **options)
  attachment.respond_to?(:leading_unfreeze?) && attachment.leading_unfreeze?(freeze_token)
end

#ruleset_attachment_strategyObject



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

def ruleset_attachment_strategy
  comment_attachment_strategy
end

#ruleset_capabilitiesObject



248
249
250
251
252
253
# File 'lib/ast/merge/file_analyzable.rb', line 248

def ruleset_capabilities
  {
    layout_aware: true,
    logical_owner: ruleset_logical_owners.any?
  }
end

#ruleset_comment_styleObject



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

def ruleset_comment_style
  comment_support_style.details[:style] || comment_capability.details[:style]
end

#ruleset_delegation_policiesObject



267
268
269
# File 'lib/ast/merge/file_analyzable.rb', line 267

def ruleset_delegation_policies
  []
end

#ruleset_logical_ownersObject



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

def ruleset_logical_owners
  {}
end

#ruleset_match_keyObject



225
226
227
# File 'lib/ast/merge/file_analyzable.rb', line 225

def ruleset_match_key
  :signature
end

#ruleset_owner_selectorObject



221
222
223
# File 'lib/ast/merge/file_analyzable.rb', line 221

def ruleset_owner_selector
  :shared_default
end

#ruleset_read_strategyObject



229
230
231
232
233
234
# File 'lib/ast/merge/file_analyzable.rb', line 229

def ruleset_read_strategy
  support_style = comment_support_style
  return unless support_style.respond_to?(:available?) && support_style.available?

  support_style.style
end

#ruleset_render_familyObject



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

def ruleset_render_family
  nil
end

#ruleset_repair_policiesObject



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

def ruleset_repair_policies
  []
end

#ruleset_surfacesObject



263
264
265
# File 'lib/ast/merge/file_analyzable.rb', line 263

def ruleset_surfaces
  []
end

#shared_comment_attachment_for(owner, tracker_attachment: nil, strategy: comment_attachment_strategy, **options) ⇒ Comment::Attachment

Build a comment attachment through one of the shared runtime strategies.

This surfaces the current repeated runtime seam explicitly so format-specific analyses can declare which ownership path they use while still preserving any custom tracker attachment selection they need.

Parameters:

  • owner (Object)

    structural owner

  • tracker_attachment (Comment::Attachment, nil) (defaults to: nil)

    tracker-built or custom base attachment

  • strategy (Symbol) (defaults to: comment_attachment_strategy)

    attachment strategy override

  • options (Hash)

    attachment metadata

Returns:



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/ast/merge/file_analyzable.rb', line 344

def shared_comment_attachment_for(owner, tracker_attachment: nil, strategy: comment_attachment_strategy,
                                  **options)
  case strategy
  when :layout_only
    merge_comment_attachment_with_layout(owner, nil, **options)
  when :tracker_layout_merge
    merge_comment_attachment_with_layout(owner, tracker_attachment, **options)
  when :augmenter_preferred_tracker_layout
    merge_augmented_comment_attachment_with_layout(owner, tracker_attachment: tracker_attachment, **options)
  when :normalize_tracked_layout_merge
    normalize_tracked_comment_attachment_with_layout(owner, tracker_attachment: tracker_attachment, **options)
  else
    raise ArgumentError, "Unknown comment attachment strategy: #{strategy.inspect}"
  end
end

#shared_comment_support_style(source:, style:, read_strategy:, capability: comment_capability.level) ⇒ Comment::SupportStyle

Build a shared support-style declaration for analyses that already know their read/write model.

Parameters:

  • source (Symbol)

    support-style source identifier

  • style (Symbol)

    comment syntax family

  • read_strategy (Symbol)

    support-style read/write strategy

  • capability (Symbol) (defaults to: comment_capability.level)

    advertised capability level

Returns:



182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/ast/merge/file_analyzable.rb', line 182

def shared_comment_support_style(source:, style:, read_strategy:, capability: comment_capability.level)
  details = {
    source: source,
    capability: capability,
    style: style
  }

  Ruleset::SupportStyleResolver.call(read: read_strategy, **details)
rescue ArgumentError => e
  raise unless e.message.start_with?('Unknown ruleset read strategy:')

  raise ArgumentError, "Unknown comment support read strategy: #{read_strategy.inspect}"
end

#signature_at(index) ⇒ Array?

Get structural signature for a statement at given index.

Parameters:

  • index (Integer)

    Statement index (0-based)

Returns:

  • (Array, nil)

    Signature array or nil if index out of bounds



98
99
100
101
102
# File 'lib/ast/merge/file_analyzable.rb', line 98

def signature_at(index)
  return if index.negative? || index >= statements.length

  generate_signature(statements[index])
end

#statementsArray

Get all top-level statements (nodes and freeze blocks). Override this method in including classes to return the appropriate collection. The default implementation returns @statements if set, otherwise an empty array.

Returns:

  • (Array)

    All top-level statements



66
67
68
# File 'lib/ast/merge/file_analyzable.rb', line 66

def statements
  @statements ||= []
end