Enable recursive typechecking in specs

This commit is contained in:
Douglas Eichelberger 2025-09-05 19:31:34 -07:00
parent 5f1241b953
commit 3646b17de7
No known key found for this signature in database
GPG Key ID: F90193CBD547EB81
45 changed files with 223 additions and 141 deletions

View File

@ -9,7 +9,7 @@ class BottleSpecification
attr_reader :collector
sig { returns(T::Hash[String, T.untyped]) }
sig { returns(T::Hash[Symbol, T.untyped]) }
attr_reader :root_url_specs
sig { returns(String) }
@ -20,7 +20,7 @@ class BottleSpecification
@rebuild = T.let(0, Integer)
@repository = T.let(Homebrew::DEFAULT_REPOSITORY, String)
@collector = T.let(Utils::Bottles::Collector.new, Utils::Bottles::Collector)
@root_url_specs = T.let({}, T::Hash[String, T.untyped])
@root_url_specs = T.let({}, T::Hash[Symbol, T.untyped])
@root_url = T.let(nil, T.nilable(String))
end

View File

@ -135,9 +135,9 @@ class Build
end
new_env = {
"TMPDIR" => HOMEBREW_TEMP,
"TEMP" => HOMEBREW_TEMP,
"TMP" => HOMEBREW_TEMP,
"TMPDIR" => HOMEBREW_TEMP.to_s,
"TEMP" => HOMEBREW_TEMP.to_s,
"TMP" => HOMEBREW_TEMP.to_s,
}
with_env(new_env) do

View File

@ -37,11 +37,11 @@ module Homebrew
private
sig { returns(T::Hash[Symbol, T::Array[String]]) }
sig { returns(T::Hash[Symbol, T.nilable(T::Array[String])]) }
def skipped_entries
return @skipped_entries if @skipped_entries
@skipped_entries ||= T.let({}, T.nilable(T::Hash[Symbol, T::Array[String]]))
@skipped_entries ||= T.let({}, T.nilable(T::Hash[Symbol, T.nilable(T::Array[String])]))
[:brew, :cask, :mas, :tap, :whalebrew].each do |type|
@skipped_entries[type] =
ENV["HOMEBREW_BUNDLE_#{type.to_s.upcase}_SKIP"]&.split

View File

@ -12,6 +12,7 @@ module Cask
#
# @api internal
class Config
ConfigHash = T.type_alias { T::Hash[Symbol, T.any(LazyObject, String, Pathname, T::Array[String])] }
DEFAULT_DIRS = T.let(
{
appdir: "/Applications",
@ -33,7 +34,8 @@ module Cask
T::Hash[Symbol, String],
)
sig { returns(T::Hash[Symbol, String]) }
# runtime recursive evaluation forces the LazyObject to be evaluated
T::Sig::WithoutRuntime.sig { returns(T::Hash[Symbol, T.any(LazyObject, String)]) }
def self.defaults
{
languages: LazyObject.new { ::OS::Mac.languages },
@ -67,35 +69,25 @@ module Cask
sig { params(json: String, ignore_invalid_keys: T::Boolean).returns(T.attached_class) }
def self.from_json(json, ignore_invalid_keys: false)
config = JSON.parse(json)
config = JSON.parse(json, symbolize_names: true)
new(
default: config.fetch("default", {}),
env: config.fetch("env", {}),
explicit: config.fetch("explicit", {}),
default: config.fetch(:default, {}),
env: config.fetch(:env, {}),
explicit: config.fetch(:explicit, {}),
ignore_invalid_keys:,
)
end
sig {
params(
config: T::Enumerable[
[T.any(String, Symbol), T.any(String, Pathname, T::Array[String])],
],
).returns(
T::Hash[Symbol, T.any(String, Pathname, T::Array[String])],
)
}
sig { params(config: ConfigHash).returns(ConfigHash) }
def self.canonicalize(config)
config.to_h do |k, v|
key = k.to_sym
if DEFAULT_DIRS.key?(key)
if DEFAULT_DIRS.key?(k)
raise TypeError, "Invalid path for default dir #{k}: #{v.inspect}" if v.is_a?(Array)
[key, Pathname(v).expand_path]
[k, Pathname(v.to_s).expand_path]
else
[key, v]
[k, v]
end
end
end
@ -103,14 +95,14 @@ module Cask
# Get the explicit configuration.
#
# @api internal
sig { returns(T::Hash[Symbol, T.any(String, Pathname, T::Array[String])]) }
sig { returns(ConfigHash) }
attr_accessor :explicit
sig {
params(
default: T.nilable(T::Hash[Symbol, T.any(String, Pathname, T::Array[String])]),
env: T.nilable(T::Hash[Symbol, T.any(String, Pathname, T::Array[String])]),
explicit: T::Hash[Symbol, T.any(String, Pathname, T::Array[String])],
default: T.nilable(ConfigHash),
env: T.nilable(ConfigHash),
explicit: ConfigHash,
ignore_invalid_keys: T::Boolean,
).void
}
@ -118,18 +110,18 @@ module Cask
if default
@default = T.let(
self.class.canonicalize(self.class.defaults.merge(default)),
T.nilable(T::Hash[Symbol, T.any(String, Pathname, T::Array[String])]),
T.nilable(ConfigHash),
)
end
if env
@env = T.let(
self.class.canonicalize(env),
T.nilable(T::Hash[Symbol, T.any(String, Pathname, T::Array[String])]),
T.nilable(ConfigHash),
)
end
@explicit = T.let(
self.class.canonicalize(explicit),
T::Hash[Symbol, T.any(String, Pathname, T::Array[String])],
ConfigHash,
)
if ignore_invalid_keys
@ -142,18 +134,18 @@ module Cask
@explicit.assert_valid_keys(*self.class.defaults.keys)
end
sig { returns(T::Hash[Symbol, T.any(String, Pathname, T::Array[String])]) }
sig { returns(ConfigHash) }
def default
@default ||= self.class.canonicalize(self.class.defaults)
end
sig { returns(T::Hash[Symbol, T.any(String, Pathname, T::Array[String])]) }
sig { returns(ConfigHash) }
def env
@env ||= self.class.canonicalize(
Homebrew::EnvConfig.cask_opts
.select { |arg| arg.include?("=") }
.map { |arg| T.cast(arg.split("=", 2), [String, String]) }
.map do |(flag, value)|
.to_h do |(flag, value)|
key = flag.sub(/^--/, "")
# converts --language flag to :languages config key
if key == "language"
@ -161,7 +153,7 @@ module Cask
value = value.split(",")
end
[key, value]
[key.to_sym, value]
end,
)
end

View File

@ -11,7 +11,7 @@ module Cask
sig { returns(T.nilable(T::Array[T.untyped])) }
attr_accessor :uninstall_artifacts
sig { params(attributes: T::Hash[String, T.untyped]).void }
sig { params(attributes: T.any(T::Hash[String, T.untyped], T::Hash[Symbol, T.untyped])).void }
def initialize(attributes = {})
@uninstall_flight_blocks = T.let(nil, T.nilable(T::Boolean))
@uninstall_artifacts = T.let(nil, T.nilable(T::Array[T.untyped]))

View File

@ -54,7 +54,7 @@ module Cask
revisions: T.nilable(T::Hash[T.any(Symbol, String), String]),
revision: T.nilable(String),
trust_cert: T.nilable(T::Boolean),
cookies: T.nilable(T::Hash[String, String]),
cookies: T.nilable(T::Hash[T.any(String, Symbol), String]),
referer: T.nilable(T.any(URI::Generic, String)),
header: T.nilable(T.any(String, T::Array[String])),
user_agent: T.nilable(T.any(Symbol, String)),
@ -80,7 +80,8 @@ module Cask
specs[:revisions] = @revisions = T.let(revisions, T.nilable(T::Hash[T.any(Symbol, String), String]))
specs[:revision] = @revision = T.let(revision, T.nilable(String))
specs[:trust_cert] = @trust_cert = T.let(trust_cert, T.nilable(T::Boolean))
specs[:cookies] = @cookies = T.let(cookies, T.nilable(T::Hash[String, String]))
specs[:cookies] =
@cookies = T.let(cookies&.transform_keys(&:to_s), T.nilable(T::Hash[String, String]))
specs[:referer] = @referer = T.let(referer, T.nilable(T.any(URI::Generic, String)))
specs[:headers] = @header = T.let(header, T.nilable(T.any(String, T::Array[String])))
specs[:user_agent] = @user_agent = T.let(user_agent || :default, T.nilable(T.any(Symbol, String)))

View File

@ -45,7 +45,7 @@ class CaskDependent
)
end
sig { returns(T::Array[CaskDependent::Requirement]) }
sig { returns(T::Array[::Requirement]) }
def requirements
@requirements ||= T.let(
begin
@ -73,7 +73,7 @@ class CaskDependent
requirements
end,
T.nilable(T::Array[CaskDependent::Requirement]),
T.nilable(T::Array[::Requirement]),
)
end

View File

@ -9,7 +9,7 @@ module Homebrew
# 1: long option name (e.g. "--debug")
# 2: option description (e.g. "Print debugging information")
# 3: whether the option is hidden
OptionsType = T.type_alias { T::Array[[String, T.nilable(String), String, T::Boolean]] }
OptionsType = T.type_alias { T::Array[[T.nilable(String), T.nilable(String), String, T::Boolean]] }
sig { returns(T::Array[String]) }
attr_reader :options_only, :flags_only, :remaining
@ -170,7 +170,7 @@ module Homebrew
sig { returns(T::Array[String]) }
def cli_args
@cli_args ||= @processed_options.filter_map do |short, long|
option = long || short
option = T.must(long || short)
switch = :"#{option_to_name(option)}?"
flag = option_to_name(option).to_sym
if @table[switch] == true || @table[flag] == true

View File

@ -127,9 +127,7 @@ module Homebrew
sig {
params(
formulae_or_casks: T::Array[T.any(Formula, Cask::Cask)],
).returns(
T::Array[T.any(T::Hash[String, T.untyped], T::Hash[String, T.untyped])],
)
).returns(T::Array[T::Hash[Symbol, T.untyped]])
}
def json_info(formulae_or_casks)
formulae_or_casks.map do |formula_or_cask|

View File

@ -427,6 +427,19 @@ require "extend/os/cmd/update-report"
class Reporter
include Utils::Output::Mixin
Report = T.type_alias do
{
A: T::Array[String],
AC: T::Array[String],
D: T::Array[String],
DC: T::Array[String],
M: T::Array[String],
MC: T::Array[String],
R: T::Array[[String, String]],
RC: T::Array[[String, String]],
}
end
class ReporterRevisionUnsetError < RuntimeError
sig { params(var_name: String).void }
def initialize(var_name)
@ -456,14 +469,17 @@ class Reporter
raise ReporterRevisionUnsetError, current_revision_var if @current_revision.empty?
end
@report = T.let(nil, T.nilable(T::Hash[Symbol, T::Array[String]]))
@report = T.let(nil, T.nilable(Report))
end
sig { params(auto_update: T::Boolean).returns(T::Hash[Symbol, T::Array[String]]) }
sig { params(auto_update: T::Boolean).returns(Report) }
def report(auto_update: false)
return @report if @report
@report = Hash.new { |h, k| h[k] = [] }
@report = {
A: [], AC: [], D: [], DC: [], M: [], MC: [], R: T.let([], T::Array[[String, String]]),
RC: T.let([], T::Array[[String, String]])
}
return @report unless updated?
diff.each_line do |line|
@ -791,13 +807,15 @@ class ReporterHub
sig { void }
def initialize
@hash = T.let({}, T::Hash[Symbol, T::Array[String]])
@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)
@hash.fetch(key, [])
raise "Unsupported key #{key}" unless [:A, :AC, :D, :DC, :M, :MC].include?(key)
T.cast(@hash.fetch(key, []), T::Array[String])
end
sig { params(reporter: Reporter, auto_update: T::Boolean).void }

View File

@ -204,7 +204,7 @@ module Commands
cmd_parser.processed_options.filter_map do |short, long, desc, hidden|
next if hidden
[long || short, desc]
[T.must(long || short), desc]
end
else
options = []

View File

@ -135,7 +135,7 @@ class DependencyCollector
sig {
params(spec: T.any(String, Resource, Symbol, Requirement, Dependency, Class),
tags: T::Array[Symbol]).returns(T.any(Dependency, Requirement, Array, NilClass))
tags: T::Array[T.any(String, Symbol)]).returns(T.any(Dependency, Requirement, Array, NilClass))
}
def parse_spec(spec, tags)
raise ArgumentError, "Implicit dependencies cannot be manually specified" if tags.include?(:implicit)

View File

@ -181,7 +181,7 @@ module Homebrew
end
sig {
params(old_keys: T::Array[String], old_bottle_spec: BottleSpecification,
params(old_keys: T::Array[Symbol], old_bottle_spec: BottleSpecification,
new_bottle_hash: T::Hash[String, T.untyped])
.returns([T::Array[String], T::Array[T::Hash[Symbol, T.any(String, Symbol)]]])
}
@ -506,7 +506,7 @@ module Homebrew
tab.time = nil
tab.changed_files = changed_files.dup
if args.only_json_tab?
tab.changed_files.delete(Pathname.new(AbstractTab::FILENAME))
tab.changed_files&.delete(Pathname.new(AbstractTab::FILENAME))
tab.tabfile.unlink
else
tab.write

View File

@ -119,7 +119,7 @@ module Homebrew
# seeds being output when running parallel tests.
seed = args.seed || rand(0xFFFF).to_i
bundle_args = ["-I", HOMEBREW_LIBRARY_PATH/"test"]
bundle_args = ["-I", (HOMEBREW_LIBRARY_PATH/"test").to_s]
bundle_args += %W[
--seed #{seed}
--color
@ -249,6 +249,7 @@ module Homebrew
ENV["HOMEBREW_TEST_GENERIC_OS"] = "1" if args.generic?
ENV["HOMEBREW_TEST_ONLINE"] = "1" if args.online?
ENV["HOMEBREW_SORBET_RUNTIME"] = "1"
ENV["HOMEBREW_SORBET_RECURSIVE"] = "1"
ENV["USER"] ||= system_command!("id", args: ["-nu"]).stdout.chomp

View File

@ -1198,9 +1198,9 @@ module Homebrew
private
sig { returns(T::Array[Pathname]) }
sig { returns(T::Array[String]) }
def paths
@paths ||= T.let(ORIGINAL_PATHS.uniq.map(&:to_s), T.nilable(T::Array[Pathname]))
@paths ||= T.let(ORIGINAL_PATHS.uniq.map(&:to_s), T.nilable(T::Array[String]))
end
end
end

View File

@ -643,11 +643,9 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy
args
end
sig { returns(T::Hash[Symbol, String]) }
sig { returns(T::Hash[Symbol, T.any(String, Symbol)]) }
def _curl_opts
return { user_agent: meta.fetch(:user_agent) } if meta.key?(:user_agent)
{}
meta.slice(:user_agent)
end
sig { override.params(args: String, options: T.untyped).returns(SystemCommand::Result) }

View File

@ -471,6 +471,11 @@ module Homebrew
"of macOS. This is useful in development on new macOS versions.",
boolean: true,
},
HOMEBREW_SORBET_RECURSIVE: {
description: "If set along with `$HOMEBREW_SORBET_RUNTIME`, enable recursive typechecking using Sorbet. " \
"Auomatically enabled when running tests.",
boolean: true,
},
HOMEBREW_SORBET_RUNTIME: {
description: "If set, enable runtime typechecking using Sorbet. " \
"Set by default for `$HOMEBREW_DEVELOPER` or when running some developer commands.",

View File

@ -244,7 +244,7 @@ module Superenv
end
# Don't add `llvm` to library paths; this leads to undesired linkage to LLVM's `libunwind`
paths << keg_only_deps.reject { |dep| dep.name.match?(/^llvm(@\d+)?$/) }
paths += keg_only_deps.reject { |dep| dep.name.match?(/^llvm(@\d+)?$/) }
.map(&:opt_lib)
paths << (HOMEBREW_PREFIX/"lib")

View File

@ -34,7 +34,7 @@ module Kernel
sig { type_parameters(:U).params(block: T.proc.returns(T.type_parameter(:U))).returns(T.type_parameter(:U)) }
def with_homebrew_path(&block)
with_env(PATH: PATH.new(ORIGINAL_PATHS), &block)
with_env(PATH: PATH.new(ORIGINAL_PATHS).to_s, &block)
end
sig {
@ -283,8 +283,10 @@ module Kernel
# @api public
sig {
type_parameters(:U)
.params(hash: T::Hash[Object, String], _block: T.proc.returns(T.type_parameter(:U)))
.returns(T.type_parameter(:U))
.params(
hash: T::Hash[Object, T.any(NilClass, PATH, Pathname, String)],
_block: T.proc.returns(T.type_parameter(:U)),
).returns(T.type_parameter(:U))
}
def with_env(hash, &_block)
old_values = {}
@ -292,7 +294,7 @@ module Kernel
hash.each do |key, value|
key = key.to_s
old_values[key] = ENV.delete(key)
ENV[key] = value
ENV[key] = value&.to_s
end
yield

View File

@ -15,7 +15,7 @@ module OS
appdir: "~/.config/apps",
}.freeze, T::Hash[Symbol, String])
sig { returns(T::Hash[Symbol, String]) }
sig { returns(T::Hash[Symbol, T.any(LazyObject, String)]) }
def defaults
{
languages: LazyObject.new { Linux.languages },

View File

@ -3963,7 +3963,7 @@ class Formula
# ```
#
# @api public
sig { params(dep: T.any(String, Symbol, T::Hash[String, T.untyped], T::Class[Requirement])).void }
sig { params(dep: T.any(String, Symbol, T::Hash[T.any(String, Symbol, T::Class[Requirement]), T.untyped], T::Class[Requirement])).void }
def depends_on(dep)
specs.each { |spec| spec.depends_on(dep) }
end

View File

@ -809,7 +809,7 @@ on_request: installed_on_request?, options:)
else
[]
end
options += effective_build_options_for(formula).used_options.to_a
options += effective_build_options_for(formula).used_options.to_a.map(&:to_s)
options
end

View File

@ -51,7 +51,14 @@ module Formulary
end
private_class_method :platform_cache_tag
sig { returns(T::Hash[Symbol, T::Hash[String, T.class_of(Formula)]]) }
sig {
returns({
api: T.nilable(T::Hash[String, T.class_of(Formula)]),
formulary_factory: T.nilable(T::Hash[String, Formula]),
path: T.nilable(T::Hash[String, T.class_of(Formula)]),
stub: T.nilable(T::Hash[String, T.class_of(Formula)]),
})
}
def self.platform_cache
cache[platform_cache_tag] ||= {}
end

View File

@ -28,4 +28,7 @@ class LazyObject < Delegator
__getobj__.is_a?(klass) || super
end
def class = __getobj__.class
def to_s = __getobj__.to_s
end

View File

@ -14,7 +14,7 @@ class Messages
sig { returns(Integer) }
attr_reader :package_count
sig { returns(T::Array[T::Hash[String, Float]]) }
sig { returns(T::Array[{ package: String, time: Float }]) }
attr_reader :install_times
sig { void }
@ -22,7 +22,7 @@ class Messages
@caveats = T.let([], T::Array[{ package: String, caveats: T.any(String, Caveats) }])
@completions_and_elisp = T.let(Set.new, T::Set[String])
@package_count = T.let(0, Integer)
@install_times = T.let([], T::Array[T::Hash[String, Float]])
@install_times = T.let([], T::Array[{ package: String, time: Float }])
end
sig { params(package: String, caveats: T.any(String, Caveats)).void }

View File

@ -15,10 +15,10 @@ class XcodeRequirement < Requirement
xcode_installed_version!
end
sig { params(tags: T::Array[String]).void }
sig { params(tags: T::Array[T.any(String, Symbol)]).void }
def initialize(tags = [])
version = tags.shift if tags.first.to_s.match?(/(\d\.)+\d/)
@version = T.let(version, T.nilable(String))
@version = T.let(version.to_s, T.nilable(String))
super
end

View File

@ -83,11 +83,11 @@ module RuboCop
)
end
sig { returns(T::Hash[Parser::Source::Range, T::Array[Parser::Source::Comment]]) }
sig { returns(T::Hash[Parser::Source::Map, T::Array[Parser::Source::Comment]]) }
def comments_hash
@comments_hash ||= T.let(
Parser::Source::Comment.associate_locations(stanza_node.parent, all_comments),
T.nilable(T::Hash[Parser::Source::Range, T::Array[Parser::Source::Comment]]),
T.nilable(T::Hash[Parser::Source::Map, T::Array[Parser::Source::Comment]]),
)
end

View File

@ -46,7 +46,7 @@ module RuboCop
sig {
params(
block_node: RuboCop::AST::BlockNode,
comments: T::Array[String],
comments: T::Array[Parser::Source::Comment],
).returns(
T::Array[RuboCop::Cask::AST::Stanza],
)

View File

@ -13,7 +13,7 @@ class SBOM
include Utils::Output::Mixin
FILENAME = "sbom.spdx.json"
SCHEMA_FILE = (HOMEBREW_LIBRARY_PATH/"data/schemas/sbom.json").freeze
SCHEMA_FILE = T.let((HOMEBREW_LIBRARY_PATH/"data/schemas/sbom.json").freeze, Pathname)
# Instantiates a {SBOM} for a new installation of a formula.
sig { params(formula: Formula, tab: Tab).returns(T.attached_class) }
@ -62,7 +62,7 @@ class SBOM
formula.prefix/FILENAME
end
sig { params(deps: T::Array[T::Hash[String, String]]).returns(T::Array[T::Hash[String, String]]) }
sig { params(deps: T::Array[T::Hash[String, T.untyped]]).returns(T::Array[T::Hash[String, T.anything]]) }
def self.runtime_deps_hash(deps)
deps.map do |dep|
full_name = dep.fetch("full_name")
@ -83,12 +83,12 @@ class SBOM
spdxfile(formula).exist?
end
sig { returns(T::Hash[String, T.untyped]) }
sig { returns(T::Hash[String, T.anything]) }
def self.schema
@schema ||= JSON.parse(SCHEMA_FILE.read, freeze: true)
@schema ||= T.let(JSON.parse(SCHEMA_FILE.read, freeze: true), T.nilable(T::Hash[String, T.untyped]))
end
sig { params(bottling: T::Boolean).returns(T::Array[T::Hash[String, T.untyped]]) }
sig { params(bottling: T::Boolean).returns(T::Array[String]) }
def schema_validation_errors(bottling: false)
unless Homebrew.require? "json_schemer"
error_message = "Need json_schemer to validate SBOM, run `brew install-bundler-gems --add-groups=bottle`!"
@ -134,17 +134,17 @@ class SBOM
attr_reader :name, :homebrew_version, :time, :stdlib, :source, :built_on, :license
attr_accessor :spdxfile
sig { params(attributes: Hash).void }
sig { params(attributes: T::Hash[Symbol, T.untyped]).void }
def initialize(attributes = {})
attributes.each { |key, value| instance_variable_set(:"@#{key}", value) }
end
sig {
params(
runtime_dependency_declaration: T::Array[Hash],
compiler_declaration: Hash,
runtime_dependency_declaration: T::Array[T::Hash[Symbol, T.untyped]],
compiler_declaration: T::Hash[String, T.untyped],
bottling: T::Boolean,
).returns(T::Array[Hash])
).returns(T::Array[T::Hash[Symbol, T.untyped]])
}
def generate_relations_json(runtime_dependency_declaration, compiler_declaration, bottling:)
runtime = runtime_dependency_declaration.map do |dependency|
@ -167,7 +167,7 @@ class SBOM
spdxElementId: "SPDXRef-File-#{name}",
relationshipType: "PACKAGE_OF",
relatedSpdxElement: "SPDXRef-Archive-#{name}-src",
}], T::Array[Hash])
}], T::Array[T::Hash[Symbol, T.untyped]])
unless bottling
base << {
@ -189,16 +189,11 @@ class SBOM
end
sig {
params(runtime_dependency_declaration: T::Array[Hash],
compiler_declaration: Hash,
bottling: T::Boolean).returns(
T::Array[
T::Hash[
Symbol,
T.any(String, T::Array[T::Hash[Symbol, String]]),
],
],
)
params(
runtime_dependency_declaration: T::Array[T::Hash[Symbol, T.anything]],
compiler_declaration: T::Hash[String, T::Hash[Symbol, T.anything]],
bottling: T::Boolean,
).returns(T::Array[T::Hash[Symbol, T.untyped]])
}
def generate_packages_json(runtime_dependency_declaration, compiler_declaration, bottling:)
bottle = []
@ -259,9 +254,8 @@ class SBOM
end
sig {
params(bottling: T::Boolean).returns(T::Array[T::Hash[Symbol,
T.any(T::Boolean, String,
T::Array[T::Hash[Symbol, String]])]])
params(bottling: T::Boolean)
.returns(T::Array[T::Hash[Symbol, T.any(T::Boolean, String, T::Array[T::Hash[Symbol, String]])]])
}
def full_spdx_runtime_dependencies(bottling:)
return [] if bottling || @runtime_dependencies.blank?
@ -302,7 +296,7 @@ class SBOM
end
end
sig { params(bottling: T::Boolean).returns(T::Hash[Symbol, T.any(String, T::Array[T::Hash[Symbol, String]])]) }
sig { params(bottling: T::Boolean).returns(T::Hash[Symbol, T.anything]) }
def to_spdx_sbom(bottling:)
runtime_full = full_spdx_runtime_dependencies(bottling:)
@ -360,7 +354,7 @@ class SBOM
}
end
sig { params(base: T.nilable(T::Hash[String, Hash])).returns(T.nilable(T::Hash[String, String])) }
sig { params(base: T.nilable(T::Hash[String, T.untyped])).returns(T.nilable(T::Hash[String, String])) }
def get_bottle_info(base)
return unless base.present?

View File

@ -337,7 +337,7 @@ module Homebrew
parsed
end
sig { params(variables: T::Hash[Symbol, String]).returns(T.nilable(T::Hash[Symbol, String])) }
sig { params(variables: T::Hash[Symbol, T.any(Pathname, String)]).returns(T.nilable(T::Hash[Symbol, String])) }
def environment_variables(variables = {})
@environment_variables = variables.transform_values(&:to_s)
end

View File

@ -219,7 +219,7 @@ class SoftwareSpec
options << opt
end
sig { params(hash: T::Hash[T.any(String, Symbol), T.any(String, Symbol)]).void }
sig { params(hash: T::Hash[T.any(String, Symbol, T::Array[String]), T.any(String, Symbol, T::Array[String])]).void }
def deprecated_option(hash)
raise ArgumentError, "deprecated_option hash must not be empty" if hash.empty?
@ -242,7 +242,7 @@ class SoftwareSpec
@build = BuildOptions.new(Options.create(@flags), options)
end
sig { params(spec: T.any(String, Symbol, T::Hash[String, T.untyped], T::Class[Requirement], Dependable)).void }
sig { params(spec: T.any(String, Symbol, T::Hash[T.any(String, Symbol, T::Class[Requirement]), T.untyped], T::Class[Requirement], Dependable)).void }
def depends_on(spec)
dep = dependency_collector.add(spec)
add_dep_option(dep) if dep

View File

@ -10,6 +10,55 @@ require "extend/module"
# There are mechanisms to achieve a middle ground (`default_checked_level`).
if ENV["HOMEBREW_SORBET_RUNTIME"]
T::Configuration.enable_final_checks_on_hooks
if ENV["HOMEBREW_SORBET_RECURSIVE"] == "1"
module T
module Types
class FixedArray < Base
def valid?(obj) = recursively_valid?(obj)
end
class FixedHash < Base
def valid?(obj) = recursively_valid?(obj)
end
class Intersection < Base
def valid?(obj) = recursively_valid?(obj)
end
class TypedArray < TypedEnumerable
def valid?(obj) = recursively_valid?(obj)
end
class TypedEnumerable < Base
def valid?(obj) = recursively_valid?(obj)
end
class TypedEnumeratorChain < TypedEnumerable
def valid?(obj) = recursively_valid?(obj)
end
class TypedEnumeratorLazy < TypedEnumerable
def valid?(obj) = recursively_valid?(obj)
end
class TypedHash < TypedEnumerable
def valid?(obj) = recursively_valid?(obj)
end
class TypedRange < TypedEnumerable
def valid?(obj) = recursively_valid?(obj)
end
class TypedSet < TypedEnumerable
def valid?(obj) = recursively_valid?(obj)
end
class Union < Base
def valid?(obj) = recursively_valid?(obj)
end
end
end
end
else
# Redefine `T.let`, etc. to make the `checked` parameter default to `false` rather than `true`.
# @private

View File

@ -27,7 +27,7 @@ class SystemCommand
args: T::Array[T.any(String, Integer, Float, Pathname, URI::Generic)],
sudo: T::Boolean,
sudo_as_root: T::Boolean,
env: T::Hash[String, String],
env: T::Hash[String, T.any(NilClass, String, T::Boolean)],
input: T.any(String, T::Array[String]),
must_succeed: T::Boolean,
print_stdout: T.any(T::Boolean, Symbol),
@ -56,7 +56,7 @@ class SystemCommand
args: T::Array[T.any(String, Integer, Float, Pathname, URI::Generic)],
sudo: T::Boolean,
sudo_as_root: T::Boolean,
env: T::Hash[String, String],
env: T::Hash[String, T.any(NilClass, String, T::Boolean)],
input: T.any(String, T::Array[String]),
print_stdout: T.any(T::Boolean, Symbol),
print_stderr: T.any(T::Boolean, Symbol),
@ -84,7 +84,7 @@ class SystemCommand
args: T::Array[T.any(String, Integer, Float, Pathname, URI::Generic)],
sudo: T::Boolean,
sudo_as_root: T::Boolean,
env: T::Hash[String, String],
env: T::Hash[String, T.any(NilClass, String, T::Boolean)],
input: T.any(String, T::Array[String]),
must_succeed: T::Boolean,
print_stdout: T.any(T::Boolean, Symbol),
@ -110,7 +110,7 @@ class SystemCommand
args: T::Array[T.any(String, Integer, Float, Pathname, URI::Generic)],
sudo: T::Boolean,
sudo_as_root: T::Boolean,
env: T::Hash[String, String],
env: T::Hash[String, T.any(NilClass, String, T::Boolean)],
input: T.any(String, T::Array[String]),
must_succeed: T::Boolean,
print_stdout: T.any(T::Boolean, Symbol),
@ -169,7 +169,7 @@ class SystemCommand
args: T::Array[T.any(String, Integer, Float, Pathname, URI::Generic)],
sudo: T::Boolean,
sudo_as_root: T::Boolean,
env: T::Hash[String, String],
env: T::Hash[String, T.any(NilClass, String, T::Boolean)],
input: T.any(String, T::Array[String]),
must_succeed: T::Boolean,
print_stdout: T.any(T::Boolean, Symbol),
@ -237,7 +237,7 @@ class SystemCommand
sig { returns(T.any(NilClass, String, Pathname)) }
attr_reader :chdir
sig { returns(T::Hash[String, String]) }
sig { returns(T::Hash[String, T.any(NilClass, String, T::Boolean)]) }
attr_reader :env
sig { returns(T::Boolean) }
@ -378,7 +378,7 @@ class SystemCommand
sig {
params(
env: T::Hash[String, String],
env: T::Hash[String, T.nilable(String)],
executable: String,
args: String,
options: T.untyped,
@ -480,7 +480,7 @@ class SystemCommand
sig {
params(
command: T::Array[String],
output: T::Array[[Symbol, String]],
output: T::Array[[T.any(String, Symbol), String]],
status: Process::Status,
secrets: T::Array[String],
).void

View File

@ -38,7 +38,8 @@ class AbstractTab
# @api internal
attr_accessor :runtime_dependencies
sig { params(attributes: T::Hash[String, T.untyped]).void }
# TODO: Update attributes to only accept symbol keys (kwargs style).
sig { params(attributes: T.any(T::Hash[String, T.untyped], T::Hash[Symbol, T.untyped])).void }
def initialize(attributes = {})
@installed_as_dependency = T.let(nil, T.nilable(T::Boolean))
@installed_on_request = T.let(nil, T.nilable(T::Boolean))
@ -51,7 +52,14 @@ class AbstractTab
@built_on = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
@runtime_dependencies = T.let(nil, T.nilable(T::Array[T.untyped]))
attributes.each { |key, value| instance_variable_set(:"@#{key}", value) }
attributes.each do |key, value|
case key.to_sym
when :changed_files
@changed_files = value&.map { |f| Pathname(f) }
else
instance_variable_set(:"@#{key}", value)
end
end
end
# Instantiates a {Tab} for a new installation of a formula or cask.
@ -166,15 +174,18 @@ class Tab < AbstractTab
# @api internal
attr_accessor :poured_from_bottle
attr_accessor :built_as_bottle, :changed_files, :stdlib, :aliases
attr_accessor :built_as_bottle, :stdlib, :aliases
attr_writer :used_options, :unused_options, :compiler, :source_modified_time
attr_reader :tapped_from
sig { params(attributes: T::Hash[String, T.untyped]).void }
sig { returns(T.nilable(T::Array[Pathname])) }
attr_accessor :changed_files
sig { params(attributes: T.any(T::Hash[String, T.untyped], T::Hash[Symbol, T.untyped])).void }
def initialize(attributes = {})
@poured_from_bottle = T.let(nil, T.nilable(T::Boolean))
@built_as_bottle = T.let(nil, T.nilable(T::Boolean))
@changed_files = T.let(nil, T.nilable(T::Array[Pathname]))
@changed_files = nil
@stdlib = T.let(nil, T.nilable(String))
@aliases = T.let(nil, T.nilable(T::Array[String]))
@used_options = T.let(nil, T.nilable(T::Array[String]))

View File

@ -83,7 +83,7 @@ RSpec.describe Homebrew::Cmd::UpdateReport do
expect(hub.select_formula_or_cask(:A)).to be_empty
expect(hub.select_formula_or_cask(:D)).to be_empty
expect(hub.select_formula_or_cask(:R)).to eq([["cv", "progress"]])
expect(hub.instance_variable_get(:@hash)[:R]).to eq([["cv", "progress"]])
end
context "when updating a Tap other than the core Tap" do
@ -102,7 +102,7 @@ RSpec.describe Homebrew::Cmd::UpdateReport do
expect(hub.select_formula_or_cask(:A)).to be_empty
expect(hub.select_formula_or_cask(:D)).to be_empty
expect(hub.select_formula_or_cask(:R)).to be_empty
expect(hub.instance_variable_get(:@hash)[:R]).to be_nil
end
specify "with renamed Formula and restructured Tap" do
@ -111,7 +111,7 @@ RSpec.describe Homebrew::Cmd::UpdateReport do
expect(hub.select_formula_or_cask(:A)).to be_empty
expect(hub.select_formula_or_cask(:D)).to be_empty
expect(hub.select_formula_or_cask(:R)).to eq([%w[foo/bar/xchat foo/bar/xchat2]])
expect(hub.instance_variable_get(:@hash)[:R]).to eq([%w[foo/bar/xchat foo/bar/xchat2]])
end
specify "with simulated 'homebrew/php' restructuring" do
@ -119,7 +119,7 @@ RSpec.describe Homebrew::Cmd::UpdateReport do
expect(hub.select_formula_or_cask(:A)).to be_empty
expect(hub.select_formula_or_cask(:D)).to be_empty
expect(hub.select_formula_or_cask(:R)).to be_empty
expect(hub.instance_variable_get(:@hash)[:R]).to be_nil
end
specify "with Formula changes" do
@ -127,7 +127,7 @@ RSpec.describe Homebrew::Cmd::UpdateReport do
expect(hub.select_formula_or_cask(:A)).to eq(%w[foo/bar/lua])
expect(hub.select_formula_or_cask(:M)).to eq(%w[foo/bar/git])
expect(hub.select_formula_or_cask(:D)).to be_empty
expect(hub.instance_variable_get(:@hash)[:R]).to be_nil
end
end
end

View File

@ -173,7 +173,7 @@ RSpec.describe CurlDownloadStrategy do
hash_including(args: array_including_cons("#{artifact_domain}/#{resource_path}")),
)
.at_least(:once)
.and_return(SystemCommand::Result.new(["curl"], [""], status, secrets: []))
.and_return(SystemCommand::Result.new(["curl"], [[:stdout, ""]], status, secrets: []))
strategy.fetch
end
@ -191,7 +191,7 @@ RSpec.describe CurlDownloadStrategy do
hash_including(args: array_including_cons("#{artifact_domain}/#{resource_path}")),
)
.at_least(:once)
.and_return(SystemCommand::Result.new(["curl"], [""], status, secrets: []))
.and_return(SystemCommand::Result.new(["curl"], [[:stdout, ""]], status, secrets: []))
strategy.fetch
end

View File

@ -110,7 +110,7 @@ RSpec.describe Homebrew::Livecheck::Strategy::HeaderMatch do
it "errors on an invalid return type from a block" do
expect { header_match.versions_from_headers(headers) { 123 } }
.to raise_error(TypeError, Homebrew::Livecheck::Strategy::INVALID_BLOCK_RETURN_VALUE_MSG)
.to raise_error(TypeError, /Parameter 'headers': Expected type T::Hash\[String, String\]/o)
end
end
end

View File

@ -87,8 +87,9 @@ RSpec.describe Homebrew::Services::Cli do
describe "#run" do
it "checks missing file causes error" do
expect(Homebrew::Services::System).not_to receive(:root?)
service = instance_double(Homebrew::Services::FormulaWrapper, name: "service_name")
expect do
services_cli.start(["service_name"], "/non/existent/path")
services_cli.start([service], "/non/existent/path")
end.to raise_error(UsageError, "Invalid usage: Provided service file does not exist.")
end
@ -110,8 +111,9 @@ RSpec.describe Homebrew::Services::Cli do
describe "#start" do
it "checks missing file causes error" do
expect(Homebrew::Services::System).not_to receive(:root?)
service = instance_double(Homebrew::Services::FormulaWrapper, name: "service_name")
expect do
services_cli.start(["service_name"], "/hfdkjshksdjhfkjsdhf/fdsjghsdkjhb")
services_cli.start([service], "/hfdkjshksdjhfkjsdhf/fdsjghsdkjhb")
end.to raise_error(UsageError, "Invalid usage: Provided service file does not exist.")
end

View File

@ -23,7 +23,7 @@ RSpec.describe Homebrew::Services::Commands::Info do
it "succeeds with items" do
out = "service ()\nRunning: true\nLoaded: true\nSchedulable: false\n"
formula = {
formula_wrapper = instance_double(Homebrew::Services::FormulaWrapper, to_hash: {
name: "service",
user: "user",
status: :started,
@ -31,9 +31,9 @@ RSpec.describe Homebrew::Services::Commands::Info do
running: true,
loaded: true,
schedulable: false,
}
})
expect do
described_class.run([formula], verbose: false, json: false)
described_class.run([formula_wrapper], verbose: false, json: false)
end.to output(out).to_stdout
end
@ -48,8 +48,9 @@ RSpec.describe Homebrew::Services::Commands::Info do
schedulable: false,
}
out = "#{JSON.pretty_generate([formula])}\n"
formula_wrapper = instance_double(Homebrew::Services::FormulaWrapper, to_hash: formula)
expect do
described_class.run([formula], verbose: false, json: true)
described_class.run([formula_wrapper], verbose: false, json: true)
end.to output(out).to_stdout
end
end

View File

@ -275,7 +275,7 @@ RSpec.describe SystemCommand do
FileUtils.chmod "+x", path/"tool"
expect(described_class.run("tool", env: { "PATH" => path }).stdout).to include "Hello, world!"
expect(described_class.run("tool", env: { "PATH" => path.to_s }).stdout).to include "Hello, world!"
end
end

View File

@ -241,7 +241,7 @@ RSpec.describe Tab do
tab = described_class.from_file(path)
source_path = "/usr/local/Library/Taps/homebrew/homebrew-core/Formula/foo.rb"
runtime_dependencies = [{ "full_name" => "foo", "version" => "1.0" }]
changed_files = %w[INSTALL_RECEIPT.json bin/foo]
changed_files = %w[INSTALL_RECEIPT.json bin/foo].map { Pathname.new(_1) }
expect(tab.used_options.sort).to eq(used_options.sort)
expect(tab.unused_options.sort).to eq(unused_options.sort)
@ -271,7 +271,7 @@ RSpec.describe Tab do
tab = described_class.from_file_content(path.read, path)
source_path = "/usr/local/Library/Taps/homebrew/homebrew-core/Formula/foo.rb"
runtime_dependencies = [{ "full_name" => "foo", "version" => "1.0" }]
changed_files = %w[INSTALL_RECEIPT.json bin/foo]
changed_files = %w[INSTALL_RECEIPT.json bin/foo].map { Pathname.new(_1) }
expect(tab.used_options.sort).to eq(used_options.sort)
expect(tab.unused_options.sort).to eq(unused_options.sort)

View File

@ -184,7 +184,7 @@ RSpec.describe Utils::Analytics do
end
specify "::table_output" do
results = { ack: 10, wget: 100 }
results = { "ack" => 10, "wget" => 100 }
expect { described_class.table_output("install", "30", results) }
.to output(/110 | 100.00%/).to_stdout
.and not_to_output.to_stderr

View File

@ -86,7 +86,7 @@ RSpec.describe Utils::Inreplace do
it "raises error if there is no old value" do
expect do
described_class.inreplace_pairs(file.path, [[nil, "f"]])
end.to raise_error(Utils::Inreplace::Error)
end.to raise_error(TypeError)
end
it "substitutes returned string but not file when `read_only_run: true`" do

View File

@ -36,7 +36,7 @@ module UnpackStrategy
quiet_flags = verbose ? [] : ["-qq"]
result = system_command! "unzip",
args: [*quiet_flags, "-o", path, "-d", unpack_dir],
env: { "PATH" => PATH.new(unzip&.opt_bin, ENV.fetch("PATH")) },
env: { "PATH" => PATH.new(unzip&.opt_bin, ENV.fetch("PATH")).to_s },
verbose:,
print_stderr: false

View File

@ -360,7 +360,7 @@ module Utils
nil
end
sig { returns(T::Hash[Symbol, String]) }
sig { returns(T::Hash[Symbol, T.any(T::Boolean, String)]) }
def default_package_tags
cache[:default_package_tags] ||= begin
# Only display default prefixes to reduce cardinality and improve privacy