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
Missing T::Struct options that impact type checking #122
Conversation
I made an attempt at the second approach (using an “options” hash). The one spec failing is because of how we output procs not matching the source proc, which I can look into. |
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 for taking a look at this 🙏
I like the approach, my only concern is the change in the constructor signature for TStructField
which will be breaking compatibility.
@@ -314,11 +314,11 @@ def parse_send(node) | |||
raise ParseError.new("Unexpected token `private` before `#{nested_node.type}`", loc) | |||
end | |||
when :prop | |||
name, type, default_value = parse_tstruct_prop(node) | |||
TStructProp.new(name, type, default: default_value, loc: loc, comments: comments) | |||
name, type, options = parse_tstruct_prop(node) |
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 rename this:
name, type, options = parse_tstruct_prop(node) | |
name, type, options = parse_tstruct_field(node) |
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 merge the two cases:
when :prop, :const
parse_tstruct_field(method_name, node)
else
# ...
@@ -403,20 +403,21 @@ def parse_struct(node) | |||
struct | |||
end | |||
|
|||
sig { params(node: AST::Node).returns([String, String, T.nilable(String)]) } | |||
sig { params(node: AST::Node).returns([String, String, T::Hash[Symbol, String]]) } |
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.
Maybe we can return the TStructField
instance directly:
sig { params(node: AST::Node).returns([String, String, T::Hash[Symbol, String]]) } | |
sig { params(node: AST::Node).returns(TStructField) } |
default = self.default | ||
v.print(", default: #{default}") if default | ||
options.each do |key, value| | ||
v.print(", #{key}: #{value}") |
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 need to add some magic here to properly print the value when it's a proc.
loc: T.nilable(Loc), | ||
comments: T::Array[Comment] | ||
).void | ||
end | ||
def initialize(name, type, default: nil, loc: nil, comments: []) | ||
def initialize(name, type, options: {}, loc: nil, comments: []) |
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 would rather add the new options to the constructor to avoid a breaking change that would require updating all the Tapioca's custom compilers.
I wound up fixing this with a lighter touch directly in tapioca (Shopify/tapioca#833) and don't have time to pick this PR back up so going to close it out. |
We started using the
factory
option for a field in one of our T::Structs and noticed it didn't come through the RBI that tapioca generated.factory
is basically the same asdefault
but takes a proc so the value can be set dynamically. Like default, if a prop/const has a factory set you don't have to pass a value in the initializer so its absence from the RBI meaningfully impacts type-checking. Given that tapioca uses theRBI::TStructConst/Prop
models, support needs to be added here first.I started down the path of a fix, adding handling for
factory
where there's handling fordefault
, when I realized that there's at least one more option that affects type checking:immutable
. If you setprop :x, Integer, immutable: true
it basically acts likeconst
where it will show a type error if you try to use the setter. I also realized that the current parsing logic expectsdefault
(if present at all) to be the first option so if you haveimmutable: true, default: 1
the parser will currently drop both on the floor.Here's a sorbet.run to showing these examples.
There are many more options (although many seem to be internal to stripe) and technically you can pass arbitrary options.
Before I went further I wanted to get your thoughts. I think we at least need to handle
factory
andimmutable
since those impact type checking, so there are a few options:factory
andimmutable
attrs toTStructField
(and const/prop) and handle them whenever we currently handledefault
.input == (parse(input).out)
is no longer necessarily true.default
fromTStructField
and replace it withoptions
which would capture all optionsThere's also a middle ground where the constructor for
TStructField
and friends still only takedefault
(and maybefactory
andimmutable
as well?) as an explicit option since it's the most common but also take anoptions
hash. The initializer would join the two so you'd only ever operate on the hash. This would be similar to howAttr
takes (name, names) and then joins them together in the initializer.I'm happy to take this on here and in tapioca so let me know what you think.