# frozen_string_literal: true module RuboCop module Cop module Layout # This hint checks the indentation of the here document bodies. The bodies # are indented one step. # # Note: When ``Layout/LineLength``'s `AllowHeredoc` is false (not default), # this hint does not add any flags for long here documents to # avoid `Layout/LineLength`'s flags. # # @example # # bad # <<-RUBY # something # RUBY # # # good # <<~RUBY # something # RUBY # # class HeredocIndentation < Cop include Heredoc TYPE_MSG = 'Use %d spaces for indentation in a ' \ 'heredoc by using `<<~` instead of `%s`.' WIDTH_MSG = 'Use %d spaces for indentation in a heredoc.' def on_heredoc(node) body = heredoc_body(node) return if body.strip.empty? body_indent_level = indent_level(body) if heredoc_indent_type(node) == '~' expected_indent_level = base_indent_level(node) + indentation_width return if expected_indent_level == body_indent_level else return unless body_indent_level.zero? end return if line_too_long?(node) add_offense(node, location: :heredoc_body) end def autocorrect(node) lambda do |corrector| if heredoc_indent_type(node) == '~' adjust_squiggly(corrector, node) else adjust_minus(corrector, node) end end end private def message(node) current_indent_type = "<<#{heredoc_indent_type(node)}" if current_indent_type == '<<~' width_message(indentation_width) else type_message(indentation_width, current_indent_type) end end def type_message(indentation_width, current_indent_type) format( TYPE_MSG, indentation_width: indentation_width, current_indent_type: current_indent_type ) end def width_message(indentation_width) format( WIDTH_MSG, indentation_width: indentation_width ) end def line_too_long?(node) return false if unlimited_heredoc_length? body = heredoc_body(node) expected_indent = base_indent_level(node) + indentation_width actual_indent = indent_level(body) increase_indent_level = expected_indent - actual_indent longest_line(body).size + increase_indent_level >= max_line_length end def longest_line(lines) lines.each_line.max_by { |line| line.chomp.size }.chomp end def unlimited_heredoc_length? config.for_cop('Layout/LineLength')['AllowHeredoc'] end def max_line_length config.for_cop('Layout/LineLength')['Max'] end def adjust_squiggly(corrector, node) corrector.replace(node.loc.heredoc_body, indented_body(node)) corrector.replace(node.loc.heredoc_end, indented_end(node)) end def adjust_minus(corrector, node) heredoc_beginning = node.loc.expression.source corrected = heredoc_beginning.sub(/<<-?/, '<<~') corrector.replace(node, corrected) end def indented_body(node) body = heredoc_body(node) body_indent_level = indent_level(body) correct_indent_level = base_indent_level(node) + indentation_width body.gsub(/^[^\S\r\n]{#{body_indent_level}}/, ' ' * correct_indent_level) end def indented_end(node) end_ = heredoc_end(node) end_indent_level = indent_level(end_) correct_indent_level = base_indent_level(node) if end_indent_level < correct_indent_level end_.gsub(/^\s{#{end_indent_level}}/, ' ' * correct_indent_level) else end_ end end def base_indent_level(node) base_line_num = node.loc.expression.line base_line = processed_source.lines[base_line_num - 1] indent_level(base_line) end def indent_level(str) indentations = str.lines .map { |line| line[/^\s*/] } .reject { |line| line.end_with?("\n") } indentations.empty? ? 0 : indentations.min_by(&:size).size end # Returns '~', '-' or nil def heredoc_indent_type(node) node.source[/^<<([~-])/, 1] end def indentation_width @config.for_cop('Layout/IndentationWidth')['Width'] || 2 end def heredoc_body(node) node.loc.heredoc_body.source.scrub end def heredoc_end(node) node.loc.heredoc_end.source.scrub end end end end end