365 lines
14 KiB
Ruby
Raw Normal View History

2023-03-13 18:31:26 -07:00
# typed: true
# frozen_string_literal: true
2024-03-18 11:09:30 -07:00
require "abstract_command"
require "formula"
require "formula_versions"
require "utils/curl"
2020-09-10 22:00:18 +02:00
require "utils/github/actions"
2020-08-26 09:42:39 +02:00
require "utils/shared_audits"
2020-08-04 10:07:57 -07:00
require "utils/spdx"
require "extend/ENV"
require "formula_cellar_checks"
require "cmd/search"
require "style"
2015-07-09 15:28:27 +01:00
require "date"
require "missing_formula"
require "digest"
2020-06-16 01:00:36 +08:00
require "json"
require "formula_auditor"
2020-11-18 10:05:23 +01:00
require "tap_auditor"
2012-03-17 19:49:49 -07:00
module Homebrew
2024-03-18 11:09:30 -07:00
module DevCmd
class Audit < AbstractCommand
cmd_args do
description <<~EOS
Check <formula> for Homebrew coding style violations. This should be run before
submitting a new formula or cask. If no <formula>|<cask> are provided, check all
locally available formulae and casks and skip style checks. Will exit with a
non-zero status if any errors are found.
EOS
flag "--os=",
description: "Audit the given operating system. (Pass `all` to audit all operating systems.)"
flag "--arch=",
description: "Audit the given CPU architecture. (Pass `all` to audit all architectures.)"
switch "--strict",
description: "Run additional, stricter style checks."
switch "--git",
description: "Run additional, slower style checks that navigate the Git repository."
switch "--online",
description: "Run additional, slower style checks that require a network connection."
switch "--installed",
description: "Only check formulae and casks that are currently installed."
switch "--eval-all",
description: "Evaluate all available formulae and casks, whether installed or not, to audit them. " \
"Implied if `HOMEBREW_EVAL_ALL` is set."
switch "--new",
description: "Run various additional style checks to determine if a new formula or cask is eligible " \
"for Homebrew. This should be used when creating new formulae or casks and implies " \
"`--strict` and `--online`."
switch "--new-formula",
replacement: "--new",
# odeprecated: change this to true on disable and remove `args.new_formula?` calls
disable: false,
hidden: true
switch "--new-cask",
replacement: "--new",
# odeprecated: change this to true on disable and remove `args.new_formula?` calls
disable: false,
hidden: true
switch "--[no-]signing",
description: "Audit for signed apps, which are required on ARM"
switch "--token-conflicts",
description: "Audit for token conflicts."
flag "--tap=",
description: "Check the formulae within the given tap, specified as <user>`/`<repo>."
switch "--fix",
description: "Fix style violations automatically using RuboCop's auto-correct feature."
switch "--display-cop-names",
description: "Include the RuboCop cop name for each violation in the output. This is the default.",
hidden: true
switch "--display-filename",
description: "Prefix every line of output with the file or formula name being audited, to " \
"make output easy to grep."
switch "--skip-style",
description: "Skip running non-RuboCop style checks. Useful if you plan on running " \
"`brew style` separately. Enabled by default unless a formula is specified by name."
switch "-D", "--audit-debug",
description: "Enable debugging and profiling of audit methods."
comma_array "--only",
description: "Specify a comma-separated <method> list to only run the methods named " \
"`audit_`<method>."
comma_array "--except",
description: "Specify a comma-separated <method> list to skip running the methods named " \
"`audit_`<method>."
comma_array "--only-cops",
description: "Specify a comma-separated <cops> list to check for violations of only the listed " \
"RuboCop cops."
comma_array "--except-cops",
description: "Specify a comma-separated <cops> list to skip checking for violations of the " \
"listed RuboCop cops."
switch "--formula", "--formulae",
description: "Treat all named arguments as formulae."
switch "--cask", "--casks",
description: "Treat all named arguments as casks."
conflicts "--only", "--except"
conflicts "--only-cops", "--except-cops", "--strict"
conflicts "--only-cops", "--except-cops", "--only"
conflicts "--formula", "--cask"
conflicts "--installed", "--all"
named_args [:formula, :cask], without_api: true
end
2024-03-18 11:09:30 -07:00
sig { override.void }
def run
new_cask = args.new? || args.new_cask?
new_formula = args.new? || args.new_formula?
Formulary.enable_factory_cache!
os_arch_combinations = args.os_arch_combinations
Homebrew.auditing = true
Homebrew.inject_dump_stats!(FormulaAuditor, /^audit_/) if args.audit_debug?
strict = new_formula || args.strict?
online = new_formula || args.online?
tap_audit = args.tap.present?
skip_style = args.skip_style? || args.no_named? || tap_audit
no_named_args = T.let(false, T::Boolean)
gem_groups = ["audit"]
gem_groups << "style" unless skip_style
Homebrew.install_bundler_gems!(groups: gem_groups)
2024-03-18 11:09:30 -07:00
ENV.activate_extensions!
ENV.setup_build_environment
audit_formulae, audit_casks = Homebrew.with_no_api_env do # audit requires full Ruby source
if args.tap
Tap.fetch(T.must(args.tap)).then do |tap|
[
tap.formula_files.map { |path| Formulary.factory(path) },
tap.cask_files.map { |path| Cask::CaskLoader.load(path) },
]
end
elsif args.installed?
no_named_args = true
[Formula.installed, Cask::Caskroom.casks]
elsif args.no_named?
if !args.eval_all? && !Homebrew::EnvConfig.eval_all?
# This odisabled should probably stick around indefinitely.
odisabled "brew audit",
"brew audit --eval-all or HOMEBREW_EVAL_ALL"
end
no_named_args = true
[
Formula.all(eval_all: args.eval_all?),
Cask::Cask.all(eval_all: args.eval_all?),
]
else
if args.named.any? { |named_arg| named_arg.end_with?(".rb") }
# This odisabled should probably stick around indefinitely,
# until at least we have a way to exclude error on these in the CLI parser.
odisabled "brew audit [path ...]",
"brew audit [name ...]"
end
args.named.to_formulae_and_casks
.partition { |formula_or_cask| formula_or_cask.is_a?(Formula) }
end
end
2024-03-18 11:09:30 -07:00
if audit_formulae.empty? && audit_casks.empty? && !args.tap
ofail "No matching formulae or casks to audit!"
return
end
2024-03-18 11:09:30 -07:00
style_files = args.named.to_paths unless skip_style
2024-03-18 11:09:30 -07:00
only_cops = args.only_cops
except_cops = args.except_cops
style_options = { fix: args.fix?, debug: args.debug?, verbose: args.verbose? }
2024-03-18 11:09:30 -07:00
if only_cops
style_options[:only_cops] = only_cops
elsif new_formula || new_cask
nil
elsif except_cops
style_options[:except_cops] = except_cops
elsif !strict
style_options[:except_cops] = [:FormulaAuditStrict]
end
2024-03-18 11:09:30 -07:00
# Run tap audits first
named_arg_taps = [*audit_formulae, *audit_casks].map(&:tap).uniq if !args.tap && !no_named_args
tap_problems = Tap.installed.each_with_object({}) do |tap, problems|
next if args.tap && tap != args.tap
next if named_arg_taps&.exclude?(tap)
2024-03-18 11:09:30 -07:00
ta = TapAuditor.new(tap, strict: args.strict?)
ta.audit
problems[[tap.name, tap.path]] = ta.problems if ta.problems.any?
end
2024-03-18 11:09:30 -07:00
# Check style in a single batch run up front for performance
style_offenses = Style.check_style_json(style_files, **style_options) if style_files
# load licenses
spdx_license_data = SPDX.license_data
spdx_exception_data = SPDX.exception_data
formula_problems = audit_formulae.sort.each_with_object({}) do |f, problems|
path = f.path
only = only_cops ? ["style"] : args.only
options = {
new_formula:,
strict:,
online:,
git: args.git?,
only:,
except: args.except,
spdx_license_data:,
spdx_exception_data:,
style_offenses: style_offenses&.for_path(f.path),
tap_audit:,
}.compact
errors = os_arch_combinations.flat_map do |os, arch|
SimulateSystem.with(os:, arch:) do
odebug "Auditing Formula #{f} on os #{os} and arch #{arch}"
audit_proc = proc { FormulaAuditor.new(Formulary.factory(path), **options).tap(&:audit) }
2024-03-19 12:36:30 -07:00
# Audit requires full Ruby source so disable API. We shouldn't do this for taps however so that we
# don't unnecessarily require a full Homebrew/core clone.
2024-03-18 11:09:30 -07:00
fa = if f.core_formula?
Homebrew.with_no_api_env(&audit_proc)
else
audit_proc.call
end
fa.problems + fa.new_formula_problems
end
end.uniq
problems[[f.full_name, path]] = errors if errors.any?
end
2024-03-18 11:09:30 -07:00
require "cask/auditor" if audit_casks.any?
cask_problems = audit_casks.each_with_object({}) do |cask, problems|
path = cask.sourcefile_path
errors = os_arch_combinations.flat_map do |os, arch|
next [] if os == :linux
SimulateSystem.with(os:, arch:) do
odebug "Auditing Cask #{cask} on os #{os} and arch #{arch}"
Cask::Auditor.audit(
Cask::CaskLoader.load(path),
# For switches, we add `|| nil` so that `nil` will be passed
# instead of `false` if they aren't set.
# This way, we can distinguish between "not set" and "set to false".
audit_online: args.online? || nil,
audit_strict: args.strict? || nil,
# No need for `|| nil` for `--[no-]signing`
# because boolean switches are already `nil` if not passed
audit_signing: args.signing?,
audit_new_cask: new_cask || nil,
audit_token_conflicts: args.token_conflicts? || nil,
quarantine: true,
any_named_args: !no_named_args,
only: args.only,
except: args.except,
).to_a
end
end.uniq
problems[[cask.full_name, path]] = errors if errors.any?
end
2024-03-18 11:09:30 -07:00
print_problems(tap_problems)
print_problems(formula_problems)
print_problems(cask_problems)
2024-03-18 11:09:30 -07:00
tap_count = tap_problems.keys.count
formula_count = formula_problems.keys.count
cask_count = cask_problems.keys.count
2024-03-18 11:09:30 -07:00
corrected_problem_count = (formula_problems.values + cask_problems.values)
.sum { |problems| problems.count { |problem| problem.fetch(:corrected) } }
2016-08-16 17:00:31 +01:00
2024-03-18 11:09:30 -07:00
tap_problem_count = tap_problems.sum { |_, problems| problems.count }
formula_problem_count = formula_problems.sum { |_, problems| problems.count }
cask_problem_count = cask_problems.sum { |_, problems| problems.count }
total_problems_count = formula_problem_count + cask_problem_count + tap_problem_count
2024-03-18 11:09:30 -07:00
if total_problems_count.positive?
errors_summary = Utils.pluralize("problem", total_problems_count, include_count: true)
2014-12-27 12:38:04 +00:00
2024-03-18 11:09:30 -07:00
error_sources = []
if formula_count.positive?
error_sources << Utils.pluralize("formula", formula_count, plural: "e", include_count: true)
end
error_sources << Utils.pluralize("cask", cask_count, include_count: true) if cask_count.positive?
error_sources << Utils.pluralize("tap", tap_count, include_count: true) if tap_count.positive?
2024-03-18 11:09:30 -07:00
errors_summary += " in #{error_sources.to_sentence}" if error_sources.any?
2024-03-18 11:09:30 -07:00
errors_summary += " detected"
2024-03-18 11:09:30 -07:00
if corrected_problem_count.positive?
2024-03-19 12:36:30 -07:00
errors_summary +=
", #{Utils.pluralize("problem", corrected_problem_count, include_count: true)} corrected"
end
2024-03-18 11:09:30 -07:00
ofail "#{errors_summary}."
2021-04-03 03:49:41 +02:00
end
2024-03-18 11:09:30 -07:00
return unless ENV["GITHUB_ACTIONS"]
annotations = formula_problems.merge(cask_problems).flat_map do |(_, path), problems|
problems.map do |problem|
GitHub::Actions::Annotation.new(
:error,
problem[:message],
file: path,
line: problem[:location]&.line,
column: problem[:location]&.column,
)
end
end.compact
2024-03-18 11:09:30 -07:00
annotations.each do |annotation|
puts annotation if annotation.relevant?
end
2023-02-24 08:53:04 -08:00
end
2020-11-18 12:41:18 +01:00
2024-03-18 11:09:30 -07:00
private
2020-11-18 12:41:18 +01:00
2024-03-18 11:09:30 -07:00
def print_problems(results)
results.each do |(name, path), problems|
problem_lines = format_problem_lines(problems)
2020-11-18 12:41:18 +01:00
2024-03-18 11:09:30 -07:00
if args.display_filename?
problem_lines.each do |l|
puts "#{path}: #{l}"
end
else
puts name, problem_lines.map { |l| l.dup.prepend(" ") }
end
end
end
2016-09-22 20:12:28 +02:00
2024-03-18 11:09:30 -07:00
def format_problem_lines(problems)
problems.map do |problem|
status = " #{Formatter.success("[corrected]")}" if problem.fetch(:corrected)
location = problem.fetch(:location)
if location
location = "#{location.line&.to_s&.prepend("line ")}#{location.column&.to_s&.prepend(", col ")}: "
end
message = problem.fetch(:message)
"* #{location}#{message.chomp.gsub("\n", "\n ")}#{status}"
2023-05-19 16:59:14 +02:00
end
end
end
2020-09-10 22:00:18 +02:00
end
end