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

Jsx ast #7286

Draft
wants to merge 50 commits into
base: master
Choose a base branch
from
Draft

Jsx ast #7286

wants to merge 50 commits into from

Conversation

nojaf
Copy link
Collaborator

@nojaf nojaf commented Feb 8, 2025

This is a part of #7283.
I'm introducing Pexp_jsx_fragment to represent fragment syntax <></>.

I found it insightful to just try it out and see what code changes are necessary.

In short a fragment is now parsed as:

        expression 
          attribute  "JSX"
            []
          Pexp_construct "::" 
          Some
            expression 
              Pexp_tuple
              [
                expression 
                  attribute  "JSX"
                    []
                  Pexp_apply
                  expression 
                    Pexp_ident "SectionHeader.createElement" 
                  [
                    <arg>
                    Labelled "children"
                      expression 
                        Pexp_construct "::" 
                        Some
                          expression 
                            Pexp_tuple
                            [
                              expression 
                                attribute  "res.braces"
                                  []
                                Pexp_apply
                                expression 
                                  Pexp_ident "React.string" 
                                [
                                  <arg>
                                  Nolabel
                                    expression 
                                      Pexp_constant PConst_string ("abc",Some "*j")
                                ]
                              expression 
                                Pexp_construct "[]" 
                                None
                            ]
                    <arg>
                    Nolabel
                      expression 
                        Pexp_construct "()" 
                        None
                  ]
                expression 
                  Pexp_construct "[]" 
                  None
              ]

after this change it becomes:

        expression 
          Pexp_jsx_fragment          [
            expression 
              attribute  "JSX"
                []
              Pexp_apply
              expression 
                Pexp_ident "SectionHeader.createElement" 
              [
                <arg>
                Labelled "children"
                  expression 
                    Pexp_construct "::" 
                    Some
                      expression 
                        Pexp_tuple
                        [
                          expression 
                            attribute  "res.braces"
                              []
                            Pexp_apply
                            expression 
                              Pexp_ident "React.string" 
                            [
                              <arg>
                              Nolabel
                                expression 
                                  Pexp_constant PConst_string ("abc",Some "*j")
                            ]
                          expression 
                            Pexp_construct "[]" 
                            None
                        ]
                <arg>
                Nolabel
                  expression 
                    Pexp_construct "()" 
                    None
              ]
          ]

I'll add some comment to relevant changes.


(*
* jsx-fragment ::=
* | <> </>
* | <> jsx-children </>
*)
and parse_jsx_fragment p =
and parse_jsx_fragment start_pos p =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I prefer ranges to be accurate. The location should start at the opening < token.

Doc.group
(Doc.concat
[
line_sep;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@shulhi this part isn't 100% correct.
I'm not familiar enough with this part of the codebase.

let z1 = <> <SectionHeader> {React.string("abc")} </SectionHeader> </>

will be formatted to

let z1 =
<>
  <SectionHeader> {React.string("abc")} </SectionHeader>
</>

so I'm missing some indent here. Not sure how that part works.
Would love to hear your thoughts on this.

Copy link
Member

@shulhi shulhi Feb 11, 2025

Choose a reason for hiding this comment

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

@nojaf I checked, the indentation is handled here,

let should_indent =
match opt_braces with
| Some _ -> false
| _ -> (
ParsetreeViewer.is_binary_expression expr
||
match vb.pvb_expr with
| {
pexp_attributes = [({Location.txt = "res.ternary"}, _)];
pexp_desc = Pexp_ifthenelse (if_expr, _, _);
} ->
ParsetreeViewer.is_binary_expression if_expr
|| ParsetreeViewer.has_attributes if_expr.pexp_attributes
| {pexp_desc = Pexp_newtype _} -> false
| {pexp_attributes = [({Location.txt = "res.taggedTemplate"}, _)]} ->
false
| e ->
ParsetreeViewer.has_attributes e.pexp_attributes
|| ParsetreeViewer.is_array_access e)

You might need to handle the case for Pexp_jsx_fragment here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks, the old code will instead have matched ParsetreeViewer.has_attributes e.pexp_attributes. Indentation now works as before.

@@ -1000,7 +1000,7 @@ Path Objects.Rec.

Complete src/Completion.res 120:7
posCursor:[120:7] posNoWhite:[120:6] Found expr:[119:11->123:1]
posCursor:[120:7] posNoWhite:[120:6] Found expr:[120:5->122:5]
posCursor:[120:7] posNoWhite:[120:6] Found expr:[120:5->123:0]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changes like this are to be expected as the range of a fragment spans from <> till end </>.

posCursor:[9:56] posNoWhite:[9:55] Found expr:[9:13->9:66]
JSX <SectionHeader:[9:13->9:26] > _children:9:26
posCursor:[9:56] posNoWhite:[9:55] Found expr:__ghost__[9:10->9:67]
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 ghost expression is a part of the AstHelper.make_list_expression result.
It is no longer present in the new AST, but it also didn't serve any purpose.
I believe it is okay that this test is slightly different.
In the end the result didn't change.

(* [%id] *)
(* . *)
(* represents <> foo </> , the entire range is stored in the expression , we keep track of >, children and </ *)
| Pexp_jsx_fragment of
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The reason the store the > and </ token is this edge case:
https://rescript-lang.org/try?version=v12.0.0-alpha.8&module=esmodule&code=DYUwLgBA+hC8ECgCQAeCB6AVBAzmATgIYB2A5iBJuhAHzJIDeASiIQMZgB0e+AlmQAoARAFsQACyEBKAL7IU1LBEIiIASQh9S4yFVpA

If we ever want to restore comments I suppose we need the proper anchors.

Map child expressions

Initial mapping of Pexp_jsx_fragment to 0

Correct location in mapping

Update analysis for jsx_fragment

Remove unused code

Print something for ml print

Commit invalid test results for reference

Try improve printing

Correct fragment range, try and print comments

Indent jsx

Process comments from children inside fragment

Attach comments to fragment tags

Fix comment

Improve comment formatting

Print single element on same line

Update comment

WIP: Debug

More debugging

Works

Fix some jsx printing

Fix the test

Clean up

Update tests with location changes
@nojaf
Copy link
Collaborator Author

nojaf commented Feb 21, 2025

Thank you @shulhi for fixing all the remaining formatter problem of the fragment node!
The fragment node is now in a good state.
@cristianoc could you review this PR before I continue to introduce a new node for jsx_element.

Copy link
Collaborator

@cristianoc cristianoc left a comment

Choose a reason for hiding this comment

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

Left some comments.
Just checking: is the conversion complete in that the representation of fragments is completely moved over to the new representation?

@@ -1232,7 +1232,8 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor
then ValueOrField
else Value);
}))
| Pexp_construct ({txt = Lident ("::" | "()")}, _) ->
| Pexp_construct ({txt = Lident ("::" | "()")}, _) | Pexp_jsx_fragment _
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are :: and () still used?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, for JSX tag.

@@ -407,6 +407,18 @@ module E = struct
| Pexp_open (ovf, lid, e) ->
open_ ~loc ~attrs ovf (map_loc sub lid) (sub.expr sub e)
| Pexp_extension x -> extension ~loc ~attrs (sub.extension sub x)
| Pexp_jsx_fragment (o, xs, c) ->
Copy link
Collaborator

Choose a reason for hiding this comment

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

There should be a corresponding change in ast_mapper_from0 that inverts the conversion.
Also, some extension of the test cases in tests/tools_tests/ppx/TestPpx.res to check that back and forth conversion works fine.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Alright, I think the new tests from #7318 prove this now.

@nojaf
Copy link
Collaborator Author

nojaf commented Feb 22, 2025

Left some comments. Just checking: is the conversion complete in that the representation of fragments is completely moved over to the new representation?

Yes, that is the idea. The parser only produces jsx_fragment and not the old representation.
Besides the ast_mapper_from0 I believe everything is dealt with.

@@ -61,3 +61,7 @@ let eq2 = 3 === 3

let test = async () => 12
let f = async () => (await test()) + 1

module Fragments = {
let f1 = <> </>
Copy link
Member

Choose a reason for hiding this comment

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

This should be <></>, no? (without the space).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sounds reasonable, although I do believe this is the current behavior. (Playground)

@nojaf
Copy link
Collaborator Author

nojaf commented Mar 3, 2025

@cristianoc, anything else you can think of for jsx_fragment?
If not, we can start looking at other jsx syntax.

@cristianoc
Copy link
Collaborator

@cristianoc, anything else you can think of for jsx_fragment? If not, we can start looking at other jsx syntax.

This could be ready to go. Is the absence of changes in ast-mapping what tells us that the back and forth conversion is just as before adding the fragment node?

@cristianoc
Copy link
Collaborator

Would you add a changelog too? Assuming this change will go ahead, and the rest of the JSX investigated in a separate PR.

@nojaf
Copy link
Collaborator Author

nojaf commented Mar 3, 2025

This could be ready to go. Is the absence of changes in ast-mapping what tells us that the back and forth conversion is just as before adding the fragment node?

Yes, I understand. Today, I made some necessary changes to ensure the tests remained consistent.

Would you add a changelog too? Assuming this change will go ahead, and the rest of the JSX investigated in a separate PR.

I would recommend investigating this in the same PR. There isn't much benefit to having just the fragments part. To avoid the risk of not completing a second PR, I would suggest keeping it as one.

That being said, I would like to achieve a sense of completion for the fragments before taking on more changes.

@cristianoc
Copy link
Collaborator

Sure continuing jsx sounds good.

@nojaf
Copy link
Collaborator Author

nojaf commented Mar 12, 2025

Hello @shulhi,

I've made some progress with the new nodes for JSX elements. I've added some rough code to format these. If you have the bandwidth, could you help me out with this? I would like to focus on the AST mapping myself and finally get this over the finish line.

Let me know if that works for you!

@shulhi
Copy link
Member

shulhi commented Mar 12, 2025

I've made some progress with the new nodes for JSX elements. I've added some rough code to format these. If you have the bandwidth, could you help me out with this? I would like to focus on the AST mapping myself and finally get this over the finish line.

Sure, I am happy to help.

and print_jsx_fragment ~state expr cmt_tbl =
let opening = Doc.text "<>" in
let closing = Doc.text "</>" in
and print_jsx_unary_tag ~state tag_name props cmt_tbl =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@shulhi this is incomplete.

Doc.nil;
])

and print_jsx_container_tag ~state tag_name props
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@shulhi this is incomplete.

in
let xs = exprs |> List.map (fun e -> Expression e) in
walk_list xs t rest
| Pexp_jsx_unary_element _ -> failwith "Pexp_jsx_unary_element 3"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@shulhi this is incomplete.

@@ -707,9 +707,47 @@ module SexpAst = struct
]
| Pexp_extension ext ->
Sexp.list [Sexp.atom "Pexp_extension"; extension ext]
| Pexp_jsx_fragment (_, children, _) ->
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@cristianoc what should all this code be?
I'm not sure what the lore is here.
Any pointers?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not really sure if debugger is even used anymore. @zth any ideas?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I never use it at least. Void the new nodes in there and see if anyone misses them?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@cristianoc what do you mean with debugger here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Following the use of the module, it seems to be used in the print tests here for the sexp file:

    $DUNE_BIN_DIR/res_parser $resIntf -print sexp $file > $sexpAst1
    $DUNE_BIN_DIR/res_parser $resIntf -print res $file > $rescript1

which I believe are files only kept around when the roundtrip syntax tests fail.
Can't remember if they're actually useful, or the generated .res files are enough.

In any case, it's just something for debugging tests, so the details of how things are printed are not important.

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.

5 participants