From c4c66d41ef7d7c6492d4942e096f260fc58e6244 Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Tue, 17 Jun 2025 16:33:16 +0100 Subject: [PATCH] cmd: set typed: strict --- Library/Homebrew/cmd/fetch.rb | 29 +++++--- Library/Homebrew/cmd/info.rb | 43 ++++++++---- Library/Homebrew/cmd/update-report.rb | 98 +++++++++++++++++++++------ 3 files changed, 126 insertions(+), 44 deletions(-) diff --git a/Library/Homebrew/cmd/fetch.rb b/Library/Homebrew/cmd/fetch.rb index 08b4de03d4..a7b13180db 100644 --- a/Library/Homebrew/cmd/fetch.rb +++ b/Library/Homebrew/cmd/fetch.rb @@ -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 } diff --git a/Library/Homebrew/cmd/info.rb b/Library/Homebrew/cmd/info.rb index 5b9fda067d..62fcb63242 100644 --- a/Library/Homebrew/cmd/info.rb +++ b/Library/Homebrew/cmd/info.rb @@ -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" diff --git a/Library/Homebrew/cmd/update-report.rb b/Library/Homebrew/cmd/update-report.rb index a897e83156..bcdaecb510 100644 --- a/Library/Homebrew/cmd/update-report.rb +++ b/Library/Homebrew/cmd/update-report.rb @@ -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