sds/slim-lint

View on GitHub
lib/slim_lint/ruby_extractor.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module SlimLint
  # Utility class for extracting Ruby script from a Slim template that can then
  # be linted with a Ruby linter (i.e. is "legal" Ruby).
  #
  # The goal is to turn this:
  #
  #    - if items.any?
  #      table#items
  #      - for item in items
  #        tr
  #          td.name = item.name
  #          td.price = item.price
  #    - else
  #       p No items found.
  #
  # into (something like) this:
  #
  #    if items.any?
  #      for item in items
  #        puts item.name
  #        puts item.price
  #    else
  #      puts 'No items found'
  #    end
  #
  # The translation won't be perfect, and won't make any real sense, but the
  # relationship between variable declarations/uses and the flow control graph
  # will remain intact.
  class RubyExtractor
    include SexpVisitor
    extend SexpVisitor::DSL

    # Stores the extracted source and a map of lines of generated source to the
    # original source that created them.
    #
    # @attr_reader source [String] generated source code
    # @attr_reader source_map [Hash] map of line numbers from generated source
    #   to original source line number
    RubySource = Struct.new(:source, :source_map)

    # Extracts Ruby code from Sexp representing a Slim document.
    #
    # @param sexp [SlimLint::Sexp]
    # @return [SlimLint::RubyExtractor::RubySource]
    def extract(sexp)
      trigger_pattern_callbacks(sexp)
      RubySource.new(@source_lines.join("\n"), @source_map)
    end

    on_start do |_sexp|
      @source_lines = []
      @source_map = {}
      @line_count = 0
      @dummy_puts_count = 0
    end

    on [:html, :doctype] do |sexp|
      append_dummy_puts(sexp)
    end

    on [:html, :tag] do |sexp|
      append_dummy_puts(sexp)
    end

    on [:static] do |sexp|
      append_dummy_puts(sexp)
    end

    on [:dynamic] do |sexp|
      _, ruby = sexp
      append(ruby, sexp)
    end

    on [:code] do |sexp|
      _, ruby = sexp
      append(ruby, sexp)
    end

    private

    # Append code to the buffer.
    #
    # @param code [String]
    # @param sexp [SlimLint::Sexp]
    def append(code, sexp)
      return if code.empty?

      original_line = sexp.line

      # For code that spans multiple lines, the resulting code will span
      # multiple lines, so we need to create a mapping for each line.
      code.split("\n").each_with_index do |line, index|
        @source_lines << line
        @line_count += 1
        @source_map[@line_count] = original_line + index
      end
    end

    def append_dummy_puts(sexp)
      append("_slim_lint_puts_#{@dummy_puts_count}", sexp)
      @dummy_puts_count += 1
    end
  end
end