diff --git a/lib/rfmt/prism_bridge.rb b/lib/rfmt/prism_bridge.rb index 2af825b..f735675 100644 --- a/lib/rfmt/prism_bridge.rb +++ b/lib/rfmt/prism_bridge.rb @@ -125,16 +125,14 @@ def self.extract_location(node) end end - # Check child nodes for heredoc (e.g., LocalVariableWriteNode containing StringNode) - node.child_nodes.compact.each do |child| - next unless child.respond_to?(:closing_loc) && child.closing_loc - - closing = child.closing_loc - next unless closing.end_offset > end_offset - - end_offset = closing.end_offset - end_line = closing.end_line - end_column = closing.end_column + # Recursively check all descendant nodes for heredoc closing_loc + # Issue #74: handled direct children (e.g., LocalVariableWriteNode -> StringNode) + # Issue #86: handles deeper nesting (e.g., CallNode -> ArgumentsNode -> StringNode) + max_closing = find_max_closing_loc_recursive(node) + if max_closing && max_closing[:end_offset] > end_offset + end_offset = max_closing[:end_offset] + end_line = max_closing[:end_line] + end_column = max_closing[:end_column] end { @@ -147,6 +145,32 @@ def self.extract_location(node) } end + # Recursively find the maximum closing_loc among all descendant nodes + # Returns nil if no closing_loc found, otherwise { end_offset:, end_line:, end_column: } + def self.find_max_closing_loc_recursive(node, depth: 0) + return nil if depth > 10 + + max_closing = nil + + node.child_nodes.compact.each do |child| + if child.respond_to?(:closing_loc) && child.closing_loc + closing = child.closing_loc + if max_closing.nil? || closing.end_offset > max_closing[:end_offset] + max_closing = { + end_offset: closing.end_offset, + end_line: closing.end_line, + end_column: closing.end_column + } + end + end + + child_max = find_max_closing_loc_recursive(child, depth: depth + 1) + max_closing = child_max if child_max && (max_closing.nil? || child_max[:end_offset] > max_closing[:end_offset]) + end + + max_closing + end + # Extract child nodes def self.extract_children(node) children = [] diff --git a/spec/rfmt_spec.rb b/spec/rfmt_spec.rb index 36220ee..7bdda28 100644 --- a/spec/rfmt_spec.rb +++ b/spec/rfmt_spec.rb @@ -184,6 +184,51 @@ def bar = 42 end end + describe 'heredoc in method call arguments (Issue #86)' do + it 'preserves multiple heredocs as method arguments' do + source = <<~RUBY + puts(<<~HEREDOC, <<~HEREDOC2) + This is a heredoc. + HEREDOC + This is another heredoc. + HEREDOC2 + RUBY + result = Rfmt.format(source) + + expect(result).to include('This is a heredoc.') + expect(result).to include('This is another heredoc.') + expect(result).to match(/^HEREDOC$/m) + expect(result).to match(/^HEREDOC2$/m) + expect(Prism.parse(result).errors).to be_empty + end + + it 'preserves single heredoc as method argument' do + source = <<~RUBY + puts(<<~HEREDOC) + Single heredoc content. + HEREDOC + RUBY + result = Rfmt.format(source) + + expect(result).to include('Single heredoc content.') + expect(result).to match(/^HEREDOC$/m) + expect(Prism.parse(result).errors).to be_empty + end + + it 'preserves heredoc with method chain' do + source = <<~RUBY + foo.bar(<<~SQL) + SELECT * FROM users + SQL + RUBY + result = Rfmt.format(source) + + expect(result).to include('SELECT * FROM users') + expect(result).to match(/^SQL$/m) + expect(Prism.parse(result).errors).to be_empty + end + end + describe 'inline then style preservation (Issue #75)' do describe 'case...in with then' do it 'preserves inline then style in pattern matching' do