diff --git a/Library/Homebrew/dev-cmd/pr-pull.rb b/Library/Homebrew/dev-cmd/pr-pull.rb index 0feaa50794..b95242a443 100644 --- a/Library/Homebrew/dev-cmd/pr-pull.rb +++ b/Library/Homebrew/dev-cmd/pr-pull.rb @@ -207,7 +207,7 @@ module Homebrew if pull_request # This is a tap pull request and approving reviewers should also sign-off. - tap = Tap.from_path(git_repo.pathname) + tap = T.must(Tap.from_path(git_repo.pathname)) review_trailers = GitHub.approved_reviews(tap.user, tap.full_name.split("/").last, pull_request).map do |r| "Signed-off-by: #{r["name"]} <#{r["email"]}>" @@ -253,7 +253,7 @@ module Homebrew } def determine_bump_subject(old_contents, new_contents, subject_path, reason: nil) subject_path = Pathname(subject_path) - tap = Tap.from_path(subject_path) + tap = T.must(Tap.from_path(subject_path)) subject_name = subject_path.basename.to_s.chomp(".rb") is_cask = subject_path.to_s.start_with?("#{tap.cask_dir}/") name = is_cask ? "cask" : "formula" diff --git a/Library/Homebrew/dev-cmd/tap-new.rb b/Library/Homebrew/dev-cmd/tap-new.rb index 83ef6733a6..017f204318 100644 --- a/Library/Homebrew/dev-cmd/tap-new.rb +++ b/Library/Homebrew/dev-cmd/tap-new.rb @@ -41,8 +41,8 @@ module Homebrew titleized_user = tap.user.dup titleized_repository = tap.repository.dup - titleized_user[0] = titleized_user[0].upcase - titleized_repository[0] = titleized_repository[0].upcase + titleized_user[0] = T.must(titleized_user[0]).upcase + titleized_repository[0] = T.must(titleized_repository[0]).upcase # Duplicate assignment to silence `assigned but unused variable` warning root_url = root_url = GitHubPackages.root_url(tap.user, "homebrew-#{tap.repository}") if args.github_packages? diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb index 6d7aa6900b..fee9f2bf4b 100644 --- a/Library/Homebrew/diagnostic.rb +++ b/Library/Homebrew/diagnostic.rb @@ -548,7 +548,7 @@ module Homebrew core_cask_tap = CoreCaskTap.instance return unless core_cask_tap.installed? - broken_tap(core_cask_tap) || examine_git_origin(core_cask_tap.git_repository, core_cask_tap.remote) + broken_tap(core_cask_tap) || examine_git_origin(core_cask_tap.git_repository, T.must(core_cask_tap.remote)) end sig { returns(T.nilable(String)) } diff --git a/Library/Homebrew/tap.rb b/Library/Homebrew/tap.rb index 9eb4708444..1cf3b3f36c 100644 --- a/Library/Homebrew/tap.rb +++ b/Library/Homebrew/tap.rb @@ -1,4 +1,4 @@ -# typed: true # rubocop:todo Sorbet/StrictSigil +# typed: strict # frozen_string_literal: true require "api" @@ -31,7 +31,7 @@ class Tap HOMEBREW_TAP_STYLE_EXCEPTIONS_DIR = "style_exceptions" private_constant :HOMEBREW_TAP_STYLE_EXCEPTIONS_DIR - HOMEBREW_TAP_JSON_FILES = %W[ + HOMEBREW_TAP_JSON_FILES = T.let(%W[ #{HOMEBREW_TAP_FORMULA_RENAMES_FILE} #{HOMEBREW_TAP_CASK_RENAMES_FILE} #{HOMEBREW_TAP_MIGRATIONS_FILE} @@ -39,7 +39,7 @@ class Tap #{HOMEBREW_TAP_SYNCED_VERSIONS_FORMULAE_FILE} #{HOMEBREW_TAP_AUDIT_EXCEPTIONS_DIR}/*.json #{HOMEBREW_TAP_STYLE_EXCEPTIONS_DIR}/*.json - ].freeze + ].freeze, T::Array[String]) class InvalidNameError < ArgumentError; end @@ -55,7 +55,6 @@ class Tap end user = T.must(user) - repository = T.must(repository) # We special case homebrew and linuxbrew so that users don't have to shift in a terminal. user = user.capitalize if ["homebrew", "linuxbrew"].include?(user) @@ -71,6 +70,7 @@ class Tap # Get a {Tap} from its path or a path inside of it. # # @api public + sig { params(path: T.any(Pathname, String)).returns(T.nilable(Tap)) } def self.from_path(path) match = File.expand_path(path).match(HOMEBREW_TAP_PATH_REGEX) @@ -141,18 +141,25 @@ class Tap end end - # @api public - extend Enumerable + class << self + extend T::Generic + Elem = type_member(:out) { { fixed: Tap } } + + # @api public + include Enumerable + end # The user name of this {Tap}. Usually, it's the GitHub username of # this {Tap}'s remote repository. # # @api public + sig { returns(String) } attr_reader :user # The repository name of this {Tap} without the leading `homebrew-`. # # @api public + sig { returns(String) } attr_reader :repository # The name of this {Tap}. It combines {#user} and {#repository} with a slash. @@ -160,6 +167,7 @@ class Tap # e.g. `user/repository` # # @api public + sig { returns(String) } attr_reader :name # @api public @@ -171,6 +179,7 @@ class Tap # e.g. `user/homebrew-repository` # # @api public + sig { returns(String) } attr_reader :full_name # The local path to this {Tap}. @@ -187,18 +196,20 @@ class Tap # Always use `Tap.fetch` instead of `Tap.new`. private_class_method :new + sig { params(user: String, repository: String).void } def initialize(user, repository) require "git_repository" @user = user @repository = repository - @name = "#{@user}/#{@repository}".downcase - @full_name = "#{@user}/homebrew-#{@repository}" - @path = HOMEBREW_TAP_DIRECTORY/@full_name.downcase - @git_repository = GitRepository.new(@path) + @name = T.let("#{@user}/#{@repository}".downcase, String) + @full_name = T.let("#{@user}/homebrew-#{@repository}", String) + @path = T.let(HOMEBREW_TAP_DIRECTORY/@full_name.downcase, Pathname) + @git_repository = T.let(GitRepository.new(@path), GitRepository) end # Clear internal cache. + sig { void } def clear_cache @remote = nil @repository_var_suffix = nil @@ -237,10 +248,9 @@ class Tap @synced_versions_formulae = nil @config = nil - @spell_checker = nil end - sig { void } + sig { overridable.void } def ensure_installed! return if installed? @@ -251,10 +261,11 @@ class Tap # e.g. `https://github.com/user/homebrew-repository` # # @api public + sig { overridable.returns(T.nilable(String)) } def remote return default_remote unless installed? - @remote ||= git_repository.origin_url + @remote ||= T.let(git_repository.origin_url, T.nilable(String)) end # The remote repository name of this {Tap}. @@ -266,7 +277,7 @@ class Tap return unless (remote = self.remote) return unless (match = remote.match(HOMEBREW_TAP_REPOSITORY_REGEX)) - @remote_repository ||= T.must(match[:remote_repository]) + @remote_repository ||= T.let(T.must(match[:remote_repository]), T.nilable(String)) end # The default remote path to this {Tap}. @@ -277,15 +288,16 @@ class Tap sig { returns(String) } def repository_var_suffix - @repository_var_suffix ||= path.to_s - .delete_prefix(HOMEBREW_TAP_DIRECTORY.to_s) - .tr("^A-Za-z0-9", "_") - .upcase + @repository_var_suffix ||= T.let(path.to_s + .delete_prefix(HOMEBREW_TAP_DIRECTORY.to_s) + .tr("^A-Za-z0-9", "_") + .upcase, T.nilable(String)) end # Check whether this {Tap} is a Git repository. # # @api public + sig { returns(T::Boolean) } def git? git_repository.git_repository? end @@ -293,6 +305,7 @@ class Tap # Git branch for this {Tap}. # # @api public + sig { returns(T.nilable(String)) } def git_branch raise TapUnavailableError, name unless installed? @@ -302,15 +315,17 @@ class Tap # Git HEAD for this {Tap}. # # @api public + sig { returns(T.nilable(String)) } def git_head raise TapUnavailableError, name unless installed? - @git_head ||= git_repository.head_ref + @git_head ||= T.let(git_repository.head_ref, T.nilable(String)) end # Time since last git commit for this {Tap}. # # @api public + sig { returns(T.nilable(String)) } def git_last_commit raise TapUnavailableError, name unless installed? @@ -331,6 +346,7 @@ class Tap # Check whether this {Tap} is an official Homebrew tap. # # @api public + sig { returns(T::Boolean) } def official? user == "Homebrew" end @@ -340,36 +356,40 @@ class Tap # @api public sig { returns(T::Boolean) } def private? - return @private if defined?(@private) + return @private unless @private.nil? - @private = if (value = config[:private]).nil? - config[:private] = begin - if custom_remote? + @private = T.let( + if (value = config[:private]).nil? + config[:private] = begin + if custom_remote? + true + else + # Don't store config if we don't know for sure. + return false if (value = GitHub.private_repo?(full_name)).nil? + + value + end + rescue GitHub::API::HTTPNotFoundError true - else - # Don't store config if we don't know for sure. - return false if (value = GitHub.private_repo?(full_name)).nil? - - value + rescue GitHub::API::Error + false end - rescue GitHub::API::HTTPNotFoundError - true - rescue GitHub::API::Error - false - end - else - value - end + else + value + end, + T.nilable(T::Boolean), + ) + T.must(@private) end # {TapConfig} of this {Tap}. sig { returns(TapConfig) } def config - @config ||= begin + @config ||= T.let(begin raise TapUnavailableError, name unless installed? TapConfig.new(self) - end + end, T.nilable(TapConfig)) end # Check whether this {Tap} is installed. @@ -381,11 +401,12 @@ class Tap end # Check whether this {Tap} is a shallow clone. + sig { returns(T::Boolean) } def shallow? (path/".git/shallow").exist? end - sig { returns(T::Boolean) } + sig { overridable.returns(T::Boolean) } def core_tap? false end @@ -397,13 +418,22 @@ class Tap # Install this {Tap}. # - # @param clone_target [String] If passed, it will be used as the clone remote. - # @param quiet [Boolean] If set, suppress all output. - # @param custom_remote [Boolean] If set, change the tap's remote if already installed. - # @param verify [Boolean] If set, verify all the formula, casks and aliases in the tap are valid. - # @param force [Boolean] If set, force core and cask taps to install even under API mode. + # @param clone_target If passed, it will be used as the clone remote. + # @param quiet If set, suppress all output. + # @param custom_remote If set, change the tap's remote if already installed. + # @param verify If set, verify all the formula, casks and aliases in the tap are valid. + # @param force If set, force core and cask taps to install even under API mode. # # @api public + sig { + overridable.params( + quiet: T::Boolean, + clone_target: T.any(NilClass, Pathname, String), + custom_remote: T::Boolean, + verify: T::Boolean, + force: T::Boolean, + ).void + } def install(quiet: false, clone_target: nil, custom_remote: false, verify: false, force: false) require "descriptions" @@ -538,6 +568,7 @@ class Tap EOS end + sig { void } def link_completions_and_manpages require "utils/link" @@ -553,6 +584,7 @@ class Tap end end + sig { params(requested_remote: T.any(NilClass, Pathname, String), quiet: T::Boolean).void } def fix_remote_configuration(requested_remote: nil, quiet: false) if requested_remote.present? path.cd do @@ -591,6 +623,7 @@ class Tap # Uninstall this {Tap}. # # @api public + sig { overridable.params(manual: T::Boolean).void } def uninstall(manual: false) require "descriptions" raise TapUnavailableError, name unless installed? @@ -637,28 +670,31 @@ class Tap def custom_remote? return true unless (remote = self.remote) - !remote.casecmp(default_remote).zero? + !T.must(remote.casecmp(default_remote)).zero? end # Path to the directory of all {Formula} files for this {Tap}. # # @api public - sig { returns(Pathname) } + sig { overridable.returns(Pathname) } def formula_dir # Official formulae taps always use this directory, saves time to hardcode. - @formula_dir ||= if official? - path/"Formula" - else - potential_formula_dirs.find(&:directory?) || (path/"Formula") - end + @formula_dir ||= T.let( + if official? + path/"Formula" + else + potential_formula_dirs.find(&:directory?) || (path/"Formula") + end, + T.nilable(Pathname), + ) end sig { returns(T::Array[Pathname]) } def potential_formula_dirs - @potential_formula_dirs ||= [path/"Formula", path/"HomebrewFormula", path].freeze + @potential_formula_dirs ||= T.let([path/"Formula", path/"HomebrewFormula", path].freeze, T.nilable(T::Array[Pathname])) end - sig { params(name: String).returns(Pathname) } + sig { overridable.params(name: String).returns(Pathname) } def new_formula_path(name) formula_dir/"#{name.downcase}.rb" end @@ -668,7 +704,7 @@ class Tap # @api public sig { returns(Pathname) } def cask_dir - @cask_dir ||= path/"Casks" + @cask_dir ||= T.let(path/"Casks", T.nilable(Pathname)) end sig { params(token: String).returns(Pathname) } @@ -682,6 +718,7 @@ class Tap .delete_prefix("#{path}/") end + sig { returns(T::Array[String]) } def contents contents = [] @@ -701,51 +738,57 @@ class Tap end # An array of all {Formula} files of this {Tap}. - sig { returns(T::Array[Pathname]) } + sig { overridable.returns(T::Array[Pathname]) } def formula_files - @formula_files ||= if formula_dir.directory? - if formula_dir == path - # We only want the top level here so we don't treat commands & casks as formulae. - # Sharding is only supported in Formula/ and HomebrewFormula/. - Pathname.glob(formula_dir/"*.rb") + @formula_files ||= T.let( + if formula_dir.directory? + if formula_dir == path + # We only want the top level here so we don't treat commands & casks as formulae. + # Sharding is only supported in Formula/ and HomebrewFormula/. + Pathname.glob(formula_dir/"*.rb") + else + Pathname.glob(formula_dir/"**/*.rb") + end else - Pathname.glob(formula_dir/"**/*.rb") - end - else - [] - end + [] + end, + T.nilable(T::Array[Pathname]), + ) end # A mapping of {Formula} names to {Formula} file paths. - sig { returns(T::Hash[String, Pathname]) } + sig { overridable.returns(T::Hash[String, Pathname]) } def formula_files_by_name - @formula_files_by_name ||= formula_files.each_with_object({}) do |file, hash| + @formula_files_by_name ||= T.let(formula_files.each_with_object({}) do |file, hash| # If there's more than one file with the same basename: use the longer one to prioritise more specific results. basename = file.basename(".rb").to_s existing_file = hash[basename] hash[basename] = file if existing_file.nil? || existing_file.to_s.length < file.to_s.length - end + end, T.nilable(T::Hash[String, Pathname])) end # An array of all {Cask} files of this {Tap}. sig { returns(T::Array[Pathname]) } def cask_files - @cask_files ||= if cask_dir.directory? - Pathname.glob(cask_dir/"**/*.rb") - else - [] - end + @cask_files ||= T.let( + if cask_dir.directory? + Pathname.glob(cask_dir/"**/*.rb") + else + [] + end, + T.nilable(T::Array[Pathname]), + ) end # A mapping of {Cask} tokens to {Cask} file paths. sig { returns(T::Hash[String, Pathname]) } def cask_files_by_name - @cask_files_by_name ||= cask_files.each_with_object({}) do |file, hash| + @cask_files_by_name ||= T.let(cask_files.each_with_object({}) do |file, hash| # If there's more than one file with the same basename: use the longer one to prioritise more specific results. basename = file.basename(".rb").to_s existing_file = hash[basename] hash[basename] = file if existing_file.nil? || existing_file.to_s.length < file.to_s.length - end + end, T.nilable(T::Hash[String, Pathname])) end RUBY_FILE_NAME_REGEX = %r{[^/]+\.rb} @@ -756,16 +799,19 @@ class Tap sig { returns(Regexp) } def formula_file_regex - @formula_file_regex ||= case formula_dir - when path/"Formula" - %r{^Formula/#{ZERO_OR_MORE_SUBDIRECTORIES_REGEX.source}#{RUBY_FILE_NAME_REGEX.source}$}o - when path/"HomebrewFormula" - %r{^HomebrewFormula/#{ZERO_OR_MORE_SUBDIRECTORIES_REGEX.source}#{RUBY_FILE_NAME_REGEX.source}$}o - when path - /^#{RUBY_FILE_NAME_REGEX.source}$/o - else - raise ArgumentError, "Unexpected formula_dir: #{formula_dir}" - end + @formula_file_regex ||= T.let( + case formula_dir + when path/"Formula" + %r{^Formula/#{ZERO_OR_MORE_SUBDIRECTORIES_REGEX.source}#{RUBY_FILE_NAME_REGEX.source}$}o + when path/"HomebrewFormula" + %r{^HomebrewFormula/#{ZERO_OR_MORE_SUBDIRECTORIES_REGEX.source}#{RUBY_FILE_NAME_REGEX.source}$}o + when path + /^#{RUBY_FILE_NAME_REGEX.source}$/o + else + raise ArgumentError, "Unexpected formula_dir: #{formula_dir}" + end, + T.nilable(Regexp), + ) end private :formula_file_regex @@ -785,78 +831,84 @@ class Tap end # An array of all {Formula} names of this {Tap}. - sig { returns(T::Array[String]) } + sig { overridable.returns(T::Array[String]) } def formula_names - @formula_names ||= formula_files.map { formula_file_to_name(_1) } + @formula_names ||= T.let(formula_files.map { formula_file_to_name(_1) }, T.nilable(T::Array[String])) end # A hash of all {Formula} name prefixes to versioned {Formula} in this {Tap}. sig { returns(T::Hash[String, T::Array[String]]) } def prefix_to_versioned_formulae_names - @prefix_to_versioned_formulae_names ||= formula_names - .select { |name| name.include?("@") } - .group_by { |name| name.gsub(/(@[\d.]+)?$/, "") } - .transform_values(&:sort) - .freeze + @prefix_to_versioned_formulae_names ||= T.let(formula_names + .select { |name| name.include?("@") } + .group_by { |name| name.gsub(/(@[\d.]+)?$/, "") } + .transform_values(&:sort) + .freeze, T.nilable(T::Hash[String, T::Array[String]])) end # An array of all {Cask} tokens of this {Tap}. sig { returns(T::Array[String]) } def cask_tokens - @cask_tokens ||= cask_files.map { formula_file_to_name(_1) } + @cask_tokens ||= T.let(cask_files.map { formula_file_to_name(_1) }, T.nilable(T::Array[String])) end # Path to the directory of all alias files for this {Tap}. - sig { returns(Pathname) } + sig { overridable.returns(Pathname) } def alias_dir - @alias_dir ||= path/"Aliases" + @alias_dir ||= T.let(path/"Aliases", T.nilable(Pathname)) end # An array of all alias files of this {Tap}. sig { returns(T::Array[Pathname]) } def alias_files - @alias_files ||= Pathname.glob("#{alias_dir}/*").select(&:file?) + @alias_files ||= T.let(Pathname.glob("#{alias_dir}/*").select(&:file?), T.nilable(T::Array[Pathname])) end # An array of all aliases of this {Tap}. sig { returns(T::Array[String]) } def aliases - @aliases ||= alias_table.keys + @aliases ||= T.let(alias_table.keys, T.nilable(T::Array[String])) end # Mapping from aliases to formula names. - sig { returns(T::Hash[String, String]) } + sig { overridable.returns(T::Hash[String, String]) } def alias_table - @alias_table ||= alias_files.each_with_object({}) do |alias_file, alias_table| + @alias_table ||= T.let(alias_files.each_with_object({}) do |alias_file, alias_table| alias_table[alias_file_to_name(alias_file)] = formula_file_to_name(alias_file.resolved_path) - end + end, T.nilable(T::Hash[String, String])) end # Mapping from formula names to aliases. sig { returns(T::Hash[String, T::Array[String]]) } def alias_reverse_table - @alias_reverse_table ||= alias_table.each_with_object({}) do |(alias_name, formula_name), alias_reverse_table| - alias_reverse_table[formula_name] ||= [] - alias_reverse_table[formula_name] << alias_name - end + @alias_reverse_table ||= T.let( + alias_table.each_with_object({}) do |(alias_name, formula_name), alias_reverse_table| + alias_reverse_table[formula_name] ||= [] + alias_reverse_table[formula_name] << alias_name + end, + T.nilable(T::Hash[String, T::Array[String]]), + ) end sig { returns(Pathname) } def command_dir - @command_dir ||= path/"cmd" + @command_dir ||= T.let(path/"cmd", T.nilable(Pathname)) end # An array of all commands files of this {Tap}. sig { returns(T::Array[Pathname]) } def command_files - @command_files ||= if command_dir.directory? - Commands.find_commands(command_dir) - else - [] - end + @command_files ||= T.let( + if command_dir.directory? + Commands.find_commands(command_dir) + else + [] + end, + T.nilable(T::Array[Pathname]), + ) end - sig { returns(Hash) } + sig { returns(T::Hash[String, T.untyped]) } def to_hash hash = { "name" => name, @@ -888,62 +940,73 @@ class Tap # Hash with tap cask renames. sig { returns(T::Hash[String, String]) } def cask_renames - @cask_renames ||= if (rename_file = path/HOMEBREW_TAP_CASK_RENAMES_FILE).file? - JSON.parse(rename_file.read) - else - {} - end + @cask_renames ||= T.let( + if (rename_file = path/HOMEBREW_TAP_CASK_RENAMES_FILE).file? + JSON.parse(rename_file.read) + else + {} + end, + T.nilable(T::Hash[String, String]), + ) end # Mapping from new to old cask tokens. Reverse of {#cask_renames}. sig { returns(T::Hash[String, T::Array[String]]) } def cask_reverse_renames - @cask_reverse_renames ||= cask_renames.each_with_object({}) do |(old_name, new_name), hash| + @cask_reverse_renames ||= T.let(cask_renames.each_with_object({}) do |(old_name, new_name), hash| hash[new_name] ||= [] hash[new_name] << old_name - end + end, T.nilable(T::Hash[String, T::Array[String]])) end # Hash with tap formula renames. - sig { returns(T::Hash[String, String]) } + sig { overridable.returns(T::Hash[String, String]) } def formula_renames - @formula_renames ||= if (rename_file = path/HOMEBREW_TAP_FORMULA_RENAMES_FILE).file? - JSON.parse(rename_file.read) - else - {} - end + @formula_renames ||= T.let( + if (rename_file = path/HOMEBREW_TAP_FORMULA_RENAMES_FILE).file? + JSON.parse(rename_file.read) + else + {} + end, + T.nilable(T::Hash[String, String]), + ) end # Mapping from new to old formula names. Reverse of {#formula_renames}. sig { returns(T::Hash[String, T::Array[String]]) } def formula_reverse_renames - @formula_reverse_renames ||= formula_renames.each_with_object({}) do |(old_name, new_name), hash| + @formula_reverse_renames ||= T.let(formula_renames.each_with_object({}) do |(old_name, new_name), hash| hash[new_name] ||= [] hash[new_name] << old_name - end + end, T.nilable(T::Hash[String, T::Array[String]])) end # Hash with tap migrations. - sig { returns(T::Hash[String, String]) } + sig { overridable.returns(T::Hash[String, String]) } def tap_migrations - @tap_migrations ||= if (migration_file = path/HOMEBREW_TAP_MIGRATIONS_FILE).file? - JSON.parse(migration_file.read) - else - {} - end + @tap_migrations ||= T.let( + if (migration_file = path/HOMEBREW_TAP_MIGRATIONS_FILE).file? + JSON.parse(migration_file.read) + else + {} + end, T.nilable(T::Hash[String, String]) + ) end sig { returns(T::Hash[String, T::Array[String]]) } def reverse_tap_migrations_renames - @reverse_tap_migrations_renames ||= tap_migrations.each_with_object({}) do |(old_name, new_name), hash| - # Only include renames: - # + `homebrew/cask/water-buffalo` - # - `homebrew/cask` - next if new_name.count("/") != 2 + @reverse_tap_migrations_renames ||= T.let( + tap_migrations.each_with_object({}) do |(old_name, new_name), hash| + # Only include renames: + # + `homebrew/cask/water-buffalo` + # - `homebrew/cask` + next if new_name.count("/") != 2 - hash[new_name] ||= [] - hash[new_name] << old_name - end + hash[new_name] ||= [] + hash[new_name] << old_name + end, + T.nilable(T::Hash[String, T::Array[String]]), + ) end # The old names a formula or cask had before getting migrated to the current tap. @@ -959,7 +1022,7 @@ class Tap end # Array with autobump names - sig { returns(T::Array[String]) } + sig { overridable.returns(T::Array[String]) } def autobump autobump_packages = if core_cask_tap? Homebrew::API::Cask.all_casks @@ -969,23 +1032,26 @@ class Tap {} end - @autobump ||= autobump_packages.select do |_, p| + @autobump ||= T.let(autobump_packages.select do |_, p| next if p["disabled"] next if p["deprecated"] && p["deprecation_reason"] != "unsigned" next if p["skip_livecheck"] p["autobump"] == true - end.keys + end.keys, T.nilable(T::Array[String])) if @autobump.blank? - @autobump = if (autobump_file = path/HOMEBREW_TAP_AUTOBUMP_FILE).file? - autobump_file.readlines(chomp: true) - else - [] - end + @autobump = T.let( + if (autobump_file = path/HOMEBREW_TAP_AUTOBUMP_FILE).file? + autobump_file.readlines(chomp: true) + else + [] + end, + T.nilable(T::Array[String]), + ) end - @autobump + T.must(@autobump) end # Whether this {Tap} allows running bump commands on the given {Formula} or {Cask}. @@ -995,31 +1061,42 @@ class Tap end # Hash with audit exceptions - sig { returns(Hash) } + sig { overridable.returns(T::Hash[Symbol, T.untyped]) } def audit_exceptions - @audit_exceptions ||= read_formula_list_directory("#{HOMEBREW_TAP_AUDIT_EXCEPTIONS_DIR}/*") + @audit_exceptions ||= T.let(read_formula_list_directory("#{HOMEBREW_TAP_AUDIT_EXCEPTIONS_DIR}/*"), + T.nilable(T::Hash[Symbol, T.untyped])) end # Hash with style exceptions - sig { returns(Hash) } + sig { overridable.returns(T::Hash[Symbol, T.untyped]) } def style_exceptions - @style_exceptions ||= read_formula_list_directory("#{HOMEBREW_TAP_STYLE_EXCEPTIONS_DIR}/*") + @style_exceptions ||= T.let(read_formula_list_directory("#{HOMEBREW_TAP_STYLE_EXCEPTIONS_DIR}/*"), + T.nilable(T::Hash[Symbol, T.untyped])) end # Hash with pypi formula mappings - sig { returns(Hash) } + sig { overridable.returns(T::Hash[String, T.untyped]) } def pypi_formula_mappings - @pypi_formula_mappings ||= read_formula_list(path/HOMEBREW_TAP_PYPI_FORMULA_MAPPINGS_FILE) + return @pypi_formula_mappings if @pypi_formula_mappings + + @pypi_formula_mappings = T.let( + T.cast(read_formula_list(path/HOMEBREW_TAP_PYPI_FORMULA_MAPPINGS_FILE), T::Hash[String, T.untyped]), + T.nilable(T::Hash[String, T.untyped]), + ) + T.must(@pypi_formula_mappings) end # Array with synced versions formulae - sig { returns(T::Array[T::Array[String]]) } + sig { overridable.returns(T::Array[T::Array[String]]) } def synced_versions_formulae - @synced_versions_formulae ||= if (synced_file = path/HOMEBREW_TAP_SYNCED_VERSIONS_FORMULAE_FILE).file? - JSON.parse(synced_file.read) - else - [] - end + @synced_versions_formulae ||= T.let( + if (synced_file = path/HOMEBREW_TAP_SYNCED_VERSIONS_FORMULAE_FILE).file? + JSON.parse(synced_file.read) + else + [] + end, + T.nilable(T::Array[T::Array[String]]), + ) end sig { returns(T::Boolean) } @@ -1065,7 +1142,10 @@ class Tap # Enumerate all available {Tap}s. # # @api public + sig { override.params(block: T.nilable(T.proc.params(tap: Tap).void)).returns(T.any(T::Array[Tap], T::Enumerator[Tap])) } def self.each(&block) + return to_enum unless block_given? + if Homebrew::EnvConfig.no_install_from_api? installed.each(&block) else @@ -1079,16 +1159,20 @@ class Tap Homebrew::Settings.read(:untapped)&.split(";") || [] end - sig { params(file: Pathname).returns(String) } + sig { overridable.params(file: Pathname).returns(String) } def formula_file_to_name(file) "#{name}/#{file.basename(".rb")}" end - sig { params(file: Pathname).returns(String) } + sig { overridable.params(file: Pathname).returns(String) } def alias_file_to_name(file) "#{name}/#{file.basename}" end + sig { + overridable.params(list: Symbol, formula_or_cask: String, value: T.any(NilClass, String, Version)) + .returns(T.any(T::Boolean, String)) + } def audit_exception(list, formula_or_cask, value = nil) return false if audit_exceptions.blank? return false unless audit_exceptions.key? list @@ -1110,21 +1194,21 @@ class Tap sig { returns(T::Boolean) } def allowed_by_env? - @allowed_by_env ||= begin + @allowed_by_env ||= T.let(begin allowed_taps = self.class.allowed_taps official? || allowed_taps.blank? || allowed_taps.include?(self) - end + end, T.nilable(T::Boolean)) end sig { returns(T::Boolean) } def forbidden_by_env? - @forbidden_by_env ||= self.class.forbidden_taps.include?(self) + @forbidden_by_env ||= T.let(self.class.forbidden_taps.include?(self), T.nilable(T::Boolean)) end private - sig { params(file: Pathname).returns(T.any(T::Array[String], Hash)) } + sig { params(file: Pathname).returns(T.any(T::Array[String], T::Hash[String, T.untyped])) } def read_formula_list(file) JSON.parse file.read rescue JSON::ParserError @@ -1134,7 +1218,7 @@ class Tap {} end - sig { params(directory: String).returns(Hash) } + sig { params(directory: String).returns(T::Hash[Symbol, T.untyped]) } def read_formula_list_directory(directory) list = {} @@ -1156,6 +1240,10 @@ class AbstractCoreTap < Tap abstract! + class << self + Elem = type_member(:out) { { fixed: Tap } } + end + private_class_method :fetch # Get the singleton instance for this {Tap}. @@ -1163,7 +1251,7 @@ class AbstractCoreTap < Tap # @api internal sig { returns(T.attached_class) } def self.instance - @instance ||= T.unsafe(self).new + @instance ||= T.let(T.unsafe(self).new, T.nilable(T.attached_class)) end sig { override.void } @@ -1174,7 +1262,7 @@ class AbstractCoreTap < Tap super end - sig { params(file: Pathname).returns(String) } + sig { override.params(file: Pathname).returns(String) } def formula_file_to_name(file) file.basename(".rb").to_s end @@ -1189,6 +1277,10 @@ end # A specialized {Tap} class for the core formulae. class CoreTap < AbstractCoreTap + class << self + Elem = type_member(:out) { { fixed: Tap } } + end + sig { void } def initialize super "Homebrew", "core" @@ -1201,7 +1293,7 @@ class CoreTap < AbstractCoreTap super end - sig { returns(T.nilable(String)) } + sig { override.returns(T.nilable(String)) } def remote return super if Homebrew::EnvConfig.no_install_from_api? @@ -1209,6 +1301,10 @@ class CoreTap < AbstractCoreTap end # CoreTap never allows shallow clones (on request from GitHub). + sig { + override.params(quiet: T::Boolean, clone_target: T.any(NilClass, Pathname, String), + custom_remote: T::Boolean, verify: T::Boolean, force: T::Boolean).void + } def install(quiet: false, clone_target: nil, custom_remote: false, verify: false, force: false) remote = Homebrew::EnvConfig.core_git_remote # set by HOMEBREW_CORE_GIT_REMOTE @@ -1224,14 +1320,14 @@ class CoreTap < AbstractCoreTap super(quiet:, clone_target: remote, custom_remote:, force:) end - sig { params(manual: T::Boolean).void } + sig { override.params(manual: T::Boolean).void } def uninstall(manual: false) raise "Tap#uninstall is not available for CoreTap" if Homebrew::EnvConfig.no_install_from_api? super end - sig { returns(T::Boolean) } + sig { override.returns(T::Boolean) } def core_tap? true end @@ -1241,15 +1337,15 @@ class CoreTap < AbstractCoreTap remote_repository.to_s.end_with?("/linuxbrew-core") || remote_repository == "Linuxbrew/homebrew-core" end - sig { returns(Pathname) } + sig { override.returns(Pathname) } def formula_dir - @formula_dir ||= begin + @formula_dir ||= T.let(begin ensure_installed! super - end + end, T.nilable(Pathname)) end - sig { params(name: String).returns(Pathname) } + sig { override.params(name: String).returns(Pathname) } def new_formula_path(name) formula_subdir = if name.start_with?("lib") "lib" @@ -1262,122 +1358,138 @@ class CoreTap < AbstractCoreTap formula_dir/formula_subdir/"#{name.downcase}.rb" end - sig { returns(Pathname) } + sig { override.returns(Pathname) } def alias_dir - @alias_dir ||= begin + @alias_dir ||= T.let(begin ensure_installed! super - end + end, T.nilable(Pathname)) end - sig { returns(T::Hash[String, String]) } + sig { override.returns(T::Hash[String, String]) } def formula_renames - @formula_renames ||= if Homebrew::EnvConfig.no_install_from_api? - ensure_installed! - super - else - Homebrew::API::Formula.all_renames - end + @formula_renames ||= T.let( + if Homebrew::EnvConfig.no_install_from_api? + ensure_installed! + super + else + Homebrew::API::Formula.all_renames + end, + T.nilable(T::Hash[String, String]), + ) end - sig { returns(Hash) } + sig { override.returns(T::Hash[String, T.untyped]) } def tap_migrations - @tap_migrations ||= if Homebrew::EnvConfig.no_install_from_api? - ensure_installed! - super - else - Homebrew::API::Formula.tap_migrations - end + @tap_migrations ||= T.let( + if Homebrew::EnvConfig.no_install_from_api? + ensure_installed! + super + else + Homebrew::API::Formula.tap_migrations + end, + T.nilable(T::Hash[String, T.untyped]), + ) end - sig { returns(T::Array[String]) } + sig { override.returns(T::Array[String]) } def autobump - @autobump ||= begin + @autobump ||= T.let(begin ensure_installed! super - end + end, T.nilable(T::Array[String])) end - sig { returns(Hash) } + sig { override.returns(T::Hash[Symbol, T.untyped]) } def audit_exceptions - @audit_exceptions ||= begin + @audit_exceptions ||= T.let(begin ensure_installed! super - end + end, T.nilable(T::Hash[Symbol, T.untyped])) end - sig { returns(Hash) } + sig { override.returns(T::Hash[Symbol, T.untyped]) } def style_exceptions - @style_exceptions ||= begin + @style_exceptions ||= T.let(begin ensure_installed! super - end + end, T.nilable(T::Hash[Symbol, T.untyped])) end - sig { returns(Hash) } + sig { override.returns(T::Hash[String, T.untyped]) } def pypi_formula_mappings - @pypi_formula_mappings ||= begin + @pypi_formula_mappings ||= T.let(begin ensure_installed! super - end + end, T.nilable(T::Hash[String, T.untyped])) end - sig { returns(T::Array[T::Array[String]]) } + sig { override.returns(T::Array[T::Array[String]]) } def synced_versions_formulae - @synced_versions_formulae ||= begin + @synced_versions_formulae ||= T.let(begin ensure_installed! super - end + end, T.nilable(T::Array[T::Array[String]])) end - sig { params(file: Pathname).returns(String) } + sig { override.params(file: Pathname).returns(String) } def alias_file_to_name(file) file.basename.to_s end - sig { returns(T::Hash[String, String]) } + sig { override.returns(T::Hash[String, String]) } def alias_table - @alias_table ||= if Homebrew::EnvConfig.no_install_from_api? - super - else - Homebrew::API::Formula.all_aliases - end + @alias_table ||= T.let( + if Homebrew::EnvConfig.no_install_from_api? + super + else + Homebrew::API::Formula.all_aliases + end, + T.nilable(T::Hash[String, String]), + ) end - sig { returns(T::Array[Pathname]) } + sig { override.returns(T::Array[Pathname]) } def formula_files return super if Homebrew::EnvConfig.no_install_from_api? formula_files_by_name.values end - sig { returns(T::Array[String]) } + sig { override.returns(T::Array[String]) } def formula_names return super if Homebrew::EnvConfig.no_install_from_api? Homebrew::API::Formula.all_formulae.keys end - sig { returns(T::Hash[String, Pathname]) } + sig { override.returns(T::Hash[String, Pathname]) } def formula_files_by_name return super if Homebrew::EnvConfig.no_install_from_api? - @formula_files_by_name ||= begin - tap_path = path.to_s - Homebrew::API::Formula.all_formulae.each_with_object({}) do |item, hash| - name, formula_hash = item - # If there's more than one item with the same path: use the longer one to prioritise more specific results. - existing_path = hash[name] - # Pathname equivalent is slow in a tight loop - new_path = File.join(tap_path, formula_hash.fetch("ruby_source_path")) - hash[name] = Pathname(new_path) if existing_path.nil? || existing_path.to_s.length < new_path.length - end - end + @formula_files_by_name ||= T.let( + begin + tap_path = path.to_s + Homebrew::API::Formula.all_formulae.each_with_object({}) do |item, hash| + name, formula_hash = item + # If there's more than one item with the same path: use the longer one to prioritise more specific results. + existing_path = hash[name] + # Pathname equivalent is slow in a tight loop + new_path = File.join(tap_path, formula_hash.fetch("ruby_source_path")) + hash[name] = Pathname(new_path) if existing_path.nil? || existing_path.to_s.length < new_path.length + end + end, + T.nilable(T::Hash[String, Pathname]), + ) end end # A specialized {Tap} class for homebrew-cask. class CoreCaskTap < AbstractCoreTap + class << self + Elem = type_member(:out) { { fixed: Tap } } + end + sig { void } def initialize super "Homebrew", "cask" @@ -1388,7 +1500,7 @@ class CoreCaskTap < AbstractCoreTap true end - sig { params(token: String).returns(Pathname) } + sig { override.params(token: String).returns(Pathname) } def new_cask_path(token) cask_subdir = if token.start_with?("font-") "font/font-#{token.delete_prefix("font-")[0]}" @@ -1416,35 +1528,44 @@ class CoreCaskTap < AbstractCoreTap def cask_files_by_name return super if Homebrew::EnvConfig.no_install_from_api? - @cask_files_by_name ||= begin - tap_path = path.to_s - Homebrew::API::Cask.all_casks.each_with_object({}) do |item, hash| - name, cask_hash = item - # If there's more than one item with the same path: use the longer one to prioritise more specific results. - existing_path = hash[name] - # Pathname equivalent is slow in a tight loop - new_path = File.join(tap_path, cask_hash.fetch("ruby_source_path")) - hash[name] = Pathname(new_path) if existing_path.nil? || existing_path.to_s.length < new_path.length - end - end + @cask_files_by_name ||= T.let( + begin + tap_path = path.to_s + Homebrew::API::Cask.all_casks.each_with_object({}) do |item, hash| + name, cask_hash = item + # If there's more than one item with the same path: use the longer one to prioritise more specific results. + existing_path = hash[name] + # Pathname equivalent is slow in a tight loop + new_path = File.join(tap_path, cask_hash.fetch("ruby_source_path")) + hash[name] = Pathname(new_path) if existing_path.nil? || existing_path.to_s.length < new_path.length + end + end, + T.nilable(T::Hash[String, Pathname]), + ) end sig { override.returns(T::Hash[String, String]) } def cask_renames - @cask_renames ||= if Homebrew::EnvConfig.no_install_from_api? - super - else - Homebrew::API::Cask.all_renames - end + @cask_renames ||= T.let( + if Homebrew::EnvConfig.no_install_from_api? + super + else + Homebrew::API::Cask.all_renames + end, + T.nilable(T::Hash[String, String]), + ) end - sig { override.returns(Hash) } + sig { override.returns(T::Hash[String, T.untyped]) } def tap_migrations - @tap_migrations ||= if Homebrew::EnvConfig.no_install_from_api? - super - else - Homebrew::API::Cask.tap_migrations - end + @tap_migrations ||= T.let( + if Homebrew::EnvConfig.no_install_from_api? + super + else + Homebrew::API::Cask.tap_migrations + end, + T.nilable(T::Hash[String, T.untyped]), + ) end end