class HamlLint::RubyExtraction::ScriptChunk

Chunk for handling outputting and silent scripts, so ‘ = foo` and ` - bar` Does NOT handle a script beside a tag (ex: `%div= spam`)

Constants

ALLOW_EXPRESSION_AFTER_LINE_ENDING_WITH
MID_BLOCK_KEYWORDS

Attributes

first_output_haml_prefix[R]

@return [String] The prefix for the first outputting string of this script. (One of = != &=)

The outputting scripts after the first are always with =
must_start_chunk[R]

@return [Boolean] true if this ScriptChunk must be at the beginning of a chunk.

This blocks this ScriptChunk from being fused to a ScriptChunk that is before it.
Needed to handle some patterns of outputting script.
previous_chunk[R]

@return [HamlLint::RubyExtraction::BaseChunk] The previous chunk can affect how

our starting marker must be indented.
skip_line_indexes_in_source_map[R]

@return [Array<Integer>] Line indexes to ignore when building the source_map. For examples,

implicit `end` are on their own line in the Ruby file, but in the HAML, they are absent.

Public Class Methods

find_statement_start_line_indexes(to_ruby_lines) click to toggle source
# File lib/haml_lint/ruby_extraction/script_chunk.rb, line 169
def self.find_statement_start_line_indexes(to_ruby_lines) # rubocop:disable Metrics
  if to_ruby_lines.size == 1
    if to_ruby_lines.first[/\S/]
      return [0]
    else
      return []
    end
  end
  statement_start_line_indexes = [] # 0-indexed
  allow_expression_after_line_number = 0 # 1-indexed
  last_do_keyword_line_number = nil # 1-indexed, like Ripper.lex

  to_ruby_string = to_ruby_lines.join("\n")
  if RUBY_VERSION < '3.1'
    # Ruby 2.6's Ripper has issues when it encounters a else, when, elsif without a matching if/case before.
    # It literally stop lexing at that point without any error.
    # Ex from 2.7.8:
    #   require 'ripper'
    #   Ripper.lex("a\nelse\nb")
    #   #=> [[[1, 0], :on_ident, "a", CMDARG], [[1, 1], :on_nl, "\n", BEG], [[2, 0], :on_kw, "else", BEG]]
    # So we add enough ifs to last quite a few layer. Hopefully enough for all needs. To clarify, there would need
    # as many "end" keyword in a single ScriptChunk followed by one of the problematic keyword for the problem
    # to show up.
    # Considering that a `end` without anything else on the line is removed from to_ruby_lines before getting here
    # (in format_ruby_lines_to_haml_lines), 10 ifs should be plenty.
    to_ruby_string = ('if a;' * 10) + to_ruby_string
  end

  last_line_number_seen = nil
  Ripper.lex(to_ruby_string).each do |start_loc, token, str|
    last_line_number_seen = start_loc[0]
    if token == :on_nl
      # :on_nl happens when we have a meaningful line change.
      allow_expression_after_line_number = start_loc[0]
      next
    elsif token == :on_ignored_nl
      # :on_ignored_nl happens for newlines within an expression, or consecutive newlines..
      #    and some cases we care about such as a newline after the pipes after arguments of a block
      if last_do_keyword_line_number == start_loc[0]
        # When starting a block, Ripper.lex gives :on_ignored_nl
        allow_expression_after_line_number = start_loc[0]
      end
      next
    end

    if allow_expression_after_line_number && str[/\S/]
      if allow_expression_after_line_number < start_loc[0]
        # Ripper.lex returns line numbers 1-indexed, we want 0-indexed
        statement_start_line_indexes << start_loc[0] - 1
      end
      allow_expression_after_line_number = nil
    end

    if token == :on_comment
      # :on_comment contain its own newline at the end of the content
      allow_expression_after_line_number = start_loc[0]
    elsif token == :on_kw
      if str == 'do'
        # Because of the possible arguments for the block, we can't simply set is_between_expressions to true
        last_do_keyword_line_number = start_loc[0]
      elsif ALLOW_EXPRESSION_AFTER_LINE_ENDING_WITH.include?(str)
        allow_expression_after_line_number = start_loc[0]
      end
    end
  end

  # number is 1-indexed, and we want the line after it, so that's great
  if last_line_number_seen < to_ruby_lines.size && to_ruby_lines[last_line_number_seen..].any? { |l| l[/\S/] }
    # There are non-empty lines after the last line Ripper showed us, that's a problem!
    msg = +'It seems Ripper did not properly process some source code. Please make sure you are on the '
    msg << 'latest Haml-Lint version, then create an issue at '
    msg << "https://github.com/sds/haml-lint/issues and include the following information:\n"
    msg << "Ruby version: #{RUBY_VERSION}\n"
    msg << "Haml-Lint version: #{HamlLint::VERSION}\n"
    msg << "HAML version: #{Haml::VERSION}\n"
    msg << "problematic source code:\n```\n#{to_ruby_lines.join("\n")}\n```"
    raise msg
  end

  statement_start_line_indexes
end
format_ruby_lines_to_haml_lines(to_ruby_lines, script_output_ruby_prefix:, first_output_haml_prefix: '=') click to toggle source
# File lib/haml_lint/ruby_extraction/script_chunk.rb, line 103
def self.format_ruby_lines_to_haml_lines(to_ruby_lines, script_output_ruby_prefix:, first_output_haml_prefix: '=') # rubocop:disable Metrics
  to_ruby_lines.reject! { |l| l.strip == 'end' }
  return [] if to_ruby_lines.empty?

  statement_start_line_indexes = find_statement_start_line_indexes(to_ruby_lines)

  continued_line_indent_delta = 2
  continued_line_min_indent = 2

  cur_line_start_index = nil
  line_start_indexes_that_need_pipes = []
  haml_output_prefix = first_output_haml_prefix
  to_haml_lines = to_ruby_lines.map.with_index do |line, i| # rubocop:disable Metrics/BlockLength
    if !/\S/.match?(line)
      # whitespace or empty lines, we don't want any indentation
      ''
    elsif statement_start_line_indexes.include?(i)
      cur_line_start_index = i
      code_start = line.index(/\S/)
      continued_line_min_indent = code_start + 2
      if line[code_start..].start_with?(script_output_ruby_prefix)
        line = line.sub(script_output_ruby_prefix, '')
        # The next lines may have been too indented because of the "HL.out = " prefix
        continued_line_indent_delta = 2 - script_output_ruby_prefix.size
        new_line = "#{line[0...code_start]}#{haml_output_prefix} #{line[code_start..]}"
        haml_output_prefix = '='
        new_line
      else
        continued_line_indent_delta = 2
        "#{line[0...code_start]}- #{line[code_start..]}"
      end
    else
      unless to_ruby_lines[i - 1].end_with?(',')
        line_start_indexes_that_need_pipes << cur_line_start_index
      end

      line = HamlLint::Utils.indent(line, continued_line_indent_delta)
      cur_indent = line[/^ */].size
      if cur_indent < continued_line_min_indent
        line = HamlLint::Utils.indent(line, continued_line_min_indent - cur_indent)
      end
      line
    end
  end

  # Starting from the end because we need to add newlines when 2 groups of lines need pipes, so that they are
  # separate.
  line_start_indexes_that_need_pipes.reverse_each do |cur_line_i|
    loop do
      cur_line = to_haml_lines[cur_line_i]
      break if cur_line.nil? || cur_line.empty?
      to_haml_lines[cur_line_i] = cur_line + ' |'
      cur_line_i += 1

      break if statement_start_line_indexes.include?(cur_line_i)
    end

    next_line = to_haml_lines[cur_line_i]
    if next_line && HamlLint::RubyExtraction::ChunkExtractor::HAML_PARSER_INSTANCE.send(:is_multiline?, next_line)
      to_haml_lines.insert(cur_line_i, '')
    end
  end

  to_haml_lines
end
new(*args, previous_chunk:, must_start_chunk: false, skip_line_indexes_in_source_map: [], first_output_haml_prefix: '=', **kwargs) click to toggle source
# File lib/haml_lint/ruby_extraction/script_chunk.rb, line 28
def initialize(*args, previous_chunk:, must_start_chunk: false, # rubocop:disable Metrics/ParameterLists
               skip_line_indexes_in_source_map: [], first_output_haml_prefix: '=', **kwargs)
  super(*args, **kwargs)
  @must_start_chunk = must_start_chunk
  @skip_line_indexes_in_source_map = skip_line_indexes_in_source_map
  @previous_chunk = previous_chunk
  @first_output_haml_prefix = first_output_haml_prefix
end

Public Instance Methods

fuse(following_chunk) click to toggle source
# File lib/haml_lint/ruby_extraction/script_chunk.rb, line 37
def fuse(following_chunk)
  case following_chunk
  when ScriptChunk
    fuse_script_chunk(following_chunk)
  when ImplicitEndChunk
    fuse_implicit_end(following_chunk)
  end
end
fuse_implicit_end(following_chunk) click to toggle source
# File lib/haml_lint/ruby_extraction/script_chunk.rb, line 67
def fuse_implicit_end(following_chunk)
  new_lines = @ruby_lines.dup
  last_non_empty_line_index = new_lines.rindex { |line| line =~ /\S/ }

  # There is only one line in ImplicitEndChunk
  new_end_index = last_non_empty_line_index + 1
  new_lines.insert(new_end_index, following_chunk.ruby_lines.first)
  source_map_skips = @skip_line_indexes_in_source_map + [new_end_index]

  ScriptChunk.new(node,
                  new_lines,
                  haml_line_index: haml_line_index,
                  skip_line_indexes_in_source_map: source_map_skips,
                  end_marker_indent: following_chunk.end_marker_indent,
                  previous_chunk: previous_chunk,
                  first_output_haml_prefix: @first_output_haml_prefix)
end
fuse_script_chunk(following_chunk) click to toggle source
# File lib/haml_lint/ruby_extraction/script_chunk.rb, line 46
def fuse_script_chunk(following_chunk)
  return if following_chunk.end_marker_indent.nil?
  return if following_chunk.must_start_chunk

  nb_blank_lines_between = following_chunk.haml_line_index - haml_line_index - nb_haml_lines
  blank_lines = nb_blank_lines_between > 0 ? [''] * nb_blank_lines_between : []
  new_lines = @ruby_lines + blank_lines + following_chunk.ruby_lines

  source_map_skips = @skip_line_indexes_in_source_map
  source_map_skips.concat(following_chunk.skip_line_indexes_in_source_map
                            .map { |i| i + @ruby_lines.size })

  ScriptChunk.new(node,
                  new_lines,
                  haml_line_index: haml_line_index,
                  skip_line_indexes_in_source_map: source_map_skips,
                  end_marker_indent: following_chunk.end_marker_indent,
                  previous_chunk: previous_chunk,
                  first_output_haml_prefix: @first_output_haml_prefix)
end
start_marker_indent() click to toggle source
# File lib/haml_lint/ruby_extraction/script_chunk.rb, line 85
def start_marker_indent
  default_indent = super
  default_indent += 2 if MID_BLOCK_KEYWORDS.include?(ChunkExtractor.block_keyword(ruby_lines.first))
  [default_indent, previous_chunk&.end_marker_indent || previous_chunk&.start_marker_indent].compact.max
end
transfer_correction_logic(coordinator, to_ruby_lines, haml_lines) click to toggle source
# File lib/haml_lint/ruby_extraction/script_chunk.rb, line 91
def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines)
  to_haml_lines = self.class.format_ruby_lines_to_haml_lines(
    to_ruby_lines,
    script_output_ruby_prefix: coordinator.script_output_prefix,
    first_output_haml_prefix: @first_output_haml_prefix
  )

  haml_lines[@haml_line_index..haml_end_line_index] = to_haml_lines
end