diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aa9bf1fad..063eb7713d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ #### :house: Internal +- AST: Use jsx_tag_name instead of Longindent.t to store jsx tag name. https://github.com/rescript-lang/rescript/pull/7760 + # 12.0.0-beta.5 #### :bug: Bug fix diff --git a/analysis/src/CompletionFrontEnd.ml b/analysis/src/CompletionFrontEnd.ml index fb92e81d19..ff605b5de7 100644 --- a/analysis/src/CompletionFrontEnd.ml +++ b/analysis/src/CompletionFrontEnd.ml @@ -1341,8 +1341,13 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor { jsx_container_element_tag_name_start = compName; jsx_container_element_props = props; - } ) -> + } ) -> ( inJsxContext := true; + let is_valid_tag_for_props = + match compName.txt with + | Parsetree.JsxTagInvalid _ -> false + | _ -> true + in let children = match expr.pexp_desc with | Pexp_jsx_element @@ -1351,43 +1356,81 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor children | _ -> JSXChildrenItems [] in - let jsxProps = - CompletionJsx.extractJsxProps ~compName ~props ~children + let compName_loc = compName.loc in + let compName_lid = + Ast_helper.Jsx.longident_of_jsx_tag_name compName.txt in - let compNamePath = flattenLidCheckDot ~jsx:true compName in - if debug then - Printf.printf "JSX <%s:%s %s> _children:%s\n" - (compNamePath |> String.concat ".") - (Loc.toString compName.loc) - (jsxProps.props - |> List.map - (fun ({name; posStart; posEnd; exp} : CompletionJsx.prop) -> - Printf.sprintf "%s[%s->%s]=...%s" name - (Pos.toString posStart) (Pos.toString posEnd) - (Loc.toString exp.pexp_loc)) - |> String.concat " ") - (match jsxProps.childrenStart with - | None -> "None" - | Some childrenPosStart -> Pos.toString childrenPosStart); + let jsxPropsOpt = + if is_valid_tag_for_props then + Some + (CompletionJsx.extractJsxProps + ~compName:(Location.mkloc compName_lid compName_loc) + ~props ~children) + else None + in + let compNamePath = + flattenLidCheckDot ~jsx:true + {txt = compName_lid; loc = compName_loc} + in + (if debug then + match jsxPropsOpt with + | Some jsxProps -> + Printf.printf "JSX <%s:%s %s> _children:%s\n" + (compNamePath |> String.concat ".") + (Loc.toString compName_loc) + (jsxProps.props + |> List.map + (fun + ({name; posStart; posEnd; exp} : CompletionJsx.prop) -> + Printf.sprintf "%s[%s->%s]=...%s" name + (Pos.toString posStart) (Pos.toString posEnd) + (Loc.toString exp.pexp_loc)) + |> String.concat " ") + (match jsxProps.childrenStart with + | None -> "None" + | Some childrenPosStart -> Pos.toString childrenPosStart) + | None -> + Printf.printf "JSX <%s:%s > _children:None\n" + (compNamePath |> String.concat ".") + (Loc.toString compName_loc)); + (* If the tag name is an uppercase path and the cursor is right after a dot (e.g., + setResult + (Cpath + (CPId + { + loc = compName_loc; + path = compNamePath; + completionContext = Module; + })) + | _ -> ()); let jsxCompletable = - match expr.pexp_desc with - | Pexp_jsx_element - (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 *) + match (jsxPropsOpt, expr.pexp_desc) with + | ( Some _, + Pexp_jsx_element + (Jsx_container_element + { + jsx_container_element_closing_tag = None; + jsx_container_element_children = + JSXChildrenSpreading _ | JSXChildrenItems (_ :: _); + }) ) -> None - | _ -> + | Some jsxProps, _ -> CompletionJsx.findJsxPropsCompletable ~jsxProps ~endPos:(Loc.end_ expr.pexp_loc) ~posBeforeCursor - ~posAfterCompName:(Loc.end_ compName.loc) + ~posAfterCompName:(Loc.end_ compName_loc) ~firstCharBeforeCursorNoWhite ~charAtCursor + | None, _ -> None in - if jsxCompletable <> None then setResultOpt jsxCompletable - else if compName.loc |> Loc.hasPos ~pos:posBeforeCursor then + (match jsxCompletable with + | Some _ as res -> setResultOpt res + | None -> ()); + if + jsxCompletable = None + && compName_loc |> Loc.hasPos ~pos:posBeforeCursor + then setResult (match compNamePath with | [prefix] when Char.lowercase_ascii prefix.[0] = prefix.[0] -> @@ -1396,11 +1439,14 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor Cpath (CPId { - loc = compName.loc; + loc = compName_loc; path = compNamePath; completionContext = Module; })) - else iterateJsxProps ~iterator jsxProps + else + match jsxPropsOpt with + | Some jsxProps -> iterateJsxProps ~iterator jsxProps + | None -> ()) | Pexp_apply { funct = {pexp_desc = Pexp_ident {txt = Lident "->"}}; diff --git a/analysis/src/SemanticTokens.ml b/analysis/src/SemanticTokens.ml index a3b509a493..ad22221186 100644 --- a/analysis/src/SemanticTokens.ml +++ b/analysis/src/SemanticTokens.ml @@ -265,7 +265,9 @@ let command ~debug ~emitter ~path = *) emitter (* --> emitJsxTag ~debug ~name:"<" ~pos:(Loc.start e.pexp_loc); - emitter |> emitJsxOpen ~lid:lident.txt ~debug ~loc:lident.loc; + let lid = Ast_helper.Jsx.longident_of_jsx_tag_name lident.txt in + let loc = lident.loc in + emitter |> emitJsxOpen ~lid ~debug ~loc; let closing_line, closing_column = Loc.end_ e.pexp_loc in emitter (* <-- *) |> emitJsxTag ~debug ~name:"/>" ~pos:(closing_line, closing_column - 2) @@ -281,7 +283,9 @@ let command ~debug ~emitter ~path = (* opening tag *) emitter (* --> emitJsxTag ~debug ~name:"<" ~pos:(Loc.start e.pexp_loc); - emitter |> emitJsxOpen ~lid:lident.txt ~debug ~loc:lident.loc; + let lid = Ast_helper.Jsx.longident_of_jsx_tag_name lident.txt in + let loc = lident.loc in + emitter |> emitJsxOpen ~lid ~debug ~loc; emitter (* <-- *) |> emitJsxTag ~debug ~name:">" ~pos:(Pos.ofLexing posOfGreatherthanAfterProps); @@ -308,9 +312,11 @@ let command ~debug ~emitter ~path = emitter |> emitJsxTag ~debug ~name:" emitJsxClose ~debug ~lid:lident.txt - ~pos:(Loc.start tag_name_end.loc); + let lid = + Ast_helper.Jsx.longident_of_jsx_tag_name tag_name_end.txt + in + let loc = tag_name_end.loc in + emitter |> emitJsxClose ~debug ~lid ~pos:(Loc.end_ loc); emitter (* ... <-- *) |> emitJsxTag ~debug ~name:">" ~pos:(Pos.ofLexing final_greather_than)) diff --git a/compiler/ml/ast_helper.ml b/compiler/ml/ast_helper.ml index baab03db21..2fae640eb0 100644 --- a/compiler/ml/ast_helper.ml +++ b/compiler/ml/ast_helper.ml @@ -428,3 +428,21 @@ module Te = struct pext_attributes = attrs; } end + +module Jsx = struct + let string_of_jsx_tag_name (tag_name : Parsetree.jsx_tag_name) : string = + match tag_name with + | Parsetree.JsxLowerTag name -> name + | Parsetree.JsxQualifiedLowerTag {path; name} -> + String.concat "." (Longident.flatten path) ^ "." ^ name + | Parsetree.JsxUpperTag path -> String.concat "." (Longident.flatten path) + | Parsetree.JsxTagInvalid name -> name + + let longident_of_jsx_tag_name (tag_name : Parsetree.jsx_tag_name) : + Longident.t = + match tag_name with + | Parsetree.JsxLowerTag name -> Longident.Lident name + | Parsetree.JsxQualifiedLowerTag {path; name} -> Longident.Ldot (path, name) + | Parsetree.JsxUpperTag path -> path + | Parsetree.JsxTagInvalid name -> Longident.Lident name +end diff --git a/compiler/ml/ast_helper.mli b/compiler/ml/ast_helper.mli index 889e617b7d..11227b903a 100644 --- a/compiler/ml/ast_helper.mli +++ b/compiler/ml/ast_helper.mli @@ -212,13 +212,13 @@ module Exp : sig val jsx_unary_element : ?loc:loc -> ?attrs:attrs -> - Longident.t Location.loc -> + Parsetree.jsx_tag_name Location.loc -> Parsetree.jsx_props -> expression val jsx_container_element : ?loc:loc -> ?attrs:attrs -> - Longident.t Location.loc -> + Parsetree.jsx_tag_name Location.loc -> Parsetree.jsx_props -> Lexing.position -> Parsetree.jsx_children -> @@ -301,6 +301,11 @@ module Te : sig val rebind : ?loc:loc -> ?attrs:attrs -> str -> lid -> extension_constructor end +module Jsx : sig + val string_of_jsx_tag_name : Parsetree.jsx_tag_name -> string + val longident_of_jsx_tag_name : Parsetree.jsx_tag_name -> Longident.t +end + (** {1 Module language} *) (** Module type expressions *) diff --git a/compiler/ml/ast_mapper_from0.ml b/compiler/ml/ast_mapper_from0.ml index 4f1f6fff60..eb01c014f7 100644 --- a/compiler/ml/ast_mapper_from0.ml +++ b/compiler/ml/ast_mapper_from0.ml @@ -396,10 +396,23 @@ module E = struct when has_jsx_attribute () -> ( let attrs = attrs |> List.filter (fun ({txt}, _) -> txt <> "JSX") in let props, children = extract_props_and_children sub args in + let jsx_tag : Pt.jsx_tag_name = + match tag_name.txt with + | Longident.Lident s + when String.length s > 0 && Char.lowercase_ascii s.[0] = s.[0] -> + Pt.JsxLowerTag s + | Longident.Lident _ -> Pt.JsxUpperTag tag_name.txt + | Longident.Ldot (path, last) + when String.length last > 0 + && Char.lowercase_ascii last.[0] = last.[0] -> + Pt.JsxQualifiedLowerTag {path; name = last} + | _ -> Pt.JsxUpperTag tag_name.txt + in + let jsx_tag_name = {txt = jsx_tag; loc = tag_name.loc} in match children with - | None -> jsx_unary_element ~loc ~attrs tag_name props + | None -> jsx_unary_element ~loc ~attrs jsx_tag_name props | Some children -> - jsx_container_element ~loc ~attrs tag_name props Lexing.dummy_pos + jsx_container_element ~loc ~attrs jsx_tag_name props Lexing.dummy_pos children None) | Pexp_apply (e, l) -> let e = diff --git a/compiler/ml/ast_mapper_to0.ml b/compiler/ml/ast_mapper_to0.ml index 57101ff671..3f87389bcf 100644 --- a/compiler/ml/ast_mapper_to0.ml +++ b/compiler/ml/ast_mapper_to0.ml @@ -495,7 +495,9 @@ module E = struct jsx_unary_element_tag_name = tag_name; jsx_unary_element_props = props; }) -> - let tag_ident = map_loc sub tag_name in + let tag_ident : Longident.t Location.loc = + tag_name |> Location.map_loc Ast_helper.Jsx.longident_of_jsx_tag_name + in let props = map_jsx_props sub props in let children_expr = let loc = @@ -525,7 +527,9 @@ module E = struct jsx_container_element_props = props; jsx_container_element_children = children; }) -> - let tag_ident = map_loc sub tag_name in + let tag_ident : Longident.t Location.loc = + tag_name |> Location.map_loc Ast_helper.Jsx.longident_of_jsx_tag_name + in let props = map_jsx_props sub props in let children_expr = map_jsx_children sub loc children in apply ~loc ~attrs:(jsx_attr sub :: attrs) (ident tag_ident) diff --git a/compiler/ml/depend.ml b/compiler/ml/depend.ml index 63be3846ba..bb467ef592 100644 --- a/compiler/ml/depend.ml +++ b/compiler/ml/depend.ml @@ -294,7 +294,10 @@ let rec add_expr bv exp = (Jsx_unary_element {jsx_unary_element_tag_name = name; jsx_unary_element_props = props}) -> - add bv name; + (* Conservatively add all module path segments referenced by the tag name *) + (match name.txt with + | JsxLowerTag _ | JsxTagInvalid _ -> () + | JsxQualifiedLowerTag {path; _} | JsxUpperTag path -> add_path bv path); and_jsx_props bv props | Pexp_jsx_element (Jsx_container_element @@ -303,7 +306,9 @@ let rec add_expr bv exp = jsx_container_element_props = props; jsx_container_element_children = children; }) -> - add bv name; + (match name.txt with + | JsxLowerTag _ | JsxTagInvalid _ -> () + | JsxQualifiedLowerTag {path; _} | JsxUpperTag path -> add_path bv path); and_jsx_props bv props; add_jsx_children bv children diff --git a/compiler/ml/location.ml b/compiler/ml/location.ml index 19de2b7125..caa0f98c86 100644 --- a/compiler/ml/location.ml +++ b/compiler/ml/location.ml @@ -300,3 +300,5 @@ let raise_errorf ?(loc = none) ?(sub = []) ?(if_highlight = "") = let deprecated ?(def = none) ?(use = none) loc msg = prerr_warning loc (Warnings.Deprecated (msg, def, use)) + +let map_loc f {txt; loc} = {txt = f txt; loc} diff --git a/compiler/ml/location.mli b/compiler/ml/location.mli index 49758de42a..bff4609205 100644 --- a/compiler/ml/location.mli +++ b/compiler/ml/location.mli @@ -131,3 +131,5 @@ val report_exception : formatter -> exn -> unit (** Reraise the exception if it is unknown. *) val deprecated : ?def:t -> ?use:t -> t -> string -> unit + +val map_loc : ('a -> 'b) -> 'a loc -> 'b loc diff --git a/compiler/ml/parsetree.ml b/compiler/ml/parsetree.ml index 77194522e8..cc99af9b92 100644 --- a/compiler/ml/parsetree.ml +++ b/compiler/ml/parsetree.ml @@ -325,6 +325,12 @@ and jsx_element = | Jsx_unary_element of jsx_unary_element | Jsx_container_element of jsx_container_element +and jsx_tag_name = + | JsxLowerTag of string + | JsxQualifiedLowerTag of {path: Longident.t; name: string} + | JsxUpperTag of Longident.t + | JsxTagInvalid of string + and jsx_fragment = { (* > *) jsx_fragment_opening: Lexing.position; (* children *) jsx_fragment_children: jsx_children; @@ -332,13 +338,13 @@ and jsx_fragment = { } and jsx_unary_element = { - jsx_unary_element_tag_name: Longident.t loc; + jsx_unary_element_tag_name: jsx_tag_name loc; jsx_unary_element_props: jsx_props; } and jsx_container_element = { (* jsx_container_element_opening_tag_start: Lexing.position; *) - jsx_container_element_tag_name_start: Longident.t loc; + jsx_container_element_tag_name_start: jsx_tag_name loc; (* > *) jsx_container_element_opening_tag_end: Lexing.position; jsx_container_element_props: jsx_props; @@ -376,7 +382,7 @@ and jsx_closing_container_tag = { (* *) jsx_closing_container_tag_end: Lexing.position; } diff --git a/compiler/ml/pprintast.ml b/compiler/ml/pprintast.ml index ff7f3f8c70..2a91e8fd1f 100644 --- a/compiler/ml/pprintast.ml +++ b/compiler/ml/pprintast.ml @@ -805,7 +805,7 @@ and simple_expr ctxt f x = jsx_unary_element_tag_name = tag_name; jsx_unary_element_props = props; }) -> ( - let name = Longident.flatten tag_name.txt |> String.concat "." in + let name = Ast_helper.Jsx.string_of_jsx_tag_name tag_name.txt in match props with | [] -> pp f "<%s />" name | _ -> pp f "<%s %a />" name (print_jsx_props ctxt) props) @@ -815,19 +815,28 @@ and simple_expr ctxt f x = jsx_container_element_tag_name_start = tag_name; jsx_container_element_props = props; jsx_container_element_children = children; + jsx_container_element_closing_tag = closing_tag; }) -> ( - let name = Longident.flatten tag_name.txt |> String.concat "." in + let name = Ast_helper.Jsx.string_of_jsx_tag_name tag_name.txt in + let closing_name = + match closing_tag with + | None -> "" + | Some closing_tag -> + Format.sprintf "" + (Ast_helper.Jsx.string_of_jsx_tag_name + closing_tag.jsx_closing_container_tag_name.txt) + in match props with | [] -> - pp f "<%s>%a" name + pp f "<%s>%a%s" name (list (simple_expr ctxt)) (collect_jsx_children children) - name + closing_name | _ -> - pp f "<%s %a>%a" name (print_jsx_props ctxt) props + pp f "<%s %a>%a%s" name (print_jsx_props ctxt) props (list (simple_expr ctxt)) (collect_jsx_children children) - name) + closing_name) | _ -> paren true (expression ctxt) f x and collect_jsx_children = function diff --git a/compiler/ml/printast.ml b/compiler/ml/printast.ml index 4d2ea003d0..756511f0bb 100644 --- a/compiler/ml/printast.ml +++ b/compiler/ml/printast.ml @@ -42,6 +42,16 @@ let rec fmt_longident_aux f x = let fmt_longident_loc f (x : Longident.t loc) = fprintf f "\"%a\" %a" fmt_longident_aux x.txt fmt_location x.loc +let fmt_jsx_tag_name f (x : jsx_tag_name loc) = + let loc = x.loc in + match x.txt with + | JsxLowerTag name -> fprintf f "\"%s\" %a" name fmt_location loc + | JsxQualifiedLowerTag {path; name} -> + fprintf f "\"%a.%s\" %a" fmt_longident_aux path name fmt_location loc + | JsxUpperTag path -> + fprintf f "\"%a\" %a" fmt_longident_aux path fmt_location loc + | JsxTagInvalid name -> fprintf f "\"%s\" %a" name fmt_location loc + let fmt_string_loc f (x : string loc) = fprintf f "\"%s\" %a" x.txt fmt_location x.loc @@ -350,7 +360,7 @@ and expression i ppf x = (Jsx_unary_element {jsx_unary_element_tag_name = name; jsx_unary_element_props = props}) -> - line i ppf "Pexp_jsx_unary_element %a\n" fmt_longident_loc name; + line i ppf "Pexp_jsx_unary_element %a\n" fmt_jsx_tag_name name; jsx_props i ppf props | Pexp_jsx_element (Jsx_container_element @@ -359,11 +369,17 @@ and expression i ppf x = jsx_container_element_props = props; jsx_container_element_opening_tag_end = gt; jsx_container_element_children = children; - }) -> - line i ppf "Pexp_jsx_container_element %a\n" fmt_longident_loc name; + jsx_container_element_closing_tag = closing_tag; + }) -> ( + line i ppf "Pexp_jsx_container_element %a\n" fmt_jsx_tag_name name; jsx_props i ppf props; if !Clflags.dump_location then line i ppf "> %a\n" (fmt_position false) gt; - jsx_children i ppf children + jsx_children i ppf children; + match closing_tag with + | None -> () + | Some closing_tag -> + line i ppf "closing_tag =%a\n" fmt_jsx_tag_name + closing_tag.jsx_closing_container_tag_name) and jsx_children i ppf children = line i ppf "jsx_children =\n"; diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index b7ec8b15e3..7351aaf384 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -1083,18 +1083,6 @@ let transform_signature_item ~config item = "Only one JSX component call can exist on a component at one time") | _ -> [item] -let starts_with_lowercase s = - if String.length s = 0 then false - else - let c = s.[0] in - Char.lowercase_ascii c = c - -let starts_with_uppercase s = - if String.length s = 0 then false - else - let c = s.[0] in - Char.uppercase_ascii c = c - (* There appear to be slightly different rules of transformation whether the component is upper-, lowercase or a fragment *) type componentDescription = | LowercasedComponent @@ -1289,12 +1277,14 @@ let mk_react_jsx (config : Jsx_common.jsx_config) mapper loc attrs *) let mk_uppercase_tag_name_expr tag_name = let tag_identifier : Longident.t = - if Longident.flatten tag_name.txt |> List.for_all starts_with_uppercase then - (* All parts are uppercase, so we append .make *) - Ldot (tag_name.txt, "make") - else tag_name.txt + match tag_name.txt with + | JsxTagInvalid _ | JsxLowerTag _ -> + failwith "Unreachable code at mk_uppercase_tag_name_expr" + | JsxQualifiedLowerTag {path; name} -> Longident.Ldot (path, name) + | JsxUpperTag path -> Longident.Ldot (path, "make") in - Exp.ident ~loc:tag_name.loc {txt = tag_identifier; loc = tag_name.loc} + let loc = tag_name.loc in + Exp.ident ~loc {txt = tag_identifier; loc} let expr ~(config : Jsx_common.jsx_config) mapper expression = match expression with @@ -1312,45 +1302,48 @@ let expr ~(config : Jsx_common.jsx_config) mapper expression = children | Jsx_unary_element {jsx_unary_element_tag_name = tag_name; jsx_unary_element_props = props} - -> - let name = Longident.flatten tag_name.txt |> String.concat "." in - if starts_with_lowercase name then + -> ( + let name = Ast_helper.Jsx.string_of_jsx_tag_name tag_name.txt in + let tag_loc = tag_name.loc in + match tag_name.txt with + | JsxLowerTag _ -> (* For example 'input' *) - let component_name_expr = constant_string ~loc:tag_name.loc name in + let component_name_expr = constant_string ~loc:tag_loc name in mk_react_jsx config mapper loc attrs LowercasedComponent component_name_expr props (JSXChildrenItems []) - else if starts_with_uppercase name then + | JsxUpperTag _ | JsxQualifiedLowerTag _ -> (* MyModule.make *) let make_id = mk_uppercase_tag_name_expr tag_name in mk_react_jsx config mapper loc attrs UppercasedComponent make_id props (JSXChildrenItems []) - else + | JsxTagInvalid name -> Jsx_common.raise_error ~loc - "JSX: element name is neither upper- or lowercase, got \"%s\"" - (Longident.flatten tag_name.txt |> String.concat ".") + "JSX: element name is neither upper- or lowercase, got \"%s\"" name) | Jsx_container_element { jsx_container_element_tag_name_start = tag_name; jsx_container_element_props = props; jsx_container_element_children = children; - } -> - let name = Longident.flatten tag_name.txt |> String.concat "." in + } -> ( + let name, tag_loc = + (Ast_helper.Jsx.string_of_jsx_tag_name tag_name.txt, tag_name.loc) + in (* For example:


This has an impact if we want to use ReactDOM.jsx or ReactDOM.jsxs *) - if starts_with_lowercase name then - let component_name_expr = constant_string ~loc:tag_name.loc name in + match tag_name.txt with + | JsxLowerTag _ -> + let component_name_expr = constant_string ~loc:tag_loc name in mk_react_jsx config mapper loc attrs LowercasedComponent component_name_expr props children - else if starts_with_uppercase name then + | JsxQualifiedLowerTag _ | JsxUpperTag _ -> (* MyModule.make *) let make_id = mk_uppercase_tag_name_expr tag_name in mk_react_jsx config mapper loc attrs UppercasedComponent make_id props children - else + | JsxTagInvalid name -> Jsx_common.raise_error ~loc - "JSX: element name is neither upper- or lowercase, got \"%s\"" - (Longident.flatten tag_name.txt |> String.concat ".")) + "JSX: element name is neither upper- or lowercase, got \"%s\"" name)) | e -> default_mapper.expr mapper e let module_binding ~(config : Jsx_common.jsx_config) mapper module_binding = diff --git a/compiler/syntax/src/res_comments_table.ml b/compiler/syntax/src/res_comments_table.ml index 59946df5e1..2c4608476f 100644 --- a/compiler/syntax/src/res_comments_table.ml +++ b/compiler/syntax/src/res_comments_table.ml @@ -1682,12 +1682,14 @@ and walk_expression expr t comments = | [] -> closing_token_loc | head :: _ -> ParsetreeViewer.get_jsx_prop_loc head in - partition_adjacent_trailing_before_next_token_on_same_line tag_name.loc + let name_loc = tag_name.loc in + partition_adjacent_trailing_before_next_token_on_same_line name_loc next_token comments in (* Only attach comments to the element name if they are on the same line *) - attach t.trailing tag_name.loc after_opening_tag_name; + let name_loc = tag_name.loc in + attach t.trailing name_loc after_opening_tag_name; match props with | [] -> let before_closing_token, _rest = @@ -1726,11 +1728,13 @@ and walk_expression expr t comments = | [] -> opening_greater_than_loc | head :: _ -> ParsetreeViewer.get_jsx_prop_loc head in - partition_adjacent_trailing_before_next_token_on_same_line - tag_name_start.loc next_token comments + let name_loc = tag_name_start.loc in + partition_adjacent_trailing_before_next_token_on_same_line name_loc + next_token comments in (* Only attach comments to the element name if they are on the same line *) - attach t.trailing tag_name_start.loc after_opening_tag_name; + let name_loc = tag_name_start.loc in + attach t.trailing name_loc after_opening_tag_name; let rest = match props with | [] -> diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index 3702403c1b..ab9d0d4cee 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -721,46 +721,131 @@ let parse_module_long_ident_tail ~lowercase p start_pos ident = - All immediately following ("-" IDENT) chunks have been consumed from the scanner - No hyphen that belongs to the JSX name remains unconsumed - The returned token is the combined Lident/Uident for the full name *) -let parse_jsx_ident (p : Parser.t) : Token.t option = - (* check if the next tokens are minus and ident, if so, add them to the buffer *) - let rec visit buffer = - match p.Parser.token with - | Minus -> ( - Parser.next p; - match p.Parser.token with - | Lident txt | Uident txt -> - Buffer.add_char buffer '-'; - Buffer.add_string buffer txt; - if Scanner.peekMinus p.scanner then ( - Parser.next p; - visit buffer) - else buffer - | _ -> - (* Error: hyphen must be followed by an identifier *) - Parser.err p - (Diagnostics.message "JSX identifier cannot end with a hyphen"); - buffer) - | _ -> buffer - in +(* Non-mutating helpers to parse JSX identifiers with optional hyphen chains *) +type jsx_ident_kind = [`Lower | `Upper] + +(* Inspect current token; do not advance *) +let peek_ident (p : Parser.t) : (string * Location.t * jsx_ident_kind) option = match p.Parser.token with - | Lident txt when Scanner.peekMinus p.scanner -> - let buffer = Buffer.create (String.length txt) in - Buffer.add_string buffer txt; + | Lident txt -> Some (txt, mk_loc p.start_pos p.end_pos, `Lower) + | Uident txt -> Some (txt, mk_loc p.start_pos p.end_pos, `Upper) + | _ -> None + +(* Consume one Lident/Uident if present *) +let expect_ident (p : Parser.t) : (string * Location.t * jsx_ident_kind) option + = + match peek_ident p with + | None -> None + | Some (txt, loc, k) -> Parser.next p; - let name = visit buffer |> Buffer.contents in - Some (Token.Lident name) - | Uident txt when Scanner.peekMinus p.scanner -> - let buffer = Buffer.create (String.length txt) in - Buffer.add_string buffer txt; + Some (txt, loc, k) + +(* Consume ("-" IDENT)*, appending to buffer; update last_end; diagnose trailing '-' *) +let rec read_hyphen_chain (p : Parser.t) (buf : Buffer.t) + (last_end : Lexing.position ref) : unit = + match p.Parser.token with + | Minus -> ( Parser.next p; - let name = visit buffer |> Buffer.contents in - Some (Token.Uident name) - | _ -> None + (* after '-' *) + match peek_ident p with + | Some (txt, _loc, _) -> + Buffer.add_char buf '-'; + Buffer.add_string buf txt; + (* consume ident *) + Parser.next p; + last_end := p.prev_end_pos; + read_hyphen_chain p buf last_end + | None -> + (* Match previous behavior: rely on parser's current location *) + Parser.err p + (Diagnostics.message "JSX identifier cannot end with a hyphen")) + | _ -> () + +(* Read local jsx name: returns combined name + loc + kind of head ident *) +let read_local_jsx_name (p : Parser.t) : + (string * Location.t * jsx_ident_kind) option = + match expect_ident p with + | None -> None + | Some (head, head_loc, kind) -> + let buf = Buffer.create (String.length head + 8) in + Buffer.add_string buf head; + let start_pos = head_loc.Location.loc_start in + let last_end = ref head_loc.Location.loc_end in + read_hyphen_chain p buf last_end; + let name = Buffer.contents buf in + let loc = mk_loc start_pos !last_end in + Some (name, loc, kind) + +(* Build a Longident from a non-empty list of segments *) +let longident_of_segments (segs : string list) : Longident.t = + match segs with + | [] -> invalid_arg "longident_of_segments: empty list" + | hd :: tl -> + List.fold_left + (fun acc s -> Longident.Ldot (acc, s)) + (Longident.Lident hd) tl + +(* Read a JSX tag name and return a jsx_tag_name; does not mutate tokens beyond what it consumes *) +let read_jsx_tag_name (p : Parser.t) : + (Parsetree.jsx_tag_name Location.loc, string) result = + match peek_ident p with + | None -> Error "" + | Some (_, _, `Lower) -> + read_local_jsx_name p + |> Option.map (fun (name, loc, _) -> + {Location.txt = Parsetree.JsxLowerTag name; loc}) + |> Option.to_result ~none:"" + | Some (first_seg, first_loc, `Upper) -> + let start_pos = first_loc.Location.loc_start in + (* consume first Uident *) + Parser.next p; + let string_of_rev_segments segs = String.concat "." (List.rev segs) in + let rec loop rev_segs last_end = + match p.Parser.token with + | Dot -> ( + Parser.next p; + (* after '.' *) + match peek_ident p with + | None -> + Parser.err p + (Diagnostics.message "expected identifier after '.' in JSX tag name"); + Error (string_of_rev_segments rev_segs ^ ".") + | Some (txt, _loc, `Upper) -> + (* another path segment *) + Parser.next p; + loop (txt :: rev_segs) p.prev_end_pos + | Some (_, _, `Lower) -> ( + (* final lowercase with optional hyphens *) + match read_local_jsx_name p with + | Some (lname, l_loc, _) -> ( + match rev_segs with + | [] -> Error "" + | _ -> + let path = longident_of_segments (List.rev rev_segs) in + let loc = mk_loc start_pos l_loc.Location.loc_end in + Ok + { + Location.txt = + Parsetree.JsxQualifiedLowerTag {path; name = lname}; + loc; + }) + | None -> Error "")) + | _ -> ( + (* pure Upper path *) + match rev_segs with + | [] -> Error "" + | _ -> + let path = longident_of_segments (List.rev rev_segs) in + let loc = mk_loc start_pos last_end in + Ok {txt = Parsetree.JsxUpperTag path; loc}) + in + (* seed with the first segment already consumed *) + loop [first_seg] first_loc.Location.loc_end (* Parses module identifiers: Foo Foo.Bar *) -let parse_module_long_ident ~lowercase ?(is_jsx_name : bool = false) p = +let parse_module_long_ident ~lowercase p = (* Parser.leaveBreadcrumb p Reporting.ModuleLongIdent; *) let start_pos = p.Parser.start_pos in let module_ident = @@ -777,8 +862,6 @@ let parse_module_long_ident ~lowercase ?(is_jsx_name : bool = false) p = match p.Parser.token with | Dot -> Parser.next p; - if is_jsx_name then - parse_jsx_ident p |> Option.iter (fun t -> p.Parser.token <- t); parse_module_long_ident_tail ~lowercase p start_pos lident | _ -> Location.mkloc lident (mk_loc start_pos end_pos)) | t -> @@ -788,25 +871,6 @@ let parse_module_long_ident ~lowercase ?(is_jsx_name : bool = false) p = (* Parser.eatBreadcrumb p; *) module_ident -let verify_jsx_opening_closing_name p - (name_longident : Longident.t Location.loc) : bool = - let closing = - match p.Parser.token with - | Lident lident -> - Parser.next p; - Longident.Lident lident - | Uident _ -> - (parse_module_long_ident ~lowercase:true ~is_jsx_name:true p).txt - | _ -> Longident.Lident "" - in - let opening = name_longident.txt in - opening = closing - -let string_of_longident (longindent : Longident.t Location.loc) = - Longident.flatten longindent.txt - (* |> List.filter (fun s -> s <> "createElement") *) - |> String.concat "." - (* open-def ::= * | open module-path * | open! module-path *) @@ -2584,28 +2648,16 @@ and parse_let_bindings ~attrs ~start_pos p = in (rec_flag, loop p [first]) -and parse_jsx_name p : Longident.t Location.loc = - (* jsx allows for `-` token in the name, we need to combine some tokens *) - parse_jsx_ident p |> Option.iter (fun t -> p.Parser.token <- t); - match p.Parser.token with - | Lident ident -> - let ident_start = p.start_pos in - let ident_end = p.end_pos in - Parser.next p; - let loc = mk_loc ident_start ident_end in - Location.mkloc (Longident.Lident ident) loc - | Uident _ -> - let longident = - parse_module_long_ident ~lowercase:true ~is_jsx_name:true p - in - longident - | _ -> +and parse_jsx_name p : Parsetree.jsx_tag_name Location.loc = + match read_jsx_tag_name p with + | Ok name -> name + | Error invalid_str -> let msg = "A jsx name must be a lowercase or uppercase name, like: div in
\ or Navbar in " in Parser.err p (Diagnostics.message msg); - Location.mknoloc (Longident.Lident "_") + {txt = Parsetree.JsxTagInvalid invalid_str; loc = Location.none} and parse_jsx_opening_or_self_closing_element (* start of the opening < *) ~start_pos p : Parsetree.expression = @@ -2639,42 +2691,91 @@ and parse_jsx_opening_or_self_closing_element (* start of the opening < *) (Diagnostics.message "Did you forget a ` Option.iter (fun t -> p.Parser.token <- t); - match p.Parser.token with - | (Lident _ | Uident _) when verify_jsx_opening_closing_name p name -> - let end_tag_name = {name with loc = mk_loc p.start_pos p.end_pos} in - let closing_tag_end = p.start_pos in - Parser.expect GreaterThan p; - let loc = mk_loc start_pos p.prev_end_pos in - let closing_tag = - closing_tag_start - |> Option.map (fun closing_tag_start -> - { - Parsetree.jsx_closing_container_tag_start = closing_tag_start; - jsx_closing_container_tag_name = end_tag_name; - jsx_closing_container_tag_end = closing_tag_end; - }) - in - - Ast_helper.Exp.jsx_container_element ~loc name jsx_props opening_tag_end - children closing_tag + (* Read the closing tag name and verify it matches the opening name *) + let token0 = p.Parser.token in + match token0 with + | Lident _ | Uident _ -> ( + (* Consume the closing name without mutating tokens beforehand *) + let closing_name_res = read_jsx_tag_name p in + match closing_name_res with + | Ok closing_name + when Ast_helper.Jsx.longident_of_jsx_tag_name closing_name.txt + = Ast_helper.Jsx.longident_of_jsx_tag_name name.txt -> + let end_tag_name = closing_name in + let closing_tag_end = p.start_pos in + Parser.expect GreaterThan p; + let loc = mk_loc start_pos p.prev_end_pos in + let closing_tag = + closing_tag_start + |> Option.map (fun closing_tag_start -> + { + Parsetree.jsx_closing_container_tag_start = closing_tag_start; + jsx_closing_container_tag_name = end_tag_name; + jsx_closing_container_tag_end = closing_tag_end; + }) + in + Ast_helper.Exp.jsx_container_element ~loc name jsx_props opening_tag_end + children closing_tag + | _ -> + let () = + if Grammar.is_structure_item_start token0 then ( + let closing = + "" + in + let msg = Diagnostics.message ("Missing " ^ closing) in + Parser.err ~start_pos ~end_pos:p.prev_end_pos p msg; + (* We attempted to read a closing name; consume the '>' to keep AST shape stable *) + Parser.expect GreaterThan p) + else + let opening = + "" + in + let msg = + "Closing jsx name should be the same as the opening name. Did \ + you mean " ^ opening ^ " ?" + in + Parser.err ~start_pos ~end_pos:p.prev_end_pos p + (Diagnostics.message msg); + (* read_jsx_tag_name already consumed the name; expect the '>') *) + Parser.expect GreaterThan p + in + let end_tag_name = + match closing_name_res with + | Ok closing_name -> closing_name + | Error invalid_str -> + {txt = Parsetree.JsxTagInvalid invalid_str; loc = Location.none} + in + let closing_tag_end = p.prev_end_pos in + let closing_tag = + closing_tag_start + |> Option.map (fun closing_tag_start -> + { + Parsetree.jsx_closing_container_tag_start = closing_tag_start; + jsx_closing_container_tag_name = end_tag_name; + jsx_closing_container_tag_end = closing_tag_end; + }) + in + Ast_helper.Exp.jsx_container_element + ~loc:(mk_loc start_pos p.prev_end_pos) + name jsx_props opening_tag_end children closing_tag) | token -> let () = if Grammar.is_structure_item_start token then - let closing = "" in + let closing = + "" + in let msg = Diagnostics.message ("Missing " ^ closing) in Parser.err ~start_pos ~end_pos:p.prev_end_pos p msg else - let opening = "" in + let opening = + "" + in let msg = "Closing jsx name should be the same as the opening name. Did you \ mean " ^ opening ^ " ?" in Parser.err ~start_pos ~end_pos:p.prev_end_pos p - (Diagnostics.message msg); - Parser.expect GreaterThan p + (Diagnostics.message msg) in Ast_helper.Exp.jsx_container_element ~loc:(mk_loc start_pos p.prev_end_pos) @@ -2705,8 +2806,10 @@ and parse_jsx p = (* fragment: <> foo *) parse_jsx_fragment start_pos p | _ -> - let longident = parse_jsx_name p in - Ast_helper.Exp.ident ~loc:longident.loc longident + let tag_name = parse_jsx_name p in + let (loc : Location.t) = tag_name.loc in + let lid = Ast_helper.Jsx.longident_of_jsx_tag_name tag_name.txt in + Ast_helper.Exp.ident ~loc (Location.mkloc lid loc) in Parser.eat_breadcrumb p; jsx_expr @@ -2739,23 +2842,25 @@ and parse_jsx_fragment start_pos p = * | {...jsx_expr} *) and parse_jsx_prop p : Parsetree.jsx_prop option = - (* prop can have `-`, we need to combine some tokens into a single ident *) - parse_jsx_ident p |> Option.iter (fun t -> p.Parser.token <- t); match p.Parser.token with | Question | Lident _ -> ( let optional = Parser.optional p Question in - let name, loc = parse_lident p in - (* optional punning: *) - if optional then Some (Parsetree.JSXPropPunning (true, {txt = name; loc})) - else - match p.Parser.token with - | Equal -> - Parser.next p; - (* no punning *) - let optional = Parser.optional p Question in - let attr_expr = parse_primary_expr ~operand:(parse_atomic_expr p) p in - Some (Parsetree.JSXPropValue ({txt = name; loc}, optional, attr_expr)) - | _ -> Some (Parsetree.JSXPropPunning (false, {txt = name; loc}))) + (* allow hyphens inside prop names by reading a local jsx name *) + match read_local_jsx_name p with + | Some (name, loc, `Lower) -> ( + if optional then Some (Parsetree.JSXPropPunning (true, {txt = name; loc})) + else + match p.Parser.token with + | Equal -> + Parser.next p; + let optional = Parser.optional p Question in + let attr_expr = parse_primary_expr ~operand:(parse_atomic_expr p) p in + Some (Parsetree.JSXPropValue ({txt = name; loc}, optional, attr_expr)) + | _ -> Some (Parsetree.JSXPropPunning (false, {txt = name; loc}))) + | Some (_name, _loc, `Upper) -> + Parser.err p (Diagnostics.message "JSX prop names must be lowercase"); + None + | None -> None) (* {...props} *) | Lbrace -> ( let spread_start = p.Parser.start_pos in diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 845cf31cde..a7c95cf5ee 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -4440,9 +4440,10 @@ and print_pexp_apply ~state expr cmt_tbl = | _ -> assert false and print_jsx_unary_tag ~state tag_name props expr_loc cmt_tbl = - let name = print_jsx_name tag_name in + let name = print_jsx_name tag_name.txt in let formatted_props = print_jsx_props ~state props cmt_tbl in - let tag_has_trailing_comment = has_trailing_comments cmt_tbl tag_name.loc in + let tag_loc = tag_name.loc in + let tag_has_trailing_comment = has_trailing_comments cmt_tbl tag_loc in let tag_has_no_props = List.length props == 0 in let closing_token_loc = ParsetreeViewer.unary_element_closing_token expr_loc @@ -4462,9 +4463,7 @@ and print_jsx_unary_tag ~state tag_name props expr_loc cmt_tbl = ] in let opening_tag = - print_comments - (Doc.concat [Doc.less_than; name]) - cmt_tbl tag_name.Asttypes.loc + print_comments (Doc.concat [Doc.less_than; name]) cmt_tbl tag_loc in let opening_tag_doc = if tag_has_trailing_comment && not tag_has_no_props then @@ -4489,7 +4488,7 @@ and print_jsx_container_tag ~state tag_name (children : Parsetree.jsx_children) (closing_tag : Parsetree.jsx_closing_container_tag option) (pexp_loc : Location.t) cmt_tbl = - let name = print_jsx_name tag_name in + let name = print_jsx_name tag_name.txt in let last_prop_has_comment_after = let rec visit props = match props with @@ -4543,8 +4542,11 @@ and print_jsx_container_tag ~state tag_name let closing_tag_loc = ParsetreeViewer.container_element_closing_tag_loc closing_tag in + let closing_name = + print_jsx_name closing_tag.jsx_closing_container_tag_name.txt + in print_comments - (Doc.concat [Doc.text " - - - We need to force a newline. - *) - (if - has_trailing_single_line_comment cmt_tbl - tag_name.Asttypes.loc - then Doc.concat [Doc.hard_line; opening_greater_than_doc] - (* - if the last prop has trailing comment - - - - - or there are leading comments before `>` - - - - then put > on the next line - *) - else if - last_prop_has_comment_after - || opening_greater_than_has_leading_comments - then Doc.concat [Doc.soft_line; opening_greater_than_doc] - else opening_greater_than_doc); + (* Opening tag name and props *) + (let tag_loc = tag_name.loc in + + let opening_tag_name_doc = + print_comments + (Doc.concat [Doc.less_than; name]) + cmt_tbl tag_loc + in + let props_block_doc = + if List.length formatted_props == 0 then Doc.nil + else + Doc.indent + (Doc.concat + [ + Doc.line; + Doc.group (Doc.join formatted_props ~sep:Doc.line); + ]) + in + let after_name_and_props_doc = + (* if the element name has a single comment on the same line, force newline before '>' *) + if has_trailing_single_line_comment cmt_tbl tag_loc then + Doc.concat [Doc.hard_line; opening_greater_than_doc] + else if + last_prop_has_comment_after + || opening_greater_than_has_leading_comments + then Doc.concat [Doc.soft_line; opening_greater_than_doc] + else opening_greater_than_doc + in + Doc.concat + [ + opening_tag_name_doc; + props_block_doc; + after_name_and_props_doc; + ]); ]); Doc.concat [ @@ -4783,19 +4773,26 @@ and print_jsx_prop ~state prop cmt_tbl = and print_jsx_props ~state props cmt_tbl : Doc.t list = props |> List.map (fun prop -> print_jsx_prop ~state prop cmt_tbl) -and print_jsx_name {txt = lident} = - let print_ident = print_ident_like ~allow_uident:true ~allow_hyphen:true in - let rec flatten acc lident = - match lident with - | Longident.Lident txt -> print_ident txt :: acc - | Ldot (lident, txt) -> flatten (print_ident txt :: acc) lident - | _ -> acc - in - match lident with - | Longident.Lident txt -> print_ident txt - | _ as lident -> - let segments = flatten [] lident in - Doc.join ~sep:Doc.dot segments +and print_jsx_name (tag_name : Parsetree.jsx_tag_name) = + match tag_name with + | Parsetree.JsxTagInvalid invalid -> + (* Preserve exactly what the parser recorded as invalid *) + Doc.text invalid + | Parsetree.JsxLowerTag name -> + print_ident_like ~allow_uident:true ~allow_hyphen:true name + | Parsetree.JsxQualifiedLowerTag {path; name} -> + let upper_segs = Longident.flatten path in + let printed_upper = + upper_segs |> List.map (print_ident_like ~allow_uident:true) + in + let printed_lower = + print_ident_like ~allow_uident:true ~allow_hyphen:true name + in + Doc.join ~sep:Doc.dot (printed_upper @ [printed_lower]) + | Parsetree.JsxUpperTag path -> + let segs = Longident.flatten path in + let printed = segs |> List.map (print_ident_like ~allow_uident:true) in + Doc.join ~sep:Doc.dot printed and print_arguments_with_callback_in_first_position ~state ~partial args cmt_tbl = diff --git a/tests/analysis_tests/tests/src/expected/Completion.res.txt b/tests/analysis_tests/tests/src/expected/Completion.res.txt index 3361871351..2f8e8231ed 100644 --- a/tests/analysis_tests/tests/src/expected/Completion.res.txt +++ b/tests/analysis_tests/tests/src/expected/Completion.res.txt @@ -1065,19 +1065,8 @@ Path Objects.object Complete src/Completion.res 151:6 posCursor:[151:6] posNoWhite:[151:5] Found expr:[151:3->151:6] -JSX 151:6] > _children:None -Completable: Cpath Module[O, ""] -Package opens Stdlib.place holder Pervasives.JsxModules.place holder -Resolved opens 1 Stdlib -ContextPath Module[O, ""] -Path O. -[{ - "label": "Comp", - "kind": 9, - "tags": [], - "detail": "module Comp", - "documentation": null - }] +JSX 0:-1] > _children:None +[] Complete src/Completion.res 157:8 posCursor:[157:8] posNoWhite:[157:7] Found expr:[157:3->157:8] diff --git a/tests/syntax_tests/data/parsing/errors/expressions/expected/jsx.res.txt b/tests/syntax_tests/data/parsing/errors/expressions/expected/jsx.res.txt index dd4bdcf6b7..67191bd9ca 100644 --- a/tests/syntax_tests/data/parsing/errors/expressions/expected/jsx.res.txt +++ b/tests/syntax_tests/data/parsing/errors/expressions/expected/jsx.res.txt @@ -89,15 +89,39 @@ 10 │ let x = 11 │ let x = 12 │ + 13 │ // Trailing dots in tag names JSX identifier cannot end with a hyphen + + Syntax error! + syntax_tests/data/parsing/errors/expressions/jsx.res:14:15 + + 12 │ + 13 │ // Trailing dots in tag names + 14 │ let x = + 15 │ let x = + + expected identifier after '.' in JSX tag name + + + Syntax error! + syntax_tests/data/parsing/errors/expressions/jsx.res:15:20 + + 13 │ // Trailing dots in tag names + 14 │ let x = + 15 │ let x = + + expected identifier after '.' in JSX tag name + let x = -let x = -let x = > ([%rescript.exprhole ]) -let x = > ([%rescript.exprhole ]) -let x = > ([%rescript.exprhole ]) +let x = +let x = +let x = +let x = let x = -let x = -let x = -let x = \ No newline at end of file +let x = +let x = +let x = +let x = +let x = \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/errors/expressions/jsx.res b/tests/syntax_tests/data/parsing/errors/expressions/jsx.res index f27b5d3ebf..1be9e395d3 100644 --- a/tests/syntax_tests/data/parsing/errors/expressions/jsx.res +++ b/tests/syntax_tests/data/parsing/errors/expressions/jsx.res @@ -9,3 +9,7 @@ let x = let x = let x = let x = + +// Trailing dots in tag names +let x = +let x = \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/infiniteLoops/expected/jsxChildren.res.txt b/tests/syntax_tests/data/parsing/infiniteLoops/expected/jsxChildren.res.txt index bc2fbd3a60..04158f30aa 100644 --- a/tests/syntax_tests/data/parsing/infiniteLoops/expected/jsxChildren.res.txt +++ b/tests/syntax_tests/data/parsing/infiniteLoops/expected/jsxChildren.res.txt @@ -20,7 +20,7 @@ type nonrec action = | AddUser -;; +;; let (a : action) = AddUser {js|test|js} ;;etype ;;s = { x = ((list < i) > ([%rescript.exprhole ])) } \ No newline at end of file