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

Missing T::Struct options that impact type checking #122

Closed
wants to merge 2 commits into from

Conversation

jeffcarbs
Copy link
Contributor

@jeffcarbs jeffcarbs commented Feb 26, 2022

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 as default 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 the RBI::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 for default, when I realized that there's at least one more option that affects type checking: immutable. If you set prop :x, Integer, immutable: true it basically acts like const where it will show a type error if you try to use the setter. I also realized that the current parsing logic expects default (if present at all) to be the first option so if you have immutable: 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 and immutable since those impact type checking, so there are a few options:

  • add factory and immutable attrs to TStructField (and const/prop) and handle them whenever we currently handle default.
    • Slightly more verbose but very explicit
    • We'll drop other options on the floor. This is probably fine but it would mean that input == (parse(input).out) is no longer necessarily true.
  • remove default from TStructField and replace it with options which would capture all options
    • More flexible but less explicit
  • just add factory, and when we see “immutable: true” parse it as a Const instead of a Prop.

There's also a middle ground where the constructor for TStructField and friends still only take default (and maybe factory and immutable as well?) as an explicit option since it's the most common but also take an options hash. The initializer would join the two so you'd only ever operate on the hash. This would be similar to how Attr 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.

@jeffcarbs
Copy link
Contributor Author

jeffcarbs commented Feb 26, 2022

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.

Copy link
Collaborator

@Morriar Morriar left a 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)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's rename this:

Suggested change
name, type, options = parse_tstruct_prop(node)
name, type, options = parse_tstruct_field(node)

Copy link
Collaborator

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

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:

Suggested change
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}")
Copy link
Collaborator

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: [])
Copy link
Collaborator

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.

@jeffcarbs
Copy link
Contributor Author

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.

@jeffcarbs jeffcarbs closed this Apr 5, 2022
@jeffcarbs jeffcarbs deleted the tstruct-factory branch April 5, 2022 04:59
@Morriar Morriar added the bugfix Fix a bug label Mar 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bugfix Fix a bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants