diff --git a/Library/Homebrew/utils/analytics.rb b/Library/Homebrew/utils/analytics.rb index f7fb2e6564..52393ecd54 100644 --- a/Library/Homebrew/utils/analytics.rb +++ b/Library/Homebrew/utils/analytics.rb @@ -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("%.2f", percent:) end diff --git a/Library/Homebrew/utils/fork.rb b/Library/Homebrew/utils/fork.rb index 8c19ac282f..5da2621622 100644 --- a/Library/Homebrew/utils/fork.rb +++ b/Library/Homebrew/utils/fork.rb @@ -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 diff --git a/Library/Homebrew/utils/pypi.rb b/Library/Homebrew/utils/pypi.rb index 6432d8b8c4..9b0940ef2a 100644 --- a/Library/Homebrew/utils/pypi.rb +++ b/Library/Homebrew/utils/pypi.rb @@ -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? diff --git a/Library/Homebrew/utils/shell.rb b/Library/Homebrew/utils/shell.rb index a621fbff8d..f5b3424a49 100644 --- a/Library/Homebrew/utils/shell.rb +++ b/Library/Homebrew/utils/shell.rb @@ -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])} diff --git a/Library/Homebrew/utils/spdx.rb b/Library/Homebrew/utils/spdx.rb index 6ecf406573..bf45bf377c 100644 --- a/Library/Homebrew/utils/spdx.rb +++ b/Library/Homebrew/utils/spdx.rb @@ -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(/-(?[0-9.]+)(?:-.*?)??(?\+|-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 diff --git a/Library/Homebrew/utils/tty.rb b/Library/Homebrew/utils/tty.rb index a539e37b5b..e033f85111 100644 --- a/Library/Homebrew/utils/tty.rb +++ b/Library/Homebrew/utils/tty.rb @@ -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