Skip to content

Commit

Permalink
Add HeredocMethodCallPosition cop
Browse files Browse the repository at this point in the history
  • Loading branch information
Buildkite authored and bbatsov committed Apr 29, 2019
1 parent 557ad20 commit 0391069
Show file tree
Hide file tree
Showing 7 changed files with 373 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,8 @@
* Add initial autocorrection support to `Metrics/LineLength`. ([@maxh][])
* Add `Layout/IndentFirstParameter`. ([@maxh][])
* [#6974](https://github.com/rubocop-hq/rubocop/issues/6974): Make `Layout/FirstMethodArgumentLineBreak` aware of calling using `super`. ([@koic][])
* Add new `Lint/HeredocMethodCallPosition` cop. ([@maxh][])


### Bug fixes

Expand Down
7 changes: 7 additions & 0 deletions config/default.yml
Expand Up @@ -1360,6 +1360,13 @@ Lint/HandleExceptions:
Enabled: true
VersionAdded: '0.9'

Lint/HeredocMethodCallPosition:
Description: >-
Checks for the ordering of a method call where
the receiver of the call is a HEREDOC.
Enabled: false
VersionAdded: '0.68'

Lint/ImplicitStringConcatenation:
Description: >-
Checks for adjacent string literals on the same line, which
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Expand Up @@ -293,6 +293,7 @@
require_relative 'rubocop/cop/lint/float_out_of_range'
require_relative 'rubocop/cop/lint/format_parameter_mismatch'
require_relative 'rubocop/cop/lint/handle_exceptions'
require_relative 'rubocop/cop/lint/heredoc_method_call_position'
require_relative 'rubocop/cop/lint/implicit_string_concatenation'
require_relative 'rubocop/cop/lint/inherit_exception'
require_relative 'rubocop/cop/lint/ineffective_access_modifier'
Expand Down
157 changes: 157 additions & 0 deletions lib/rubocop/cop/lint/heredoc_method_call_position.rb
@@ -0,0 +1,157 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Lint
# This cop checks for the ordering of a method call where
# the receiver of the call is a HEREDOC.
#
# @example
# # bad
#
# <<-SQL
# bar
# SQL
# .strip_indent
#
# <<-SQL
# bar
# SQL
# .strip_indent
# .trim
#
# # good
#
# <<-SQL.strip_indent
# bar
# SQL
#
# <<-SQL.strip_indent.trim
# bar
# SQL
#
class HeredocMethodCallPosition < Cop
include RangeHelp

MSG = 'Put a method call with a HEREDOC receiver on the ' \
'same line as the HEREDOC opening.'.freeze

STRING_TYPES = %i[str dstr xstr].freeze
def on_send(node)
heredoc = heredoc_node_descendent_receiver(node)
return unless heredoc
return if correctly_positioned?(node, heredoc)

add_offense(node, location: call_after_heredoc_range(heredoc))
end
alias on_csend on_send

def autocorrect(node)
heredoc = heredoc_node_descendent_receiver(node)

lambda do |corrector|
call_range = call_range_to_safely_reposition(node, heredoc)
return if call_range.nil?

call_source = call_range.source.strip
corrector.remove(call_range)
corrector.insert_after(heredoc_begin_line_range(node), call_source)
end
end

private

def heredoc_node_descendent_receiver(node)
while send_node?(node)
return node.receiver if heredoc_node?(node.receiver)

node = node.receiver
end
end

def send_node?(node)
return nil unless node

node.send_type? || node.csend_type?
end

def heredoc_node?(node)
node && STRING_TYPES.include?(node.type) && node.heredoc?
end

def call_after_heredoc_range(heredoc)
pos = heredoc_end_pos(heredoc)
range_between(pos + 1, pos + 2)
end

def correctly_positioned?(node, heredoc)
heredoc_end_pos(heredoc) > call_end_pos(node)
end

def calls_on_multiple_lines?(node, _heredoc)
last_line = node.last_line
while send_node?(node)
return true unless last_line == node.last_line
return true unless all_on_same_line?(node.arguments)

node = node.receiver
end
false
end

def all_on_same_line?(nodes)
return true if nodes.empty?

nodes.first.first_line == nodes.last.last_line
end

def heredoc_end_pos(heredoc)
heredoc.location.heredoc_end.end_pos
end

def call_end_pos(node)
node.source_range.end_pos
end

def heredoc_begin_line_range(heredoc)
pos = heredoc.source_range.begin_pos
range_by_whole_lines(range_between(pos, pos))
end

def call_line_range(node)
pos = node.source_range.end_pos
range_by_whole_lines(range_between(pos, pos))
end

# Returns nil if no range can be safely repositioned.
def call_range_to_safely_reposition(node, heredoc)
return nil if calls_on_multiple_lines?(node, heredoc)

heredoc_end_pos = heredoc_end_pos(heredoc)
call_end_pos = call_end_pos(node)

call_range = range_between(heredoc_end_pos, call_end_pos)
call_line_range = call_line_range(node)

call_source = call_range.source.strip
call_line_source = call_line_range.source.strip

return call_range if call_source == call_line_source

if trailing_comma?(call_source, call_line_source)
# If there's some on the last line other than the call, e.g.
# a trailing comma, then we leave the "\n" following the
# heredoc_end in place.
return range_between(heredoc_end_pos, call_end_pos + 1)
end

nil
end

def trailing_comma?(call_source, call_line_source)
call_source + ',' == call_line_source
end
end
end
end
end
1 change: 1 addition & 0 deletions manual/cops.md
Expand Up @@ -216,6 +216,7 @@ In the following section you find all available cops:
* [Lint/FloatOutOfRange](cops_lint.md#lintfloatoutofrange)
* [Lint/FormatParameterMismatch](cops_lint.md#lintformatparametermismatch)
* [Lint/HandleExceptions](cops_lint.md#linthandleexceptions)
* [Lint/HeredocMethodCallPosition](cops_lint.md#lintheredocmethodcallposition)
* [Lint/ImplicitStringConcatenation](cops_lint.md#lintimplicitstringconcatenation)
* [Lint/IneffectiveAccessModifier](cops_lint.md#lintineffectiveaccessmodifier)
* [Lint/InheritException](cops_lint.md#lintinheritexception)
Expand Down
36 changes: 36 additions & 0 deletions manual/cops_lint.md
Expand Up @@ -856,6 +856,42 @@ end

* [https://github.com/rubocop-hq/ruby-style-guide#dont-hide-exceptions](https://github.com/rubocop-hq/ruby-style-guide#dont-hide-exceptions)

## Lint/HeredocMethodCallPosition

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
--- | --- | --- | --- | ---
Disabled | Yes | Yes | 0.68 | -

This cop checks for the ordering of a method call where
the receiver of the call is a HEREDOC.

### Examples

```ruby
# bad

<<-SQL
bar
SQL
.strip_indent

<<-SQL
bar
SQL
.strip_indent
.trim

# good

<<-SQL.strip_indent
bar
SQL

<<-SQL.strip_indent.trim
bar
SQL
```

## Lint/ImplicitStringConcatenation

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
Expand Down

0 comments on commit 0391069

Please sign in to comment.