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
Conversation
Library/Homebrew/formulary.rb
Outdated
uri = URI(url) | ||
formula = File.basename(uri.path, ".rb") | ||
super formula, HOMEBREW_CACHE_FORMULA/File.basename(uri.path) | ||
uri_path = T.must(URI(url).path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should just be an ArgumentError
instead. The path
is only guaranteed to be present since we only pass specific URLs to this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could I trouble you to be more specific in how you would like this to look? I'm not following where you want to raise an ArgumentError
. (The T.must
will raise a TypeError
here on a falsy path
, which I think is the correct error type, but also not something worth splitting hairs over.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like
raise ArgumentError, "URL has no path component" unless uri.path
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll probably never hit this, but semantically an argument error makes more sense to me here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorbet (rightfully) doesn't assume successive invocations of the same function will return the same type. so this would actually need to be something like:
uri_path = uri.path
raise ArgumentError, "URL has no path component" unless uri_path
# code that uses non-nil uri_path
which is a lot of code just to return a specific error type, even if I agreed with it. (Which I'm still not sold on – if this is not a TypeError
, I'm not sure what would be.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Semantically, it's an argument error caused by invalid input (i.e. a URL without path component), which in turn leads to a type error if left unhandled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed with @reitermarkus here
Library/Homebrew/formulary.rb
Outdated
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 | ||
if (md = %r{githubusercontent.com/[\w-]+/[\w-]+/[a-f0-9]{40}(?:/Formula)?/(?<name>[\w+-.@]+).rb}.match(url)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we usually use match
instead of md
or match_data
. May make sense to check for other outliers to make this more consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also flip the order here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can switch the order. The md
here is an exception for readability, as otherwise this needs to span multiple lines. The regexp should arguably be a const anyway, but I try to minimize the refactoring not directly related to enabling typing (which is also why I preserve order by default).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's please try to avoid using single or double letter abbreviations like this that make things harder to read. match
or match_data` are preferable.
PTAL @reitermarkus |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, nice work so far!
@@ -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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
previous_version = nil | ||
previous_version_scheme = nil | ||
previous_revision = nil | ||
previous_version = T.let(nil, T.nilable(Version)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same deal as above
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
.
There was a problem hiding this comment.
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
/false
usually 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.
There was a problem hiding this comment.
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!
@@ -407,7 +418,7 @@ def cpuid_instruction?(file, objdump = "objdump") | |||
end | |||
end | |||
|
|||
has_cpuid_instruction = false | |||
has_cpuid_instruction = T.let(false, T::Boolean) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same
@@ -67,6 +67,7 @@ def self.clear_cache | |||
module PathnameWriteMkpath | |||
refine Pathname do | |||
def write(content, offset = nil, **open_args) | |||
T.bind(self, Pathname) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this doing?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
Library/Homebrew/formulary.rb
Outdated
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 | ||
if (md = url.match(%r{githubusercontent.com/[\w-]+/[\w-]+/[a-f0-9]{40}(?:/Formula)?/(?<name>[\w+-.@]+).rb})) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (md = url.match(%r{githubusercontent.com/[\w-]+/[\w-]+/[a-f0-9]{40}(?:/Formula)?/(?<name>[\w+-.@]+).rb})) | |
if (match = url.match(%r{githubusercontent.com/[\w-]+/[\w-]+/[a-f0-9]{40}(?:/Formula)?/(?<name>[\w+-.@]+).rb})) |
or
if (md = url.match(%r{githubusercontent.com/[\w-]+/[\w-]+/[a-f0-9]{40}(?:/Formula)?/(?<name>[\w+-.@]+).rb})) | |
if (match_data = url.match(%r{githubusercontent.com/[\w-]+/[\w-]+/[a-f0-9]{40}(?:/Formula)?/(?<name>[\w+-.@]+).rb})) |
PTAL @MikeMcQuaid |
@@ -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) |
There was a problem hiding this comment.
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.
previous_version = nil | ||
previous_version_scheme = nil | ||
previous_revision = nil | ||
previous_version = T.let(nil, T.nilable(Version)) |
There was a problem hiding this comment.
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?
@@ -67,6 +67,7 @@ def self.clear_cache | |||
module PathnameWriteMkpath | |||
refine Pathname do | |||
def write(content, offset = nil, **open_args) | |||
T.bind(self, Pathname) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
Thanks again @dduugg! One question for post-merge that wasn't blocking. |
brew style
with your changes locally?brew typecheck
with your changes locally?brew tests
with your changes locally?Enables typing in files relating to
Formula
. (After this change there are ~26 non-test files with typing disabled remaining)