Merge pull request #20130 from Homebrew/sorbet_cmd

cmd: set `typed: strict`
This commit is contained in:
Mike McQuaid 2025-06-17 18:29:23 +00:00 committed by GitHub
commit 2916610699
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 126 additions and 44 deletions

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "abstract_command"
@ -6,6 +6,7 @@ require "formula"
require "fetch"
require "cask/download"
require "retryable_download"
require "download_queue"
module Homebrew
module Cmd
@ -69,15 +70,16 @@ module Homebrew
named_args [:formula, :cask], min: 1
end
sig { returns(Integer) }
def concurrency
@concurrency ||= args.concurrency&.to_i || 1
@concurrency ||= T.let(args.concurrency&.to_i || 1, T.nilable(Integer))
end
sig { returns(DownloadQueue) }
def download_queue
@download_queue ||= begin
require "download_queue"
@download_queue ||= T.let(begin
DownloadQueue.new(concurrency)
end
end, T.nilable(DownloadQueue))
end
class Spinner
@ -96,8 +98,8 @@ module Homebrew
sig { void }
def initialize
@start = Time.now
@i = 0
@start = T.let(Time.now, Time)
@i = T.let(0, Integer)
end
sig { returns(String) }
@ -136,7 +138,7 @@ module Homebrew
bucket.each do |formula_or_cask|
case formula_or_cask
when Formula
formula = T.cast(formula_or_cask, Formula)
formula = formula_or_cask
ref = formula.loaded_from_api? ? formula.full_name : formula.path
os_arch_combinations.each do |os, arch|
@ -189,7 +191,9 @@ module Homebrew
next if fetched_bottle
fetch_downloadable(formula.resource)
if (resource = formula.resource)
fetch_downloadable(resource)
end
formula.resources.each do |r|
fetch_downloadable(r)
@ -231,7 +235,7 @@ module Homebrew
end
else
spinner = Spinner.new
remaining_downloads = downloads.dup
remaining_downloads = downloads.dup.to_a
previous_pending_line_count = 0
begin
@ -332,10 +336,13 @@ module Homebrew
private
sig { returns(T::Hash[T.any(Resource, Bottle, Cask::Download), Concurrent::Promises::Future]) }
def downloads
@downloads ||= {}
@downloads ||= T.let({}, T.nilable(T::Hash[T.any(Resource, Bottle, Cask::Download),
Concurrent::Promises::Future]))
end
sig { params(downloadable: T.any(Resource, Bottle, Cask::Download)).void }
def fetch_downloadable(downloadable)
downloads[downloadable] ||= begin
tries = args.retry? ? {} : { tries: 1 }

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "abstract_command"
@ -18,7 +18,7 @@ module Homebrew
class Info < AbstractCommand
VALID_DAYS = %w[30 90 365].freeze
VALID_FORMULA_CATEGORIES = %w[install install-on-request build-error].freeze
VALID_CATEGORIES = (VALID_FORMULA_CATEGORIES + %w[cask-install os-version]).freeze
VALID_CATEGORIES = T.let((VALID_FORMULA_CATEGORIES + %w[cask-install os-version]).freeze, T::Array[String])
cmd_args do
description <<~EOS
@ -96,14 +96,17 @@ module Homebrew
end
print_analytics
elsif args.json
elsif (json = args.json)
all = args.eval_all?
print_json(all)
print_json(json, all)
elsif args.github?
raise FormulaOrCaskUnspecifiedError if args.no_named?
exec_browser(*args.named.to_formulae_and_casks.map { |f| github_info(f) })
exec_browser(*args.named.to_formulae_and_casks.map do |formula_keg_or_cask|
formula_or_cask = T.cast(formula_keg_or_cask, T.any(Formula, Cask::Cask))
github_info(formula_or_cask)
end)
elsif args.no_named?
print_statistics
else
@ -111,6 +114,7 @@ module Homebrew
end
end
sig { params(remote: String, path: String).returns(String) }
def github_remote_path(remote, path)
if remote =~ %r{^(?:https?://|git(?:@|://))github\.com[:/](.+)/(.+?)(?:\.git)?$}
"https://github.com/#{Regexp.last_match(1)}/#{Regexp.last_match(2)}/blob/HEAD/#{path}"
@ -175,6 +179,7 @@ module Homebrew
end
end
sig { params(version: T.any(T::Boolean, String)).returns(Symbol) }
def json_version(version)
version_hash = {
true => :default,
@ -187,11 +192,11 @@ module Homebrew
version_hash[version]
end
sig { params(all: T::Boolean).void }
def print_json(all)
sig { params(json: T.any(T::Boolean, String), all: T::Boolean).void }
def print_json(json, all)
raise FormulaOrCaskUnspecifiedError if !(all || args.installed?) && args.no_named?
json = case json_version(args.json)
json = case json_version(json)
when :v1, :default
raise UsageError, "Cannot specify `--cask` when using `--json=v1`!" if args.cask?
@ -240,25 +245,31 @@ module Homebrew
puts JSON.pretty_generate(json)
end
sig { params(formula_or_cask: T.any(Formula, Cask::Cask)).returns(String) }
def github_info(formula_or_cask)
return formula_or_cask.path if formula_or_cask.tap.blank? || formula_or_cask.tap.remote.blank?
path = case formula_or_cask
when Formula
formula = formula_or_cask
formula.path.relative_path_from(T.must(formula.tap).path)
tap = formula.tap
return formula.path.to_s if tap.blank? || tap.remote.blank?
formula.path.relative_path_from(tap.path)
when Cask::Cask
cask = formula_or_cask
tap = cask.tap
return cask.sourcefile_path.to_s if tap.blank? || tap.remote.blank?
if cask.sourcefile_path.blank? || cask.sourcefile_path.extname != ".rb"
return "#{cask.tap.default_remote}/blob/HEAD/#{cask.tap.relative_cask_path(cask.token)}"
return "#{tap.default_remote}/blob/HEAD/#{tap.relative_cask_path(cask.token)}"
end
cask.sourcefile_path.relative_path_from(cask.tap.path)
cask.sourcefile_path.relative_path_from(tap.path)
end
github_remote_path(formula_or_cask.tap.remote, path)
github_remote_path(tap.remote, path)
end
sig { params(formula: Formula).void }
def info_formula(formula)
specs = []
@ -356,6 +367,7 @@ module Homebrew
Utils::Analytics.formula_output(formula, args:)
end
sig { params(dependencies: T::Array[Dependency]).returns(String) }
def decorate_dependencies(dependencies)
deps_status = dependencies.map do |dep|
if dep.satisfied?([])
@ -367,6 +379,7 @@ module Homebrew
deps_status.join(", ")
end
sig { params(requirements: T::Array[Requirement]).returns(String) }
def decorate_requirements(requirements)
req_status = requirements.map do |req|
req_s = req.display_s
@ -375,12 +388,14 @@ module Homebrew
req_status.join(", ")
end
sig { params(dep: Dependency).returns(String) }
def dep_display_s(dep)
return dep.name if dep.option_tags.empty?
"#{dep.name} #{dep.option_tags.map { |o| "--#{o}" }.join(" ")}"
end
sig { params(cask: Cask::Cask).void }
def info_cask(cask)
require "cask/info"

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "abstract_command"
@ -39,13 +39,15 @@ module Homebrew
private
sig { void }
def auto_update_header
@auto_update_header ||= begin
@auto_update_header ||= T.let(begin
ohai "Auto-updated Homebrew!" if args.auto_update?
true
end
end, T.nilable(T::Boolean))
end
sig { void }
def output_update_report
# Run `brew update` (again) if we've got a linuxbrew-core CoreTap
if CoreTap.instance.installed? && CoreTap.instance.linuxbrew_core? &&
@ -293,14 +295,17 @@ module Homebrew
EOS
end
sig { returns(String) }
def no_changes_message
"No changes to formulae or casks."
end
sig { params(revision: String).returns(String) }
def shorten_revision(revision)
Utils.popen_read("git", "-C", HOMEBREW_REPOSITORY, "rev-parse", "--short", revision).chomp
end
sig { void }
def tap_or_untap_core_taps_if_necessary
return if ENV["HOMEBREW_UPDATE_TEST"]
@ -340,6 +345,7 @@ module Homebrew
end
end
sig { params(repository: Pathname).void }
def link_completions_manpages_and_docs(repository = HOMEBREW_REPOSITORY)
command = "brew update"
Utils::Link.link_completions(repository, command)
@ -352,10 +358,12 @@ module Homebrew
EOS
end
sig { void }
def migrate_gcc_dependents_if_needed
# do nothing
end
sig { void }
def analytics_message
return if Utils::Analytics.messages_displayed?
return if Utils::Analytics.no_message_output?
@ -385,6 +393,7 @@ module Homebrew
Utils::Analytics.messages_displayed! if $stdout.tty?
end
sig { void }
def donation_message
return if Settings.read("donationmessage") == "true"
@ -395,6 +404,7 @@ module Homebrew
Settings.write "donationmessage", true if $stdout.tty?
end
sig { void }
def install_from_api_message
return if Settings.read("installfromapimessage") == "true"
@ -419,30 +429,38 @@ require "extend/os/cmd/update-report"
class Reporter
class ReporterRevisionUnsetError < RuntimeError
sig { params(var_name: String).void }
def initialize(var_name)
super "#{var_name} is unset!"
end
end
sig {
params(tap: Tap, api_names_txt: T.nilable(Pathname), api_names_before_txt: T.nilable(Pathname),
api_dir_prefix: T.nilable(Pathname)).void
}
def initialize(tap, api_names_txt: nil, api_names_before_txt: nil, api_dir_prefix: nil)
@tap = tap
# This is slightly involved/weird but all the #report logic is shared so it's worth it.
if installed_from_api?(api_names_txt, api_names_before_txt, api_dir_prefix)
@api_names_txt = api_names_txt
@api_names_before_txt = api_names_before_txt
@api_dir_prefix = api_dir_prefix
@api_names_txt = T.let(api_names_txt, T.nilable(Pathname))
@api_names_before_txt = T.let(api_names_before_txt, T.nilable(Pathname))
@api_dir_prefix = T.let(api_dir_prefix, T.nilable(Pathname))
else
initial_revision_var = "HOMEBREW_UPDATE_BEFORE#{tap.repository_var_suffix}"
@initial_revision = ENV[initial_revision_var].to_s
@initial_revision = T.let(ENV[initial_revision_var].to_s, String)
raise ReporterRevisionUnsetError, initial_revision_var if @initial_revision.empty?
current_revision_var = "HOMEBREW_UPDATE_AFTER#{tap.repository_var_suffix}"
@current_revision = ENV[current_revision_var].to_s
@current_revision = T.let(ENV[current_revision_var].to_s, String)
raise ReporterRevisionUnsetError, current_revision_var if @current_revision.empty?
end
@report = T.let(nil, T.nilable(T::Hash[Symbol, T::Array[String]]))
end
sig { params(auto_update: T::Boolean).returns(T::Hash[Symbol, T::Array[String]]) }
def report(auto_update: false)
return @report if @report
@ -483,9 +501,9 @@ class Reporter
case status
when "A", "D"
full_name = tap.formula_file_to_name(src)
name = full_name.split("/").last
name = T.must(full_name.split("/").last)
new_tap = tap.tap_migrations[name]
@report[status.to_sym] << full_name unless new_tap
@report[T.must(status).to_sym] << full_name unless new_tap
when "M"
name = tap.formula_file_to_name(src)
@ -585,6 +603,7 @@ class Reporter
@report
end
sig { returns(T::Boolean) }
def updated?
if installed_from_api?
diff.present?
@ -593,9 +612,10 @@ class Reporter
end
end
sig { void }
def migrate_tap_migration
(report[:D] + report[:DC]).each do |full_name|
name = full_name.split("/").last
(Array(report[:D]) + Array(report[:DC])).each do |full_name|
name = T.must(full_name.split("/").last)
new_tap_name = tap.tap_migrations[name]
next if new_tap_name.nil? # skip if not in tap_migrations list.
@ -610,7 +630,7 @@ class Reporter
end
# This means it is a cask
if report[:DC].include? full_name
if Array(report[:DC]).include? full_name
next unless (HOMEBREW_PREFIX/"Caskroom"/new_name).exist?
new_tap = Tap.fetch(new_tap_name)
@ -676,12 +696,14 @@ class Reporter
end
end
sig { void }
def migrate_cask_rename
Cask::Caskroom.casks.each do |cask|
Cask::Migrator.migrate_if_needed(cask)
end
end
sig { params(force: T::Boolean, verbose: T::Boolean).void }
def migrate_formula_rename(force:, verbose:)
Formula.installed.each do |formula|
next unless Migrator.needs_migration?(formula)
@ -705,14 +727,36 @@ class Reporter
private
attr_reader :tap, :initial_revision, :current_revision, :api_names_txt, :api_names_before_txt, :api_dir_prefix
sig { returns(Tap) }
attr_reader :tap
sig { returns(String) }
attr_reader :initial_revision
sig { returns(String) }
attr_reader :current_revision
sig { returns(T.nilable(Pathname)) }
attr_reader :api_names_txt
sig { returns(T.nilable(Pathname)) }
attr_reader :api_names_before_txt
sig { returns(T.nilable(Pathname)) }
attr_reader :api_dir_prefix
sig {
params(api_names_txt: T.nilable(Pathname), api_names_before_txt: T.nilable(Pathname),
api_dir_prefix: T.nilable(Pathname)).returns(T::Boolean)
}
def installed_from_api?(api_names_txt = @api_names_txt, api_names_before_txt = @api_names_before_txt,
api_dir_prefix = @api_dir_prefix)
!api_names_txt.nil? && !api_names_before_txt.nil? && !api_dir_prefix.nil?
end
sig { returns(String) }
def diff
@diff ||= T.let(nil, T.nilable(String))
@diff ||= if installed_from_api?
# Hack `git diff` output with regexes to look like `git diff-tree` output.
# Yes, I know this is a bit filthy but it saves duplicating the #report logic.
@ -720,12 +764,14 @@ class Reporter
header_regex = /^(---|\+\+\+) /
add_delete_characters = ["+", "-"].freeze
api_dir_prefix_basename = T.must(api_dir_prefix).basename
diff_output.lines.filter_map do |line|
next if line.match?(header_regex)
next unless add_delete_characters.include?(line[0])
line.sub(/^\+/, "A #{api_dir_prefix.basename}/")
.sub(/^-/, "D #{api_dir_prefix.basename}/")
line.sub(/^\+/, "A #{api_dir_prefix_basename}/")
.sub(/^-/, "D #{api_dir_prefix_basename}/")
.sub(/$/, ".rb")
.chomp
end.join("\n")
@ -739,28 +785,33 @@ class Reporter
end
class ReporterHub
sig { returns(T::Array[Reporter]) }
attr_reader :reporters
sig { void }
def initialize
@hash = {}
@reporters = []
@hash = T.let({}, T::Hash[Symbol, T::Array[String]])
@reporters = T.let([], T::Array[Reporter])
end
sig { params(key: Symbol).returns(T::Array[String]) }
def select_formula_or_cask(key)
@hash.fetch(key, [])
end
sig { params(reporter: Reporter, auto_update: T::Boolean).void }
def add(reporter, auto_update: false)
@reporters << reporter
report = reporter.report(auto_update:).delete_if { |_k, v| v.empty? }
@hash.update(report) { |_key, oldval, newval| oldval.concat(newval) }
end
sig { returns(T::Boolean) }
def empty?
@hash.empty?
end
sig { params(auto_update: T::Boolean).void }
def dump(auto_update: false)
unless Homebrew::EnvConfig.no_update_report_new?
dump_new_formula_report
@ -815,12 +866,14 @@ class ReporterHub
private
sig { void }
def dump_new_formula_report
formulae = select_formula_or_cask(:A).sort.reject { |name| installed?(name) }
output_dump_formula_or_cask_report "New Formulae", formulae
end
sig { void }
def dump_new_cask_report
return if Homebrew::SimulateSystem.simulating_or_running_on_linux?
@ -831,6 +884,7 @@ class ReporterHub
output_dump_formula_or_cask_report "New Casks", casks
end
sig { void }
def dump_deleted_formula_report
formulae = select_formula_or_cask(:D).sort.filter_map do |name|
pretty_uninstalled(name) if installed?(name)
@ -839,37 +893,43 @@ class ReporterHub
output_dump_formula_or_cask_report "Deleted Installed Formulae", formulae
end
sig { void }
def dump_deleted_cask_report
return if Homebrew::SimulateSystem.simulating_or_running_on_linux?
casks = select_formula_or_cask(:DC).sort.filter_map do |name|
name = name.split("/").last
name = T.must(name.split("/").last)
pretty_uninstalled(name) if cask_installed?(name)
end
output_dump_formula_or_cask_report "Deleted Installed Casks", casks
end
sig { params(title: String, formulae_or_casks: T::Array[String]).void }
def output_dump_formula_or_cask_report(title, formulae_or_casks)
return if formulae_or_casks.blank?
ohai title, Formatter.columns(formulae_or_casks.sort)
end
sig { params(formula: String).returns(T::Boolean) }
def installed?(formula)
(HOMEBREW_CELLAR/formula.split("/").last).directory?
end
sig { params(formula: String).returns(T::Boolean) }
def outdated?(formula)
Formula[formula].outdated?
rescue FormulaUnavailableError
false
end
sig { params(cask: String).returns(T::Boolean) }
def cask_installed?(cask)
(Cask::Caskroom.path/cask).directory?
end
sig { params(cask: String).returns(T::Boolean) }
def cask_outdated?(cask)
Cask::CaskLoader.load(cask).outdated?
rescue Cask::CaskError