309 lines
11 KiB
Ruby
Raw Normal View History

2020-10-10 14:16:11 +02:00
# typed: false
# frozen_string_literal: true
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"
2019-04-17 18:25:08 +09:00
require "cli/parser"
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
2020-10-20 12:03:48 +02:00
extend T::Sig
2016-09-26 01:44:51 +02:00
module_function
2020-10-20 12:03:48 +02:00
sig { returns(CLI::Parser) }
def audit_args
Homebrew::CLI::Parser.new do
description <<~EOS
Check <formula> for Homebrew coding style violations. This should be run before
2020-11-18 12:41:18 +01:00
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
2018-09-22 09:31:30 +05:30
switch "--strict",
description: "Run additional, stricter style checks."
switch "--git",
description: "Run additional, slower style checks that navigate the Git repository."
2018-09-22 09:31:30 +05:30
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 "--all",
hidden: true
2020-11-18 12:41:18 +01:00
switch "--new", "--new-formula", "--new-cask",
2022-06-28 10:09:59 +01:00
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 formula and implies " \
2019-04-30 08:44:35 +01:00
"`--strict` and `--online`."
2020-11-27 11:41:08 -05:00
switch "--[no-]appcast",
description: "Audit the appcast."
switch "--token-conflicts",
description: "Audit for token conflicts."
flag "--tap=",
description: "Check the formulae within the given tap, specified as <user>`/`<repo>."
2018-09-22 09:31:30 +05:30
switch "--fix",
2019-04-30 08:44:35 +01:00
description: "Fix style violations automatically using RuboCop's auto-correct feature."
2018-09-22 09:31:30 +05:30
switch "--display-cop-names",
2019-04-30 08:44:35 +01:00
description: "Include the RuboCop cop name for each violation in the output."
2018-09-22 09:31:30 +05:30
switch "--display-filename",
2022-06-28 10:09:59 +01:00
description: "Prefix every line of output with the file or formula name being audited, to " \
2019-04-30 08:44:35 +01:00
"make output easy to grep."
2021-03-21 13:59:43 -04:00
switch "--display-failures-only",
description: "Only display casks that fail the audit. This is the default for formulae."
switch "--skip-style",
2022-06-28 10:09:59 +01:00
description: "Skip running non-RuboCop style checks. Useful if you plan on running " \
2020-11-12 10:40:48 -05:00
"`brew style` separately. Enabled by default unless a formula is specified by name."
2018-09-22 09:31:30 +05:30
switch "-D", "--audit-debug",
2019-04-30 08:44:35 +01:00
description: "Enable debugging and profiling of audit methods."
2018-09-22 09:31:30 +05:30
comma_array "--only",
2022-06-28 10:09:59 +01:00
description: "Specify a comma-separated <method> list to only run the methods named " \
2019-04-30 08:44:35 +01:00
"`audit_`<method>."
2018-09-22 09:31:30 +05:30
comma_array "--except",
2022-06-28 10:09:59 +01:00
description: "Specify a comma-separated <method> list to skip running the methods named " \
2019-04-30 08:44:35 +01:00
"`audit_`<method>."
2018-09-22 09:31:30 +05:30
comma_array "--only-cops",
2022-06-28 10:09:59 +01:00
description: "Specify a comma-separated <cops> list to check for violations of only the listed " \
2019-04-30 08:44:35 +01:00
"RuboCop cops."
2018-09-22 09:31:30 +05:30
comma_array "--except-cops",
2022-06-28 10:17:14 +01:00
description: "Specify a comma-separated <cops> list to skip checking for violations of the " \
"listed RuboCop cops."
2020-11-18 12:41:18 +01:00
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 "--display-cop-names", "--skip-style"
conflicts "--display-cop-names", "--only-cops"
conflicts "--display-cop-names", "--except-cops"
2020-11-27 11:41:08 -05:00
conflicts "--formula", "--cask"
conflicts "--installed", "--all"
2021-01-10 14:26:40 -05:00
named_args [:formula, :cask]
end
end
2020-11-18 12:41:18 +01:00
sig { void }
def audit
args = audit_args.parse
Homebrew.auditing = true
inject_dump_stats!(FormulaAuditor, /^audit_/) if args.audit_debug?
2012-08-07 01:37:46 -05:00
formula_count = 0
problem_count = 0
corrected_problem_count = 0
new_formula_problem_count = 0
new_formula = args.new_formula?
strict = new_formula || args.strict?
online = new_formula || args.online?
skip_style = args.skip_style? || args.no_named? || args.tap
no_named_args = false
ENV.activate_extensions!
ENV.setup_build_environment
2020-11-18 12:41:18 +01:00
audit_formulae, audit_casks = if args.tap
2022-04-23 01:47:37 +01:00
Tap.fetch(args.tap).then do |tap|
2020-11-18 18:29:20 +01:00
[
tap.formula_names.map { |name| Formula[name] },
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?
odeprecated "brew audit",
"brew audit --eval-all or HOMEBREW_EVAL_ALL"
end
no_named_args = true
[Formula.all, Cask::Cask.all]
else
2020-12-17 21:14:18 +09:00
args.named.to_formulae_and_casks
2020-11-18 12:41:18 +01:00
.partition { |formula_or_cask| formula_or_cask.is_a?(Formula) }
end
if audit_formulae.empty? && audit_casks.empty?
2022-09-26 15:37:12 +02:00
ofail "No matching formulae or casks to audit!"
return
end
2020-11-18 12:41:18 +01:00
style_files = args.named.to_paths unless skip_style
2016-08-16 17:00:31 +01:00
only_cops = args.only_cops
except_cops = args.except_cops
2020-11-18 12:41:18 +01:00
style_options = { fix: args.fix?, debug: args.debug?, verbose: args.verbose? }
if only_cops
2020-11-18 12:41:18 +01:00
style_options[:only_cops] = only_cops
elsif args.new_formula?
nil
elsif except_cops
2020-11-18 12:41:18 +01:00
style_options[:except_cops] = except_cops
elsif !strict
2020-11-18 12:41:18 +01:00
style_options[:except_cops] = [:FormulaAuditStrict]
2012-08-07 01:37:46 -05:00
end
2014-12-27 12:38:04 +00:00
# Run tap audits first
tap_problem_count = 0
tap_count = 0
Tap.each do |tap|
next if args.tap && tap != args.tap
2020-11-18 12:41:18 +01:00
ta = TapAuditor.new(tap, strict: args.strict?)
ta.audit
next if ta.problems.blank?
tap_count += 1
tap_problem_count += ta.problems.size
tap_problem_lines = format_problem_lines(ta.problems)
puts "#{tap.name}:", tap_problem_lines.map { |s| " #{s}" }
end
# Check style in a single batch run up front for performance
2020-11-18 12:41:18 +01:00
style_offenses = Style.check_style_json(style_files, style_options) if style_files
2020-06-16 00:19:32 +08:00
# load licenses
spdx_license_data = SPDX.license_data
spdx_exception_data = SPDX.exception_data
new_formula_problem_lines = []
2021-12-23 14:49:05 -05:00
formula_results = audit_formulae.sort.to_h do |f|
2018-10-30 23:44:14 +05:30
only = only_cops ? ["style"] : args.only
options = {
2021-10-04 21:45:20 -04:00
new_formula: new_formula,
strict: strict,
online: online,
git: args.git?,
only: only,
except: args.except,
spdx_license_data: spdx_license_data,
spdx_exception_data: spdx_exception_data,
style_offenses: style_offenses ? style_offenses.for_path(f.path) : nil,
display_cop_names: args.display_cop_names?,
2020-11-18 12:41:18 +01:00
}.compact
2019-09-02 10:48:19 +01:00
2020-11-18 12:41:18 +01:00
fa = FormulaAuditor.new(f, **options)
2012-08-07 01:37:46 -05:00
fa.audit
2020-09-10 22:00:18 +02:00
2021-04-03 03:49:41 +02:00
if fa.problems.any? || fa.new_formula_problems.any?
formula_count += 1
problem_count += fa.problems.size
problem_lines = format_problem_lines(fa.problems)
corrected_problem_count += options.fetch(:style_offenses, []).count(&:corrected?)
new_formula_problem_lines += format_problem_lines(fa.new_formula_problems)
if args.display_filename?
puts problem_lines.map { |s| "#{f.path}: #{s}" }
else
puts "#{f.full_name}:", problem_lines.map { |s| " #{s}" }
end
2020-09-10 22:00:18 +02:00
end
2012-08-07 01:37:46 -05:00
2021-04-03 03:49:41 +02:00
[f.path, { errors: fa.problems + fa.new_formula_problems, warnings: [] }]
2021-12-23 14:49:05 -05:00
end
2021-04-03 03:49:41 +02:00
cask_results = if audit_casks.empty?
{}
2020-11-18 12:41:18 +01:00
else
require "cask/cmd/abstract_command"
2020-11-18 12:41:18 +01:00
require "cask/cmd/audit"
2021-11-06 00:51:20 -04:00
# 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".
2020-11-18 12:41:18 +01:00
Cask::Cmd::Audit.audit_casks(
*audit_casks,
2021-03-21 13:59:43 -04:00
download: nil,
2021-11-06 00:59:46 -04:00
# No need for `|| nil` for `--[no-]appcast` because boolean switches are already `nil` if not passed
2021-03-21 13:59:43 -04:00
appcast: args.appcast?,
2021-11-06 00:51:20 -04:00
online: args.online? || nil,
strict: args.strict? || nil,
new_cask: args.new_cask? || nil,
token_conflicts: args.token_conflicts? || nil,
2021-03-21 13:59:43 -04:00
quarantine: nil,
any_named_args: !no_named_args,
language: nil,
display_passes: args.verbose? || args.named.count == 1,
display_failures_only: args.display_failures_only?,
2020-11-18 12:41:18 +01:00
)
end
2021-04-03 03:49:41 +02:00
failed_casks = cask_results.reject { |_, result| result[:errors].empty? }
2020-11-18 12:41:18 +01:00
cask_count = failed_casks.count
cask_problem_count = failed_casks.sum { |_, result| result[:warnings].count + result[:errors].count }
new_formula_problem_count += new_formula_problem_lines.count
total_problems_count = problem_count + new_formula_problem_count + cask_problem_count + tap_problem_count
2021-04-03 03:49:41 +02:00
if total_problems_count.positive?
puts new_formula_problem_lines.map { |s| " #{s}" }
errors_summary = "#{total_problems_count} #{"problem".pluralize(total_problems_count)}"
2020-11-18 12:41:18 +01:00
2021-04-03 03:49:41 +02:00
error_sources = []
error_sources << "#{formula_count} #{"formula".pluralize(formula_count)}" if formula_count.positive?
error_sources << "#{cask_count} #{"cask".pluralize(cask_count)}" if cask_count.positive?
error_sources << "#{tap_count} #{"tap".pluralize(tap_count)}" if tap_count.positive?
2020-11-18 12:41:18 +01:00
2021-04-03 03:49:41 +02:00
errors_summary += " in #{error_sources.to_sentence}" if error_sources.any?
2020-11-18 12:41:18 +01:00
2021-04-03 03:49:41 +02:00
errors_summary += " detected"
2020-11-18 12:41:18 +01:00
2021-04-03 03:49:41 +02:00
if corrected_problem_count.positive?
errors_summary += ", #{corrected_problem_count} #{"problem".pluralize(corrected_problem_count)} corrected"
end
ofail errors_summary
end
2020-11-18 12:41:18 +01:00
2021-04-03 03:49:41 +02:00
return unless ENV["GITHUB_ACTIONS"]
annotations = formula_results.merge(cask_results).flat_map do |path, result|
(
result[:warnings].map { |w| [:warning, w] } +
result[:errors].map { |e| [:error, e] }
).map do |type, problem|
GitHub::Actions::Annotation.new(
type,
problem[:message],
file: path,
line: problem[:location]&.line,
column: problem[:location]&.column,
)
end
end
2021-04-03 03:49:41 +02:00
annotations.each do |annotation|
puts annotation if annotation.relevant?
end
end
2016-09-22 20:12:28 +02:00
def format_problem_lines(problems)
2020-09-10 22:00:18 +02:00
problems.uniq
.map { |message:, location:| format_problem(message, location) }
end
def format_problem(message, location)
"* #{location&.to_s&.dup&.concat(": ")}#{message.chomp.gsub("\n", "\n ")}"
2012-08-07 01:37:46 -05:00
end
end