Skip to content
Snippets Groups Projects
style.rb 5.74 KiB
Newer Older
#:  * `style` [`--fix`] [`--display-cop-names`] [`--only-cops=`[COP1,COP2..]|`--except-cops=`[COP1,COP2..]] [<files>|<taps>|<formulae>]:
#:    Check formulae or files for conformance to Homebrew style guidelines.
#:
#:    <formulae> and <files> may not be combined. If both are omitted, style will run
#:    style checks on the whole Homebrew `Library`, including core code and all
#:    formulae.
#:
#:    If `--fix` is passed, style violations will be automatically fixed using
#:    RuboCop's `--auto-correct` feature.
#:
#:    If `--display-cop-names` is passed, the RuboCop cop name for each violation
#:    is included in the output.
#:
#:    If `--only-cops` is passed, only the given Rubocop cop(s)' violations would be checked.
#:
#:    If `--except-cops` is passed, the given Rubocop cop(s)' checks would be skipped.
#:
#:    Exits with a non-zero status if any style violations are found.

require "utils"
require "open3"
module Homebrew
  module_function

  def style
    target = if ARGV.named.empty?
    elsif ARGV.named.any? { |file| File.exist? file }
      ARGV.named
    elsif ARGV.named.any? { |tap| tap.count("/") == 1 }
      ARGV.named.map { |tap| Tap.fetch(tap).path }
    else
      ARGV.formulae.map(&:path)
    end

    only_cops = ARGV.value("only-cops").to_s.split(",")
    except_cops = ARGV.value("except-cops").to_s.split(",")
    if !only_cops.empty? && !except_cops.empty?
      odie "--only-cops and --except-cops cannot be used simultaneously!"
    end

    options = { fix: ARGV.flag?("--fix") }
    if !only_cops.empty?
      options[:only_cops] = only_cops
    elsif !except_cops.empty?
      options[:except_cops] = except_cops
    end

    Homebrew.failed = check_style_and_print(target, options)
  end

  # Checks style for a list of files, printing simple RuboCop output.
  # Returns true if violations were found, false otherwise.
  def check_style_and_print(files, options = {})
    check_style_impl(files, :print, options)
  end

  # Checks style for a list of files, returning results as a RubocopResults
  # object parsed from its JSON output.
  def check_style_json(files, options = {})
    check_style_impl(files, :json, options)
  end

  def check_style_impl(files, output_type, options = {})
    fix = options[:fix]
    Homebrew.install_gem_setup_path! "rubocop", HOMEBREW_RUBOCOP_VERSION
    require "rubocop"
    require_relative "../rubocops"
Mike McQuaid's avatar
Mike McQuaid committed
    args = %w[
Mike McQuaid's avatar
Mike McQuaid committed
    if fix
      args << "--auto-correct"
    else
      args << "--parallel"
    end
Mike McQuaid's avatar
Mike McQuaid committed
      options[:except_cops].map! { |cop| RuboCop::Cop::Cop.registry.qualified_cop_name(cop.to_s, "") }
      cops_to_exclude = options[:except_cops].select do |cop|
        RuboCop::Cop::Cop.registry.names.include?(cop) ||
          RuboCop::Cop::Cop.registry.departments.include?(cop.to_sym)
      end
      args << "--except" << cops_to_exclude.join(",") unless cops_to_exclude.empty?
    elsif options[:only_cops]
Mike McQuaid's avatar
Mike McQuaid committed
      options[:only_cops].map! { |cop| RuboCop::Cop::Cop.registry.qualified_cop_name(cop.to_s, "") }
      cops_to_include = options[:only_cops].select do |cop|
        RuboCop::Cop::Cop.registry.names.include?(cop) ||
          RuboCop::Cop::Cop.registry.departments.include?(cop.to_sym)
      end
      args << "--only" << cops_to_include.join(",") unless cops_to_include.empty?
      args << "--config" << HOMEBREW_LIBRARY_PATH/".rubocop.yml"
      args << HOMEBREW_LIBRARY_PATH
      args << "--config" << HOMEBREW_LIBRARY/".rubocop.yml"
Mike McQuaid's avatar
Mike McQuaid committed
    cache_env = { "XDG_CACHE_HOME" => "#{HOMEBREW_CACHE}/style" }

    case output_type
    when :print
      args << "--display-cop-names" if ARGV.include? "--display-cop-names"
Mike McQuaid's avatar
Mike McQuaid committed
      args << "--format" << "simple" if files
Mike McQuaid's avatar
Mike McQuaid committed
      system(cache_env, "rubocop", *args)
Mike McQuaid's avatar
Mike McQuaid committed
      json, _, status = Open3.capture3(cache_env, "rubocop", "--format", "json", *args)
      # exit status of 1 just means violations were found; other numbers mean
      # execution errors.
      # exitstatus can also be nil if RuboCop process crashes, e.g. due to
      # native extension problems.
      # JSON needs to be at least 2 characters.
      if !status.success? || json.to_s.length < 2
        raise "Error running `rubocop --format json #{args.join " "}`"
      end
      RubocopResults.new(JSON.parse(json))
    else
      raise "Invalid output_type for check_style_impl: #{output_type}"
  class RubocopResults
    def initialize(json)
      @metadata = json["metadata"]
      @file_offenses = {}
      json["files"].each do |f|
        next if f["offenses"].empty?
        file = File.realpath(f["path"])
        @file_offenses[file] = f["offenses"].map { |x| RubocopOffense.new(x) }
      end
    end

    def file_offenses(path)
      @file_offenses[path.to_s]
    end
  end
  class RubocopOffense
    attr_reader :severity, :message, :corrected, :location, :cop_name

    def initialize(json)
      @severity = json["severity"]
      @message = json["message"]
      @cop_name = json["cop_name"]
      @corrected = json["corrected"]
      @location = RubocopLineLocation.new(json["location"])
    end

    def severity_code
      @severity[0].upcase
    end

    def to_s(options = {})
      if options[:display_cop_name]
        "#{severity_code}: #{location.to_short_s}: #{cop_name}: #{message}"
      else
        "#{severity_code}: #{location.to_short_s}: #{message}"
      end
    end
  end

  class RubocopLineLocation
    attr_reader :line, :column, :length

    def initialize(json)
      @line = json["line"]
      @column = json["column"]
      @length = json["length"]
    end

    def to_s
      "#{line}: col #{column} (#{length} chars)"
    end

    def to_short_s
      "#{line}: col #{column}"
    end