
Homebrew's actually ended up using a fair few gems. While we want to avoid Bundler at runtime (and this PR still does that, in fact uses Bundler even less at runtime than it did before) writing our own version to use at build-time seems redundant.
183 lines
5.6 KiB
Ruby
183 lines
5.6 KiB
Ruby
#: * `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 "json"
|
|
|
|
module Homebrew
|
|
module_function
|
|
|
|
def style
|
|
target = if ARGV.named.empty?
|
|
nil
|
|
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.run_bundler_if_needed!
|
|
require "rubocop"
|
|
require_relative "../rubocops"
|
|
|
|
args = %w[
|
|
--force-exclusion
|
|
]
|
|
args << "--auto-correct" if fix
|
|
|
|
if options[:except_cops]
|
|
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]
|
|
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?
|
|
end
|
|
|
|
if files.nil?
|
|
args << "--config" << HOMEBREW_LIBRARY_PATH/".rubocop.yml"
|
|
args += [HOMEBREW_LIBRARY_PATH]
|
|
else
|
|
args << "--config" << HOMEBREW_LIBRARY/".rubocop.yml"
|
|
args += files
|
|
end
|
|
|
|
case output_type
|
|
when :print
|
|
args << "--display-cop-names" if ARGV.include? "--display-cop-names"
|
|
args << "--format" << "simple" if files
|
|
system "rubocop", *args
|
|
!$?.success?
|
|
when :json
|
|
json = Utils.popen_read_text("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 $?.exitstatus.nil? || $?.exitstatus > 1 || 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}"
|
|
end
|
|
end
|
|
|
|
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
|
|
end
|
|
end
|