Class: Prism::Merge::FileAnalysis

Inherits:
Object
  • Object
show all
Includes:
Ast::Merge::FileAnalyzable
Defined in:
lib/prism/merge/file_analysis.rb

Overview

Simplified file analysis using Prism's native comment attachment. This version leverages parse_result.attach_comments! to automatically attach comments to nodes, eliminating the need for manual comment tracking and the CommentNode class.

Key improvements over V1:

  • Uses Prism's native node.location.leading_comments and trailing_comments
  • No manual comment tracking or CommentNode class
  • Simpler freeze block extraction via comment scanning
  • Better performance (one attach_comments! call vs multiple iterations)
  • Enhanced freeze block validation (detects partial nodes and non-class/module contexts)

Defined Under Namespace

Classes: NativeCommentAugmenter

Constant Summary collapse

DEFAULT_FREEZE_TOKEN =

Default freeze token for identifying freeze blocks

'prism-merge'
GEMSPEC_VAR_PLACEHOLDER =

Canonical placeholder used in signatures to normalize the gemspec block variable. When Gem::Specification.new do |spec| uses a different name from the template (e.g. |gem|), assignment nodes such as spec.name = "foo" and gem.name = "foo" would otherwise produce different signatures and never match. We normalize both to this placeholder so they match regardless of the variable name chosen by the author.

Ruby::Merge::GemspecSupport::GEMSPEC_VAR_PLACEHOLDER

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, source_label: nil, **_options) ⇒ FileAnalysis

Initialize file analysis with Prism's native comment handling

Parameters:

  • source (String)

    Ruby source code to analyze

  • freeze_token (String) (defaults to: DEFAULT_FREEZE_TOKEN)

    Token for freeze block markers (default: "prism-merge")

  • signature_generator (Proc, nil) (defaults to: nil)

    Custom signature generator

  • options (Hash)

    Additional options for forward compatibility



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/prism/merge/file_analysis.rb', line 212

def initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, source_label: nil,
               **_options)
  @source = source
  @lines = source.lines
  @freeze_token = freeze_token
  @signature_generator = signature_generator
  @source_label = source_label
  # **options captured for forward compatibility
  # Route through the Prism backend that Prism::Merge registers with TreeHaver
  # during bootstrap rather than calling Prism.parse directly.
  # Store the full tree_haver result so downstream code can use @tree.comments
  # (normalized, deduplicated, with attachment hints) rather than accessing raw
  # Prism::Comment objects via @parse_result.comments or node.location.leading_comments.
  @tree = DebugLogger.time('FileAnalysis#parse') do
    TreeHaver.with_backend(Prism::Merge::BACKEND_REFERENCE.id) do
      TreeHaver.parser_for(:ruby, backend_type: :prism).parse(source)
    end
  end
  @parse_result = @tree.parse_result
  @gemspec_block_var = detect_gemspec_block_var

  # Use Prism's native comment attachment
  # On JRuby, the Comments class may not be loaded yet, so we need to require it
  attach_comments_safely!

  # Extract and validate structure
  @statements = extract_and_integrate_all_nodes

  DebugLogger.debug('FileAnalysis initialized', {
                      signature_generator: signature_generator ? 'custom' : 'default',
                      statements_count: @statements.size,
                      frozen_nodes_count: frozen_nodes.size
                    })
end

Instance Attribute Details

#gemspec_block_varObject

The block parameter name used in Gem::Specification.new do |X| (e.g. "spec", "gem", "s"). nil for non-gemspec files. Exposed so callers can override it for nested body merges where the outer wrapper is not present in the body text.



184
185
186
# File 'lib/prism/merge/file_analysis.rb', line 184

def gemspec_block_var
  @gemspec_block_var
end

#parse_resultPrism::ParseResult (readonly)

Returns The underlying Prism parse result (via TreeHaver routing).

Returns:

  • (Prism::ParseResult)

    The underlying Prism parse result (via TreeHaver routing)



179
180
181
# File 'lib/prism/merge/file_analysis.rb', line 179

def parse_result
  @parse_result
end

#treeTreeHaver::Tree (readonly)

Returns The tree_haver parse tree (includes normalized comment objects).

Returns:

  • (TreeHaver::Tree)

    The tree_haver parse tree (includes normalized comment objects)



176
177
178
# File 'lib/prism/merge/file_analysis.rb', line 176

def tree
  @tree
end

Class Method Details

.attach_comments_safely!(parse_result) ⇒ void

This method returns an undefined value.

Safely attach comments to nodes, handling JRuby compatibility issues. On JRuby, the Prism::ParseResult::Comments class may not be autoloaded, so we need to explicitly require it.

This is a class method so it can be used anywhere in prism-merge code that needs to attach comments to a parse result.

Parameters:

  • parse_result (Prism::ParseResult)

    The parse result to attach comments to



554
555
556
557
558
559
560
561
562
563
564
565
# File 'lib/prism/merge/file_analysis.rb', line 554

def attach_comments_safely!(parse_result)
  parse_result.attach_comments!
# simplecov:disable defensive - JRuby compatibility for Comments class autoloading
rescue NameError => e
  raise unless e.message.include?('Comments')

  # On JRuby, the Comments class needs to be explicitly required
  require 'prism/parse_result/comments'
  parse_result.attach_comments!

  # simplecov:enable
end

Instance Method Details

#claimed_linesSet<Integer>

Lines claimed by promoted BlockDirective nodes. These lines appear in the source as comment-only directive markers (e.g. # token:freeze) that Prism hoists onto the next code node's leading_comments. The claimed_lines set is used by the emission pipeline to avoid re-emitting those comment lines as leading_comments of adjacent code nodes.

Returns:

  • (Set<Integer>)

    1-based line numbers claimed by BlockDirective promotions



202
203
204
# File 'lib/prism/merge/file_analysis.rb', line 202

def claimed_lines
  @claimed_lines ||= Set.new
end

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

Build a native shared comment attachment for an owner.

Parameters:

  • owner (Object)

    Structural owner for the attachment

  • options (Hash)

    Additional metadata preserved on the attachment

Returns:

  • (Ast::Merge::Comment::Attachment)


373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/prism/merge/file_analysis.rb', line 373

def comment_attachment_for(owner, **options)
  leading_entries = owner_leading_comment_entries(owner)
  split_preamble = split_first_owner_preamble?(owner, leading_entries)
  _preamble_entries, leading_entries = split_first_owner_preamble_entries(leading_entries) if split_preamble

  leading_region = build_comment_region(
    :leading,
    leading_entries,
    metadata: split_preamble ? { floating: true } : {}
  )
  inline_region = build_comment_region(:inline, owner_inline_comment_entries(owner))
  trailing_region = build_comment_region(:trailing, owner_trailing_comment_entries(owner))
  layout_attachment = layout_attachment_for(owner, **options)

  Ast::Merge::Comment::Attachment.new(
    owner: owner,
    leading_region: leading_region,
    inline_region: inline_region,
    trailing_region: trailing_region,
    leading_gap: layout_attachment.leading_gap,
    trailing_gap: layout_attachment.trailing_gap,
    metadata: {
      source: :prism_native,
      line_num: owner_start_line(owner)
    }.merge(options)
  )
end

#comment_augmenter(owners: nil, **options) ⇒ NativeCommentAugmenter

Build a native shared comment augmenter for this analysis.

Parameters:

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

    Owners used for attachment exposure

  • options (Hash)

    Additional capability details

Returns:



406
407
408
# File 'lib/prism/merge/file_analysis.rb', line 406

def comment_augmenter(owners: nil, **options)
  NativeCommentAugmenter.new(self, owners: owners || comment_augmenter_default_owners, **options)
end

#comment_capabilityAst::Merge::Comment::Capability

Get shared comment capability information for this analysis.

Returns:

  • (Ast::Merge::Comment::Capability)


262
263
264
265
266
267
268
269
270
# File 'lib/prism/merge/file_analysis.rb', line 262

def comment_capability
  @comment_capability ||= Ast::Merge::Comment::Capability.native_full(
    source: :prism,
    style: :hash_comment,
    attachment_hints: true,
    comment_nodes: true,
    comment_count: comment_nodes.size
  )
end

#comment_node_at(line_num) ⇒ Prism::Merge::Comment::Line?

Get a shared/native Ruby comment node at a specific line.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:



326
327
328
# File 'lib/prism/merge/file_analysis.rb', line 326

def comment_node_at(line_num)
  native_comment_entries.find { |entry| entry[:line] == line_num }&.dig(:node)
end

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

Get all supported comments converted to shared/native Ruby comment nodes.

Returns:



318
319
320
# File 'lib/prism/merge/file_analysis.rb', line 318

def comment_nodes
  native_comment_entries.map { |entry| entry[:node] }
end

#comment_region_for_range(range, kind:, full_line_only: false) ⇒ Ast::Merge::Comment::Region

Get comments in a line range converted to a shared comment region.

Parameters:

  • range (Range)

    Range of 1-based line numbers

  • kind (Symbol)

    Region kind (:leading, :inline, :orphan, etc.)

  • full_line_only (Boolean) (defaults to: false)

    Whether to keep only full-line comments

Returns:

  • (Ast::Merge::Comment::Region)


336
337
338
339
340
# File 'lib/prism/merge/file_analysis.rb', line 336

def comment_region_for_range(range, kind:, full_line_only: false)
  entries = native_comment_entries_in_range(range)
  entries = entries.select { |entry| entry[:full_line] } if full_line_only
  build_comment_region(kind, entries, metadata: { range: range, full_line_only: full_line_only })
end

#comment_support_styleAst::Merge::Comment::SupportStyle

Describe how Prism merges own and emit comments.

Prism exposes native owned comments and attachment hints, but merge output still flows through ast-merge's synthetic ownership/emission layer.

Returns:

  • (Ast::Merge::Comment::SupportStyle)


278
279
280
281
282
283
284
# File 'lib/prism/merge/file_analysis.rb', line 278

def comment_support_style
  @comment_support_style ||= shared_comment_support_style(
    source: :prism,
    style: :hash_comment,
    read_strategy: :native_read_portable_write
  )
end

#errorsArray<Prism::ParseError>

Get parse errors for compatibility with SmartMergerBase.

Returns:

  • (Array<Prism::ParseError>)

    Array of parse errors



255
256
257
# File 'lib/prism/merge/file_analysis.rb', line 255

def errors
  @parse_result.errors
end

#fallthrough_node?(value) ⇒ Boolean

Override to detect Prism nodes for signature generator fallthrough

Parameters:

  • value (Object)

    The value to check

Returns:

  • (Boolean)

    true if this is a fallthrough node



463
464
465
# File 'lib/prism/merge/file_analysis.rb', line 463

def fallthrough_node?(value)
  value.respond_to?(:canonical_type) || value.is_a?(::Prism::Node) || super
end

#freeze_block_at(line_num) ⇒ Ast::Merge::NodeTyping::FrozenWrapper?

Return the frozen Prism-owned statement covering the given line.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:

  • (Ast::Merge::NodeTyping::FrozenWrapper, nil)


522
523
524
525
526
527
528
529
# File 'lib/prism/merge/file_analysis.rb', line 522

def freeze_block_at(line_num)
  freeze_blocks.find do |block|
    start_line = owner_start_line(block)
    end_line = owner_end_line(block)

    start_line && end_line && (start_line..end_line).cover?(line_num)
  end
end

#frozen_node?(node, claimed_lines: Set.new) ⇒ Boolean

Determine if a node is frozen (has a freeze marker in its leading comments).

For Ruby AST nodes, a freeze marker applies only to the node it directly precedes in leading comments. If a freeze marker appears INSIDE a block (nested in the body), it applies to that nested statement, NOT the outer block. This is different from comment-only formats like Markdown where checking content containment makes sense.

Nested freeze markers inside the node's body are handled during recursive body merging, where each nested statement gets its own freeze detection.

Parameters:

  • node (Prism::Node, Ast::Merge::NodeTyping::FrozenWrapper)

    The node to check

  • claimed_lines (Set<Integer>) (defaults to: Set.new)

    Line numbers already claimed by promoted BlockDirective nodes

Returns:

  • (Boolean)

    true if the node has an unclaimed freeze marker in its leading comments



481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
# File 'lib/prism/merge/file_analysis.rb', line 481

def frozen_node?(node, claimed_lines: Set.new)
  # Already wrapped as frozen
  return true if node.is_a?(Ast::Merge::Freezable)

  return false unless @freeze_token

  # Get the actual node (in case it's a Wrapper)
  actual_node = node.respond_to?(:unwrap) ? node.unwrap : node

  freeze_pattern = /#{Regexp.escape(@freeze_token)}:freeze/i

  # BlockDirectiveDetector has already promoted balanced freeze/unfreeze pairs
  # to FreezeNode synthetic nodes. Any remaining freeze marker in leading
  # comments is unbalanced (node-level freeze marker), so use simple any? check.
  # Exclude markers at lines already claimed by promoted BlockDirective nodes
  # (those were hoisted onto this node by Prism's attach_comments!).
  if actual_node.respond_to?(:location) && actual_node.location.respond_to?(:leading_comments)
    return actual_node.location.leading_comments.any? do |c|
      c.slice.match?(freeze_pattern) && !claimed_lines.include?(c.location.start_line)
    end
  end

  false
end

#frozen_nodesArray<Ast::Merge::NodeTyping::FrozenWrapper>

Get nodes that are frozen (have a freeze marker). Returns FrozenWrapper instances that include the Freezable behavior, allowing them to satisfy both is_a?(Freezable) and is_a?(NodeTyping::Wrapper).

Returns:

  • (Array<Ast::Merge::NodeTyping::FrozenWrapper>)

    Wrapped frozen nodes



536
537
538
539
540
541
542
# File 'lib/prism/merge/file_analysis.rb', line 536

def frozen_nodes
  # Return the underlying Prism nodes for tests and callers that expect
  # Prism node types. Statements may be wrapped in FrozenWrapper; unwrap
  # them here.
  statements.select { |node| node.is_a?(Ast::Merge::Freezable) }
            .map { |node| node.respond_to?(:unwrap) ? node.unwrap : node }
end

#in_freeze_block?(line_num) ⇒ Boolean

Check if a line falls within a frozen Prism-owned statement.

Prism wrapped frozen nodes expose native locations with start/end lines, but Prism::Location does not implement Range#cover?. Use explicit line range checks rather than the FileAnalyzable default.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:

  • (Boolean)


514
515
516
# File 'lib/prism/merge/file_analysis.rb', line 514

def in_freeze_block?(line_num)
  !freeze_block_at(line_num).nil?
end

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

Build a shared layout attachment for an owner using native Prism locations.

Parameters:

  • owner (Object)

    Structural owner for the attachment

  • options (Hash)

    Additional metadata preserved on the attachment

Returns:

  • (Ast::Merge::Layout::Attachment)


415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/prism/merge/file_analysis.rb', line 415

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

  augmenter.attachment_for(owner) || Ast::Merge::Layout::Attachment.new(
    owner: owner,
    metadata: {
      source: :prism_native,
      line_num: owner_start_line(owner)
    }.merge(options)
  )
end

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

Build a shared layout augmenter for this analysis using native Prism locations.

Parameters:

  • owners (Array<Object>, nil) (defaults to: nil)

    Owners used for gap inference

  • options (Hash)

    Additional metadata preserved on the augmenter

Returns:

  • (Ast::Merge::Layout::Augmenter)


437
438
439
440
441
442
443
# File 'lib/prism/merge/file_analysis.rb', line 437

def layout_augmenter(owners: nil, **options)
  if owners.nil? && options.empty?
    @layout_augmenter ||= build_layout_augmenter(layout_augmenter_default_owners)
  else
    build_layout_augmenter(owners || layout_augmenter_default_owners, **options)
  end
end

#leading_comments_for_owner(owner, owners: statements) ⇒ Array<Prism::Comment>

Return comments that lead owner within the provided top-level owner set.

CRISPR adapters use this as a parser-provider-owned projection: CRISPR selects and splices, while prism-merge owns Ruby comment attachment.

Parameters:

  • owner (Object)

    owner node from this analysis

  • owners (Array<Object>) (defaults to: statements)

    ordered owner set containing owner

Returns:

  • (Array<Prism::Comment>)


350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/prism/merge/file_analysis.rb', line 350

def leading_comments_for_owner(owner, owners: statements)
  ordered_owners = Array(owners)
  index = ordered_owners.index(owner)
  return [] unless index

  previous_owner = index.positive? ? ordered_owners[index - 1] : nil
  if previous_owner
    start_line = previous_owner.location.end_line
    end_line = owner.location.start_line
    return parse_result.comments.select do |comment|
      comment.location.start_line > start_line &&
      comment.location.start_line < end_line
    end
  end

  parse_result.comments.select { |comment| comment.location.start_line < owner.location.start_line }
end

#nodes_with_commentsArray<Hash>

Get nodes with their associated comments and metadata Comments are now accessed via Prism's native node.location API

Returns:

  • (Array<Hash>)

    Array of node info hashes



448
449
450
# File 'lib/prism/merge/file_analysis.rb', line 448

def nodes_with_comments
  @nodes_with_comments ||= extract_nodes_with_comments
end

#ruby_doc_surface_analyzerPrism::Merge::RubyDocSurfaceAnalyzer

Build a semantic sidecar that exposes Ruby doc-comment surfaces without taking ownership of reconstruction away from Prism source spans.



456
457
458
# File 'lib/prism/merge/file_analysis.rb', line 456

def ruby_doc_surface_analyzer
  @ruby_doc_surface_analyzer ||= RubyDocSurfaceAnalyzer.new(self)
end

#ruleset_delegation_policiesObject



308
309
310
311
312
313
# File 'lib/prism/merge/file_analysis.rb', line 308

def ruleset_delegation_policies
  [
    { surface_name: :ruby_doc_comment, strategy: :same_ruleset },
    { surface_name: :yard_example_block, strategy: :same_ruleset }
  ]
end

#ruleset_owner_selectorObject



286
287
288
# File 'lib/prism/merge/file_analysis.rb', line 286

def ruleset_owner_selector
  :prism_statement_sequence
end

#ruleset_render_familyObject



290
291
292
# File 'lib/prism/merge/file_analysis.rb', line 290

def ruleset_render_family
  :prism_ruby_source
end

#ruleset_repair_policiesObject



294
295
296
297
298
299
# File 'lib/prism/merge/file_analysis.rb', line 294

def ruleset_repair_policies
  [
    { kind: :comment_ownership_overlap, handling: :heal },
    { kind: :duplicate_template_leading_prefix, handling: :heal }
  ]
end

#ruleset_surfacesObject



301
302
303
304
305
306
# File 'lib/prism/merge/file_analysis.rb', line 301

def ruleset_surfaces
  [
    { name: :ruby_doc_comment, selector: :native_attachment },
    { name: :yard_example_block, selector: :yard_example_tag }
  ]
end

#valid?Boolean

Check if parse was successful

Returns:

  • (Boolean)


249
250
251
# File 'lib/prism/merge/file_analysis.rb', line 249

def valid?
  @parse_result.success?
end