Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable types in Formula files #15057

Merged
merged 3 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 0 additions & 6 deletions Library/Homebrew/extend/object.rbi

This file was deleted.

2 changes: 1 addition & 1 deletion Library/Homebrew/extend/os/mac/formula_cellar_checks.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true

require "cache_store"
Expand Down
18 changes: 9 additions & 9 deletions Library/Homebrew/formula_auditor.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true

require "deprecate_disable"
Expand Down Expand Up @@ -391,7 +391,7 @@ def audit_conflicts
"canonical name (#{conflicting_formula.name}) instead of #{conflict.name}"
end

reverse_conflict_found = false
reverse_conflict_found = T.let(false, T::Boolean)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is necessary to allow the type to widened from FalseClass to T.any(FalseClass, TrueClass) (ruby does not have a native notion of a Boolean type). This specific example is covered in the docs for the ensuing error. It's also touched on in the type annotations section.

Sorry for not elaborating on this PR, there are already ~22 uses of this boolean lvar pattern on master, so I thought it might already be understood.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, don't expect you to preemptively elaborate.

This is one of the things I dislike about Sorbet, it feels weird that it's not more intuitive/permissive on Boolean usage like this.

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, although I would characterize this as a bug in Ruby. (Mats is entrenched his no-Boolean-class position). The second link above gives the rationale for why Sorbet doesn’t special-case true/false values.

conflicting_formula.conflicts.each do |reverse_conflict|
reverse_conflict_formula = Formulary.factory(reverse_conflict.name)
if tap.formula_renames.key?(reverse_conflict.name) || tap.aliases.include?(reverse_conflict.name)
Expand Down Expand Up @@ -732,14 +732,14 @@ def audit_revision_and_version_scheme
current_revision = formula.revision
current_url = formula.stable.url

previous_version = nil
previous_version_scheme = nil
previous_revision = nil
previous_version = T.let(nil, T.nilable(Version))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same deal here: why are these needed?

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same deal as above

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dduugg This isn't a boolean so would still like to try to understand this one?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nil will just be NilClass, T.nilable(Version) is basically T.any(NilClass, Version).

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorbet will by default assign an lvar’s type as the type of the initial assignment, and use that to determine which methods are available and other types of analysis.

Attempting to change the type will thus result an error. This can be allowed, however, if the type is is widened to include all assignments with a T.let statement on initial assignment. Similarly, Ruby has FalseClass and TrueClass and true/falseusually require widening to T::Boolean on initial assignment.

Does that make sense? If this is getting too esoteric, I can pause on the typing PRs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can pause on the typing PRs.

No, all good, they are making the code better overall for sure!

previous_version_scheme = T.let(nil, T.nilable(Integer))
previous_revision = T.let(nil, T.nilable(Integer))

newest_committed_version = nil
newest_committed_checksum = nil
newest_committed_revision = nil
newest_committed_url = nil
newest_committed_version = T.let(nil, T.nilable(Version))
newest_committed_checksum = T.let(nil, T.nilable(String))
newest_committed_revision = T.let(nil, T.nilable(Integer))
newest_committed_url = T.let(nil, T.nilable(String))

fv.rev_list("origin/HEAD") do |rev|
begin
Expand Down
15 changes: 13 additions & 2 deletions Library/Homebrew/formula_cellar_checks.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true

require "utils/shell"
Expand All @@ -7,6 +7,17 @@
#
# @api private
module FormulaCellarChecks
extend T::Sig
extend T::Helpers

abstract!

sig { abstract.returns(Formula) }
def formula; end

sig { abstract.params(output: T.nilable(String)).void }
def problem_if_output(output); end

def check_env_path(bin)
# warn the user if stuff was installed outside of their PATH
return unless bin.directory?
Expand Down Expand Up @@ -407,7 +418,7 @@ def cpuid_instruction?(file, objdump = "objdump")
end
end

has_cpuid_instruction = false
has_cpuid_instruction = T.let(false, T::Boolean)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

Utils.popen_read(objdump, "--disassemble", file) do |io|
until io.eof?
instruction = io.readline.split("\t")[@instruction_column_index[objdump]]&.strip
Expand Down
5 changes: 5 additions & 0 deletions Library/Homebrew/formula_cellar_checks.rbi
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# typed: strict

module FormulaCellarChecks
include Kernel
end
8 changes: 4 additions & 4 deletions Library/Homebrew/formula_installer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true

require "formula"
Expand Down Expand Up @@ -653,7 +653,7 @@ def inherited_options_for(dep)
inherited_options
end

sig { params(deps: T::Array[[Formula, Options]]).void }
sig { params(deps: T::Array[[Dependency, Options]]).void }
def install_dependencies(deps)
if deps.empty? && only_deps?
puts "All dependencies for #{formula.full_name} are satisfied."
Expand Down Expand Up @@ -745,7 +745,7 @@ def install_dependency(dep, inherited_options)
fi.finish
rescue Exception => e # rubocop:disable Lint/RescueException
ignore_interrupts do
tmp_keg.rename(installed_keg) if tmp_keg && !installed_keg.directory?
tmp_keg.rename(installed_keg.to_path) if tmp_keg && !installed_keg.directory?
linked_keg.link(verbose: verbose?) if keg_was_linked
end
raise unless e.is_a? FormulaInstallationAlreadyAttemptedError
Expand Down Expand Up @@ -1266,7 +1266,7 @@ def pour
keg.relocate_build_prefix(keg, prefix, HOMEBREW_PREFIX)
end

sig { params(output: T.nilable(String)).void }
sig { override.params(output: T.nilable(String)).void }
def problem_if_output(output)
return unless output

Expand Down
31 changes: 18 additions & 13 deletions Library/Homebrew/formulary.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true

require "digest/md5"
Expand Down Expand Up @@ -56,7 +56,7 @@ def self.clear_cache
namespace = Utils.deconstantize(klass.name)
next if Utils.deconstantize(namespace) != name

remove_const(Utils.demodulize(namespace))
remove_const(Utils.demodulize(namespace).to_sym)
end
end

Expand All @@ -67,6 +67,7 @@ def self.clear_cache
module PathnameWriteMkpath
refine Pathname do
def write(content, offset = nil, **open_args)
T.bind(self, Pathname)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this doing?

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorbet doesn't support refinements, so we need to explicitly tell sorbet that this def block is executed under the Pathname class. Otherwise, it won't be able to locate definitions for exist? or dirname within the block.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

raise "Will not overwrite #{self}" if exist? && !offset && !open_args[:mode]&.match?(/^a\+?$/)

dirname.mkpath
Expand Down Expand Up @@ -129,7 +130,7 @@ def self.load_formula_from_path(name, path, flags:, ignore_errors:)
end

def self.load_formula_from_api(name, flags:)
namespace = "FormulaNamespaceAPI#{Digest::MD5.hexdigest(name)}"
namespace = :"FormulaNamespaceAPI#{Digest::MD5.hexdigest(name)}"

mod = Module.new
remove_const(namespace) if const_defined?(namespace)
Expand Down Expand Up @@ -268,6 +269,7 @@ def install
service_hash = Homebrew::Service.deserialize(service_hash)
run_params = service_hash.delete(:run)
service do
T.bind(self, Homebrew::Service)
if run_params.is_a?(Hash)
run(**run_params)
else
Expand Down Expand Up @@ -306,7 +308,7 @@ def versioned_formulae_names
end
end

klass.loaded_from_api = true
T.cast(klass, T.class_of(Formula)).loaded_from_api = true
reitermarkus marked this conversation as resolved.
Show resolved Hide resolved
mod.const_set(class_s, klass)

cache[:api] ||= {}
Expand Down Expand Up @@ -351,7 +353,7 @@ def self.ensure_utf8_encoding(io)

def self.class_s(name)
class_name = name.capitalize
class_name.gsub!(/[-_.\s]([a-zA-Z0-9])/) { Regexp.last_match(1).upcase }
class_name.gsub!(/[-_.\s]([a-zA-Z0-9])/) { T.must(Regexp.last_match(1)).upcase }
class_name.tr!("+", "x")
class_name.sub!(/(.)@(\d)/, "\\1AT\\2")
class_name
Expand Down Expand Up @@ -489,17 +491,20 @@ class FromUrlLoader < FormulaLoader
def initialize(url, from: nil)
@url = url
@from = from
uri = URI(url)
formula = File.basename(uri.path, ".rb")
super formula, HOMEBREW_CACHE_FORMULA/File.basename(uri.path)
uri_path = URI(url).path
raise ArgumentError, "URL has no path component" unless uri_path

formula = File.basename(uri_path, ".rb")
super formula, HOMEBREW_CACHE_FORMULA/File.basename(uri_path)
end

def load_file(flags:, ignore_errors:)
if @from != :formula_installer
if %r{githubusercontent.com/[\w-]+/[\w-]+/[a-f0-9]{40}(?:/Formula)?/(?<formula_name>[\w+-.@]+).rb} =~ url
match = url.match(%r{githubusercontent.com/[\w-]+/[\w-]+/[a-f0-9]{40}(?:/Formula)?/(?<name>[\w+-.@]+).rb})
if match
raise UnsupportedInstallationMethod,
"Installation of #{formula_name} from a GitHub commit URL is unsupported! " \
"`brew extract #{formula_name}` to a stable tap on GitHub instead."
"Installation of #{match[:name]} from a GitHub commit URL is unsupported! " \
"`brew extract #{match[:name]}` to a stable tap on GitHub instead."
elsif url.match?(%r{^(https?|ftp)://})
raise UnsupportedInstallationMethod,
"Non-checksummed download of #{name} formula file from an arbitrary URL is unsupported! " \
Expand All @@ -512,8 +517,8 @@ def load_file(flags:, ignore_errors:)
curl_download url, to: path
super
rescue MethodDeprecatedError => e
if %r{github.com/(?<user>[\w-]+)/(?<repo>[\w-]+)/} =~ url
e.issues_url = "https://github.com/#{user}/#{repo}/issues/new"
if (match_data = url.match(%r{github.com/(?<user>[\w-]+)/(?<repo>[\w-]+)/}))
e.issues_url = "https://github.com/#{match_data[:user]}/#{match_data[:repo]}/issues/new"
end
raise
end
Expand Down
5 changes: 5 additions & 0 deletions Library/Homebrew/formulary.rbi
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# typed: strict

module Formulary
include Kernel
end
1 change: 1 addition & 0 deletions Library/Homebrew/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Resource
# formula name before initialization of the formula.
attr_accessor :name

sig { params(name: T.nilable(String), block: T.nilable(T.proc.bind(Resource).void)).void }
def initialize(name = nil, &block)
# Ensure this is synced with `initialize_dup` and `freeze` (excluding simple objects like integers and booleans)
@name = name
Expand Down