diff --git a/changelog/fix_update_layoutdotposition_to_be_able_to.md b/changelog/fix_update_layoutdotposition_to_be_able_to.md new file mode 100644 index 00000000000..0d6f2e67574 --- /dev/null +++ b/changelog/fix_update_layoutdotposition_to_be_able_to.md @@ -0,0 +1 @@ +* [#10110](https://github.com/rubocop/rubocop/issues/10110): Update `Layout/DotPosition` to be able to handle heredocs. ([@dvandersluis][]) diff --git a/lib/rubocop/cop/layout/dot_position.rb b/lib/rubocop/cop/layout/dot_position.rb index 61ebc38914e..567f9590367 100644 --- a/lib/rubocop/cop/layout/dot_position.rb +++ b/lib/rubocop/cop/layout/dot_position.rb @@ -68,7 +68,7 @@ def message(dot) end def proper_dot_position?(node) - receiver_line = node.receiver.source_range.end.line + receiver_line = receiver_end_line(node.receiver) selector_line = selector_range(node).line # receiver and selector are on the same line @@ -80,7 +80,10 @@ def proper_dot_position?(node) # dot and the selector otherwise, we might break the code while # "correcting" it (even if there is just an extra blank line, treat # it the same) - return true if line_between?(selector_line, dot_line) + # Also, in the case of a heredoc, the receiver will end after the dot, + # because the heredoc body is on subsequent lines, so use the highest + # line to compare to. + return true if line_between?(selector_line, [receiver_line, dot_line].max) correct_dot_position_style?(dot_line, selector_line) end @@ -96,6 +99,28 @@ def correct_dot_position_style?(dot_line, selector_line) end end + def receiver_end_line(node) + if (line = last_heredoc_line(node)) + line + else + node.source_range.end.line + end + end + + def last_heredoc_line(node) + if node.send_type? + node.arguments.select { |arg| heredoc?(arg) } + .map { |arg| arg.loc.heredoc_end.line } + .max + elsif heredoc?(node) + node.loc.heredoc_end.line + end + end + + def heredoc?(node) + (node.str_type? || node.dstr_type?) && node.heredoc? + end + def selector_range(node) # l.(1) has no selector, so we use the opening parenthesis instead node.loc.selector || node.loc.begin diff --git a/spec/rubocop/cop/layout/dot_position_spec.rb b/spec/rubocop/cop/layout/dot_position_spec.rb index e6c012f09ee..e25974411dc 100644 --- a/spec/rubocop/cop/layout/dot_position_spec.rb +++ b/spec/rubocop/cop/layout/dot_position_spec.rb @@ -98,6 +98,25 @@ end end + context 'when a method spans multiple lines' do + it 'registers an offense' do + expect_offense(<<~RUBY) + something( + foo, bar + ). + ^ Place the . on the next line, together with the method name. + method_name + RUBY + + expect_correction(<<~RUBY) + something( + foo, bar + ) + .method_name + RUBY + end + end + context 'when using safe navigation operator' do it 'registers an offense for correct + opposite' do expect_offense(<<~RUBY) @@ -145,6 +164,119 @@ RUBY end end + + context 'when the receiver has a heredoc argument' do + context 'as the last argument' do + it 'registers an offense' do + expect_offense(<<~RUBY) + my_method. + ^ Place the . on the next line, together with the method name. + something(<<~HERE). + ^ Place the . on the next line, together with the method name. + something + HERE + somethingelse + RUBY + + expect_correction(<<~RUBY) + my_method + .something(<<~HERE) + something + HERE + .somethingelse + RUBY + end + end + + context 'with a dynamic heredoc' do + it 'registers an offense' do + expect_offense(<<~'RUBY') + my_method. + ^ Place the . on the next line, together with the method name. + something(<<~HERE). + ^ Place the . on the next line, together with the method name. + #{something} + HERE + somethingelse + RUBY + + expect_correction(<<~'RUBY') + my_method + .something(<<~HERE) + #{something} + HERE + .somethingelse + RUBY + end + end + + context 'as the first argument' do + it 'registers an offense' do + expect_offense(<<~'RUBY') + my_method. + ^ Place the . on the next line, together with the method name. + something(<<~HERE, true). + ^ Place the . on the next line, together with the method name. + #{something} + HERE + somethingelse + RUBY + + expect_correction(<<~'RUBY') + my_method + .something(<<~HERE, true) + #{something} + HERE + .somethingelse + RUBY + end + end + + context 'with multiple heredocs' do + it 'registers an offense' do + expect_offense(<<~'RUBY') + my_method. + ^ Place the . on the next line, together with the method name. + something(<<~HERE, <<~THERE). + ^ Place the . on the next line, together with the method name. + something + HERE + another thing + THERE + somethingelse + RUBY + + expect_correction(<<~'RUBY') + my_method + .something(<<~HERE, <<~THERE) + something + HERE + another thing + THERE + .somethingelse + RUBY + end + end + end + + context 'when the receiver is a heredoc' do + it 'registers an offense' do + expect_offense(<<~RUBY) + <<~HEREDOC. + ^ Place the . on the next line, together with the method name. + something + HEREDOC + method_name + RUBY + + expect_correction(<<~RUBY) + <<~HEREDOC + something + HEREDOC + .method_name + RUBY + end + end end context 'Trailing dots style' do @@ -220,5 +352,118 @@ RUBY end end + + context 'when the receiver has a heredoc argument' do + context 'as the last argument' do + it 'registers an offense' do + expect_offense(<<~RUBY) + my_method + .something(<<~HERE) + ^ Place the . on the previous line, together with the method call receiver. + something + HERE + .somethingelse + ^ Place the . on the previous line, together with the method call receiver. + RUBY + + expect_correction(<<~RUBY) + my_method. + something(<<~HERE). + something + HERE + somethingelse + RUBY + end + end + + context 'with a dynamic heredoc' do + it 'registers an offense' do + expect_offense(<<~'RUBY') + my_method + .something(<<~HERE) + ^ Place the . on the previous line, together with the method call receiver. + #{something} + HERE + .somethingelse + ^ Place the . on the previous line, together with the method call receiver. + RUBY + + expect_correction(<<~'RUBY') + my_method. + something(<<~HERE). + #{something} + HERE + somethingelse + RUBY + end + end + + context 'as the first argument' do + it 'registers an offense' do + expect_offense(<<~'RUBY') + my_method + .something(<<~HERE, true) + ^ Place the . on the previous line, together with the method call receiver. + #{something} + HERE + .somethingelse + ^ Place the . on the previous line, together with the method call receiver. + RUBY + + expect_correction(<<~'RUBY') + my_method. + something(<<~HERE, true). + #{something} + HERE + somethingelse + RUBY + end + end + + context 'with multiple heredocs' do + it 'registers an offense' do + expect_offense(<<~'RUBY') + my_method + .something(<<~HERE, <<~THERE) + ^ Place the . on the previous line, together with the method call receiver. + something + HERE + another thing + THERE + .somethingelse + ^ Place the . on the previous line, together with the method call receiver. + RUBY + + expect_correction(<<~'RUBY') + my_method. + something(<<~HERE, <<~THERE). + something + HERE + another thing + THERE + somethingelse + RUBY + end + end + end + + context 'when the receiver is a heredoc' do + it 'registers an offense' do + expect_offense(<<~RUBY) + <<~HEREDOC + something + HEREDOC + .method_name + ^ Place the . on the previous line, together with the method call receiver. + RUBY + + expect_correction(<<~RUBY) + <<~HEREDOC. + something + HEREDOC + method_name + RUBY + end + end end end