Skip to content

Preserve heredoc body when translating multi-line T.let assertions#943

Open
styrmis wants to merge 1 commit into
mainfrom
06-08-fix-spoom-heredoc-handling
Open

Preserve heredoc body when translating multi-line T.let assertions#943
styrmis wants to merge 1 commit into
mainfrom
06-08-fix-spoom-heredoc-handling

Conversation

@styrmis

@styrmis styrmis commented Jun 8, 2026

Copy link
Copy Markdown

When T.let(...) spans multiple lines and wraps a heredoc, the replacement range covers the entire call including the heredoc body. Since the value node's source range only covers the opener line (e.g. <<~MSG.strip), the body and terminator were silently dropped, producing syntactically broken Ruby.

@styrmis styrmis force-pushed the 06-08-fix-spoom-heredoc-handling branch 3 times, most recently from 3bd034d to 3c92653 Compare June 8, 2026 11:30
When `T.let(...)` spans multiple lines and wraps a heredoc, the
replacement range covers the entire call including the heredoc body.
Since the value node's source range only covers the opener line
(e.g. `<<~MSG.strip`), the body and terminator were silently dropped,
producing syntactically broken Ruby.
@styrmis styrmis force-pushed the 06-08-fix-spoom-heredoc-handling branch from 3c92653 to a5d6263 Compare June 8, 2026 11:46
@styrmis styrmis marked this pull request as ready for review June 8, 2026 12:00
@styrmis styrmis requested a review from a team as a code owner June 8, 2026 12:00

value_end = node.location.end_offset
opener_line_end = value_end
opener_line_end += 1 while opener_line_end < @ruby_bytes.size && @ruby_bytes[opener_line_end] != LINE_BREAK

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use

def adjust_to_line_end(offset)

end

#: (Prism::Node) -> Integer?
def find_heredoc_end_offset(node)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have something like

both = T.let(
  foo(<<~A, <<~B),
    first
  A
    second
  B
  String,
)

stopping at the first heredoc would end up dropping subsequent ones. I think we can support this case too.

Something like

#: (Prism::Node) -> Array[Integer]
def heredoc_end_offsets(node)
  offsets = [] #: Array[Integer]

  case node
  when Prism::StringNode, Prism::InterpolatedStringNode
    opening = node.opening_loc
    closing = node.closing_loc

    if opening && closing && opening.start_line != closing.start_line
      offsets << closing.end_offset
    end
  end

  node.each_child_node do |child|
    offsets.concat(heredoc_end_offsets(child))
  end

  offsets
end

Using each_child_node should take care of it for us. And then inside heredoc_body_within_range we can do

heredoc_end = heredoc_end_offsets(node)
  .select { |offset| offset <= replace_end_offset }
  .max
return unless heredoc_end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants