sds/slim-lint

View on GitHub
lib/slim_lint/linter/rubocop.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

require_relative '../ruby_extractor'
require_relative '../ruby_extract_engine'

require 'rubocop'

module SlimLint
  # Runs RuboCop on Ruby code extracted from Slim templates.
  class Linter::RuboCop < Linter
    include LinterRegistry

    on_start do |_sexp|
      processed_sexp = SlimLint::RubyExtractEngine.new.call(document.source)

      extractor = SlimLint::RubyExtractor.new
      extracted_source = extractor.extract(processed_sexp)

      next if extracted_source.source.empty?

      find_lints(extracted_source.source, extracted_source.source_map)
    end

    private

    # Executes RuboCop against the given Ruby code and records the offenses as
    # lints.
    #
    # @param ruby [String] Ruby code
    # @param source_map [Hash] map of Ruby code line numbers to original line
    #   numbers in the template
    def find_lints(ruby, source_map)
      rubocop = ::RuboCop::CLI.new

      filename = document.file ? "#{document.file}.rb" : 'ruby_script.rb'

      with_ruby_from_stdin(ruby) do
        extract_lints_from_offenses(lint_file(rubocop, filename), source_map)
      end
    end

    # Defined so we can stub the results in tests
    #
    # @param rubocop [RuboCop::CLI]
    # @param file [String]
    # @return [Array<RuboCop::Cop::Offense>]
    def lint_file(rubocop, file)
      rubocop.run(rubocop_flags << file)
      OffenseCollector.offenses
    end

    # Aggregates RuboCop offenses and converts them to {SlimLint::Lint}s
    # suitable for reporting.
    #
    # @param offenses [Array<RuboCop::Cop::Offense>]
    # @param source_map [Hash]
    def extract_lints_from_offenses(offenses, source_map)
      offenses.select { |offense| !config['ignored_cops'].include?(offense.cop_name) }
              .each do |offense|
        @lints << Lint.new(self,
                           document.file,
                           source_map[offense.line],
                           offense.message)
      end
    end

    # Returns flags that will be passed to RuboCop CLI.
    #
    # @return [Array<String>]
    def rubocop_flags
      flags = %w[--format SlimLint::OffenseCollector]
      flags += ['--config', ENV['SLIM_LINT_RUBOCOP_CONF']] if ENV['SLIM_LINT_RUBOCOP_CONF']
      flags += ['--stdin']
      flags
    end

    # Overrides the global stdin to allow RuboCop to read Ruby code from it.
    #
    # @param ruby [String] the Ruby code to write to the overridden stdin
    # @param _block [Block] the block to perform with the overridden stdin
    # @return [void]
    def with_ruby_from_stdin(ruby, &_block)
      original_stdin = $stdin
      stdin = StringIO.new
      stdin.write(ruby)
      stdin.rewind
      $stdin = stdin
      yield
    ensure
      $stdin = original_stdin
    end
  end

  # Collects offenses detected by RuboCop.
  class OffenseCollector < ::RuboCop::Formatter::BaseFormatter
    class << self
      # List of offenses reported by RuboCop.
      attr_accessor :offenses
    end

    # Executed when RuboCop begins linting.
    #
    # @param _target_files [Array<String>]
    def started(_target_files)
      self.class.offenses = []
    end

    # Executed when a file has been scanned by RuboCop, adding the reported
    # offenses to our collection.
    #
    # @param _file [String]
    # @param offenses [Array<RuboCop::Cop::Offense>]
    def file_finished(_file, offenses)
      self.class.offenses += offenses
    end
  end
end