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

Conversation

dduugg
Copy link
Sponsor Member

@dduugg dduugg commented Mar 25, 2023

  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't other open Pull Requests for the same change?
  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes? Here's an example.
  • Have you successfully run brew style with your changes locally?
  • Have you successfully run brew typecheck with your changes locally?
  • Have you successfully run 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)

Library/Homebrew/extend/object.rbi Outdated Show resolved Hide resolved
Library/Homebrew/formulary.rb Outdated Show resolved Hide resolved
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)
Copy link
Member

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.

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.

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.)

Copy link
Member

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

Copy link
Member

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.

Copy link
Sponsor Member Author

@dduugg dduugg Mar 26, 2023

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.)

Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed with @reitermarkus here

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))
Copy link
Member

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.

Copy link
Member

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.

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.

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).

Copy link
Member

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.

@dduugg
Copy link
Sponsor Member Author

dduugg commented Mar 26, 2023

PTAL @reitermarkus

@dduugg dduugg mentioned this pull request Mar 26, 2023
7 tasks
Copy link
Member

@MikeMcQuaid MikeMcQuaid left a 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)
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.

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!

@@ -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

@@ -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!

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}))
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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

Suggested change
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}))

@dduugg
Copy link
Sponsor Member Author

dduugg commented Mar 27, 2023

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)
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.

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.

@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)
Copy link
Member

Choose a reason for hiding this comment

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

Thanks!

@MikeMcQuaid MikeMcQuaid merged commit b97db86 into Homebrew:master Mar 28, 2023
22 checks passed
@MikeMcQuaid
Copy link
Member

Thanks again @dduugg! One question for post-merge that wasn't blocking.

@github-actions github-actions bot added the outdated PR was locked due to age label Apr 28, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 28, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
outdated PR was locked due to age
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants