Enable strict typing in Cask::Audit

This commit is contained in:
Douglas Eichelberger 2025-07-28 20:36:43 -07:00 committed by Douglas Eichelberger
parent 00c528bc54
commit 157992be17
No known key found for this signature in database
3 changed files with 69 additions and 31 deletions

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "cask/denylist" require "cask/denylist"
@ -19,6 +19,14 @@ module Cask
include SystemCommand::Mixin include SystemCommand::Mixin
include ::Utils::Curl include ::Utils::Curl
Error = T.type_alias do
{
message: T.nilable(String),
location: T.nilable(Homebrew::SourceLocation),
corrected: T::Boolean,
}
end
sig { returns(Cask) } sig { returns(Cask) }
attr_reader :cask attr_reader :cask
@ -47,6 +55,7 @@ module Cask
download ||= online || signing download ||= online || signing
@cask = cask @cask = cask
@download = T.let(nil, T.nilable(Download))
@download = Download.new(cask, quarantine:) if download @download = Download.new(cask, quarantine:) if download
@online = online @online = online
@strict = strict @strict = strict
@ -88,8 +97,9 @@ module Cask
self self
end end
sig { returns(T::Array[Error]) }
def errors def errors
@errors ||= [] @errors ||= T.let([], T.nilable(T::Array[Error]))
end end
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
@ -113,9 +123,10 @@ module Cask
# Only raise non-critical audits if the user specified `--strict`. # Only raise non-critical audits if the user specified `--strict`.
return if strict_only && !@strict return if strict_only && !@strict
errors << ({ message:, location:, corrected: false }) errors << { message:, location:, corrected: false }
end end
sig { returns(T.nilable(String)) }
def result def result
Formatter.error("failed") if errors? Formatter.error("failed") if errors?
end end
@ -346,6 +357,7 @@ module Cask
location: url.location location: url.location
end end
sig { void }
def audit_download_url_is_osdn def audit_download_url_is_osdn
return if (url = cask.url).nil? return if (url = cask.url).nil?
return if block_url_offline? return if block_url_offline?
@ -541,8 +553,15 @@ module Cask
end end
end end
sig { void } sig {
def extract_artifacts params(
_block: T.nilable(T.proc.params(
arg0: T::Array[T.any(Artifact::Pkg, Artifact::Relocated)],
arg1: Pathname,
).void),
).void
}
def extract_artifacts(&_block)
return unless online? return unless online?
return if (download = self.download).nil? return if (download = self.download).nil?
@ -557,7 +576,7 @@ module Cask
return if artifacts.empty? return if artifacts.empty?
@tmpdir ||= Pathname(Dir.mktmpdir("cask-audit", HOMEBREW_TEMP)) @tmpdir ||= T.let(Pathname(Dir.mktmpdir("cask-audit", HOMEBREW_TEMP)), T.nilable(Pathname))
# Clean up tmp dir when @tmpdir object is destroyed # Clean up tmp dir when @tmpdir object is destroyed
ObjectSpace.define_finalizer( ObjectSpace.define_finalizer(
@ -606,7 +625,8 @@ module Cask
.extract_nestedly(to: @tmpdir, verbose: false) .extract_nestedly(to: @tmpdir, verbose: false)
end end
@artifacts_extracted = true # Set the flag to indicate that extraction has occurred. # Set the flag to indicate that extraction has occurred.
@artifacts_extracted = T.let(true, T.nilable(TrueClass))
# Yield the artifacts and temp directory to the block if provided. # Yield the artifacts and temp directory to the block if provided.
yield artifacts, @tmpdir if block_given? yield artifacts, @tmpdir if block_given?
@ -626,8 +646,8 @@ module Cask
extract_artifacts do |artifacts, tmpdir| extract_artifacts do |artifacts, tmpdir|
is_container = artifacts.any? { |a| a.is_a?(Artifact::App) || a.is_a?(Artifact::Pkg) } is_container = artifacts.any? { |a| a.is_a?(Artifact::App) || a.is_a?(Artifact::Pkg) }
artifacts.filter { |a| a.is_a?(Artifact::App) || a.is_a?(Artifact::Binary) } artifacts.each do |artifact|
.each do |artifact| next if !artifact.is_a?(Artifact::App) && !artifact.is_a?(Artifact::Binary)
next if artifact.is_a?(Artifact::Binary) && is_container next if artifact.is_a?(Artifact::Binary) && is_container
path = tmpdir/artifact.source.relative_path_from(cask.staged_path) path = tmpdir/artifact.source.relative_path_from(cask.staged_path)
@ -644,10 +664,10 @@ module Cask
system_command("lipo", args: ["-archs", main_binary], print_stderr: false) system_command("lipo", args: ["-archs", main_binary], print_stderr: false)
when Artifact::Binary when Artifact::Binary
binary_path = path.to_s.gsub(cask.appdir, tmpdir) binary_path = path.to_s.gsub(cask.appdir, tmpdir.to_s)
system_command("lipo", args: ["-archs", binary_path], print_stderr: true) system_command("lipo", args: ["-archs", binary_path], print_stderr: true)
else else
add_error "Unknown artifact type: #{artifact.class}", location: url.location T.absurd(artifact)
end end
# binary stanza can contain shell scripts, so we just continue if lipo fails. # binary stanza can contain shell scripts, so we just continue if lipo fails.
@ -795,7 +815,7 @@ module Cask
return unless online? return unless online?
min_os = T.let(nil, T.untyped) min_os = T.let(nil, T.untyped)
@staged_path ||= cask.staged_path @staged_path ||= T.let(cask.staged_path, T.nilable(Pathname))
extract_artifacts do |artifacts, tmpdir| extract_artifacts do |artifacts, tmpdir|
artifacts.each do |artifact| artifacts.each do |artifact|
@ -1104,15 +1124,15 @@ module Cask
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def bad_osdn_url? def bad_osdn_url?
domain.match?(%r{^(?:\w+\.)*osdn\.jp(?=/|$)}) T.must(domain).match?(%r{^(?:\w+\.)*osdn\.jp(?=/|$)})
end end
# sig { returns(String) } sig { returns(T.nilable(String)) }
def homepage def homepage
URI(cask.homepage.to_s).host URI(cask.homepage.to_s).host
end end
# sig { returns(String) } sig { returns(T.nilable(String)) }
def domain def domain
URI(cask.url.to_s).host URI(cask.url.to_s).host
end end
@ -1127,24 +1147,25 @@ module Cask
host_uri.host host_uri.host
end end
return false if homepage.blank? home = homepage
return false if home.blank?
home = homepage.downcase home.downcase!
if (split_host = T.must(host).split(".")).length >= 3 if (split_host = T.must(host).split(".")).length >= 3
host = T.must(split_host[-2..]).join(".") host = T.must(split_host[-2..]).join(".")
end end
if (split_home = homepage.split(".")).length >= 3 if (split_home = home.split(".")).length >= 3
home = split_home[-2..].join(".") home = T.must(split_home[-2..]).join(".")
end end
host == home host == home
end end
# sig { params(url: String).returns(String) } sig { params(url: String).returns(String) }
def strip_url_scheme(url) def strip_url_scheme(url)
url.sub(%r{^[^:/]+://(www\.)?}, "") url.sub(%r{^[^:/]+://(www\.)?}, "")
end end
# sig { returns(String) } sig { returns(String) }
def url_from_verified def url_from_verified
strip_url_scheme(T.must(cask.url).verified) strip_url_scheme(T.must(cask.url).verified)
end end
@ -1154,8 +1175,10 @@ module Cask
url_domain, url_path = strip_url_scheme(cask.url.to_s).split("/", 2) url_domain, url_path = strip_url_scheme(cask.url.to_s).split("/", 2)
verified_domain, verified_path = url_from_verified.split("/", 2) verified_domain, verified_path = url_from_verified.split("/", 2)
(url_domain == verified_domain || (verified_domain && url_domain&.end_with?(".#{verified_domain}"))) && domains_match = (url_domain == verified_domain) ||
(!verified_path || url_path&.start_with?(verified_path)) (verified_domain && url_domain&.end_with?(".#{verified_domain}"))
paths_match = !verified_path || url_path&.start_with?(verified_path)
(domains_match && paths_match) || false
end end
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
@ -1177,10 +1200,10 @@ module Cask
sig { returns(Tap) } sig { returns(Tap) }
def core_tap def core_tap
@core_tap ||= CoreTap.instance @core_tap ||= T.let(CoreTap.instance, T.nilable(Tap))
end end
# sig { returns(T::Array[String]) } sig { returns(T::Array[String]) }
def core_formula_names def core_formula_names
core_tap.formula_names core_tap.formula_names
end end

View File

@ -445,7 +445,7 @@ module Homebrew
.gsub(/`(.*?)`/m, "#{Tty.bold}\\1#{Tty.reset}") .gsub(/`(.*?)`/m, "#{Tty.bold}\\1#{Tty.reset}")
.gsub(%r{<([^\s]+?://[^\s]+?)>}) { |url| Formatter.url(url) } .gsub(%r{<([^\s]+?://[^\s]+?)>}) { |url| Formatter.url(url) }
.gsub(/\*(.*?)\*|<(.*?)>/m) do |underlined| .gsub(/\*(.*?)\*|<(.*?)>/m) do |underlined|
underlined[1...-1].gsub(/^(\s*)(.*?)$/, "\\1#{Tty.underline}\\2#{Tty.reset}") T.must(underlined[1...-1]).gsub(/^(\s*)(.*?)$/, "\\1#{Tty.underline}\\2#{Tty.reset}")
end end
end end

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "utils/tty" require "utils/tty"
@ -10,6 +10,7 @@ module Formatter
COMMAND_DESC_WIDTH = 80 COMMAND_DESC_WIDTH = 80
OPTION_DESC_WIDTH = 45 OPTION_DESC_WIDTH = 45
sig { params(string: String, color: T.nilable(Symbol)).returns(String) }
def self.arrow(string, color: nil) def self.arrow(string, color: nil)
prefix("==>", string, color) prefix("==>", string, color)
end end
@ -17,18 +18,22 @@ module Formatter
# Format a string as headline. # Format a string as headline.
# #
# @api internal # @api internal
sig { params(string: String, color: T.nilable(Symbol)).returns(String) }
def self.headline(string, color: nil) def self.headline(string, color: nil)
arrow("#{Tty.bold}#{string}#{Tty.reset}", color:) arrow("#{Tty.bold}#{string}#{Tty.reset}", color:)
end end
sig { params(string: Object).returns(String) }
def self.identifier(string) def self.identifier(string)
"#{Tty.green}#{string}#{Tty.default}" "#{Tty.green}#{string}#{Tty.default}"
end end
sig { params(string: String).returns(String) }
def self.bold(string) def self.bold(string)
"#{Tty.bold}#{string}#{Tty.reset}" "#{Tty.bold}#{string}#{Tty.reset}"
end end
sig { params(string: String).returns(String) }
def self.option(string) def self.option(string)
bold(string) bold(string)
end end
@ -36,6 +41,7 @@ module Formatter
# Format a string as success, with an optional label. # Format a string as success, with an optional label.
# #
# @api internal # @api internal
sig { params(string: String, label: T.nilable(String)).returns(String) }
def self.success(string, label: nil) def self.success(string, label: nil)
label(label, string, :green) label(label, string, :green)
end end
@ -43,6 +49,7 @@ module Formatter
# Format a string as warning, with an optional label. # Format a string as warning, with an optional label.
# #
# @api internal # @api internal
sig { params(string: T.any(String, Exception), label: T.nilable(String)).returns(String) }
def self.warning(string, label: nil) def self.warning(string, label: nil)
label(label, string, :yellow) label(label, string, :yellow)
end end
@ -50,6 +57,7 @@ module Formatter
# Format a string as error, with an optional label. # Format a string as error, with an optional label.
# #
# @api internal # @api internal
sig { params(string: T.any(String, Exception), label: T.nilable(String)).returns(String) }
def self.error(string, label: nil) def self.error(string, label: nil)
label(label, string, :red) label(label, string, :red)
end end
@ -80,6 +88,7 @@ module Formatter
# so we always wrap one word before an option. # so we always wrap one word before an option.
# @see https://github.com/Homebrew/brew/pull/12672 # @see https://github.com/Homebrew/brew/pull/12672
# @see https://macromates.com/blog/2006/wrapping-text-with-regular-expressions/ # @see https://macromates.com/blog/2006/wrapping-text-with-regular-expressions/
sig { params(string: String, width: Integer).returns(String) }
def self.format_help_text(string, width: 172) def self.format_help_text(string, width: 172)
desc = OPTION_DESC_WIDTH desc = OPTION_DESC_WIDTH
indent = width - desc indent = width - desc
@ -90,21 +99,26 @@ module Formatter
.gsub(/(.{1,#{width}})( +|$)(?!-)\n?/, "\\1\n") .gsub(/(.{1,#{width}})( +|$)(?!-)\n?/, "\\1\n")
end end
sig { params(string: T.any(NilClass, String, URI::Generic)).returns(String) }
def self.url(string) def self.url(string)
"#{Tty.underline}#{string}#{Tty.no_underline}" "#{Tty.underline}#{string}#{Tty.no_underline}"
end end
sig { params(label: T.nilable(String), string: T.any(String, Exception), color: Symbol).returns(String) }
def self.label(label, string, color) def self.label(label, string, color)
label = "#{label}:" unless label.nil? label = "#{label}:" unless label.nil?
prefix(label, string, color) prefix(label, string, color)
end end
private_class_method :label private_class_method :label
sig {
params(prefix: T.nilable(String), string: T.any(String, Exception), color: T.nilable(Symbol)).returns(String)
}
def self.prefix(prefix, string, color) def self.prefix(prefix, string, color)
if prefix.nil? && color.nil? if prefix.nil? && color.nil?
string string.to_s
elsif prefix.nil? elsif prefix.nil?
"#{Tty.send(color)}#{string}#{Tty.reset}" "#{Tty.send(T.must(color))}#{string}#{Tty.reset}"
elsif color.nil? elsif color.nil?
"#{prefix} #{string}" "#{prefix} #{string}"
else else
@ -116,7 +130,8 @@ module Formatter
# Layout objects in columns that fit the current terminal width. # Layout objects in columns that fit the current terminal width.
# #
# @api internal # @api internal
def self.columns(*objects, gap_size: 2) sig { params(objects: T::Array[String], gap_size: Integer).returns(String) }
def self.columns(objects, gap_size: 2)
objects = objects.flatten.map(&:to_s) objects = objects.flatten.map(&:to_s)
fallback = proc do fallback = proc do
@ -145,7 +160,7 @@ module Formatter
item_indices_for_row = T.cast(row_index.step(objects.size - 1, rows).to_a, T::Array[Integer]) item_indices_for_row = T.cast(row_index.step(objects.size - 1, rows).to_a, T::Array[Integer])
first_n = T.must(item_indices_for_row[0...-1]).map do |index| first_n = T.must(item_indices_for_row[0...-1]).map do |index|
objects[index] + "".rjust(col_width - object_lengths.fetch(index)) objects.fetch(index) + "".rjust(col_width - object_lengths.fetch(index))
end end
# don't add trailing whitespace to last column # don't add trailing whitespace to last column