Skip to content

Conversation

@jchavarri
Copy link
Collaborator

This test shows some regression where the type checker is not able to infer the type of a prop for a value which type has been inferred before in a key prop.

The compilation fails with:

File "test/React__test.re", line 605, characters 26-34:
605 |           <img src=author.imageUrl />
                                ^^^^^^^^
Error: Unbound record field imageUrl
make: *** [Makefile:12: build] Error 1

When before, it would infer that author has type Author.t, as it is namespaced in line 603:

<tr key={author.Author.name}>

I bisected the test, and this used to work before #714 was merged (tested with commit 2b8819b and it builds fine)

@anmonteiro anmonteiro force-pushed the ppx-add-test-for-inference-regression branch from c9acf14 to 06402e8 Compare July 18, 2023 23:52
@anmonteiro
Copy link
Member

Check the difference in generated code.

Before:

let render author =
  ReactDOMRe.createDOMElementVariadic "tr"
    ~props:(ReactDOMRe.domProps ~key:author.Author.name ())
    [|
      ReactDOMRe.createDOMElementVariadic "td"
        [|
          ReactDOMRe.createDOMElementVariadic "img"
            ~props:(ReactDOMRe.domProps ~src:author.imageUrl ())
            [||];
        |];
    |]

After:

let render author =
  ReactDOM.jsxKeyed "tr"
    ((ReactDOM.domProps [@merlin.hide])
       ~children:
         (ReactDOM.jsx "td"
            ((ReactDOM.domProps [@merlin.hide])
               ~children:
                 (ReactDOM.jsx "img"
                    ((ReactDOM.domProps [@merlin.hide]) ~src:author.imageUrl ()))
               ()))
       ())
    ~key:author.Author.name ()

I tried this too, but it doesn't fix it:

let render author =
  ReactDOM.jsxKeyed ~key:author.Author.name "tr"
    ((ReactDOM.domProps [@merlin.hide])
       ~children:
         (ReactDOM.jsx "td"
            ((ReactDOM.domProps [@merlin.hide])
               ~children:
                 (ReactDOM.jsx "img"
                    ((ReactDOM.domProps [@merlin.hide]) ~src:author.imageUrl ()))
               ()))
       ())
    ()

@jchavarri
Copy link
Collaborator Author

Thanks for sharing the ppx expanded.

With React jsx-runtime API, key is the last argument in this function:

external jsxKeyed: (string, domProps, string) => React.element = "jsx";

(side note: should it be a key labeled arg?)

So, because what matters for inference is the ordering of arguments at the definition, there is not much we can do. Here is a simpler example, note how the call to foo succeeds while foo2 fails:

let foo = (~bar, ~baz) => bar ++ baz;
let foo2 = (~baz, ~bar) => bar ++ baz;
let _ = author => foo(~bar=author.Author.name, ~baz=author.imageUrl);
let _ = author => foo2(~bar=author.Author.name, ~baz=author.imageUrl);
                                                            ^^^^^^^^
Error: Unbound record field imageUrl

Funny enough (but understandably) this works fine:

let render = author =>
  <tr key={author.name}>
    <td>
      <img src=author.Author.imageUrl />
    </td>
  </tr>;

The only solution I can think of is to redefine the args order by doing something like:

[@bs.module "react/jsx-runtime"]
external jsxKeyed: (string, domProps, string) => React.element = "jsx";
let jsxKeyed(element, key, props) = jsxKeyed(element, props, key);

But I personally don't think it's worth it. Wdyt?

@jchavarri
Copy link
Collaborator Author

went ahead and implemented the suggestion above in 8425467.

@anmonteiro
Copy link
Member

That’s a good idea, we don’t need the let though. We should be able to keep it an external, or didn’t that work?
Key needs to be labeled and optional to fix the case in #750

@jchavarri
Copy link
Collaborator Author

jchavarri commented Jul 19, 2023

We should be able to keep it an external, or didn’t that work?

I don't know how to have both things with a single external:

  • keep the arguments in the order that React expects (key after props)
  • make the key come before props for type inference

Do you have any ideas?

@anmonteiro
Copy link
Member

Ah you’re right, of course. I think this creates an extra function call for every JSX call though, which I don’t think is desirable.

@anmonteiro
Copy link
Member

@jchavarri I pushed a different version of your fix that generates:

let render author =
  let reason_react_ppx_key_arg___x = author.Author.name in
  ReactDOM.jsxKeyed ~key:reason_react_ppx_key_arg___x "tr"
    (((ReactDOM.domProps)[@merlin.hide ])
       ~children:(ReactDOM.jsx "td"
                    (((ReactDOM.domProps)[@merlin.hide ])
                       ~children:(ReactDOM.jsx "img"
                                    (((ReactDOM.domProps)[@merlin.hide ])
                                       ~src:(author.imageUrl) ())) ())) ())
    ()

we introduce a let binding that matches the expected flow of type inference. What do you think?

Copy link
Collaborator Author

@jchavarri jchavarri left a comment

Choose a reason for hiding this comment

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

Your approach looks good as well! It is very creative :) I left a comment about locations that I think should be tackled before merging.

In general, all other things equal, I tend to prefer solving issues elsewhere than into a ppx, just because it has so much inherent complexity... but it is not a strong opinion, happy to go with this solution.

Do you know which one would be more optimal in terms of resulting size of the generated JavaScript code? I understand the extra let in the bindings is just added once in the bindings, but the ppx will add an extra let for every usage of key?

pvb_loc = loc;
};
]
(Exp.apply ~loc:parentExpLoc ~attrs jsxExpr
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This kind of breaks the rules for "well-formed" locations, because the apply will have a larger range (parentExpLoc) than the parent let expression (which has loc). I am afraid that our for now limited tests are not catching a potential regression here.

I think we should at least rename loc to callerLoc or something else, to prevent this kind of issues in the future.

Copy link
Member

Choose a reason for hiding this comment

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

The rename make sense, but I don't understand the issue here. I belive the location is pointed to the right place no?

(li ~key:e.path [@JSX]) (* <-- parentExpLoc *)

let reason_react_ppx_key_arg___x = e.path in
ReactDOM.jsxKeyed ~key:reason_react_ppx_key_arg___x "li" (* <-- parentExpLoc *)

Copy link
Member

@davesnx davesnx Sep 6, 2023

Choose a reason for hiding this comment

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

After pressing "Send", I realised that the let reason_react_ppx_key_arg___x use the location of the identifier, and this is the reason why when you hover a lowercase component it says "string"

I wonder now, what would be the right location for this case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In my experience, it doesn't matter that much which is the right location for each node. As long as they are well formed (parents have larger scope than children), things kind of work. One might need to hide some nodes to prioritize others, but the fundamental first step is that the ranges are well formed. When that fails, merlin will fail in most cases.

…-test-for-inference-regression

* 'main' of github.com:/reasonml/reason-react:
  Allow memoCustomCompareProps on ppx (#766)
  Fix rules for reason under with-test (#762)
  fix opam formula (#763)
  depexts: expand to react 17 (#761)
  add npm depexts (#760)
  Create a blog entry for 0.11 and Melange (#743)
  Install melange 1.0 (#757) (#758)
* main:
  test: add merlin test for key prop (#771)
  Migrate snapshot to cram (#767)
@jchavarri
Copy link
Collaborator Author

I updated this branch with latest main, which included some merlin tests for key prop, and they show there is a regression in this PR where many locations in key are lost (check ppx/test/keys.t/run.t), which confirms the suspicion of my previous comment.

jchavarri and others added 6 commits September 6, 2023 16:06
…-test-for-inference-regression

* 'main' of github.com:/reasonml/reason-react:
  Abstract bindings on ppx (#768)
  Remove ReactDOM.props (#764)
  update package-lock.json (#772)
@davesnx davesnx force-pushed the ppx-add-test-for-inference-regression branch from 48cb585 to 151c89b Compare September 7, 2023 19:14
@davesnx davesnx merged commit a7f6e48 into main Sep 7, 2023
davesnx added a commit that referenced this pull request Sep 7, 2023
…ional-props-in-makeProps

* 'main' of github.com:/reasonml/reason-react:
  ppx: add test for inference regression in keys (#752)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants