Class: Prism::Merge::FileAnalysis
- Inherits:
-
Object
- Object
- Prism::Merge::FileAnalysis
- 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 asspec.name = "foo"andgem.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
-
#gemspec_block_var ⇒ Object
The block parameter name used in
Gem::Specification.new do |X|(e.g. "spec", "gem", "s"). -
#parse_result ⇒ Prism::ParseResult
readonly
The underlying Prism parse result (via TreeHaver routing).
-
#tree ⇒ TreeHaver::Tree
readonly
The tree_haver parse tree (includes normalized comment objects).
Class Method Summary collapse
-
.attach_comments_safely!(parse_result) ⇒ void
Safely attach comments to nodes, handling JRuby compatibility issues.
Instance Method Summary collapse
-
#claimed_lines ⇒ Set<Integer>
Lines claimed by promoted BlockDirective nodes.
-
#comment_attachment_for(owner, **options) ⇒ Ast::Merge::Comment::Attachment
Build a native shared comment attachment for an owner.
-
#comment_augmenter(owners: nil, **options) ⇒ NativeCommentAugmenter
Build a native shared comment augmenter for this analysis.
-
#comment_capability ⇒ Ast::Merge::Comment::Capability
Get shared comment capability information for this analysis.
-
#comment_node_at(line_num) ⇒ Prism::Merge::Comment::Line?
Get a shared/native Ruby comment node at a specific line.
-
#comment_nodes ⇒ Array<Prism::Merge::Comment::Line>
Get all supported comments converted to shared/native Ruby comment nodes.
-
#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.
-
#comment_support_style ⇒ Ast::Merge::Comment::SupportStyle
Describe how Prism merges own and emit comments.
-
#errors ⇒ Array<Prism::ParseError>
Get parse errors for compatibility with SmartMergerBase.
-
#fallthrough_node?(value) ⇒ Boolean
Override to detect Prism nodes for signature generator fallthrough.
-
#freeze_block_at(line_num) ⇒ Ast::Merge::NodeTyping::FrozenWrapper?
Return the frozen Prism-owned statement covering the given line.
-
#frozen_node?(node, claimed_lines: Set.new) ⇒ Boolean
Determine if a node is frozen (has a freeze marker in its leading comments).
-
#frozen_nodes ⇒ Array<Ast::Merge::NodeTyping::FrozenWrapper>
Get nodes that are frozen (have a freeze marker).
-
#in_freeze_block?(line_num) ⇒ Boolean
Check if a line falls within a frozen Prism-owned statement.
-
#initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, source_label: nil, **_options) ⇒ FileAnalysis
constructor
Initialize file analysis with Prism's native comment handling.
-
#layout_attachment_for(owner, **options) ⇒ Ast::Merge::Layout::Attachment
Build a shared layout attachment for an owner using native Prism locations.
-
#layout_augmenter(owners: nil, **options) ⇒ Ast::Merge::Layout::Augmenter
Build a shared layout augmenter for this analysis using native Prism locations.
-
#leading_comments_for_owner(owner, owners: statements) ⇒ Array<Prism::Comment>
Return comments that lead
ownerwithin the provided top-level owner set. -
#nodes_with_comments ⇒ Array<Hash>
Get nodes with their associated comments and metadata Comments are now accessed via Prism's native node.location API.
-
#ruby_doc_surface_analyzer ⇒ Prism::Merge::RubyDocSurfaceAnalyzer
Build a semantic sidecar that exposes Ruby doc-comment surfaces without taking ownership of reconstruction away from Prism source spans.
- #ruleset_delegation_policies ⇒ Object
- #ruleset_owner_selector ⇒ Object
- #ruleset_render_family ⇒ Object
- #ruleset_repair_policies ⇒ Object
- #ruleset_surfaces ⇒ Object
-
#valid? ⇒ Boolean
Check if parse was successful.
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
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, **) @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_var ⇒ Object
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_result ⇒ Prism::ParseResult (readonly)
Returns 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 |
#tree ⇒ TreeHaver::Tree (readonly)
Returns 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.
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..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_lines ⇒ Set<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.
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.
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 (owner, **) 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)) = (owner, **) Ast::Merge::Comment::Attachment.new( owner: owner, leading_region: leading_region, inline_region: inline_region, trailing_region: trailing_region, leading_gap: .leading_gap, trailing_gap: .trailing_gap, metadata: { source: :prism_native, line_num: owner_start_line(owner) }.merge() ) end |
#comment_augmenter(owners: nil, **options) ⇒ NativeCommentAugmenter
Build a native shared comment augmenter for this analysis.
406 407 408 |
# File 'lib/prism/merge/file_analysis.rb', line 406 def comment_augmenter(owners: nil, **) NativeCommentAugmenter.new(self, owners: owners || comment_augmenter_default_owners, **) end |
#comment_capability ⇒ Ast::Merge::Comment::Capability
Get shared comment capability information for this analysis.
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.
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_nodes ⇒ Array<Prism::Merge::Comment::Line>
Get all supported comments converted to shared/native Ruby comment nodes.
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.
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_style ⇒ Ast::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.
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 |
#errors ⇒ Array<Prism::ParseError>
Get parse errors for compatibility with SmartMergerBase.
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
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.
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.
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_nodes ⇒ Array<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).
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.
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.
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 (owner, **) owners = layout_augmenter_default_owners augmenter = if owners.any? { |candidate| candidate.equal?(owner) } layout_augmenter(**) else layout_augmenter(owners: [owner], **) end augmenter.(owner) || Ast::Merge::Layout::Attachment.new( owner: owner, metadata: { source: :prism_native, line_num: owner_start_line(owner) }.merge() ) end |
#layout_augmenter(owners: nil, **options) ⇒ Ast::Merge::Layout::Augmenter
Build a shared layout augmenter for this analysis using native Prism locations.
437 438 439 440 441 442 443 |
# File 'lib/prism/merge/file_analysis.rb', line 437 def layout_augmenter(owners: nil, **) if owners.nil? && .empty? @layout_augmenter ||= build_layout_augmenter(layout_augmenter_default_owners) else build_layout_augmenter(owners || layout_augmenter_default_owners, **) 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.
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_comments ⇒ Array<Hash>
Get nodes with their associated comments and metadata Comments are now accessed via Prism's native node.location API
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_analyzer ⇒ Prism::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_policies ⇒ Object
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_selector ⇒ Object
286 287 288 |
# File 'lib/prism/merge/file_analysis.rb', line 286 def ruleset_owner_selector :prism_statement_sequence end |
#ruleset_render_family ⇒ Object
290 291 292 |
# File 'lib/prism/merge/file_analysis.rb', line 290 def ruleset_render_family :prism_ruby_source end |
#ruleset_repair_policies ⇒ Object
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_surfaces ⇒ Object
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
249 250 251 |
# File 'lib/prism/merge/file_analysis.rb', line 249 def valid? @parse_result.success? end |