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 59 commits into
base: master
Choose a base branch
from
Draft

Jsx ast #7286

Changes from 29 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
db7eee6
Intial exploration of JSX ast
nojaf Feb 6, 2025
28aa287
Initial mapping from0
nojaf Feb 24, 2025
b57f2df
Format code
nojaf Feb 24, 2025
32161a3
Merge branch 'master' into jsx-ast
nojaf Mar 3, 2025
d714829
Fix jsx fragment mapping
nojaf Mar 3, 2025
e27b339
Remove fragment
nojaf Mar 3, 2025
92657ee
Update test output
nojaf Mar 3, 2025
441de6e
Introducing Pexp_jsx_unary_element & Pexp_jsx_container_element
nojaf Mar 6, 2025
bf1b411
Refactor fragment transformation.
nojaf Mar 6, 2025
5fe5f12
Initial transform of Pexp_jsx_unary_element in automatic mode
nojaf Mar 7, 2025
9251609
Make props for unary element in automatic mode.
nojaf Mar 7, 2025
1aaa686
Initial custom component unary tag
nojaf Mar 8, 2025
b374212
lowercase container element with children.
nojaf Mar 8, 2025
c91aeda
Uppercase container elements
nojaf Mar 8, 2025
85ccab4
Streamline automatic element calls
nojaf Mar 9, 2025
86c9f5f
lowercase container element in classic mode
nojaf Mar 9, 2025
0dfd5b6
Deal with uppercase tags in classic mode.
nojaf Mar 10, 2025
203ff25
Remove old code
nojaf Mar 10, 2025
a273f2e
Improve recovery of incomplete jsx tags.
nojaf Mar 11, 2025
998edbe
Correct range of incomplete jsx elements
nojaf Mar 11, 2025
fcf2d90
Update semantic tokens
nojaf Mar 11, 2025
162b7a2
Make the closing tag optional for jsx_container_element.
nojaf Mar 11, 2025
9b815a0
Add tighter pattern match for edge case in jsx props completion.
nojaf Mar 11, 2025
9408d06
Update analysis tests for jsx elements ast.
nojaf Mar 11, 2025
cc5beb4
print_jsx_unary_tag
nojaf Mar 11, 2025
02e9fc9
Merge branch 'master' into jsx-ast
nojaf Mar 11, 2025
5e060ea
Rough print_jsx_container_tag
nojaf Mar 12, 2025
e57ab30
Add ml printing
nojaf Mar 12, 2025
b6407d0
Wing the sexp thing
nojaf Mar 17, 2025
e565ca7
First step towards ast mapping
nojaf Mar 18, 2025
84e9cbe
prop punning conversion
nojaf Mar 19, 2025
bbfe816
Map prop value
nojaf Mar 19, 2025
63fc87b
Map prop spreading
nojaf Mar 19, 2025
d6c9fa8
Initial container element mapping.
nojaf Mar 19, 2025
e446ae1
Try support children spreading
nojaf Mar 19, 2025
5fef337
Only print space when there are props
nojaf Mar 19, 2025
251244b
Add space after children.
nojaf Mar 19, 2025
aec0d8e
Restore braces in props and children
nojaf Mar 20, 2025
6a3f4c9
Inline is_jsx_expression and remove old code
nojaf Mar 21, 2025
506dda3
Better indentation of children
nojaf Mar 21, 2025
96839f4
Handle unary tag
shulhi Mar 21, 2025
ff1bb65
Refactor
shulhi Mar 21, 2025
91c283e
WIP: Fix unary tag comments handling
shulhi Mar 22, 2025
2b9b5ce
Fix comments inside prop expression
shulhi Mar 22, 2025
0133f91
Formats
shulhi Mar 22, 2025
aaef8ac
Fix closing tag indentation
shulhi Mar 22, 2025
b863888
Fix closing tag indentation for cases with break vs inline
shulhi Mar 22, 2025
7f8bf01
Handle some edge cases
shulhi Mar 22, 2025
4e0951c
Refactor
shulhi Mar 22, 2025
7843795
Merge pull request #3 from shulhi/shulhi-jsx-ast
nojaf Mar 22, 2025
9e68c1a
WIP: Handle punning
shulhi Mar 22, 2025
53616a7
Fix optional printing with braces
shulhi Mar 22, 2025
25e20b8
WIP: Handle comments attachment in a different way
shulhi Mar 23, 2025
ad72bf2
Handle empty props
shulhi Mar 23, 2025
7ba4996
Handle props spread
shulhi Mar 24, 2025
f09cd70
Fix optional with punning
shulhi Mar 24, 2025
aa1d978
Merge pull request #4 from shulhi/shulhi-jsx-ast-2
nojaf Mar 24, 2025
0ee7601
Indent props if they don't fit on one line.
nojaf Mar 24, 2025
bd45146
Fix indentation of children when props are multiline.
nojaf Mar 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions analysis/src/CompletionFrontEnd.ml
Original file line number Diff line number Diff line change
@@ -1232,8 +1232,6 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor
then ValueOrField
else Value);
}))
| Pexp_construct ({txt = Lident ("::" | "()")}, _) ->
(* Ignore list expressions, used in JSX, unit, and more *) ()
| Pexp_construct (lid, eOpt) -> (
let lidPath = flattenLidCheckDot lid in
if debug then
@@ -1324,10 +1322,27 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor
inJsx = !inJsxContext;
}))
| None -> ())
| Pexp_apply {funct = {pexp_desc = Pexp_ident compName}; args}
when Res_parsetree_viewer.is_jsx_expression expr ->
| Pexp_jsx_unary_element
{
jsx_unary_element_tag_name = compName;
jsx_unary_element_props = props;
}
| Pexp_jsx_container_element
{
jsx_container_element_tag_name_start = compName;
jsx_container_element_props = props;
} ->
inJsxContext := true;
let jsxProps = CompletionJsx.extractJsxProps ~compName ~args in
let children =
match expr.pexp_desc with
| Pexp_jsx_container_element
{jsx_container_element_children = children} ->
children
| _ -> JSXChildrenItems []
in
let jsxProps =
CompletionJsx.extractJsxProps ~compName ~props ~children
in
let compNamePath = flattenLidCheckDot ~jsx:true compName in
if debug then
Printf.printf "JSX <%s:%s %s> _children:%s\n"
@@ -1344,10 +1359,20 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor
| None -> "None"
| Some childrenPosStart -> Pos.toString childrenPosStart);
let jsxCompletable =
CompletionJsx.findJsxPropsCompletable ~jsxProps
~endPos:(Loc.end_ expr.pexp_loc) ~posBeforeCursor
~posAfterCompName:(Loc.end_ compName.loc)
~firstCharBeforeCursorNoWhite ~charAtCursor
match expr.pexp_desc with
| Pexp_jsx_container_element
{
jsx_container_element_closing_tag = None;
jsx_container_element_children =
JSXChildrenSpreading _ | JSXChildrenItems (_ :: _);
} ->
(* This is a weird edge case where there is no closing tag but there are children *)
None
| _ ->
CompletionJsx.findJsxPropsCompletable ~jsxProps
~endPos:(Loc.end_ expr.pexp_loc) ~posBeforeCursor
~posAfterCompName:(Loc.end_ compName.loc)
~firstCharBeforeCursorNoWhite ~charAtCursor
in
if jsxCompletable <> None then setResultOpt jsxCompletable
else if compName.loc |> Loc.hasPos ~pos:posBeforeCursor then
69 changes: 34 additions & 35 deletions analysis/src/CompletionJsx.ml
Original file line number Diff line number Diff line change
@@ -455,40 +455,39 @@ let findJsxPropsCompletable ~jsxProps ~endPos ~posBeforeCursor
in
loop jsxProps.props

let extractJsxProps ~(compName : Longident.t Location.loc) ~args =
let thisCaseShouldNotHappen =
{
compName = Location.mknoloc (Longident.Lident "");
props = [];
childrenStart = None;
}
let extractJsxProps ~(compName : Longident.t Location.loc) ~props ~children =
let open Parsetree in
let childrenStart =
match children with
| JSXChildrenItems [] -> None
| JSXChildrenSpreading child | JSXChildrenItems (child :: _) ->
if child.pexp_loc.loc_ghost then None else Some (Loc.start child.pexp_loc)
in
let rec processProps ~acc args =
match args with
| (Asttypes.Labelled {txt = "children"}, {Parsetree.pexp_loc}) :: _ ->
{
compName;
props = List.rev acc;
childrenStart =
(if pexp_loc.loc_ghost then None else Some (Loc.start pexp_loc));
}
| ( (Labelled {txt = s; loc} | Optional {txt = s; loc}),
(eProp : Parsetree.expression) )
:: rest -> (
let namedArgLoc = if loc = Location.none then None else Some loc in
match namedArgLoc with
| Some loc ->
processProps
~acc:
({
name = s;
posStart = Loc.start loc;
posEnd = Loc.end_ loc;
exp = eProp;
}
:: acc)
rest
| None -> processProps ~acc rest)
| _ -> thisCaseShouldNotHappen
let props =
props
|> List.map (function
| JSXPropPunning (_, name) ->
{
name = name.txt;
posStart = Loc.start name.loc;
posEnd = Loc.end_ name.loc;
exp =
Ast_helper.Exp.ident ~loc:name.loc
{txt = Longident.Lident name.txt; loc = name.loc};
}
| JSXPropValue (name, _, value) ->
{
name = name.txt;
posStart = Loc.start name.loc;
posEnd = Loc.end_ name.loc;
exp = value;
}
| JSXPropSpreading (loc, expr) ->
{
name = "_spreadProps";
posStart = Loc.start loc;
posEnd = Loc.end_ loc;
exp = expr;
})
in
args |> processProps ~acc:[]
{compName; props; childrenStart}
96 changes: 51 additions & 45 deletions analysis/src/SemanticTokens.ml
Original file line number Diff line number Diff line change
@@ -120,9 +120,7 @@ let emitLongident ?(backwards = false) ?(jsx = false)
let rec flatten acc lid =
match lid with
| Longident.Lident txt -> txt :: acc
| Ldot (lid, txt) ->
let acc = if jsx && txt = "createElement" then acc else txt :: acc in
flatten acc lid
| Ldot (lid, txt) -> flatten (txt :: acc) lid
| _ -> acc
in
let rec loop pos segments =
@@ -247,8 +245,7 @@ let command ~debug ~emitter ~path =
~posEnd:(Some (Loc.end_ loc))
~lid ~debug;
Ast_iterator.default_iterator.expr iterator e
| Pexp_apply {funct = {pexp_desc = Pexp_ident lident; pexp_loc}; args}
when Res_parsetree_viewer.is_jsx_expression e ->
| Pexp_jsx_unary_element {jsx_unary_element_tag_name = lident} ->
(*
Angled brackets:
- These are handled in the grammar: <> </> </ />
@@ -258,46 +255,55 @@ let command ~debug ~emitter ~path =
- handled like other Longitent.t, except lowercase id is marked Token.JsxLowercase
*)
emitter (* --> <div... *)
|> emitJsxTag ~debug ~name:"<"
~pos:
(let pos = Loc.start e.pexp_loc in
(fst pos, snd pos - 1 (* the AST skips the loc of < somehow *)));
emitter |> emitJsxOpen ~lid:lident.txt ~debug ~loc:pexp_loc;

let posOfGreatherthanAfterProps =
let rec loop = function
| (Asttypes.Labelled {txt = "children"}, {Parsetree.pexp_loc}) :: _ ->
Loc.start pexp_loc
| _ :: args -> loop args
| [] -> (* should not happen *) (-1, -1)
in

loop args
in
let posOfFinalGreatherthan =
let pos = Loc.end_ e.pexp_loc in
(fst pos, snd pos - 1)
in
let selfClosing =
fst posOfGreatherthanAfterProps == fst posOfFinalGreatherthan
&& snd posOfGreatherthanAfterProps + 1 == snd posOfFinalGreatherthan
(* there's an off-by one somehow in the AST *)
in
(if not selfClosing then
let lineStart, colStart = Loc.start pexp_loc in
let lineEnd, colEnd = Loc.end_ pexp_loc in
let length = if lineStart = lineEnd then colEnd - colStart else 0 in
let lineEndWhole, colEndWhole = Loc.end_ e.pexp_loc in
if length > 0 && colEndWhole > length then (
emitter
|> emitJsxClose ~debug ~lid:lident.txt
~pos:(lineEndWhole, colEndWhole - 1);
emitter (* <foo ...props > <-- *)
|> emitJsxTag ~debug ~name:">" ~pos:posOfGreatherthanAfterProps;
emitter (* <foo> ... </foo> <-- *)
|> emitJsxTag ~debug ~name:">" ~pos:posOfFinalGreatherthan));

args |> List.iter (fun (_lbl, arg) -> iterator.expr iterator arg)
|> emitJsxTag ~debug ~name:"<" ~pos:(Loc.start e.pexp_loc);
emitter |> emitJsxOpen ~lid:lident.txt ~debug ~loc:lident.loc;
let closing_line, closing_column = Loc.end_ e.pexp_loc in
emitter (* <foo ...props /> <-- *)
|> emitJsxTag ~debug ~name:"/>" ~pos:(closing_line, closing_column - 2)
(* minus two for /> *)
| Pexp_jsx_container_element
{
jsx_container_element_tag_name_start = lident;
jsx_container_element_opening_tag_end = posOfGreatherthanAfterProps;
jsx_container_element_children = children;
jsx_container_element_closing_tag = closing_tag_opt;
} ->
(* opening tag *)
emitter (* --> <div... *)
|> emitJsxTag ~debug ~name:"<" ~pos:(Loc.start e.pexp_loc);
emitter |> emitJsxOpen ~lid:lident.txt ~debug ~loc:lident.loc;
emitter (* <foo ...props > <-- *)
|> emitJsxTag ~debug ~name:">"
~pos:(Pos.ofLexing posOfGreatherthanAfterProps);

(* children *)
(match children with
| Parsetree.JSXChildrenSpreading child -> iterator.expr iterator child
| Parsetree.JSXChildrenItems children ->
List.iter (iterator.expr iterator) children);

(* closing tag *)
closing_tag_opt
|> Option.iter
(fun
{
(* </ *)
Parsetree.jsx_closing_container_tag_start = closing_less_than;
(* name *)
jsx_closing_container_tag_name = tag_name_end;
(* > *)
jsx_closing_container_tag_end = final_greather_than;
}
->
emitter
|> emitJsxTag ~debug ~name:"</"
~pos:(Pos.ofLexing closing_less_than);
emitter
|> emitJsxClose ~debug ~lid:lident.txt
~pos:(Loc.start tag_name_end.loc);
emitter (* <foo> ... </foo> <-- *)
|> emitJsxTag ~debug ~name:">"
~pos:(Pos.ofLexing final_greather_than))
| Pexp_apply
{
funct =
7 changes: 7 additions & 0 deletions analysis/src/Utils.ml
Original file line number Diff line number Diff line change
@@ -111,6 +111,9 @@ let identifyPexp pexp =
| Pexp_pack _ -> "Pexp_pack"
| Pexp_extension _ -> "Pexp_extension"
| Pexp_open _ -> "Pexp_open"
| Pexp_jsx_fragment _ -> "Pexp_jsx_fragment"
| Pexp_jsx_unary_element _ -> "Pexp_jsx_unary_element"
| Pexp_jsx_container_element _ -> "Pexp_jsx_container_element"

let identifyPpat pat =
match pat with
@@ -154,6 +157,10 @@ let isJsxComponent (vb : Parsetree.value_binding) =
|> List.exists (function
| {Location.txt = "react.component" | "jsx.component"}, _payload -> true
| _ -> false)
||
match vb.pvb_expr.pexp_desc with
| Parsetree.Pexp_jsx_fragment _ -> true
| _ -> false

let checkName name ~prefix ~exact =
if exact then name = prefix else startsWith name prefix
30 changes: 30 additions & 0 deletions compiler/frontend/bs_ast_mapper.ml
Original file line number Diff line number Diff line change
@@ -292,6 +292,20 @@ module M = struct
end

module E = struct
let map_jsx_children sub = function
| JSXChildrenSpreading e -> JSXChildrenSpreading (sub.expr sub e)
| JSXChildrenItems xs -> JSXChildrenItems (List.map (sub.expr sub) xs)

let map_jsx_prop sub = function
| JSXPropPunning (optional, name) ->
JSXPropPunning (optional, map_loc sub name)
| JSXPropValue (name, optional, value) ->
JSXPropValue (map_loc sub name, optional, sub.expr sub value)
| JSXPropSpreading (loc, e) ->
JSXPropSpreading (sub.location sub loc, sub.expr sub e)

let map_jsx_props sub = List.map (map_jsx_prop sub)

(* Value expressions for the core language *)

let map sub {pexp_loc = loc; pexp_desc = desc; pexp_attributes = attrs} =
@@ -366,6 +380,22 @@ 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, children, c) ->
jsx_fragment o (map_jsx_children sub children) c
| Pexp_jsx_unary_element
{jsx_unary_element_tag_name = name; jsx_unary_element_props = props} ->
jsx_unary_element ~loc ~attrs name (map_jsx_props sub props)
| Pexp_jsx_container_element
{
jsx_container_element_tag_name_start = name;
jsx_container_element_opening_tag_end = ote;
jsx_container_element_props = props;
jsx_container_element_children = children;
jsx_container_element_closing_tag = closing_tag;
} ->
jsx_container_element ~loc ~attrs name (map_jsx_props sub props) ote
(map_jsx_children sub children)
closing_tag
end

module P = struct
42 changes: 42 additions & 0 deletions compiler/ml/ast_helper.ml
Original file line number Diff line number Diff line change
@@ -180,8 +180,50 @@ module Exp = struct
let pack ?loc ?attrs a = mk ?loc ?attrs (Pexp_pack a)
let open_ ?loc ?attrs a b c = mk ?loc ?attrs (Pexp_open (a, b, c))
let extension ?loc ?attrs a = mk ?loc ?attrs (Pexp_extension a)
let jsx_fragment ?loc ?attrs a b c =
mk ?loc ?attrs (Pexp_jsx_fragment (a, b, c))
let jsx_unary_element ?loc ?attrs a b =
mk ?loc ?attrs
(Pexp_jsx_unary_element
{jsx_unary_element_tag_name = a; jsx_unary_element_props = b})

let jsx_container_element ?loc ?attrs a b c d e =
mk ?loc ?attrs
(Pexp_jsx_container_element
{
jsx_container_element_tag_name_start = a;
jsx_container_element_props = b;
jsx_container_element_opening_tag_end = c;
jsx_container_element_children = d;
jsx_container_element_closing_tag = e;
})

let case lhs ?guard rhs = {pc_lhs = lhs; pc_guard = guard; pc_rhs = rhs}

let make_list_expression loc seq ext_opt =
let rec handle_seq = function
| [] -> (
match ext_opt with
| Some ext -> ext
| None ->
let loc = {loc with Location.loc_ghost = true} in
let nil = Location.mkloc (Longident.Lident "[]") loc in
construct ~loc nil None)
| e1 :: el ->
let exp_el = handle_seq el in
let loc =
Location.
{
loc_start = e1.Parsetree.pexp_loc.Location.loc_start;
loc_end = exp_el.pexp_loc.loc_end;
loc_ghost = false;
}
in
let arg = tuple ~loc [e1; exp_el] in
construct ~loc (Location.mkloc (Longident.Lident "::") loc) (Some arg)
in
let expr = handle_seq seq in
{expr with pexp_loc = loc}
end

module Mty = struct
Loading