class RuboCop::Cop::Style::StringConcatenation

Checks for places where string concatenation can be replaced with string interpolation.

The cop can autocorrect simple cases but will skip autocorrecting more complex cases where the resulting code would be harder to read. In those cases, it might be useful to extract statements to local variables or methods which you can then interpolate in a string.

NOTE: When concatenation between two strings is broken over multiple lines, this cop does not register an offense; instead, ‘Style/LineEndConcatenation` will pick up the offense if enabled.

Two modes are supported:

  1. ‘aggressive` style checks and corrects all occurrences of `+` where

either the left or right side of ‘+` is a string literal.

  1. ‘conservative` style on the other hand, checks and corrects only if

left side (receiver of ‘+` method call) is a string literal. This is useful when the receiver is some expression that returns string like `Pathname` instead of a string literal.

@safety

This cop is unsafe in `aggressive` mode, as it cannot be guaranteed that
the receiver is actually a string, which can result in a false positive.

@example Mode: aggressive (default)

# bad
email_with_name = user.name + ' <' + user.email + '>'
Pathname.new('/') + 'test'

# good
email_with_name = "#{user.name} <#{user.email}>"
email_with_name = format('%s <%s>', user.name, user.email)
"#{Pathname.new('/')}test"

# accepted, line-end concatenation
name = 'First' +
  'Last'

@example Mode: conservative

# bad
'Hello' + user.name

# good
"Hello #{user.name}"
user.name + '!!'
Pathname.new('/') + 'test'

Constants

MSG
RESTRICT_ON_SEND

Public Instance Methods

on_new_investigation() click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 69
def on_new_investigation
  @corrected_nodes = nil
end
on_send(node) click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 73
def on_send(node)
  return unless string_concatenation?(node)
  return if line_end_concatenation?(node)

  topmost_plus_node = find_topmost_plus_node(node)
  parts = collect_parts(topmost_plus_node)
  return if mode == :conservative && !parts.first.str_type?

  register_offense(topmost_plus_node, parts)
end

Private Instance Methods

collect_parts(node, parts = []) click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 116
def collect_parts(node, parts = [])
  return unless node

  if plus_node?(node)
    collect_parts(node.receiver, parts)
    collect_parts(node.first_argument, parts)
  else
    parts << node
  end
end
corrected_ancestor?(node) click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 141
def corrected_ancestor?(node)
  node.each_ancestor(:send).any? { |ancestor| @corrected_nodes&.include?(ancestor) }
end
find_topmost_plus_node(node) click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 108
def find_topmost_plus_node(node)
  current = node
  while (parent = current.parent) && plus_node?(parent)
    current = parent
  end
  current
end
handle_quotes(parts) click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 162
def handle_quotes(parts)
  parts.map do |part|
    part == '"' ? '\"' : part
  end
end
heredoc?(node) click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 135
def heredoc?(node)
  return false unless node.str_type? || node.dstr_type?

  node.heredoc?
end
line_end_concatenation?(node) click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 98
def line_end_concatenation?(node)
  # If the concatenation happens at the end of the line,
  # and both the receiver and argument are strings, allow
  # `Style/LineEndConcatenation` to handle it instead.
  node.receiver.str_type? &&
    node.first_argument.str_type? &&
    node.multiline? &&
    node.source =~ /\+\s*\n/
end
mode() click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 172
def mode
  cop_config['Mode'].to_sym
end
plus_node?(node) click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 127
def plus_node?(node)
  node.send_type? && node.method?(:+)
end
register_offense(topmost_plus_node, parts) click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 86
def register_offense(topmost_plus_node, parts)
  add_offense(topmost_plus_node) do |corrector|
    correctable_parts = parts.none? { |part| uncorrectable?(part) }
    if correctable_parts && !corrected_ancestor?(topmost_plus_node)
      corrector.replace(topmost_plus_node, replacement(parts))

      @corrected_nodes ||= Set.new.compare_by_identity
      @corrected_nodes.add(topmost_plus_node)
    end
  end
end
replacement(parts) click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 145
def replacement(parts)
  interpolated_parts =
    parts.map do |part|
      case part.type
      when :str
        value = part.value
        single_quoted?(part) ? value.gsub(/(\\|")/, '\\\\\&') : value.inspect[1..-2]
      when :dstr
        contents_range(part).source
      else
        "\#{#{part.source}}"
      end
    end

  "\"#{handle_quotes(interpolated_parts).join}\""
end
single_quoted?(str_node) click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 168
def single_quoted?(str_node)
  str_node.source.start_with?("'")
end
uncorrectable?(part) click to toggle source
# File lib/rubocop/cop/style/string_concatenation.rb, line 131
def uncorrectable?(part)
  part.multiline? || heredoc?(part) || part.each_descendant(:block).any?
end