Files
brew/Library/Homebrew/cmd/update_report/reporter_hub.rb
T
Mike McQuaid 56f42e0e1d Make tests use public APIs and enforce via RuboCop
- Tests reached into private APIs with `send` and poked internal
  state via `instance_variable_get`/`instance_variable_set`, hiding
  which interfaces specs really exercise and letting visibility rot.
- Make every statically-poked method public and call it directly,
  keeping `public_send` only for dynamically-named public methods.
- Read and write state through public `attr_*` accessors instead of
  instance variable reflection.
- Add `Homebrew/NoSendInTests` and
  `Homebrew/NoInstanceVariableAccessInTests` cops scoped to
  `Library/Homebrew/test` so neither pattern creeps back in.
- Keep rare justified disables, e.g. `Module#remove_const` is
  private core Ruby and raw memoisation state has no accessor.
2026-07-24 19:55:53 +01:00

237 lines
6.8 KiB
Ruby

# typed: strict
# frozen_string_literal: true
class ReporterHub
include Utils::Output::Mixin
sig { returns(T::Array[Reporter]) }
attr_reader :reporters
sig { void }
def initialize
@hash = T.let({}, T::Hash[Symbol, T::Array[T.any(String, [String, String])]])
@reporters = T.let([], T::Array[Reporter])
end
sig { params(key: Symbol).returns(T::Array[String]) }
def select_formula_or_cask(key)
raise "Unsupported key #{key}" unless [:A, :AC, :D, :DC, :M, :MC, :R, :RC, :T].include?(key)
T.cast(@hash.fetch(key, []), T::Array[String])
end
sig { returns(T::Array[[String, String]]) }
def renamed_formulae
T.cast(@hash.fetch(:R, []), T::Array[[String, String]])
end
sig { params(reporter: Reporter, auto_update: T::Boolean).void }
def add(reporter, auto_update: false)
@reporters << reporter
report = reporter.report(auto_update:).reject { |_k, v| v.empty? }
@hash.update(report) { |_key, oldval, newval| oldval + 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
dump_new_cask_report
end
dump_deleted_formula_report
dump_deleted_cask_report
outdated_formulae = Formula.installed.select(&:outdated?).map(&:name)
outdated_casks = Cask::Caskroom.casks.select(&:outdated?).map(&:token)
unless auto_update
output_dump_formula_or_cask_report "Outdated Formulae", outdated_formulae
output_dump_formula_or_cask_report "Outdated Casks", outdated_casks
end
return if outdated_formulae.blank? && outdated_casks.blank?
outdated_formulae = outdated_formulae.count
outdated_casks = outdated_casks.count
update_pronoun = if (outdated_formulae + outdated_casks) == 1
"it"
else
"them"
end
msg = ""
if outdated_formulae.positive?
noun = Utils.pluralize("formula", outdated_formulae)
msg += "#{Tty.bold}#{outdated_formulae}#{Tty.reset} outdated #{noun}"
end
if outdated_casks.positive?
msg += " and " if msg.present?
msg += "#{Tty.bold}#{outdated_casks}#{Tty.reset} outdated #{Utils.pluralize("cask", outdated_casks)}"
end
return if msg.blank?
# When auto-updating before a zero-argument `brew upgrade` or `brew outdated`,
# that command lists the outdated packages itself so don't duplicate it here.
# Two-way sync: `auto-update` in `Library/Homebrew/brew.sh`.
return if auto_update && ENV["HOMEBREW_AUTO_UPDATE_SKIP_OUTDATED"].present?
puts
puts "You have #{msg} installed."
# If we're auto-updating, don't need to suggest commands that we're perhaps
# already running.
return if auto_update
puts <<~EOS
You can upgrade #{update_pronoun} with #{Tty.bold}brew upgrade#{Tty.reset}
or list #{update_pronoun} with #{Tty.bold}brew outdated#{Tty.reset}.
EOS
end
private
sig { void }
def dump_new_formula_report
formulae = select_formula_or_cask(:A).sort.reject { |name| installed?(name) }
return if formulae.blank?
ohai "New Formulae"
should_display_descriptions = if Homebrew::EnvConfig.no_install_from_api?
formulae.size <= 100
else
true
end
formulae.each do |formula|
if should_display_descriptions && (desc = description(formula))
puts "#{formula}: #{desc}"
else
puts formula
end
end
end
sig { void }
def dump_new_cask_report
return unless Cask::Caskroom.any_casks_installed?
casks = select_formula_or_cask(:AC).sort.reject { |name| cask_installed?(name) }
return if casks.blank?
ohai "New Casks"
should_display_descriptions = if Homebrew::EnvConfig.no_install_from_api?
casks.size <= 100
else
true
end
casks.each do |cask|
cask_token = Utils.name_from_full_name(cask)
if should_display_descriptions && (desc = cask_description(cask))
puts "#{cask_token}: #{desc}"
else
puts cask_token
end
end
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)
end
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 = Utils.name_from_full_name(name)
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/Utils.name_from_full_name(formula)).directory?
end
sig { params(cask: String).returns(T::Boolean) }
def cask_installed?(cask)
(Cask::Caskroom.path/cask).directory?
end
sig { returns(T::Array[T.untyped]) }
def all_formula_json
return @all_formula_json if @all_formula_json
@all_formula_json = T.let(nil, T.nilable(T::Array[T.untyped]))
all_formula_json, = Homebrew::API.fetch_json_api_file "formula.jws.json"
all_formula_json = T.cast(all_formula_json, T::Array[T.untyped])
@all_formula_json = all_formula_json
end
sig { returns(T::Array[T.untyped]) }
def all_cask_json
return @all_cask_json if @all_cask_json
@all_cask_json = T.let(nil, T.nilable(T::Array[T.untyped]))
all_cask_json, = Homebrew::API.fetch_json_api_file "cask.jws.json"
all_cask_json = T.cast(all_cask_json, T::Array[T.untyped])
@all_cask_json = all_cask_json
end
sig { params(formula: String).returns(T.nilable(String)) }
def description(formula)
if Homebrew::EnvConfig.no_install_from_api?
# Skip non-homebrew/core formulae for security.
return if formula.include?("/")
begin
Formula[formula].desc&.presence
rescue FormulaUnavailableError
nil
end
else
all_formula_json.find { |f| f["name"] == formula }
&.fetch("desc", nil)
&.presence
end
end
sig { params(cask: String).returns(T.nilable(String)) }
def cask_description(cask)
if Homebrew::EnvConfig.no_install_from_api?
# Skip non-homebrew/cask formulae for security.
return if cask.include?("/")
begin
Cask::CaskLoader.load(cask).desc&.presence
rescue Cask::CaskError
nil
end
else
all_cask_json.find { |f| f["token"] == cask }
&.fetch("desc", nil)
&.presence
end
end
end