Bump some utils/ files to Sorbet typed: strict

This commit is contained in:
Issy Long 2025-01-11 21:36:59 +00:00
parent 55475cb11a
commit 975a707b3c
No known key found for this signature in database
6 changed files with 194 additions and 83 deletions

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "context"
@ -60,7 +60,7 @@ module Utils
puts Utils.popen_read(curl, *args, url)
else
pid = spawn curl, *args, url
Process.detach T.must(pid)
Process.detach(pid)
end
end
@ -161,52 +161,62 @@ module Utils
report_influx(:test_bot_test, tags, fields)
end
sig { returns(T::Boolean) }
def influx_message_displayed?
config_true?(:influxanalyticsmessage)
end
sig { returns(T::Boolean) }
def messages_displayed?
config_true?(:analyticsmessage) &&
!!(config_true?(:analyticsmessage) &&
config_true?(:caskanalyticsmessage) &&
influx_message_displayed?
influx_message_displayed?)
end
sig { returns(T::Boolean) }
def disabled?
return true if Homebrew::EnvConfig.no_analytics?
config_true?(:analyticsdisabled)
end
sig { returns(T::Boolean) }
def not_this_run?
ENV["HOMEBREW_NO_ANALYTICS_THIS_RUN"].present?
end
sig { returns(T::Boolean) }
def no_message_output?
# Used by Homebrew/install
ENV["HOMEBREW_NO_ANALYTICS_MESSAGE_OUTPUT"].present?
end
sig { void }
def messages_displayed!
Homebrew::Settings.write :analyticsmessage, true
Homebrew::Settings.write :caskanalyticsmessage, true
Homebrew::Settings.write :influxanalyticsmessage, true
end
sig { void }
def enable!
Homebrew::Settings.write :analyticsdisabled, false
delete_uuid!
messages_displayed!
end
sig { void }
def disable!
Homebrew::Settings.write :analyticsdisabled, true
delete_uuid!
end
sig { void }
def delete_uuid!
Homebrew::Settings.delete :analyticsuuid
end
sig { params(args: Homebrew::Cmd::Info::Args, filter: T.nilable(String)).void }
def output(args:, filter: nil)
require "api"
@ -244,6 +254,7 @@ module Utils
table_output(category, days, results, os_version:, cask_install:)
end
sig { params(json: T::Hash[String, T.untyped], args: Homebrew::Cmd::Info::Args).void }
def output_analytics(json, args:)
full_analytics = args.analytics? || verbose?
@ -273,6 +284,7 @@ module Utils
# It relies on screen scraping some GitHub HTML that's not available as an API.
# This seems very likely to break in the future.
# That said, it's the only way to get the data we want right now.
sig { params(formula: Formula, args: Homebrew::Cmd::Info::Args).void }
def output_github_packages_downloads(formula, args:)
return unless args.github_packages_downloads?
return unless formula.core_formula?
@ -316,6 +328,7 @@ module Utils
puts "#{number_readable(thirty_day_download_count)} (30 days)"
end
sig { params(formula: Formula, args: Homebrew::Cmd::Info::Args).void }
def formula_output(formula, args:)
return if Homebrew::EnvConfig.no_analytics? || Homebrew::EnvConfig.no_github_api?
@ -331,6 +344,7 @@ module Utils
nil
end
sig { params(cask: Cask::Cask, args: Homebrew::Cmd::Info::Args).void }
def cask_output(cask, args:)
return if Homebrew::EnvConfig.no_analytics? || Homebrew::EnvConfig.no_github_api?
@ -388,6 +402,12 @@ module Utils
end
end
sig {
params(
category: String, days: String, results: T::Hash[String, Integer], os_version: T::Boolean,
cask_install: T::Boolean
).void
}
def table_output(category, days, results, os_version: false, cask_install: false)
oh1 "#{category} (#{days} days)"
total_count = results.values.inject("+")
@ -475,14 +495,17 @@ module Utils
"#{formatted_total_count_footer} | #{formatted_total_percent_footer}%"
end
sig { params(key: Symbol).returns(T::Boolean) }
def config_true?(key)
Homebrew::Settings.read(key) == "true"
end
sig { params(count: Integer).returns(String) }
def format_count(count)
count.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
end
sig { params(percent: T.any(Integer, Float)).returns(String) }
def format_percent(percent)
format("%<percent>.2f", percent:)
end

View File

@ -1,10 +1,11 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "fcntl"
require "utils/socket"
module Utils
sig { params(child_error: T::Hash[String, T.untyped]).returns(Exception) }
def self.rewrite_child_error(child_error)
inner_class = Object.const_get(child_error["json_class"])
error = if child_error["cmd"] && inner_class == ErrorDuringExecution
@ -33,7 +34,11 @@ module Utils
# When using this function, remember to call `exec` as soon as reasonably possible.
# This function does not protect against the pitfalls of what you can do pre-exec in a fork.
# See `man fork` for more information.
def self.safe_fork(directory: nil, yield_parent: false)
sig {
params(directory: T.nilable(String), yield_parent: T::Boolean,
_blk: T.proc.params(arg0: T.nilable(String)).void).void
}
def self.safe_fork(directory: nil, yield_parent: false, &_blk)
require "json/add/exception"
block = proc do |tmpdir|
@ -80,8 +85,6 @@ module Utils
exit!(true)
end
pid = T.must(pid)
begin
yield(nil) if yield_parent

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "utils/inreplace"
@ -14,20 +14,20 @@ module PyPI
class Package
sig { params(package_string: String, is_url: T::Boolean, python_name: String).void }
def initialize(package_string, is_url: false, python_name: "python")
@pypi_info = nil
@pypi_info = T.let(nil, T.nilable(T::Array[String]))
@package_string = package_string
@is_url = is_url
@is_pypi_url = package_string.start_with? PYTHONHOSTED_URL_PREFIX
@is_pypi_url = T.let(package_string.start_with?(PYTHONHOSTED_URL_PREFIX), T::Boolean)
@python_name = python_name
end
sig { returns(String) }
sig { returns(T.nilable(String)) }
def name
basic_metadata if @name.blank?
@name
end
sig { returns(T::Array[T.nilable(String)]) }
sig { returns(T.nilable(T::Array[String])) }
def extras
basic_metadata if @extras.blank?
@extras
@ -43,7 +43,7 @@ module PyPI
def version=(new_version)
raise ArgumentError, "can't update version for non-PyPI packages" unless valid_pypi_package?
@version = new_version
@version = T.let(new_version, T.nilable(String))
end
sig { returns(T::Boolean) }
@ -97,8 +97,8 @@ module PyPI
sig { returns(String) }
def to_s
if valid_pypi_package?
out = name
out += "[#{extras.join(",")}]" if extras.present?
out = T.must(name)
out += "[#{extras&.join(",")}]" if extras.present?
out += "==#{version}" if version.present?
out
else
@ -132,14 +132,15 @@ module PyPI
private
# Returns [name, [extras], version] for this package.
sig { returns(T.nilable(T.any(String, T::Array[String]))) }
def basic_metadata
if @is_pypi_url
match = File.basename(@package_string).match(/^(.+)-([a-z\d.]+?)(?:.tar.gz|.zip)$/)
raise ArgumentError, "Package should be a valid PyPI URL" if match.blank?
@name ||= PyPI.normalize_python_package match[1]
@extras ||= []
@version ||= match[2]
@name ||= T.let(PyPI.normalize_python_package(T.must(match[1])), T.nilable(String))
@extras ||= T.let([], T.nilable(T::Array[String]))
@version ||= T.let(match[2], T.nilable(String))
elsif @is_url
ensure_formula_installed!(@python_name)
@ -162,9 +163,9 @@ module PyPI
metadata = JSON.parse(pip_output)["install"].first["metadata"]
@name ||= PyPI.normalize_python_package metadata["name"]
@extras ||= []
@version ||= metadata["version"]
@name ||= T.let(PyPI.normalize_python_package(metadata["name"]), T.nilable(String))
@extras ||= T.let([], T.nilable(T::Array[String]))
@version ||= T.let(metadata["version"], T.nilable(String))
else
if @package_string.include? "=="
name, version = @package_string.split("==")
@ -180,7 +181,7 @@ module PyPI
extras = []
end
@name ||= PyPI.normalize_python_package name
@name ||= T.let(PyPI.normalize_python_package(T.must(name)), T.nilable(String))
@extras ||= extras
@version ||= version
end
@ -248,7 +249,7 @@ module PyPI
missing_msg = "formulae required to update \"#{formula.name}\" resources: #{missing_dependencies.join(", ")}"
odie "Missing #{missing_msg}" unless install_dependencies
ohai "Installing #{missing_msg}"
missing_dependencies.each(&method(:ensure_formula_installed!))
missing_dependencies.each(&:ensure_formula_installed!)
end
python_deps = formula.deps
@ -327,12 +328,12 @@ module PyPI
# Resolve the dependency tree of all input packages
show_info = !print_only && !silent
ohai "Retrieving PyPI dependencies for \"#{input_packages.join(" ")}\"..." if show_info
found_packages = pip_report(input_packages, python_name:, print_stderr: verbose && show_info)
found_packages = pip_report(input_packages, python_name:, print_stderr: !!(verbose && show_info))
# Resolve the dependency tree of excluded packages to prune the above
exclude_packages.delete_if { |package| found_packages.exclude? package }
ohai "Retrieving PyPI dependencies for excluded \"#{exclude_packages.join(" ")}\"..." if show_info
exclude_packages = pip_report(exclude_packages, python_name:, print_stderr: verbose && show_info)
exclude_packages += [Package.new(main_package.name)] unless main_package.nil?
exclude_packages = pip_report(exclude_packages, python_name:, print_stderr: !!(verbose && show_info))
exclude_packages += [Package.new(T.must(main_package.name))] unless main_package.nil?
new_resource_blocks = ""
found_packages.sort.each do |package|
@ -404,12 +405,18 @@ module PyPI
true
end
sig { params(name: String).returns(String) }
def self.normalize_python_package(name)
# This normalization is defined in the PyPA packaging specifications;
# https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization
name.gsub(/[-_.]+/, "-").downcase
end
sig {
params(
packages: T::Array[Package], python_name: String, print_stderr: T::Boolean,
).returns(T::Array[Package])
}
def self.pip_report(packages, python_name: "python", print_stderr: false)
return [] if packages.blank?
@ -430,6 +437,7 @@ module PyPI
pip_report_to_packages(JSON.parse(pip_output)).uniq
end
sig { params(report: T::Hash[String, T.untyped]).returns(T::Array[Package]) }
def self.pip_report_to_packages(report)
return [] if report.blank?

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module Utils
@ -98,17 +98,20 @@ module Utils
end
end
SHELL_PROFILE_MAP = {
bash: "~/.profile",
csh: "~/.cshrc",
fish: "~/.config/fish/config.fish",
ksh: "~/.kshrc",
mksh: "~/.kshrc",
rc: "~/.rcrc",
sh: "~/.profile",
tcsh: "~/.tcshrc",
zsh: "~/.zshrc",
}.freeze
SHELL_PROFILE_MAP = T.let(
{
bash: "~/.profile",
csh: "~/.cshrc",
fish: "~/.config/fish/config.fish",
ksh: "~/.kshrc",
mksh: "~/.kshrc",
rc: "~/.rcrc",
sh: "~/.profile",
tcsh: "~/.tcshrc",
zsh: "~/.zshrc",
}.freeze,
T::Hash[T.nilable(Symbol), String],
)
UNSAFE_SHELL_CHAR = %r{([^A-Za-z0-9_\-.,:/@~+\n])}

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "utils/curl"
@ -8,7 +8,7 @@ require "utils/github"
module SPDX
module_function
DATA_PATH = (HOMEBREW_DATA_PATH/"spdx").freeze
DATA_PATH = T.let((HOMEBREW_DATA_PATH/"spdx").freeze, Pathname)
API_URL = "https://api.github.com/repos/spdx/license-list-data/releases/latest"
LICENSEREF_PREFIX = "LicenseRef-Homebrew-"
ALLOWED_LICENSE_SYMBOLS = [
@ -16,24 +16,43 @@ module SPDX
:cannot_represent,
].freeze
sig { returns(T::Hash[String, T.untyped]) }
def license_data
@license_data ||= JSON.parse (DATA_PATH/"spdx_licenses.json").read
@license_data ||= T.let(JSON.parse((DATA_PATH/"spdx_licenses.json").read), T.nilable(T::Hash[String, T.untyped]))
end
sig { returns(T::Hash[String, T.untyped]) }
def exception_data
@exception_data ||= JSON.parse (DATA_PATH/"spdx_exceptions.json").read
@exception_data ||= T.let(JSON.parse((DATA_PATH/"spdx_exceptions.json").read),
T.nilable(T::Hash[String, T.untyped]))
end
sig { returns(String) }
def latest_tag
@latest_tag ||= GitHub::API.open_rest(API_URL)["tag_name"]
@latest_tag ||= T.let(GitHub::API.open_rest(API_URL)["tag_name"], T.nilable(String))
end
sig { params(to: Pathname).void }
def download_latest_license_data!(to: DATA_PATH)
data_url = "https://raw.githubusercontent.com/spdx/license-list-data/#{latest_tag}/json/"
Utils::Curl.curl_download("#{data_url}licenses.json", to: to/"spdx_licenses.json")
Utils::Curl.curl_download("#{data_url}exceptions.json", to: to/"spdx_exceptions.json")
end
sig {
params(
license_expression: T.any(
String,
Symbol,
T::Hash[T.any(Symbol, String), T.untyped],
T::Array[String],
),
).returns(
[
T::Array[T.any(String, Symbol)], T::Array[String]
],
)
}
def parse_license_expression(license_expression)
licenses = T.let([], T::Array[T.any(String, Symbol)])
exceptions = T.let([], T::Array[String])
@ -63,6 +82,7 @@ module SPDX
[licenses, exceptions]
end
sig { params(license: T.any(String, Symbol)).returns(T::Boolean) }
def valid_license?(license)
return ALLOWED_LICENSE_SYMBOLS.include? license if license.is_a? Symbol
@ -70,22 +90,31 @@ module SPDX
license_data["licenses"].any? { |spdx_license| spdx_license["licenseId"] == license }
end
sig { params(license: T.any(String, Symbol)).returns(T::Boolean) }
def deprecated_license?(license)
return false if ALLOWED_LICENSE_SYMBOLS.include? license
return false unless valid_license?(license)
license = license.delete_suffix "+"
license = license.to_s.delete_suffix "+"
license_data["licenses"].none? do |spdx_license|
spdx_license["licenseId"] == license && !spdx_license["isDeprecatedLicenseId"]
end
end
sig { params(exception: String).returns(T::Boolean) }
def valid_license_exception?(exception)
exception_data["exceptions"].any? do |spdx_exception|
spdx_exception["licenseExceptionId"] == exception && !spdx_exception["isDeprecatedLicenseId"]
end
end
sig {
params(
license_expression: T.any(String, Symbol, T::Hash[T.nilable(T.any(Symbol, String)), T.untyped]),
bracket: T::Boolean,
hash_type: T.nilable(T.any(String, Symbol)),
).returns(T.nilable(String))
}
def license_expression_to_string(license_expression, bracket: false, hash_type: nil)
case license_expression
when String
@ -125,6 +154,19 @@ module SPDX
end
end
sig {
params(
string: T.nilable(String),
).returns(
T.nilable(
T.any(
String,
Symbol,
T::Hash[T.any(String, Symbol), T.untyped],
),
),
)
}
def string_to_license_expression(string)
return if string.blank?
@ -162,13 +204,23 @@ module SPDX
end
end
sig {
params(
license: T.any(String, Symbol),
).returns(
T.any(
[T.any(String, Symbol)],
[String, T.nilable(String), T::Boolean],
),
)
}
def license_version_info(license)
return [license] if ALLOWED_LICENSE_SYMBOLS.include? license
match = license.match(/-(?<version>[0-9.]+)(?:-.*?)??(?<or_later>\+|-only|-or-later)?$/)
return [license] if match.blank?
license_name = license.split(match[0]).first
license_name = license.to_s.split(match[0].to_s).first
or_later = match["or_later"].present? && %w[+ -or-later].include?(match["or_later"])
# [name, version, later versions allowed?]
@ -176,12 +228,18 @@ module SPDX
[license_name, match["version"], or_later]
end
sig {
params(license_expression: T.any(String, Symbol, T::Hash[Symbol, T.untyped]),
forbidden_licenses: T::Hash[Symbol, T.untyped]).returns(T::Boolean)
}
def licenses_forbid_installation?(license_expression, forbidden_licenses)
case license_expression
when String, Symbol
forbidden_licenses_include? license_expression.to_s, forbidden_licenses
when Hash
key = license_expression.keys.first
return false if key.nil?
case key
when :any_of
license_expression[key].all? { |license| licenses_forbid_installation? license, forbidden_licenses }
@ -193,6 +251,12 @@ module SPDX
end
end
sig {
params(
license: T.any(Symbol, String),
forbidden_licenses: T::Hash[T.any(Symbol, String), T.untyped],
).returns(T::Boolean)
}
def forbidden_licenses_include?(license, forbidden_licenses)
return true if forbidden_licenses.key? license

View File

@ -1,49 +1,58 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
# Various helper functions for interacting with TTYs.
module Tty
@stream = $stdout
@stream = T.let($stdout, T.nilable(T.any(IO, StringIO)))
COLOR_CODES = {
red: 31,
green: 32,
yellow: 33,
blue: 34,
magenta: 35,
cyan: 36,
default: 39,
}.freeze
COLOR_CODES = T.let(
{
red: 31,
green: 32,
yellow: 33,
blue: 34,
magenta: 35,
cyan: 36,
default: 39,
}.freeze,
T::Hash[Symbol, Integer],
)
STYLE_CODES = {
reset: 0,
bold: 1,
italic: 3,
underline: 4,
strikethrough: 9,
no_underline: 24,
}.freeze
STYLE_CODES = T.let(
{
reset: 0,
bold: 1,
italic: 3,
underline: 4,
strikethrough: 9,
no_underline: 24,
}.freeze,
T::Hash[Symbol, Integer],
)
SPECIAL_CODES = {
up: "1A",
down: "1B",
right: "1C",
left: "1D",
erase_line: "K",
erase_char: "P",
}.freeze
SPECIAL_CODES = T.let(
{
up: "1A",
down: "1B",
right: "1C",
left: "1D",
erase_line: "K",
erase_char: "P",
}.freeze,
T::Hash[Symbol, String],
)
CODES = COLOR_CODES.merge(STYLE_CODES).freeze
CODES = T.let(COLOR_CODES.merge(STYLE_CODES).freeze, T::Hash[Symbol, Integer])
class << self
sig { params(stream: T.any(IO, StringIO), _block: T.proc.params(arg0: T.any(IO, StringIO)).void).void }
def with(stream, &_block)
previous_stream = @stream
@stream = stream
@stream = T.let(stream, T.nilable(T.any(IO, StringIO)))
yield stream
ensure
@stream = previous_stream
@stream = T.let(previous_stream, T.nilable(T.any(IO, StringIO)))
end
sig { params(string: String).returns(String) }
@ -88,17 +97,17 @@ module Tty
height, width = `/bin/stty size 2>/dev/null`.presence&.split&.map(&:to_i)
return if height.nil? || width.nil?
@size = [height, width]
@size = T.let([height, width], T.nilable([Integer, Integer]))
end
sig { returns(Integer) }
def height
@height ||= size&.first || `/usr/bin/tput lines 2>/dev/null`.presence&.to_i || 40
@height ||= T.let(size&.first || `/usr/bin/tput lines 2>/dev/null`.presence&.to_i || 40, T.nilable(Integer))
end
sig { returns(Integer) }
def width
@width ||= size&.second || `/usr/bin/tput cols 2>/dev/null`.presence&.to_i || 80
@width ||= T.let(size&.second || `/usr/bin/tput cols 2>/dev/null`.presence&.to_i || 80, T.nilable(Integer))
end
sig { params(string: String).returns(String) }
@ -115,12 +124,12 @@ module Tty
sig { void }
def reset_escape_sequence!
@escape_sequence = nil
@escape_sequence = T.let(nil, T.nilable(T::Array[Integer]))
end
CODES.each do |name, code|
define_method(name) do
@escape_sequence ||= []
@escape_sequence ||= T.let([], T.nilable(T::Array[Integer]))
@escape_sequence << code
self
end
@ -128,7 +137,8 @@ module Tty
SPECIAL_CODES.each do |name, code|
define_method(name) do
if @stream.tty?
@stream = T.let($stdout, T.nilable(T.any(IO, StringIO)))
if @stream&.tty?
"\033[#{code}"
else
""
@ -152,7 +162,7 @@ module Tty
return false if Homebrew::EnvConfig.no_color?
return true if Homebrew::EnvConfig.color?
@stream.tty?
!!@stream&.tty?
end
end
end