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

Add support for functors in JSX tags #1927

Merged
merged 9 commits into from
May 17, 2018
Merged

Conversation

OhadRau
Copy link
Contributor

@OhadRau OhadRau commented May 15, 2018

Fixes #1924

This PR enables the use of functors in JSX tags. It may also alter some edge cases in the syntax (though these would not compile properly in the past) -- previously, <M./> would parse correctly but fail to bind to a module; in certain cases, this will still parse but it should result in an error earlier in the pipeline (indicating a syntax error). This PR allows for the following new syntax:

<F(X)/>;
<F(X)> </F>;
// <F(X)> </F(X)>; is not currently supported, but this would be an easy addition

The syntax is designed to make it clear that the functor application only occurs once, but again it is a relatively easy change to add support for (or even require) a matching end tag.

Internally, the transformation is done by creating a let module expression to temporarily bind the result of the applied functor and then calling the createElement function on that result. Some more info, as well as an example use-case, is show in #1924.

@facebook-github-bot
Copy link

Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file. In order for us to review and merge your code, please sign up at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need the corporate CLA signed.

If you have received this in error or have any questions, please contact us at [email protected]. Thanks!

@facebook-github-bot
Copy link

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!

@cristianoc
Copy link
Contributor

Could you check what happens with the use case

<Foo.Bar/>

That's where the . character is expected to be used.
Perhaps there isn't a test for that use at the moment, and it should be added.

@OhadRau
Copy link
Contributor Author

OhadRau commented May 15, 2018

Yeah, that seems to still work. To clarify, it should only affect the case with trailing dots, since I'm basically allowing for an arbitrary Longident (rather than only the Lident and Ldot constructors) for the capitalized case. However, the <M.x/> case doesn't work with this change--is this something that's intended to work? (Update: I've pushed a commit that should fix that specific case hopefully)

@@ -532,7 +532,7 @@ rule token = parse
| "[|" { LBRACKETBAR }
| "[<" { LBRACKETLESS }
| "[>" { LBRACKETGREATER }
| "<" uppercase_or_lowercase (identchar | '.') * {
| "<" (uppercase identchar* '.')* lowercase identchar* {
Copy link
Contributor

Choose a reason for hiding this comment

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

To double check: the main change here is uppercase_or_lowercase to lowercase, plus some . cleanup correct?

The cleanup can be applied to the two cases below too, using I guess
(uppercase identchar* '.')* uppercase_or_lowercase identchar*

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. The change to lowercase as the main pattern is to make it only parse lowercase tags (e.g. <div>) as LESSIDENT. The parser implements the uppercase (opening) tags as a different pattern without LESSIDENT. The . cleanup was specifically added to allow tags like <X.x>, but doesn't support <X.Y>. I could add this to the other cases, but they work simply by resetting the lexer to the beginning of the pattern, consuming only up to the < token. I assume that the current patterns will run slightly faster, and should work correctly at the parsing level. That said, it may make more sense to switch to (uppercase identchar* '.')* lowercase identchar* and (uppercase identchar* '.')* uppercase identchar* for the other patterns to make them look more similar and make the patterns more accurate.

let createElement = X.createElement;
};

let body = <div> <F(X) /> </div>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Are several arguments supported?

<F(X,Y) />

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, as well as full paths , e.g.(<G.F(X, Y)/>). Applying multiple separate functors, e.g. <G(X).F(Y, Z)/> doesn't work (the AST transformation required for that is a lot more complex).

Copy link
Contributor

Choose a reason for hiding this comment

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

I've tried to add a test for <F(X,Y) /> but I'm getting a syntax error on the file generated in actual_output. Could you try adding a multi argument example to the tests?

Copy link
Contributor Author

@OhadRau OhadRau May 15, 2018

Choose a reason for hiding this comment

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

Hm, I think the syntax error is from the idempotent tests. When I test it with refmt it doesn't seem to print right. Just fixed it

@OhadRau
Copy link
Contributor Author

OhadRau commented May 17, 2018

Not sure if you got a notification from the last pushes, but I've fixed the pretty-printing for functors with multiple arguments. Let me know if there's anything else that might be an issue with this.

@cristianoc
Copy link
Contributor

I wasn't sure what the plan was about tests. The PR currently adds empty files for jsx_functor.re.

@OhadRau
Copy link
Contributor Author

OhadRau commented May 17, 2018

Weird, I must've accidentally cleared those files before pushing. Should be fixed now.

@cristianoc
Copy link
Contributor

Thanks!
From the CI, it looks like idempotent tests are failing with a syntax error

Idempotent Test: ./jsx_functor.re
File "/home/opam/project/formatTest/unit_tests/actual_output/./jsx_functor.re", line 36, characters 26-28:

@OhadRau
Copy link
Contributor Author

OhadRau commented May 17, 2018

I feel stupid cause I keep running make test and thinking it worked only to realize I pushed broken code lol. Should work now

the tag, then it would be consistent with array spread:
[...list] evaluates to the thing as list.
*)
let rec extract_apps args = function
Copy link
Contributor

Choose a reason for hiding this comment

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

The compiler gives a non-exhaustive pattern matching warning here.

if hasLabelledChildrenLiteral && hasSingleNonLabelledUnitAndIsAtTheEnd l then
if List.length (Longident.flatten loc.txt) > 1 then
if Longident.last loc.txt = "createElement" then
let ftor::args = extract_apps [] app in
Copy link
Contributor

Choose a reason for hiding this comment

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

non-exhaustive pattern matching here too

perhaps the code can be refactored a bit

@cristianoc
Copy link
Contributor

Great!
This looks good to me. Is it ready to merge?

@OhadRau
Copy link
Contributor Author

OhadRau commented May 17, 2018

Yep, seems like it's all done. Thank you!

@cristianoc cristianoc merged commit 9bf799d into reasonml:master May 17, 2018
@jordwalke
Copy link
Member

jordwalke commented May 17, 2018

Do you think it might surprise some people that each time you use the JSX functor application, it will instantiate a new module each time?
<X(Y)> is not the same resulting module as <X(Y)> used a second time, although their types are identical (thank you module aliases).

So if the functor application has a side effect it would occur twice, or if it has a reference cell, it will not be shared. Do you have thoughts on that?

@OhadRau
Copy link
Contributor Author

OhadRau commented May 18, 2018

I think anyone who's used OCaml/Reason a lot would assume it based on syntax, because each instance of X(Y) isn't the module. Also, I think intuitively it looks like a function application, which would hopefully be enough to get the user to realize the side effects are occurring each time. Maybe this is something that could be addressed in the docs? I doubt anyone but long-time users of OCaml/Reason would try this out, since it's not something that exists in the "real" JSX. Because of that, I think >90% of Reason users will only be introduced to this syntax by the docs, meaning that we can specifically mention it with a big warning. The reference cell might be something to be concerned with, but as far as I know, there's no way of accessing anything other than createElement without a PPX and hopefully that is enough to push people towards reducing or eliminating side effects of module instantiation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants