From 697cb3388b705fcda21f9c91dc5f1d5a87c492b6 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 4 Oct 2025 08:23:02 +0200 Subject: [PATCH 1/9] JSX PPX: add Jsx.element return constraint --- compiler/syntax/src/jsx_v4.ml | 92 ++++++++++++++----- .../tests/src/CompletionJsx.res | 2 +- .../tests/src/CompletionPattern.res | 1 + .../tests/src/CreateInterface.res | 10 +- .../tests/src/expected/CodeLens.res.txt | 3 + .../src/expected/CompletionPattern.res.txt | 7 +- .../src/expected/CreateInterface.res.txt | 30 +++--- .../tests/src/expected/Hover.res.txt | 4 +- .../tests/src/expected/InlayHint.res.txt | 6 ++ .../tests/src/expected/Jsx2.resi.txt | 2 +- .../tests/src/expected/JsxV4.res.txt | 6 +- ...component_missing_jsx_element.res.expected | 13 +++ ...WithProps_missing_jsx_element.res.expected | 13 +++ ...component_missing_jsx_element.res.expected | 13 +++ ...orwardRef_missing_jsx_element.res.expected | 13 +++ ...ct_async_component_missing_jsx_element.res | 4 + ...componentWithProps_missing_jsx_element.res | 6 ++ .../react_component_missing_jsx_element.res | 4 + .../react_forwardRef_missing_jsx_element.res | 4 + .../src/Hooks.gen.tsx | 8 +- .../typescript-react-example/src/Hooks.res | 15 ++- .../typescript-react-example/src/Hooks.res.js | 36 +++----- .../ppx/react/expected/aliasProps.res.txt | 17 ++-- .../ppx/react/expected/asyncAwait.res.txt | 4 +- .../ppx/react/expected/commentAtTop.res.txt | 2 +- .../react/expected/defaultValueProp.res.txt | 8 +- .../expected/externalWithCustomName.res.txt | 2 +- .../react/expected/externalWithRef.res.txt | 2 +- .../externalWithTypeVariables.res.txt | 2 +- .../react/expected/fileLevelConfig.res.txt | 2 +- .../react/expected/firstClassModules.resi.txt | 2 +- .../ppx/react/expected/forwardRef.res.txt | 14 ++- .../ppx/react/expected/forwardRef.resi.txt | 16 ++-- .../data/ppx/react/expected/interface.res.txt | 4 +- .../ppx/react/expected/interface.resi.txt | 4 +- .../react/expected/interfaceWithRef.res.txt | 2 +- .../react/expected/interfaceWithRef.resi.txt | 2 +- .../ppx/react/expected/mangleKeyword.res.txt | 5 +- .../data/ppx/react/expected/nested.res.txt | 4 +- .../data/ppx/react/expected/newtype.res.txt | 13 +-- .../ppx/react/expected/noPropsWithKey.res.txt | 6 +- .../expected/optimizeAutomaticMode.res.txt | 2 +- .../react/expected/returnConstraint.res.txt | 49 ++++++++++ .../ppx/react/expected/sharedProps.res.txt | 16 ++-- .../ppx/react/expected/sharedProps.resi.txt | 16 ++-- .../expected/sharedPropsWithProps.res.txt | 28 +++--- .../data/ppx/react/expected/topLevel.res.txt | 2 +- .../ppx/react/expected/typeConstraint.res.txt | 2 +- .../ppx/react/expected/uncurriedProps.res.txt | 6 +- .../data/ppx/react/expected/v4.res.txt | 18 ++-- .../data/ppx/react/returnConstraint.res | 24 +++++ tests/tests/src/alias_default_value_test.res | 4 +- tests/tests/src/jsxv4_newtype.mjs | 2 +- tests/tests/src/jsxv4_newtype.res | 2 +- 54 files changed, 385 insertions(+), 189 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/react_async_component_missing_jsx_element.res.expected create mode 100644 tests/build_tests/super_errors/expected/react_componentWithProps_missing_jsx_element.res.expected create mode 100644 tests/build_tests/super_errors/expected/react_component_missing_jsx_element.res.expected create mode 100644 tests/build_tests/super_errors/expected/react_forwardRef_missing_jsx_element.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/react_async_component_missing_jsx_element.res create mode 100644 tests/build_tests/super_errors/fixtures/react_componentWithProps_missing_jsx_element.res create mode 100644 tests/build_tests/super_errors/fixtures/react_component_missing_jsx_element.res create mode 100644 tests/build_tests/super_errors/fixtures/react_forwardRef_missing_jsx_element.res create mode 100644 tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt create mode 100644 tests/syntax_tests/data/ppx/react/returnConstraint.res diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index f35e3014ab..af40718d48 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -46,6 +46,38 @@ let ref_type loc = {loc; txt = Ldot (Ldot (Lident "Js", "Nullable"), "t")} [ref_type_var loc] +let jsx_element_type ~loc = + Typ.constr ~loc {loc; txt = Ldot (Lident "Jsx", "element")} [] + +let jsx_element_constraint ~loc expr = + Exp.constraint_ ~loc expr (jsx_element_type ~loc) + +let rec constrain_jsx_return ~loc expr = + match expr.pexp_desc with + | Pexp_fun ({rhs} as desc) -> + { + expr with + pexp_desc = Pexp_fun {desc with rhs = constrain_jsx_return ~loc rhs}; + } + | Pexp_newtype (param, inner) -> + { + expr with + pexp_desc = Pexp_newtype (param, constrain_jsx_return ~loc inner); + } + | Pexp_constraint (inner, _) -> + jsx_element_constraint ~loc (constrain_jsx_return ~loc inner) + | Pexp_let (rec_flag, bindings, body) -> + { + expr with + pexp_desc = Pexp_let (rec_flag, bindings, constrain_jsx_return ~loc body); + } + | Pexp_sequence (first, second) -> + { + expr with + pexp_desc = Pexp_sequence (first, constrain_jsx_return ~loc second); + } + | _ -> jsx_element_constraint ~loc expr + let merlin_focus = ({loc = Location.none; txt = "merlin.focus"}, PStr []) (* Helper method to filter out any attribute that isn't [@react.component] *) @@ -713,6 +745,10 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = vb_match_expr named_arg_list expression else expression in + let expression = + Exp.constraint_ ~loc:binding_loc expression + (jsx_element_type ~loc:binding_loc) + in (* (ref) => expr *) let expression = List.fold_left @@ -784,6 +820,7 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = (Some props_record_type, binding, new_binding)) else if Jsx_common.has_attr_on_binding Jsx_common.has_attr_with_props binding then + let binding_loc = binding.pvb_loc in let modified_binding = { binding with @@ -839,21 +876,28 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = | _ -> Pat.var {txt = "props"; loc} in + let applied_expression = + Exp.apply + (Exp.ident + { + txt = + Lident + (match rec_flag with + | Recursive -> internal_fn_name + | Nonrecursive -> fn_name); + loc; + }) + [(Nolabel, Exp.ident {txt = Lident "props"; loc})] + in + let applied_expression = + Jsx_common.async_component ~async:is_async applied_expression + in + let applied_expression = + Exp.constraint_ ~loc applied_expression (jsx_element_type ~loc) + in let wrapper_expr = Exp.fun_ ~arity:None Nolabel None props_pattern - ~attrs:binding.pvb_expr.pexp_attributes - (Jsx_common.async_component ~async:is_async - (Exp.apply - (Exp.ident - { - txt = - Lident - (match rec_flag with - | Recursive -> internal_fn_name - | Nonrecursive -> fn_name); - loc; - }) - [(Nolabel, Exp.ident {txt = Lident "props"; loc})])) + ~attrs:binding.pvb_expr.pexp_attributes applied_expression in let wrapper_expr = Ast_uncurried.uncurried_fun ~arity:1 wrapper_expr in @@ -874,18 +918,20 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = | Recursive -> None | Nonrecursive -> Some - (make_new_binding ~loc:empty_loc ~full_module_name modified_binding) + (make_new_binding ~loc:binding_loc ~full_module_name modified_binding) in ( None, { binding with pvb_attributes = binding.pvb_attributes |> List.filter other_attrs_pure; pvb_expr = - { - binding.pvb_expr with - (* moved to wrapper_expr *) - pexp_attributes = []; - }; + ( binding.pvb_expr |> fun expr -> + { + expr with + (* moved to wrapper_expr *) + pexp_attributes = []; + } + |> constrain_jsx_return ~loc:binding_loc ); }, new_binding ) else (None, binding, None) @@ -934,7 +980,7 @@ let transform_structure_item ~config item = (arg.lbl, arg.attrs, return_value.ptyp_loc, arg.typ) :: types ) | _ -> (full_type, types) in - let inner_type, prop_types = get_prop_types [] pval_type in + let _, prop_types = get_prop_types [] pval_type in let named_type_list = List.fold_left arg_to_concrete_type [] prop_types in let ret_props_type = Typ.constr ~loc:pstr_loc @@ -955,7 +1001,7 @@ let transform_structure_item ~config item = let new_external_type = Ptyp_constr ( {loc = pstr_loc; txt = module_access_name config "componentLike"}, - [ret_props_type; inner_type] ) + [ret_props_type; jsx_element_type ~loc:pstr_loc] ) in let new_structure = { @@ -1046,7 +1092,7 @@ let transform_signature_item ~config item = (arg.lbl, arg.attrs, return_value.ptyp_loc, arg.typ) :: types ) | _ -> (full_type, types) in - let inner_type, prop_types = get_prop_types [] pval_type in + let _, prop_types = get_prop_types [] pval_type in let named_type_list = List.fold_left arg_to_concrete_type [] prop_types in let ret_props_type = Typ.constr @@ -1067,7 +1113,7 @@ let transform_signature_item ~config item = let new_external_type = Ptyp_constr ( {loc = psig_loc; txt = module_access_name config "componentLike"}, - [ret_props_type; inner_type] ) + [ret_props_type; jsx_element_type ~loc:psig_loc] ) in let new_structure = { diff --git a/tests/analysis_tests/tests/src/CompletionJsx.res b/tests/analysis_tests/tests/src/CompletionJsx.res index f2fd067d3c..655d61298d 100644 --- a/tests/analysis_tests/tests/src/CompletionJsx.res +++ b/tests/analysis_tests/tests/src/CompletionJsx.res @@ -67,7 +67,7 @@ module MultiPropComp = { @react.component let make = (~name, ~age, ~time: time) => { ignore(time) - name ++ age + React.string(name ++ age) } } diff --git a/tests/analysis_tests/tests/src/CompletionPattern.res b/tests/analysis_tests/tests/src/CompletionPattern.res index e3fe6d342e..e470dc85e7 100644 --- a/tests/analysis_tests/tests/src/CompletionPattern.res +++ b/tests/analysis_tests/tests/src/CompletionPattern.res @@ -229,6 +229,7 @@ let make = (~thing: result) => { // ^com | _ => () } + React.null } type results = { diff --git a/tests/analysis_tests/tests/src/CreateInterface.res b/tests/analysis_tests/tests/src/CreateInterface.res index 845067e1eb..f3b747b943 100644 --- a/tests/analysis_tests/tests/src/CreateInterface.res +++ b/tests/analysis_tests/tests/src/CreateInterface.res @@ -109,27 +109,27 @@ module type OptT = { module Opt = { @react.component - let withOpt1 = (~x=3, ~y) => x + y + let withOpt1 = (~x=3, ~y) => React.int(x + y) module Opt2 = { @react.component let withOpt2 = (~x: option=?, ~y: int) => - switch x { + React.int(switch x { | None => 0 | Some(x) => x } + - y + y) } module type Opt2 = module type of Opt2 module Opt3 = { @react.component let withOpt3 = (~x: option, ~y: int) => - switch x { + React.int(switch x { | None => 0 | Some(x) => x } + - y + y) } module type Opt3 = module type of Opt3 } diff --git a/tests/analysis_tests/tests/src/expected/CodeLens.res.txt b/tests/analysis_tests/tests/src/expected/CodeLens.res.txt index e75a286516..a31512458f 100644 --- a/tests/analysis_tests/tests/src/expected/CodeLens.res.txt +++ b/tests/analysis_tests/tests/src/expected/CodeLens.res.txt @@ -1,5 +1,8 @@ Code Lens src/CodeLens.res [{ + "range": {"start": {"line": 9, "character": 4}, "end": {"line": 9, "character": 8}}, + "command": {"title": "Jsx.element", "command": ""} + }, { "range": {"start": {"line": 4, "character": 4}, "end": {"line": 4, "character": 6}}, "command": {"title": "(~opt1: int=?, ~a: int, ~b: int, unit, ~opt2: int=?, unit, ~c: int) => int", "command": ""} }, { diff --git a/tests/analysis_tests/tests/src/expected/CompletionPattern.res.txt b/tests/analysis_tests/tests/src/expected/CompletionPattern.res.txt index eb7955b96f..3ee897359e 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionPattern.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionPattern.res.txt @@ -1178,7 +1178,8 @@ Path res }] Complete src/CompletionPattern.res 227:25 -posCursor:[227:25] posNoWhite:[227:24] Found expr:[223:11->231:1] +posCursor:[227:25] posNoWhite:[227:24] Found expr:[223:11->232:1] +posCursor:[227:25] posNoWhite:[227:24] Found expr:[224:2->231:12] posCursor:[227:25] posNoWhite:[227:24] Found expr:[226:4->227:28] posCursor:[227:25] posNoWhite:[227:24] Found pattern:[227:18->227:27] Completable: Cpattern Value[r]->recordBody @@ -1206,8 +1207,8 @@ Path r "documentation": {"kind": "markdown", "value": "```rescript\nnest: nestedRecord\n```\n\n```rescript\ntype someRecord = {first: int, second: (bool, option), optThird: option<[#first | #second(someRecord)]>, nest: nestedRecord}\n```"} }] -Complete src/CompletionPattern.res 242:33 -posCursor:[242:33] posNoWhite:[242:32] Found pattern:[242:7->242:35] +Complete src/CompletionPattern.res 243:33 +posCursor:[243:33] posNoWhite:[243:32] Found pattern:[243:7->243:35] Completable: Cpattern Value[hitsUse](Nolabel)->recordBody Package opens Stdlib.place holder Pervasives.JsxModules.place holder Resolved opens 1 Stdlib diff --git a/tests/analysis_tests/tests/src/expected/CreateInterface.res.txt b/tests/analysis_tests/tests/src/expected/CreateInterface.res.txt index 19567088a3..65abad9729 100644 --- a/tests/analysis_tests/tests/src/expected/CreateInterface.res.txt +++ b/tests/analysis_tests/tests/src/expected/CreateInterface.res.txt @@ -2,18 +2,18 @@ Create Interface src/CreateInterface.res type r = {name: string, age: int} let add: (~x: int, ~y: int) => int @react.component -let make: (~name: string) => React.element +let make: (~name: string) => Jsx.element module Other: { @react.component - let otherComponentName: (~name: string) => React.element + let otherComponentName: (~name: string) => Jsx.element } module Mod: { @react.component - let make: (~name: string) => React.element + let make: (~name: string) => Jsx.element } module type ModTyp = { @react.component - let make: (~name: string) => React.element + let make: (~name: string) => Jsx.element } @module("path") external dirname: string => string = "dirname" @module("path") @variadic @@ -49,21 +49,21 @@ module RFS: { module Functor: () => { @react.component - let make: unit => React.element + let make: unit => Jsx.element } module type FT = { module Functor: ( X: { let a: int @react.component - let make: (~name: string) => React.element + let make: (~name: string) => Jsx.element let b: int }, Y: ModTyp, ) => { @react.component - let make: (~name: string) => React.element + let make: (~name: string) => Jsx.element } } module NormaList = List @@ -73,34 +73,34 @@ module rec RM: ModTyp and D: ModTyp module type OptT = { @react.component - let withOpt1: (~x: int=?, ~y: int) => int + let withOpt1: (~x: int=?, ~y: int) => Jsx.element module type Opt2 = { @react.component - let withOpt2: (~x: int=?, ~y: int) => int + let withOpt2: (~x: int=?, ~y: int) => Jsx.element } module type Opt3 = { @react.component - let withOpt3: (~x: option, ~y: int) => int + let withOpt3: (~x: option, ~y: int) => Jsx.element } } module Opt: { @react.component - let withOpt1: (~x: int=?, ~y: int) => int + let withOpt1: (~x: int=?, ~y: int) => Jsx.element module Opt2: { @react.component - let withOpt2: (~x: int=?, ~y: int) => int + let withOpt2: (~x: int=?, ~y: int) => Jsx.element } module type Opt2 = { @react.component - let withOpt2: (~x: int=?, ~y: int) => int + let withOpt2: (~x: int=?, ~y: int) => Jsx.element } module Opt3: { @react.component - let withOpt3: (~x: option, ~y: int) => int + let withOpt3: (~x: option, ~y: int) => Jsx.element } module type Opt3 = { @react.component - let withOpt3: (~x: option, ~y: int) => int + let withOpt3: (~x: option, ~y: int) => Jsx.element } } module Opt2: OptT diff --git a/tests/analysis_tests/tests/src/expected/Hover.res.txt b/tests/analysis_tests/tests/src/expected/Hover.res.txt index f9f1746e15..dca7496ba6 100644 --- a/tests/analysis_tests/tests/src/expected/Hover.res.txt +++ b/tests/analysis_tests/tests/src/expected/Hover.res.txt @@ -22,10 +22,10 @@ Hover src/Hover.res 33:4 {"contents": {"kind": "markdown", "value": "```rescript\nunit => int\n```\n---\nDoc comment for functionWithTypeAnnotation"}} Hover src/Hover.res 37:13 -{"contents": {"kind": "markdown", "value": "```rescript\nstring\n```"}} +{"contents": {"kind": "markdown", "value": "```rescript\nJsx.element\n```\n\n---\n\n```\n \n```\n```rescript\ntype Jsx.element\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22Jsx.res%22%2C25%2C0%5D)\n"}} Hover src/Hover.res 42:15 -{"contents": {"kind": "markdown", "value": "```rescript\nstring\n```"}} +{"contents": {"kind": "markdown", "value": "```rescript\nJsx.element\n```\n\n---\n\n```\n \n```\n```rescript\ntype Jsx.element\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22Jsx.res%22%2C25%2C0%5D)\n"}} Hover src/Hover.res 46:10 {"contents": {"kind": "markdown", "value": "```rescript\nint\n```"}} diff --git a/tests/analysis_tests/tests/src/expected/InlayHint.res.txt b/tests/analysis_tests/tests/src/expected/InlayHint.res.txt index aad649db3b..606e7ba3ab 100644 --- a/tests/analysis_tests/tests/src/expected/InlayHint.res.txt +++ b/tests/analysis_tests/tests/src/expected/InlayHint.res.txt @@ -35,6 +35,12 @@ Inlay Hint src/InlayHint.res 1:34 "kind": 1, "paddingLeft": true, "paddingRight": false +}, { + "position": {"line": 14, "character": 8}, + "label": ": Jsx.element", + "kind": 1, + "paddingLeft": true, + "paddingRight": false }, { "position": {"line": 10, "character": 10}, "label": ": (~xx: int) => int", diff --git a/tests/analysis_tests/tests/src/expected/Jsx2.resi.txt b/tests/analysis_tests/tests/src/expected/Jsx2.resi.txt index c34ab6fca5..c03cade0d2 100644 --- a/tests/analysis_tests/tests/src/expected/Jsx2.resi.txt +++ b/tests/analysis_tests/tests/src/expected/Jsx2.resi.txt @@ -1,5 +1,5 @@ Hover src/Jsx2.resi 1:4 -{"contents": {"kind": "markdown", "value": "```rescript\nprops\n```\n\n---\n\n```\n \n```\n```rescript\ntype props<'first> = {first: 'first}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22Jsx2.resi%22%2C0%2C0%5D)\n"}} +{"contents": {"kind": "markdown", "value": "```rescript\nJsx.element\n```\n\n---\n\n```\n \n```\n```rescript\ntype Jsx.element\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22Jsx.res%22%2C25%2C0%5D)\n"}} Hover src/Jsx2.resi 4:4 {"contents": {"kind": "markdown", "value": "```rescript\nint\n```"}} diff --git a/tests/analysis_tests/tests/src/expected/JsxV4.res.txt b/tests/analysis_tests/tests/src/expected/JsxV4.res.txt index ea1e781cb4..cdbb72e5b4 100644 --- a/tests/analysis_tests/tests/src/expected/JsxV4.res.txt +++ b/tests/analysis_tests/tests/src/expected/JsxV4.res.txt @@ -22,14 +22,14 @@ Hover src/JsxV4.res 14:9 Create Interface src/JsxV4.res module M4: { @react.component - let make: (~first: string, ~fun: string=?, ~second: string=?) => React.element + let make: (~first: string, ~fun: string=?, ~second: string=?) => Jsx.element } module MM: { @react.component - let make: unit => React.element + let make: unit => Jsx.element } module Other: { @react.component - let make: (~name: string) => React.element + let make: (~name: string) => Jsx.element } diff --git a/tests/build_tests/super_errors/expected/react_async_component_missing_jsx_element.res.expected b/tests/build_tests/super_errors/expected/react_async_component_missing_jsx_element.res.expected new file mode 100644 index 0000000000..d6fabd9cc5 --- /dev/null +++ b/tests/build_tests/super_errors/expected/react_async_component_missing_jsx_element.res.expected @@ -0,0 +1,13 @@ + + We've found a bug for you! + /.../fixtures/react_async_component_missing_jsx_element.res:4:24 + + 2 │ + 3 │ @react.component + 4 │ let make = async () => 1 + 5 │ + + This has type: int + But it's expected to have type: Jsx.element + + In JSX, all content must be JSX elements. You can convert int to a JSX element with React.int. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/react_componentWithProps_missing_jsx_element.res.expected b/tests/build_tests/super_errors/expected/react_componentWithProps_missing_jsx_element.res.expected new file mode 100644 index 0000000000..68272490c4 --- /dev/null +++ b/tests/build_tests/super_errors/expected/react_componentWithProps_missing_jsx_element.res.expected @@ -0,0 +1,13 @@ + + We've found a bug for you! + /.../fixtures/react_componentWithProps_missing_jsx_element.res:6:35-45 + + 4 │ + 5 │ @react.componentWithProps + 6 │ let make = (props: props) => props.value + 7 │ + + This has type: int + But it's expected to have type: Jsx.element + + In JSX, all content must be JSX elements. You can convert int to a JSX element with React.int. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/react_component_missing_jsx_element.res.expected b/tests/build_tests/super_errors/expected/react_component_missing_jsx_element.res.expected new file mode 100644 index 0000000000..a5a7987b3a --- /dev/null +++ b/tests/build_tests/super_errors/expected/react_component_missing_jsx_element.res.expected @@ -0,0 +1,13 @@ + + We've found a bug for you! + /.../fixtures/react_component_missing_jsx_element.res:4:18 + + 2 │ + 3 │ @react.component + 4 │ let make = () => 1 + 5 │ + + This has type: int + But it's expected to have type: Jsx.element + + In JSX, all content must be JSX elements. You can convert int to a JSX element with React.int. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/react_forwardRef_missing_jsx_element.res.expected b/tests/build_tests/super_errors/expected/react_forwardRef_missing_jsx_element.res.expected new file mode 100644 index 0000000000..a26ebafe82 --- /dev/null +++ b/tests/build_tests/super_errors/expected/react_forwardRef_missing_jsx_element.res.expected @@ -0,0 +1,13 @@ + + We've found a bug for you! + /.../fixtures/react_forwardRef_missing_jsx_element.res:4:42 + + 2 │ + 3 │ @react.component + 4 │ let make = React.forwardRef((_, _ref) => 1) + 5 │ + + This has type: int + But it's expected to have type: Jsx.element + + In JSX, all content must be JSX elements. You can convert int to a JSX element with React.int. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/react_async_component_missing_jsx_element.res b/tests/build_tests/super_errors/fixtures/react_async_component_missing_jsx_element.res new file mode 100644 index 0000000000..ccba311a4e --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/react_async_component_missing_jsx_element.res @@ -0,0 +1,4 @@ +@@jsxConfig({version: 4}) + +@react.component +let make = async () => 1 diff --git a/tests/build_tests/super_errors/fixtures/react_componentWithProps_missing_jsx_element.res b/tests/build_tests/super_errors/fixtures/react_componentWithProps_missing_jsx_element.res new file mode 100644 index 0000000000..b99c35a7bc --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/react_componentWithProps_missing_jsx_element.res @@ -0,0 +1,6 @@ +@@jsxConfig({version: 4}) + +type props<'a> = {value: 'a} + +@react.componentWithProps +let make = (props: props) => props.value diff --git a/tests/build_tests/super_errors/fixtures/react_component_missing_jsx_element.res b/tests/build_tests/super_errors/fixtures/react_component_missing_jsx_element.res new file mode 100644 index 0000000000..d274fdc004 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/react_component_missing_jsx_element.res @@ -0,0 +1,4 @@ +@@jsxConfig({version: 4}) + +@react.component +let make = () => 1 diff --git a/tests/build_tests/super_errors/fixtures/react_forwardRef_missing_jsx_element.res b/tests/build_tests/super_errors/fixtures/react_forwardRef_missing_jsx_element.res new file mode 100644 index 0000000000..7bdc993b71 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/react_forwardRef_missing_jsx_element.res @@ -0,0 +1,4 @@ +@@jsxConfig({version: 4}) + +@react.component +let make = React.forwardRef((_, _ref) => 1) diff --git a/tests/gentype_tests/typescript-react-example/src/Hooks.gen.tsx b/tests/gentype_tests/typescript-react-example/src/Hooks.gen.tsx index 1d9a1e530f..3ebfd58803 100644 --- a/tests/gentype_tests/typescript-react-example/src/Hooks.gen.tsx +++ b/tests/gentype_tests/typescript-react-example/src/Hooks.gen.tsx @@ -31,7 +31,7 @@ export type WithRename_props = { readonly cb: cb }; -export type WithRef_props = { readonly vehicle: vehicle }; +export type WithRef_props = { readonly vehicle: vehicle; readonly ref?: ref }; export type r = { readonly x: string }; @@ -79,9 +79,7 @@ export const WithRename_componentWithRenamedArgs: React.ComponentType<{ readonly cb: cb }> = HooksJS.WithRename.componentWithRenamedArgs as any; -export const WithRef_makeWithRef: (_1:WithRef_props) => (_1:(null | undefined | any)) => JSX.Element = HooksJS.WithRef.makeWithRef as any; - -export const testForwardRef: React.ComponentType<{ readonly vehicle: vehicle }> = HooksJS.testForwardRef as any; +export const WithRef_make: React.ComponentType<{ readonly vehicle: vehicle; readonly ref?: any }> = HooksJS.WithRef.make as any; export const ForwardRef_input: (_1:r) => JSX.Element = HooksJS.ForwardRef.input as any; @@ -130,7 +128,7 @@ export const ForwardRef: { input: (_1:r) => JSX.Element } = HooksJS.ForwardRef a export const Fun: { functionReturningReactElement: React.ComponentType<{ readonly name: string }> } = HooksJS.Fun as any; -export const WithRef: { makeWithRef: (_1:WithRef_props) => (_1:(null | undefined | any)) => JSX.Element } = HooksJS.WithRef as any; +export const WithRef: { make: React.ComponentType<{ readonly vehicle: vehicle; readonly ref?: any }> } = HooksJS.WithRef as any; export const WithChildren: { aComponentWithChildren: React.ComponentType<{ readonly vehicle: vehicle; readonly children: React.ReactNode }> } = HooksJS.WithChildren as any; diff --git a/tests/gentype_tests/typescript-react-example/src/Hooks.res b/tests/gentype_tests/typescript-react-example/src/Hooks.res index 0fb4c048b0..05f93d0f25 100644 --- a/tests/gentype_tests/typescript-react-example/src/Hooks.res +++ b/tests/gentype_tests/typescript-react-example/src/Hooks.res @@ -81,18 +81,15 @@ module WithRename = { module WithRef = { @genType @react.component - let makeWithRef = (~vehicle) => { + let make = React.forwardRef((~vehicle, ref) => { let _ = 34 - ref => - switch ref->Js.Nullable.toOption { - | Some(ref) => - | None => React.null - } - } + switch ref->Js.Nullable.toOption { + | Some(ref) => + | None => React.null + } + }) } -@genType let testForwardRef = React.forwardRef((x, y) => WithRef.makeWithRef(x)(y)) - type r = {x: string} module ForwardRef = { diff --git a/tests/gentype_tests/typescript-react-example/src/Hooks.res.js b/tests/gentype_tests/typescript-react-example/src/Hooks.res.js index 2abbe0b568..4618007a52 100644 --- a/tests/gentype_tests/typescript-react-example/src/Hooks.res.js +++ b/tests/gentype_tests/typescript-react-example/src/Hooks.res.js @@ -124,28 +124,21 @@ let WithRename = { componentWithRenamedArgs: Hooks$WithRename$componentWithRenamedArgs }; -function makeWithRef(param) { - let vehicle = param.vehicle; - return ref => { - if (ref == null) { - return null; - } else { - return JsxRuntime.jsx("button", { - children: vehicle.name, - ref: Primitive_option.some(ref) - }); - } - }; -} - -let Hooks$WithRef$makeWithRef = makeWithRef; +let make = React.forwardRef((props, ref) => { + if (ref == null) { + return null; + } else { + return JsxRuntime.jsx("button", { + children: props.vehicle.name, + ref: Primitive_option.some(ref) + }); + } +}); let WithRef = { - makeWithRef: Hooks$WithRef$makeWithRef + make: make }; -let testForwardRef = React.forwardRef((x, y) => makeWithRef(x)(y)); - let input = React.forwardRef((r, ref) => JsxRuntime.jsx("div", { children: r.x, ref: Primitive_option.some(ref) @@ -207,12 +200,12 @@ let DD = { make: Hooks$DD }; -let make = Hooks; +let make$1 = Hooks; let $$default = Hooks; export { - make, + make$1 as make, $$default as default, Another, Inner, @@ -220,7 +213,6 @@ export { functionWithRenamedArgs, WithRename, WithRef, - testForwardRef, ForwardRef, Poly, Fun, @@ -228,4 +220,4 @@ export { WithChildren, DD, } -/* testForwardRef Not a pure module */ +/* make Not a pure module */ diff --git a/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt index e9d0ab30a8..05ab1afd85 100644 --- a/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt @@ -4,7 +4,7 @@ module C0 = { @res.jsxComponentProps type props<'priority, 'text> = {priority: 'priority, text?: 'text} - let make = ({priority: _, text: ?__text, _}: props<_, _>) => { + let make = ({priority: _, text: ?__text, _}: props<_, _>): Jsx.element => { let text = switch __text { | Some(text) => text | None => "Test" @@ -23,7 +23,7 @@ module C1 = { @res.jsxComponentProps type props<'priority, 'text> = {priority: 'priority, text?: 'text} - let make = ({priority: p, text: ?__text, _}: props<_, _>) => { + let make = ({priority: p, text: ?__text, _}: props<_, _>): Jsx.element => { let text = switch __text { | Some(text) => text | None => "Test" @@ -42,7 +42,7 @@ module C2 = { @res.jsxComponentProps type props<'foo> = {foo?: 'foo} - let make = ({foo: ?__bar, _}: props<_>) => { + let make = ({foo: ?__bar, _}: props<_>): Jsx.element => { let bar = switch __bar { | Some(foo) => foo | None => "" @@ -61,7 +61,7 @@ module C3 = { @res.jsxComponentProps type props<'foo, 'a, 'b> = {foo?: 'foo, a?: 'a, b: 'b} - let make = ({foo: ?__bar, a: ?__a, b, _}: props<_, _, _>) => { + let make = ({foo: ?__bar, a: ?__a, b, _}: props<_, _, _>): Jsx.element => { let bar = switch __bar { | Some(foo) => foo | None => "" @@ -86,7 +86,7 @@ module C4 = { @res.jsxComponentProps type props<'a, 'x> = {a: 'a, x?: 'x} - let make = ({a: b, x: ?__x, _}: props<_, _>) => { + let make = ({a: b, x: ?__x, _}: props<_, _>): Jsx.element => { let x = switch __x { | Some(x) => x | None => true @@ -105,7 +105,7 @@ module C5 = { @res.jsxComponentProps type props<'a, 'z> = {a: 'a, z?: 'z} - let make = ({a: (x, y), z: ?__z, _}: props<_, _>) => { + let make = ({a: (x, y), z: ?__z, _}: props<_, _>): Jsx.element => { let z = switch __z { | Some(z) => z | None => 3 @@ -125,12 +125,13 @@ module C6 = { @res.jsxComponentProps type props = {} - let make: React.componentLike + let make: React.componentLike } @res.jsxComponentProps type props<'comp, 'x> = {comp: 'comp, x: 'x} - let make = ({comp: module(Comp: Comp), x: (a, b), _}: props<_, _>) => React.jsx(Comp.make, {}) + let make = ({comp: module(Comp: Comp), x: (a, b), _}: props<_, _>): Jsx.element => + React.jsx(Comp.make, {}) let make = { let \"AliasProps$C6" = (props: props<_>) => make(props) diff --git a/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt b/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt index 66441d1f69..c7695b8192 100644 --- a/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt @@ -4,7 +4,7 @@ module C0 = { @res.jsxComponentProps type props<'a> = {a: 'a} - let make = async ({a, _}: props<_>) => { + let make = async ({a, _}: props<_>): Jsx.element => { let a = await f(a) ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.int(a)})}) } @@ -19,7 +19,7 @@ module C1 = { @res.jsxComponentProps type props<'status> = {status: 'status} - let make = async ({status, _}: props<_>) => { + let make = async ({status, _}: props<_>): Jsx.element => { switch status { | #on => React.string("on") | #off => React.string("off") diff --git a/tests/syntax_tests/data/ppx/react/expected/commentAtTop.res.txt b/tests/syntax_tests/data/ppx/react/expected/commentAtTop.res.txt index 3f4656ddda..e00dbc110b 100644 --- a/tests/syntax_tests/data/ppx/react/expected/commentAtTop.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/commentAtTop.res.txt @@ -1,7 +1,7 @@ @res.jsxComponentProps type props<'msg> = {msg: 'msg} // test React JSX file -let make = ({msg, _}: props<_>) => { +let make = ({msg, _}: props<_>): Jsx.element => { ReactDOM.jsx("div", {children: ?ReactDOM.someElement({msg->React.string})}) } let make = { diff --git a/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt b/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt index 5cb93d4681..129412ac0d 100644 --- a/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt @@ -1,7 +1,7 @@ module C0 = { @res.jsxComponentProps type props<'a, 'b> = {a?: 'a, b?: 'b} - let make = ({a: ?__a, b: ?__b, _}: props<_, _>) => { + let make = ({a: ?__a, b: ?__b, _}: props<_, _>): Jsx.element => { let a = switch __a { | Some(a) => a | None => 2 @@ -23,7 +23,7 @@ module C1 = { @res.jsxComponentProps type props<'a, 'b> = {a?: 'a, b: 'b} - let make = ({a: ?__a, b, _}: props<_, _>) => { + let make = ({a: ?__a, b, _}: props<_, _>): Jsx.element => { let a = switch __a { | Some(a) => a | None => 2 @@ -43,7 +43,7 @@ module C2 = { @res.jsxComponentProps type props<'a> = {a?: 'a} - let make = ({a: ?__a, _}: props<_>) => { + let make = ({a: ?__a, _}: props<_>): Jsx.element => { let a = switch __a { | Some(a) => a | None => a @@ -62,7 +62,7 @@ module C3 = { @res.jsxComponentProps type props<'disabled> = {disabled?: 'disabled} - let make = ({disabled: ?__everythingDisabled, _}: props) => { + let make = ({disabled: ?__everythingDisabled, _}: props): Jsx.element => { let everythingDisabled = switch __everythingDisabled { | Some(disabled) => disabled | None => false diff --git a/tests/syntax_tests/data/ppx/react/expected/externalWithCustomName.res.txt b/tests/syntax_tests/data/ppx/react/expected/externalWithCustomName.res.txt index c0ece33e20..529d5aac22 100644 --- a/tests/syntax_tests/data/ppx/react/expected/externalWithCustomName.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/externalWithCustomName.res.txt @@ -5,7 +5,7 @@ module Foo = { type props<'a, 'b> = {a: 'a, b: 'b} @module("Foo") - external component: React.componentLike, React.element> = "component" + external component: React.componentLike, Jsx.element> = "component" } let t = React.jsx(Foo.component, {a: 1, b: {"1"}}) diff --git a/tests/syntax_tests/data/ppx/react/expected/externalWithRef.res.txt b/tests/syntax_tests/data/ppx/react/expected/externalWithRef.res.txt index cce0f077f0..aa1566889e 100644 --- a/tests/syntax_tests/data/ppx/react/expected/externalWithRef.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/externalWithRef.res.txt @@ -8,6 +8,6 @@ module V4C = { } @module("componentForwardRef") - external make: React.componentLike, React.element> = + external make: React.componentLike, Jsx.element> = "component" } diff --git a/tests/syntax_tests/data/ppx/react/expected/externalWithTypeVariables.res.txt b/tests/syntax_tests/data/ppx/react/expected/externalWithTypeVariables.res.txt index 5d0966916d..774e52c953 100644 --- a/tests/syntax_tests/data/ppx/react/expected/externalWithTypeVariables.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/externalWithTypeVariables.res.txt @@ -8,5 +8,5 @@ module V4C = { } @module("c") - external make: React.componentLike, React.element>, React.element> = "component" + external make: React.componentLike, React.element>, Jsx.element> = "component" } diff --git a/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt b/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt index c11e849074..ed8d843ff5 100644 --- a/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt @@ -4,7 +4,7 @@ module V4A = { @res.jsxComponentProps type props<'msg> = {msg: 'msg} - let make = ({msg, _}: props<_>) => { + let make = ({msg, _}: props<_>): Jsx.element => { ReactDOM.jsx("div", {children: ?ReactDOM.someElement({msg->React.string})}) } let make = { diff --git a/tests/syntax_tests/data/ppx/react/expected/firstClassModules.resi.txt b/tests/syntax_tests/data/ppx/react/expected/firstClassModules.resi.txt index 556f6b9f0e..1779203db4 100644 --- a/tests/syntax_tests/data/ppx/react/expected/firstClassModules.resi.txt +++ b/tests/syntax_tests/data/ppx/react/expected/firstClassModules.resi.txt @@ -20,6 +20,6 @@ module Select: { option<'key> => unit, array<'a>, >, - React.element, + Jsx.element, > } diff --git a/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt b/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt index 9b8b541317..d756dd7085 100644 --- a/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt @@ -9,7 +9,10 @@ module V4A = { ref?: 'ref, } - let make = ({?className, children, _}: props<_, _, 'ref>, ref: Js.Nullable.t<'ref>) => + let make = ( + {?className, children, _}: props<_, _, 'ref>, + ref: Js.Nullable.t<'ref>, + ): Jsx.element => ReactDOM.jsxs( "div", { @@ -35,7 +38,7 @@ module V4A = { @res.jsxComponentProps type props = {} - let make = (_: props) => { + let make = (_: props): Jsx.element => { let input = React.useRef(Js.Nullable.null) ReactDOM.jsx( @@ -63,7 +66,10 @@ module V4AUncurried = { ref?: 'ref, } - let make = ({?className, children, _}: props<_, _, 'ref>, ref: Js.Nullable.t<'ref>) => + let make = ( + {?className, children, _}: props<_, _, 'ref>, + ref: Js.Nullable.t<'ref>, + ): Jsx.element => ReactDOM.jsxs( "div", { @@ -89,7 +95,7 @@ module V4AUncurried = { @res.jsxComponentProps type props = {} - let make = (_: props) => { + let make = (_: props): Jsx.element => { let input = React.useRef(Js.Nullable.null) ReactDOM.jsx( diff --git a/tests/syntax_tests/data/ppx/react/expected/forwardRef.resi.txt b/tests/syntax_tests/data/ppx/react/expected/forwardRef.resi.txt index e0a81497aa..e76f5cef4c 100644 --- a/tests/syntax_tests/data/ppx/react/expected/forwardRef.resi.txt +++ b/tests/syntax_tests/data/ppx/react/expected/forwardRef.resi.txt @@ -11,7 +11,7 @@ module V4C: { let make: React.componentLike< props, - React.element, + Jsx.element, > } @@ -19,7 +19,7 @@ module V4C: { @res.jsxComponentProps type props<'ref> = {ref?: 'ref} - let make: React.componentLike>>, React.element> + let make: React.componentLike>>, Jsx.element> } } @@ -34,7 +34,7 @@ module V4CUncurried: { let make: React.componentLike< props, - React.element, + Jsx.element, > } @@ -42,7 +42,7 @@ module V4CUncurried: { @res.jsxComponentProps type props<'ref> = {ref?: 'ref} - let make: React.componentLike>>, React.element> + let make: React.componentLike>>, Jsx.element> } } @@ -59,7 +59,7 @@ module V4A: { let make: React.componentLike< props, - React.element, + Jsx.element, > } @@ -67,7 +67,7 @@ module V4A: { @res.jsxComponentProps type props<'ref> = {ref?: 'ref} - let make: React.componentLike>>, React.element> + let make: React.componentLike>>, Jsx.element> } } @@ -82,7 +82,7 @@ module V4AUncurried: { let make: React.componentLike< props, - React.element, + Jsx.element, > } @@ -90,6 +90,6 @@ module V4AUncurried: { @res.jsxComponentProps type props<'ref> = {ref?: 'ref} - let make: React.componentLike>>, React.element> + let make: React.componentLike>>, Jsx.element> } } diff --git a/tests/syntax_tests/data/ppx/react/expected/interface.res.txt b/tests/syntax_tests/data/ppx/react/expected/interface.res.txt index 645636451e..0c3e8dee9d 100644 --- a/tests/syntax_tests/data/ppx/react/expected/interface.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/interface.res.txt @@ -1,7 +1,7 @@ module A = { @res.jsxComponentProps type props<'x> = {x: 'x} - let make = ({x, _}: props<_>) => React.string(x) + let make = ({x, _}: props<_>): Jsx.element => React.string(x) let make = { let \"Interface$A" = (props: props<_>) => make(props) \"Interface$A" @@ -12,7 +12,7 @@ module NoProps = { @res.jsxComponentProps type props = {} - let make = (_: props) => ReactDOM.jsx("div", {}) + let make = (_: props): Jsx.element => ReactDOM.jsx("div", {}) let make = { let \"Interface$NoProps" = props => make(props) diff --git a/tests/syntax_tests/data/ppx/react/expected/interface.resi.txt b/tests/syntax_tests/data/ppx/react/expected/interface.resi.txt index 0f045c0beb..74bebac9f9 100644 --- a/tests/syntax_tests/data/ppx/react/expected/interface.resi.txt +++ b/tests/syntax_tests/data/ppx/react/expected/interface.resi.txt @@ -1,12 +1,12 @@ module A: { @res.jsxComponentProps type props<'x> = {x: 'x} - let make: React.componentLike, React.element> + let make: React.componentLike, Jsx.element> } module NoProps: { @res.jsxComponentProps type props = {} - let make: React.componentLike + let make: React.componentLike } diff --git a/tests/syntax_tests/data/ppx/react/expected/interfaceWithRef.res.txt b/tests/syntax_tests/data/ppx/react/expected/interfaceWithRef.res.txt index eb7616f71f..46269f71d3 100644 --- a/tests/syntax_tests/data/ppx/react/expected/interfaceWithRef.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/interfaceWithRef.res.txt @@ -2,7 +2,7 @@ let make = ( {x, _}: props, ref: Js.Nullable.t, -) => { +): Jsx.element => { let _ = ref->Js.Nullable.toOption->Belt.Option.map(ReactDOM.Ref.domRef) React.string(x) } diff --git a/tests/syntax_tests/data/ppx/react/expected/interfaceWithRef.resi.txt b/tests/syntax_tests/data/ppx/react/expected/interfaceWithRef.resi.txt index c28c75f9d9..49db0fd26d 100644 --- a/tests/syntax_tests/data/ppx/react/expected/interfaceWithRef.resi.txt +++ b/tests/syntax_tests/data/ppx/react/expected/interfaceWithRef.resi.txt @@ -1,2 +1,2 @@ @res.jsxComponentProps type props<'x, 'ref> = {x: 'x, ref?: 'ref} -let make: React.componentLike, React.element> +let make: React.componentLike, Jsx.element> diff --git a/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt b/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt index 4413b38798..ec684dac0d 100644 --- a/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt @@ -4,7 +4,8 @@ module C4A0 = { @res.jsxComponentProps type props<'T_open, 'T_type> = {@as("open") _open: 'T_open, @as("type") _type: 'T_type} - let make = ({@as("open") _open, @as("type") _type, _}: props<_, string>) => React.string(_open) + let make = ({@as("open") _open, @as("type") _type, _}: props<_, string>): Jsx.element => + React.string(_open) let make = { let \"MangleKeyword$C4A0" = (props: props<_>) => make(props) @@ -15,7 +16,7 @@ module C4A1 = { @res.jsxComponentProps @live type props<'T_open, 'T_type> = {@as("open") _open: 'T_open, @as("type") _type: 'T_type} - external make: React.componentLike, React.element> = "default" + external make: React.componentLike, Jsx.element> = "default" } let c4a0 = React.jsx(C4A0.make, {_open: "x", _type: "t"}) diff --git a/tests/syntax_tests/data/ppx/react/expected/nested.res.txt b/tests/syntax_tests/data/ppx/react/expected/nested.res.txt index 0775867aa9..56835c786c 100644 --- a/tests/syntax_tests/data/ppx/react/expected/nested.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/nested.res.txt @@ -1,12 +1,12 @@ module Outer = { @res.jsxComponentProps type props = {} - let make = (_: props) => { + let make = (_: props): Jsx.element => { module Inner = { @res.jsxComponentProps type props = {} - let make = (_: props) => ReactDOM.jsx("div", {}) + let make = (_: props): Jsx.element => ReactDOM.jsx("div", {}) let make = { let \"Nested$Outer" = props => make(props) diff --git a/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt b/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt index fb513bf122..1556c6b383 100644 --- a/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt @@ -4,7 +4,7 @@ module V4A = { @res.jsxComponentProps type props<'a, 'b, 'c> = {a: 'a, b: 'b, c: 'c} - let make = (type a, {a, b, c, _}: props>, 'a>) => + let make = (type a, {a, b, c, _}: props>, 'a>): Jsx.element => ReactDOM.jsx("div", {}) let make = { let \"Newtype$V4A" = (props: props<_>) => make(props) @@ -17,7 +17,8 @@ module V4A1 = { @res.jsxComponentProps type props<'a, 'b, 'c> = {a: 'a, b: 'b, c: 'c} - let make = (type x y, {a, b, c, _}: props, 'a>) => ReactDOM.jsx("div", {}) + let make = (type x y, {a, b, c, _}: props, 'a>): Jsx.element => + ReactDOM.jsx("div", {}) let make = { let \"Newtype$V4A1" = (props: props<_>) => make(props) @@ -33,7 +34,7 @@ module V4A2 = { @res.jsxComponentProps type props<'foo> = {foo: 'foo} - let make = (type a, {foo: (foo: module(T with type t = a)), _}: props<_>) => { + let make = (type a, {foo: (foo: module(T with type t = a)), _}: props<_>): Jsx.element => { module T = unpack(foo) ReactDOM.jsx("div", {}) } @@ -48,7 +49,7 @@ module V4A3 = { @res.jsxComponentProps type props<'foo> = {foo: 'foo} - let make = (type a, {foo, _}: props<_>) => { + let make = (type a, {foo, _}: props<_>): Jsx.element => { module T = unpack(foo: T with type t = a) foo } @@ -61,7 +62,7 @@ module V4A3 = { @res.jsxComponentProps type props<'x, 'q> = {x: 'x, q: 'q} -let make = ({x, q, _}: props<('a, 'b), 'a>) => [fst(x), q] +let make = ({x, q, _}: props<('a, 'b), 'a>): Jsx.element => [fst(x), q] let make = { let \"Newtype" = (props: props<_>) => make(props) @@ -74,7 +75,7 @@ module Uncurried = { @res.jsxComponentProps type props<'foo> = {foo?: 'foo} - let make = (type a, {?foo, _}: props<_>) => React.null + let make = (type a, {?foo, _}: props<_>): Jsx.element => React.null let make = { let \"Newtype$Uncurried" = (props: props<_>) => make(props) diff --git a/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt b/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt index 21d9d0c417..e347f34fb7 100644 --- a/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt @@ -4,7 +4,7 @@ module V4CA = { @res.jsxComponentProps type props = {} - let make = (_: props) => ReactDOM.jsx("div", {}) + let make = (_: props): Jsx.element => ReactDOM.jsx("div", {}) let make = { let \"NoPropsWithKey$V4CA" = props => make(props) @@ -17,14 +17,14 @@ module V4CB = { type props = {} @module("c") - external make: React.componentLike = "component" + external make: React.componentLike = "component" } module V4C = { @res.jsxComponentProps type props = {} - let make = (_: props) => + let make = (_: props): Jsx.element => React.jsxs( React.jsxFragment, { diff --git a/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt b/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt index 19a34b8d05..1b51baa277 100644 --- a/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt @@ -7,7 +7,7 @@ module User = { @res.jsxComponentProps type props<'doctor> = {doctor: 'doctor} - let make = ({doctor, _}: props<_>) => { + let make = ({doctor, _}: props<_>): Jsx.element => { ReactDOM.jsx("h1", {id: "h1", children: ?ReactDOM.someElement({React.string(format(doctor))})}) } let make = { diff --git a/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt b/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt new file mode 100644 index 0000000000..538dc0d6a6 --- /dev/null +++ b/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt @@ -0,0 +1,49 @@ +@@jsxConfig({version: 4}) + +module Standard = { + @res.jsxComponentProps + type props = {} + + let make = (_: props): Jsx.element => React.string("ok") + let make = { + let \"ReturnConstraint$Standard" = props => make(props) + + \"ReturnConstraint$Standard" + } +} + +module ForwardRef = { + @res.jsxComponentProps + type props = {} + + let make = (_: props, _ref): Jsx.element => ReactDOM.jsx("div", {}) + let make = React.forwardRef({ + let \"ReturnConstraint$ForwardRef" = (props, ref) => make(props, ref) + + \"ReturnConstraint$ForwardRef" + }) +} + +module WithProps = { + type props = {value: int} + + let make = (props: props): Jsx.element => + ReactDOM.jsx("span", {children: ?ReactDOM.someElement({React.int(props.value)})}) + let make = { + let \"ReturnConstraint$WithProps" = (props: props): Jsx.element => make(props) + \"ReturnConstraint$WithProps" + } +} + +module Async = { + @res.jsxComponentProps + type props = {} + + let make = async (_: props): Jsx.element => + ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.string("async")})}) + let make = { + let \"ReturnConstraint$Async" = props => Jsx.promise(make(props)) + + \"ReturnConstraint$Async" + } +} diff --git a/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt index a878b7e544..27cb194cb5 100644 --- a/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt @@ -3,7 +3,7 @@ module V4A1 = { type props = sharedProps - let make = ({x, y, _}: props) => React.string(x ++ y) + let make = ({x, y, _}: props): Jsx.element => React.string(x ++ y) let make = { let \"SharedProps$V4A1" = props => make(props) @@ -14,7 +14,7 @@ module V4A1 = { module V4A2 = { type props<'a> = sharedProps<'a> - let make = ({x, y, _}: props<_>) => React.string(x ++ y) + let make = ({x, y, _}: props<_>): Jsx.element => React.string(x ++ y) let make = { let \"SharedProps$V4A2" = (props: props<_>) => make(props) @@ -25,7 +25,7 @@ module V4A2 = { module V4A3 = { type props<'a> = sharedProps - let make = ({x, y, _}: props<_>) => React.string(x ++ y) + let make = ({x, y, _}: props<_>): Jsx.element => React.string(x ++ y) let make = { let \"SharedProps$V4A3" = (props: props<_>) => make(props) @@ -36,7 +36,7 @@ module V4A3 = { module V4A4 = { type props = sharedProps - let make = ({x, y, _}: props) => React.string(x ++ y) + let make = ({x, y, _}: props): Jsx.element => React.string(x ++ y) let make = { let \"SharedProps$V4A4" = props => make(props) @@ -47,23 +47,23 @@ module V4A4 = { module V4A5 = { type props = sharedProps - external make: React.componentLike = "default" + external make: React.componentLike = "default" } module V4A6 = { type props<'a> = sharedProps<'a> - external make: React.componentLike, React.element> = "default" + external make: React.componentLike, Jsx.element> = "default" } module V4A7 = { type props<'a> = sharedProps - external make: React.componentLike, React.element> = "default" + external make: React.componentLike, Jsx.element> = "default" } module V4A8 = { type props = sharedProps - external make: React.componentLike = "default" + external make: React.componentLike = "default" } diff --git a/tests/syntax_tests/data/ppx/react/expected/sharedProps.resi.txt b/tests/syntax_tests/data/ppx/react/expected/sharedProps.resi.txt index e87f202e9f..621cccc8ab 100644 --- a/tests/syntax_tests/data/ppx/react/expected/sharedProps.resi.txt +++ b/tests/syntax_tests/data/ppx/react/expected/sharedProps.resi.txt @@ -3,25 +3,25 @@ module V4C1: { type props = sharedProps - let make: React.componentLike + let make: React.componentLike } module V4C2: { type props<'a> = sharedProps<'a> - let make: React.componentLike, React.element> + let make: React.componentLike, Jsx.element> } module V4C3: { type props<'a> = sharedProps - let make: React.componentLike, React.element> + let make: React.componentLike, Jsx.element> } module V4C4: { type props = sharedProps - let make: React.componentLike + let make: React.componentLike } @@jsxConfig({version: 4, mode: "automatic"}) @@ -29,23 +29,23 @@ module V4C4: { module V4A1: { type props = sharedProps - let make: React.componentLike + let make: React.componentLike } module V4A2: { type props<'a> = sharedProps<'a> - let make: React.componentLike, React.element> + let make: React.componentLike, Jsx.element> } module V4A3: { type props<'a> = sharedProps - let make: React.componentLike, React.element> + let make: React.componentLike, Jsx.element> } module V4A4: { type props = sharedProps - let make: React.componentLike + let make: React.componentLike } diff --git a/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt index 72072f8985..db42afbac9 100644 --- a/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt @@ -4,36 +4,36 @@ let f = a => Js.Promise.resolve(a + a) module V4A1 = { type props = sharedProps - let make = props => React.string(props.x ++ props.y) + let make = (props): Jsx.element => React.string(props.x ++ props.y) let make = { - let \"SharedPropsWithProps$V4A1" = props => make(props) + let \"SharedPropsWithProps$V4A1" = (props): Jsx.element => make(props) \"SharedPropsWithProps$V4A1" } } module V4A2 = { type props = sharedProps - let make = (props: props) => React.string(props.x ++ props.y) + let make = (props: props): Jsx.element => React.string(props.x ++ props.y) let make = { - let \"SharedPropsWithProps$V4A2" = (props: props) => make(props) + let \"SharedPropsWithProps$V4A2" = (props: props): Jsx.element => make(props) \"SharedPropsWithProps$V4A2" } } module V4A3 = { type props<'a> = sharedProps<'a> - let make = ({x, y}: props<_>) => React.string(x ++ y) + let make = ({x, y}: props<_>): Jsx.element => React.string(x ++ y) let make = { - let \"SharedPropsWithProps$V4A3" = (props: props<_>) => make(props) + let \"SharedPropsWithProps$V4A3" = (props: props<_>): Jsx.element => make(props) \"SharedPropsWithProps$V4A3" } } module V4A4 = { type props<'a> = sharedProps - let make = ({x, y}: props<_>) => React.string(x ++ y) + let make = ({x, y}: props<_>): Jsx.element => React.string(x ++ y) let make = { - let \"SharedPropsWithProps$V4A4" = (props: props<_>) => make(props) + let \"SharedPropsWithProps$V4A4" = (props: props<_>): Jsx.element => make(props) \"SharedPropsWithProps$V4A4" } } @@ -42,24 +42,24 @@ module V4A5 = { type props<'a> = {a: 'a} let make = async ({a}: props<_>) => { let a = await f(a) - ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.int(a)})}) + (ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.int(a)})}): Jsx.element) } let make = { - let \"SharedPropsWithProps$V4A5" = (props: props<_>) => Jsx.promise(make(props)) + let \"SharedPropsWithProps$V4A5" = (props: props<_>): Jsx.element => Jsx.promise(make(props)) \"SharedPropsWithProps$V4A5" } } module V4A6 = { type props<'status> = {status: 'status} - let make = async ({status}: props<_>) => { + let make = async ({status}: props<_>): Jsx.element => { switch status { | #on => React.string("on") | #off => React.string("off") } } let make = { - let \"SharedPropsWithProps$V4A6" = (props: props<_>) => Jsx.promise(make(props)) + let \"SharedPropsWithProps$V4A6" = (props: props<_>): Jsx.element => Jsx.promise(make(props)) \"SharedPropsWithProps$V4A6" } } @@ -67,11 +67,11 @@ module V4A6 = { module V4A7 = { type props = {count: int} - let make = props => { + let make = (props): Jsx.element => { React.int(props.count) } let make = { - let \"SharedPropsWithProps$V4A7" = @directive("'use memo'") props => make(props) + let \"SharedPropsWithProps$V4A7" = @directive("'use memo'") (props): Jsx.element => make(props) \"SharedPropsWithProps$V4A7" } } diff --git a/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt b/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt index cbb6a01b6b..d3524f22b2 100644 --- a/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt @@ -4,7 +4,7 @@ module V4A = { @res.jsxComponentProps type props<'a, 'b> = {a: 'a, b: 'b} - let make = ({a, b, _}: props<_, _>) => { + let make = ({a, b, _}: props<_, _>): Jsx.element => { Js.log("This function should be named 'TopLevel.react'") ReactDOM.jsx("div", {}) } diff --git a/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt b/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt index c612921f06..d6464ac318 100644 --- a/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt @@ -4,7 +4,7 @@ module V4A = { @res.jsxComponentProps type props<'a, 'b> = {a: 'a, b: 'b} - let make = (type a, {a, b, _}: props<_, _>) => ReactDOM.jsx("div", {}) + let make = (type a, {a, b, _}: props<_, _>): Jsx.element => ReactDOM.jsx("div", {}) let make = { let \"TypeConstraint$V4A" = (props: props<_>) => make(props) diff --git a/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt index 3cd05a73c2..6770e5517a 100644 --- a/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt @@ -2,7 +2,7 @@ @res.jsxComponentProps type props<'a> = {a?: 'a} -let make = ({a: ?__a, _}: props unit>) => { +let make = ({a: ?__a, _}: props unit>): Jsx.element => { let a = switch __a { | Some(a) => a | None => () => () @@ -30,7 +30,7 @@ module Foo = { @res.jsxComponentProps type props<'callback> = {callback?: 'callback} - let make = ({callback: ?__callback, _}: props<(string, bool, bool) => unit>) => { + let make = ({callback: ?__callback, _}: props<(string, bool, bool) => unit>): Jsx.element => { let callback = switch __callback { | Some(callback) => callback | None => (_, _, _) => () @@ -51,7 +51,7 @@ module Bar = { @res.jsxComponentProps type props = {} - let make = (_: props) => React.jsx(Foo.make, {callback: {(_, _, _) => ()}}) + let make = (_: props): Jsx.element => React.jsx(Foo.make, {callback: {(_, _, _) => ()}}) let make = { let \"UncurriedProps$Bar" = props => make(props) diff --git a/tests/syntax_tests/data/ppx/react/expected/v4.res.txt b/tests/syntax_tests/data/ppx/react/expected/v4.res.txt index 001501391f..474c0feb80 100644 --- a/tests/syntax_tests/data/ppx/react/expected/v4.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/v4.res.txt @@ -1,6 +1,6 @@ @res.jsxComponentProps type props<'x, 'y> = {x: 'x, y: 'y} // Component with type constraint -let make = ({x, y, _}: props) => React.string(x ++ y) +let make = ({x, y, _}: props): Jsx.element => React.string(x ++ y) let make = { let \"V4" = (props: props<_>) => make(props) \"V4" @@ -11,7 +11,7 @@ module AnotherName = { type // Component with another name than "make" props<'x> = {x: 'x} - let anotherName = ({x, _}: props<_>) => React.string(x) + let anotherName = ({x, _}: props<_>): Jsx.element => React.string(x) let anotherName = { let \"V4$AnotherName$anotherName" = (props: props<_>) => anotherName(props) @@ -23,7 +23,7 @@ module Uncurried = { @res.jsxComponentProps type props<'x> = {x: 'x} - let make = ({x, _}: props<_>) => React.string(x) + let make = ({x, _}: props<_>): Jsx.element => React.string(x) let make = { let \"V4$Uncurried" = (props: props<_>) => make(props) @@ -35,21 +35,21 @@ module type TUncurried = { @res.jsxComponentProps type props<'x> = {x: 'x} - let make: React.componentLike, React.element> + let make: React.componentLike, Jsx.element> } module E = { @res.jsxComponentProps @live type props<'x> = {x: 'x} - external make: React.componentLike, React.element> = "default" + external make: React.componentLike, Jsx.element> = "default" } module EUncurried = { @res.jsxComponentProps @live type props<'x> = {x: 'x} - external make: React.componentLike, React.element> = "default" + external make: React.componentLike, Jsx.element> = "default" } module Rec = { @@ -58,7 +58,7 @@ module Rec = { let rec make = { @merlin.focus - let \"make$Internal" = (_: props) => { + let \"make$Internal" = (_: props): Jsx.element => { make(({}: props)) } let make = { @@ -76,7 +76,7 @@ module Rec1 = { let rec make = { @merlin.focus - let \"make$Internal" = (_: props) => { + let \"make$Internal" = (_: props): Jsx.element => { React.null } let make = { @@ -94,7 +94,7 @@ module Rec2 = { let rec make = { @merlin.focus - let \"make$Internal" = (_: props) => { + let \"make$Internal" = (_: props): Jsx.element => { mm(({}: props)) } let make = { diff --git a/tests/syntax_tests/data/ppx/react/returnConstraint.res b/tests/syntax_tests/data/ppx/react/returnConstraint.res new file mode 100644 index 0000000000..c082474ff6 --- /dev/null +++ b/tests/syntax_tests/data/ppx/react/returnConstraint.res @@ -0,0 +1,24 @@ +@@jsxConfig({version: 4}) + +module Standard = { + @react.component + let make = () => React.string("ok") +} + +module ForwardRef = { + @react.component + let make = React.forwardRef((_, _ref) => ReactDOM.jsx("div", {})) +} + +module WithProps = { + type props = {value: int} + + @react.componentWithProps + let make = (props: props) => + ReactDOM.jsx("span", {children: ?ReactDOM.someElement({React.int(props.value)})}) +} + +module Async = { + @react.component + let make = async () => ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.string("async")})}) +} diff --git a/tests/tests/src/alias_default_value_test.res b/tests/tests/src/alias_default_value_test.res index 2ed6dfc4b1..f5837af7a6 100644 --- a/tests/tests/src/alias_default_value_test.res +++ b/tests/tests/src/alias_default_value_test.res @@ -30,7 +30,7 @@ module C3 = { module C4 = { @react.component - let make = (~a as b, ~x=true) => b + let make = (~a as b, ~x=true) => React.int(b) } module C6 = { @@ -41,7 +41,7 @@ module C6 = { } @react.component - let make = (~comp as module(Comp: Comp), ~x as (a, b)) => Comp.xx + let make = (~comp as module(Comp: Comp), ~x as (a, b)) => React.int(Comp.xx) } module C7 = { diff --git a/tests/tests/src/jsxv4_newtype.mjs b/tests/tests/src/jsxv4_newtype.mjs index 6e12bf12b9..a6e2b5a2dc 100644 --- a/tests/tests/src/jsxv4_newtype.mjs +++ b/tests/tests/src/jsxv4_newtype.mjs @@ -26,7 +26,7 @@ let V4A2 = { }; function Jsxv4_newtype$V4A3(props) { - return props.foo; + return null; } let V4A3 = { diff --git a/tests/tests/src/jsxv4_newtype.res b/tests/tests/src/jsxv4_newtype.res index 64b48c9823..141d014723 100644 --- a/tests/tests/src/jsxv4_newtype.res +++ b/tests/tests/src/jsxv4_newtype.res @@ -28,6 +28,6 @@ module V4A3 = { @react.component let make = (type a, ~foo) => { module T = unpack(foo: T with type t = a) - foo + React.null } } From 44f24c95998cf2ac53bab343834d44872e5f5920 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 4 Oct 2025 14:47:29 +0200 Subject: [PATCH 2/9] Extract collect_prop_types helper --- compiler/syntax/src/jsx_v4.ml | 50 +++++++++-------------------------- 1 file changed, 13 insertions(+), 37 deletions(-) diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index af40718d48..aba00d65b2 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -936,6 +936,17 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = new_binding ) else (None, binding, None) +let rec collect_prop_types types ({ptyp_loc; ptyp_desc} as full_type) = + match ptyp_desc with + | Ptyp_arrow {arg; ret = {ptyp_desc = Ptyp_arrow _} as rest} + when is_labelled arg.lbl || is_optional arg.lbl -> + collect_prop_types ((arg.lbl, arg.attrs, ptyp_loc, arg.typ) :: types) rest + | Ptyp_arrow {arg = {lbl = Nolabel}; ret} -> collect_prop_types types ret + | Ptyp_arrow {arg; ret = return_value} + when is_labelled arg.lbl || is_optional arg.lbl -> + (return_value, (arg.lbl, arg.attrs, return_value.ptyp_loc, arg.typ) :: types) + | _ -> (full_type, types) + let transform_structure_item ~config item = match item with (* external *) @@ -968,19 +979,7 @@ let transform_structure_item ~config item = |> Option.map Jsx_common.typ_vars_of_core_type |> Option.value ~default:[] in - let rec get_prop_types types ({ptyp_loc; ptyp_desc} as full_type) = - match ptyp_desc with - | Ptyp_arrow {arg; ret = {ptyp_desc = Ptyp_arrow _} as typ2} - when is_labelled arg.lbl || is_optional arg.lbl -> - get_prop_types ((arg.lbl, arg.attrs, ptyp_loc, arg.typ) :: types) typ2 - | Ptyp_arrow {arg = {lbl = Nolabel}; ret} -> get_prop_types types ret - | Ptyp_arrow {arg; ret = return_value} - when is_labelled arg.lbl || is_optional arg.lbl -> - ( return_value, - (arg.lbl, arg.attrs, return_value.ptyp_loc, arg.typ) :: types ) - | _ -> (full_type, types) - in - let _, prop_types = get_prop_types [] pval_type in + let _, prop_types = collect_prop_types [] pval_type in let named_type_list = List.fold_left arg_to_concrete_type [] prop_types in let ret_props_type = Typ.constr ~loc:pstr_loc @@ -1069,30 +1068,7 @@ let transform_signature_item ~config item = |> Option.map Jsx_common.typ_vars_of_core_type |> Option.value ~default:[] in - let rec get_prop_types types ({ptyp_loc; ptyp_desc} as full_type) = - match ptyp_desc with - | Ptyp_arrow {arg; ret = {ptyp_desc = Ptyp_arrow _} as rest} - when is_optional arg.lbl || is_labelled arg.lbl -> - get_prop_types ((arg.lbl, arg.attrs, ptyp_loc, arg.typ) :: types) rest - | Ptyp_arrow - { - arg = - { - lbl = Nolabel; - typ = {ptyp_desc = Ptyp_constr ({txt = Lident "unit"}, _)}; - }; - ret = rest; - } -> - get_prop_types types rest - | Ptyp_arrow {arg = {lbl = Nolabel}; ret = rest} -> - get_prop_types types rest - | Ptyp_arrow {arg; ret = return_value} - when is_optional arg.lbl || is_labelled arg.lbl -> - ( return_value, - (arg.lbl, arg.attrs, return_value.ptyp_loc, arg.typ) :: types ) - | _ -> (full_type, types) - in - let _, prop_types = get_prop_types [] pval_type in + let _, prop_types = collect_prop_types [] pval_type in let named_type_list = List.fold_left arg_to_concrete_type [] prop_types in let ret_props_type = Typ.constr From 9e16cb7381144998cfc4e5ba7e2b31879b7a17b9 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 4 Oct 2025 14:50:02 +0200 Subject: [PATCH 3/9] Simplify collect_prop_types --- compiler/syntax/src/jsx_v4.ml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index aba00d65b2..958968af6d 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -936,7 +936,7 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = new_binding ) else (None, binding, None) -let rec collect_prop_types types ({ptyp_loc; ptyp_desc} as full_type) = +let rec collect_prop_types types {ptyp_loc; ptyp_desc} = match ptyp_desc with | Ptyp_arrow {arg; ret = {ptyp_desc = Ptyp_arrow _} as rest} when is_labelled arg.lbl || is_optional arg.lbl -> @@ -944,8 +944,8 @@ let rec collect_prop_types types ({ptyp_loc; ptyp_desc} as full_type) = | Ptyp_arrow {arg = {lbl = Nolabel}; ret} -> collect_prop_types types ret | Ptyp_arrow {arg; ret = return_value} when is_labelled arg.lbl || is_optional arg.lbl -> - (return_value, (arg.lbl, arg.attrs, return_value.ptyp_loc, arg.typ) :: types) - | _ -> (full_type, types) + (arg.lbl, arg.attrs, return_value.ptyp_loc, arg.typ) :: types + | _ -> types let transform_structure_item ~config item = match item with @@ -979,7 +979,7 @@ let transform_structure_item ~config item = |> Option.map Jsx_common.typ_vars_of_core_type |> Option.value ~default:[] in - let _, prop_types = collect_prop_types [] pval_type in + let prop_types = collect_prop_types [] pval_type in let named_type_list = List.fold_left arg_to_concrete_type [] prop_types in let ret_props_type = Typ.constr ~loc:pstr_loc @@ -1068,7 +1068,7 @@ let transform_signature_item ~config item = |> Option.map Jsx_common.typ_vars_of_core_type |> Option.value ~default:[] in - let _, prop_types = collect_prop_types [] pval_type in + let prop_types = collect_prop_types [] pval_type in let named_type_list = List.fold_left arg_to_concrete_type [] prop_types in let ret_props_type = Typ.constr From 8c5d3ee7cbd13f5061e92b83d2b3b67bab757ecf Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 4 Oct 2025 14:58:33 +0200 Subject: [PATCH 4/9] Cleanup --- compiler/syntax/src/jsx_v4.ml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index 958968af6d..3d39a6d2f3 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -920,18 +920,18 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = Some (make_new_binding ~loc:binding_loc ~full_module_name modified_binding) in + let binding_expr = + { + binding.pvb_expr with + (* moved to wrapper_expr *) + pexp_attributes = []; + } + in ( None, { binding with pvb_attributes = binding.pvb_attributes |> List.filter other_attrs_pure; - pvb_expr = - ( binding.pvb_expr |> fun expr -> - { - expr with - (* moved to wrapper_expr *) - pexp_attributes = []; - } - |> constrain_jsx_return ~loc:binding_loc ); + pvb_expr = binding_expr |> constrain_jsx_return ~loc:binding_loc; }, new_binding ) else (None, binding, None) From eae024973c71c995877416002b49ecf33d1b160e Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Mon, 6 Oct 2025 08:29:26 +0200 Subject: [PATCH 5/9] Remove merlin.focus attribute --- compiler/syntax/src/jsx_v4.ml | 4 +--- tests/syntax_tests/data/ppx/react/expected/v4.res.txt | 3 --- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index 3d39a6d2f3..4fc4afa6fa 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -78,8 +78,6 @@ let rec constrain_jsx_return ~loc expr = } | _ -> jsx_element_constraint ~loc expr -let merlin_focus = ({loc = Location.none; txt = "merlin.focus"}, PStr []) - (* Helper method to filter out any attribute that isn't [@react.component] *) let other_attrs_pure (loc, _) = match loc.txt with @@ -105,7 +103,7 @@ let make_new_binding binding expression new_name = pvb_pat = {pvb_pat with ppat_desc = Ppat_var {ppat_var with txt = new_name}}; pvb_expr = expression; - pvb_attributes = [merlin_focus]; + pvb_attributes = []; } | {pvb_loc} -> Jsx_common.raise_error ~loc:pvb_loc diff --git a/tests/syntax_tests/data/ppx/react/expected/v4.res.txt b/tests/syntax_tests/data/ppx/react/expected/v4.res.txt index 474c0feb80..233f96fdf8 100644 --- a/tests/syntax_tests/data/ppx/react/expected/v4.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/v4.res.txt @@ -57,7 +57,6 @@ module Rec = { type props = {} let rec make = { - @merlin.focus let \"make$Internal" = (_: props): Jsx.element => { make(({}: props)) } @@ -75,7 +74,6 @@ module Rec1 = { type props = {} let rec make = { - @merlin.focus let \"make$Internal" = (_: props): Jsx.element => { React.null } @@ -93,7 +91,6 @@ module Rec2 = { type props = {} let rec make = { - @merlin.focus let \"make$Internal" = (_: props): Jsx.element => { mm(({}: props)) } From 1e23493bad8199b386796ef53a5912fb682b5304 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Mon, 6 Oct 2025 09:52:16 +0200 Subject: [PATCH 6/9] CHANGELOG # Conflicts: # CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index deb232d09b..212fad6cc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ #### :nail_care: Polish - Improve circular dependency errors, and make sure they end up in the compiler log so the editor tooling can surface them. https://github.com/rescript-lang/rescript/pull/7940 +- JSX PPX: add Jsx.element return constraint. https://github.com/rescript-lang/rescript/pull/7939 #### :house: Internal From 9b77ccb966f347490e7e84666b173d0e841aff19 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Mon, 6 Oct 2025 11:03:53 +0200 Subject: [PATCH 7/9] Update JSXV4 docs. --- docs/JSXV4.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/JSXV4.md b/docs/JSXV4.md index fe90432236..7b2f156208 100644 --- a/docs/JSXV4.md +++ b/docs/JSXV4.md @@ -272,7 +272,7 @@ is transformed to ```rescript type props<'x, 'y, 'z> = {x: 'x, y?: 'y, z?: 'z} -({x, ?y, ?z}: props<_, _, _>) => { +({x, ?y, ?z}: props<_, _, _>): Jsx.element => { let y = switch y { | None => 3 + x | Some(y) => y @@ -281,7 +281,9 @@ type props<'x, 'y, 'z> = {x: 'x, y?: 'y, z?: 'z} } ``` -> Note: this implicit definition of type `props` means that there cannot be other type definitions of `props` in the same scope, or it will be a compiler error about multiple definitions of the type name. +> Note: +> - This implicit definition of type `props` means that there cannot be other type definitions of `props` in the same scope, or it will be a compiler error about multiple definitions of the type name. +> - JSX V4 automatically adds a `Jsx.element` return constraint to all component functions. This ensures that components always return valid JSX elements and provides better type safety. If a component returns a non-JSX value, you'll get a helpful error message suggesting how to convert it (e.g., use `React.int` for integers, `React.string` for strings, etc.). ### Transformation for Component Application From 823a6de65f86f61b79161799b958b480da434716 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 9 Oct 2025 19:53:33 +0200 Subject: [PATCH 8/9] Constrain return expression; do not use outer loc --- compiler/syntax/src/jsx_v4.ml | 45 ++++++++----------- .../tests/src/expected/CodeLens.res.txt | 3 -- .../tests/src/expected/Hover.res.txt | 4 +- .../tests/src/expected/InlayHint.res.txt | 6 --- .../ppx/react/expected/aliasProps.res.txt | 36 +++++++-------- .../ppx/react/expected/asyncAwait.res.txt | 4 +- .../react/expected/defaultValueProp.res.txt | 26 +++++------ .../ppx/react/expected/forwardRef.res.txt | 38 ++++++++-------- .../react/expected/interfaceWithRef.res.txt | 4 +- .../data/ppx/react/expected/topLevel.res.txt | 4 +- .../ppx/react/expected/uncurriedProps.res.txt | 16 +++---- 11 files changed, 82 insertions(+), 104 deletions(-) diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index 4fc4afa6fa..d84c6aaf2a 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -49,34 +49,30 @@ let ref_type loc = let jsx_element_type ~loc = Typ.constr ~loc {loc; txt = Ldot (Lident "Jsx", "element")} [] -let jsx_element_constraint ~loc expr = - Exp.constraint_ ~loc expr (jsx_element_type ~loc) - -let rec constrain_jsx_return ~loc expr = +let jsx_element_constraint expr = + Exp.constraint_ expr (jsx_element_type ~loc:expr.pexp_loc) + +(* Traverse the component body and force every reachable return expression to + be annotated as `Jsx.element`. This walks through the wrapper constructs the + PPX introduces (fun/newtype/let/sequence) so that the constraint ends up on + the real return position even after we rewrite the function. *) +let rec constrain_jsx_return expr = match expr.pexp_desc with | Pexp_fun ({rhs} as desc) -> - { - expr with - pexp_desc = Pexp_fun {desc with rhs = constrain_jsx_return ~loc rhs}; - } + {expr with pexp_desc = Pexp_fun {desc with rhs = constrain_jsx_return rhs}} | Pexp_newtype (param, inner) -> - { - expr with - pexp_desc = Pexp_newtype (param, constrain_jsx_return ~loc inner); - } + {expr with pexp_desc = Pexp_newtype (param, constrain_jsx_return inner)} | Pexp_constraint (inner, _) -> - jsx_element_constraint ~loc (constrain_jsx_return ~loc inner) + let constrained_inner = constrain_jsx_return inner in + jsx_element_constraint constrained_inner | Pexp_let (rec_flag, bindings, body) -> { expr with - pexp_desc = Pexp_let (rec_flag, bindings, constrain_jsx_return ~loc body); + pexp_desc = Pexp_let (rec_flag, bindings, constrain_jsx_return body); } | Pexp_sequence (first, second) -> - { - expr with - pexp_desc = Pexp_sequence (first, constrain_jsx_return ~loc second); - } - | _ -> jsx_element_constraint ~loc expr + {expr with pexp_desc = Pexp_sequence (first, constrain_jsx_return second)} + | _ -> jsx_element_constraint expr (* Helper method to filter out any attribute that isn't [@react.component] *) let other_attrs_pure (loc, _) = @@ -743,10 +739,7 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = vb_match_expr named_arg_list expression else expression in - let expression = - Exp.constraint_ ~loc:binding_loc expression - (jsx_element_type ~loc:binding_loc) - in + let expression = constrain_jsx_return expression in (* (ref) => expr *) let expression = List.fold_left @@ -890,9 +883,7 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = let applied_expression = Jsx_common.async_component ~async:is_async applied_expression in - let applied_expression = - Exp.constraint_ ~loc applied_expression (jsx_element_type ~loc) - in + let applied_expression = constrain_jsx_return applied_expression in let wrapper_expr = Exp.fun_ ~arity:None Nolabel None props_pattern ~attrs:binding.pvb_expr.pexp_attributes applied_expression @@ -929,7 +920,7 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = { binding with pvb_attributes = binding.pvb_attributes |> List.filter other_attrs_pure; - pvb_expr = binding_expr |> constrain_jsx_return ~loc:binding_loc; + pvb_expr = binding_expr |> constrain_jsx_return; }, new_binding ) else (None, binding, None) diff --git a/tests/analysis_tests/tests/src/expected/CodeLens.res.txt b/tests/analysis_tests/tests/src/expected/CodeLens.res.txt index a31512458f..e75a286516 100644 --- a/tests/analysis_tests/tests/src/expected/CodeLens.res.txt +++ b/tests/analysis_tests/tests/src/expected/CodeLens.res.txt @@ -1,8 +1,5 @@ Code Lens src/CodeLens.res [{ - "range": {"start": {"line": 9, "character": 4}, "end": {"line": 9, "character": 8}}, - "command": {"title": "Jsx.element", "command": ""} - }, { "range": {"start": {"line": 4, "character": 4}, "end": {"line": 4, "character": 6}}, "command": {"title": "(~opt1: int=?, ~a: int, ~b: int, unit, ~opt2: int=?, unit, ~c: int) => int", "command": ""} }, { diff --git a/tests/analysis_tests/tests/src/expected/Hover.res.txt b/tests/analysis_tests/tests/src/expected/Hover.res.txt index dca7496ba6..f9f1746e15 100644 --- a/tests/analysis_tests/tests/src/expected/Hover.res.txt +++ b/tests/analysis_tests/tests/src/expected/Hover.res.txt @@ -22,10 +22,10 @@ Hover src/Hover.res 33:4 {"contents": {"kind": "markdown", "value": "```rescript\nunit => int\n```\n---\nDoc comment for functionWithTypeAnnotation"}} Hover src/Hover.res 37:13 -{"contents": {"kind": "markdown", "value": "```rescript\nJsx.element\n```\n\n---\n\n```\n \n```\n```rescript\ntype Jsx.element\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22Jsx.res%22%2C25%2C0%5D)\n"}} +{"contents": {"kind": "markdown", "value": "```rescript\nstring\n```"}} Hover src/Hover.res 42:15 -{"contents": {"kind": "markdown", "value": "```rescript\nJsx.element\n```\n\n---\n\n```\n \n```\n```rescript\ntype Jsx.element\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22Jsx.res%22%2C25%2C0%5D)\n"}} +{"contents": {"kind": "markdown", "value": "```rescript\nstring\n```"}} Hover src/Hover.res 46:10 {"contents": {"kind": "markdown", "value": "```rescript\nint\n```"}} diff --git a/tests/analysis_tests/tests/src/expected/InlayHint.res.txt b/tests/analysis_tests/tests/src/expected/InlayHint.res.txt index 606e7ba3ab..aad649db3b 100644 --- a/tests/analysis_tests/tests/src/expected/InlayHint.res.txt +++ b/tests/analysis_tests/tests/src/expected/InlayHint.res.txt @@ -35,12 +35,6 @@ Inlay Hint src/InlayHint.res 1:34 "kind": 1, "paddingLeft": true, "paddingRight": false -}, { - "position": {"line": 14, "character": 8}, - "label": ": Jsx.element", - "kind": 1, - "paddingLeft": true, - "paddingRight": false }, { "position": {"line": 10, "character": 10}, "label": ": (~xx: int) => int", diff --git a/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt index 05ab1afd85..e60e7a5c89 100644 --- a/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt @@ -4,13 +4,12 @@ module C0 = { @res.jsxComponentProps type props<'priority, 'text> = {priority: 'priority, text?: 'text} - let make = ({priority: _, text: ?__text, _}: props<_, _>): Jsx.element => { + let make = ({priority: _, text: ?__text, _}: props<_, _>) => { let text = switch __text { | Some(text) => text | None => "Test" } - - React.string(text) + (React.string(text): Jsx.element) } let make = { let \"AliasProps$C0" = (props: props<_>) => make(props) @@ -23,13 +22,12 @@ module C1 = { @res.jsxComponentProps type props<'priority, 'text> = {priority: 'priority, text?: 'text} - let make = ({priority: p, text: ?__text, _}: props<_, _>): Jsx.element => { + let make = ({priority: p, text: ?__text, _}: props<_, _>) => { let text = switch __text { | Some(text) => text | None => "Test" } - - React.string(p ++ text) + (React.string(p ++ text): Jsx.element) } let make = { let \"AliasProps$C1" = (props: props<_>) => make(props) @@ -42,13 +40,12 @@ module C2 = { @res.jsxComponentProps type props<'foo> = {foo?: 'foo} - let make = ({foo: ?__bar, _}: props<_>): Jsx.element => { + let make = ({foo: ?__bar, _}: props<_>) => { let bar = switch __bar { | Some(foo) => foo | None => "" } - - React.string(bar) + (React.string(bar): Jsx.element) } let make = { let \"AliasProps$C2" = (props: props<_>) => make(props) @@ -61,7 +58,7 @@ module C3 = { @res.jsxComponentProps type props<'foo, 'a, 'b> = {foo?: 'foo, a?: 'a, b: 'b} - let make = ({foo: ?__bar, a: ?__a, b, _}: props<_, _, _>): Jsx.element => { + let make = ({foo: ?__bar, a: ?__a, b, _}: props<_, _, _>) => { let bar = switch __bar { | Some(foo) => foo | None => "" @@ -70,10 +67,11 @@ module C3 = { | Some(a) => a | None => bar } - - { - React.string(bar ++ a ++ b) - } + ( + { + React.string(bar ++ a ++ b) + }: Jsx.element + ) } let make = { let \"AliasProps$C3" = (props: props<_>) => make(props) @@ -86,13 +84,12 @@ module C4 = { @res.jsxComponentProps type props<'a, 'x> = {a: 'a, x?: 'x} - let make = ({a: b, x: ?__x, _}: props<_, _>): Jsx.element => { + let make = ({a: b, x: ?__x, _}: props<_, _>) => { let x = switch __x { | Some(x) => x | None => true } - - ReactDOM.jsx("div", {children: ?ReactDOM.someElement(b)}) + (ReactDOM.jsx("div", {children: ?ReactDOM.someElement(b)}): Jsx.element) } let make = { let \"AliasProps$C4" = (props: props<_>) => make(props) @@ -105,13 +102,12 @@ module C5 = { @res.jsxComponentProps type props<'a, 'z> = {a: 'a, z?: 'z} - let make = ({a: (x, y), z: ?__z, _}: props<_, _>): Jsx.element => { + let make = ({a: (x, y), z: ?__z, _}: props<_, _>) => { let z = switch __z { | Some(z) => z | None => 3 } - - x + y + z + (x + y + z: Jsx.element) } let make = { let \"AliasProps$C5" = (props: props<_>) => make(props) diff --git a/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt b/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt index c7695b8192..420871c61a 100644 --- a/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt @@ -4,9 +4,9 @@ module C0 = { @res.jsxComponentProps type props<'a> = {a: 'a} - let make = async ({a, _}: props<_>): Jsx.element => { + let make = async ({a, _}: props<_>) => { let a = await f(a) - ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.int(a)})}) + (ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.int(a)})}): Jsx.element) } let make = { let \"AsyncAwait$C0" = (props: props<_>) => Jsx.promise(make(props)) diff --git a/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt b/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt index 129412ac0d..f5fb0e715b 100644 --- a/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt @@ -1,7 +1,7 @@ module C0 = { @res.jsxComponentProps type props<'a, 'b> = {a?: 'a, b?: 'b} - let make = ({a: ?__a, b: ?__b, _}: props<_, _>): Jsx.element => { + let make = ({a: ?__a, b: ?__b, _}: props<_, _>) => { let a = switch __a { | Some(a) => a | None => 2 @@ -10,8 +10,7 @@ module C0 = { | Some(b) => b | None => a * 2 } - - React.int(a + b) + (React.int(a + b): Jsx.element) } let make = { let \"DefaultValueProp$C0" = (props: props<_>) => make(props) @@ -23,13 +22,12 @@ module C1 = { @res.jsxComponentProps type props<'a, 'b> = {a?: 'a, b: 'b} - let make = ({a: ?__a, b, _}: props<_, _>): Jsx.element => { + let make = ({a: ?__a, b, _}: props<_, _>) => { let a = switch __a { | Some(a) => a | None => 2 } - - React.int(a + b) + (React.int(a + b): Jsx.element) } let make = { let \"DefaultValueProp$C1" = (props: props<_>) => make(props) @@ -43,13 +41,12 @@ module C2 = { @res.jsxComponentProps type props<'a> = {a?: 'a} - let make = ({a: ?__a, _}: props<_>): Jsx.element => { + let make = ({a: ?__a, _}: props<_>) => { let a = switch __a { | Some(a) => a | None => a } - - React.string(a) + (React.string(a): Jsx.element) } let make = { let \"DefaultValueProp$C2" = (props: props<_>) => make(props) @@ -62,15 +59,16 @@ module C3 = { @res.jsxComponentProps type props<'disabled> = {disabled?: 'disabled} - let make = ({disabled: ?__everythingDisabled, _}: props): Jsx.element => { + let make = ({disabled: ?__everythingDisabled, _}: props) => { let everythingDisabled = switch __everythingDisabled { | Some(disabled) => disabled | None => false } - - { - React.string(everythingDisabled ? "true" : "false") - } + ( + { + React.string(everythingDisabled ? "true" : "false") + }: Jsx.element + ) } let make = { let \"DefaultValueProp$C3" = (props: props<_>) => make(props) diff --git a/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt b/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt index d756dd7085..177dee518e 100644 --- a/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt @@ -38,16 +38,17 @@ module V4A = { @res.jsxComponentProps type props = {} - let make = (_: props): Jsx.element => { + let make = (_: props) => { let input = React.useRef(Js.Nullable.null) - - ReactDOM.jsx( - "div", - { - children: ?ReactDOM.someElement( - React.jsx(FancyInput.make, {ref: input, children: {React.string("Click to focus")}}), - ), - }, + ( + ReactDOM.jsx( + "div", + { + children: ?ReactDOM.someElement( + React.jsx(FancyInput.make, {ref: input, children: {React.string("Click to focus")}}), + ), + }, + ): Jsx.element ) } let make = { @@ -95,16 +96,17 @@ module V4AUncurried = { @res.jsxComponentProps type props = {} - let make = (_: props): Jsx.element => { + let make = (_: props) => { let input = React.useRef(Js.Nullable.null) - - ReactDOM.jsx( - "div", - { - children: ?ReactDOM.someElement( - React.jsx(FancyInput.make, {ref: input, children: {React.string("Click to focus")}}), - ), - }, + ( + ReactDOM.jsx( + "div", + { + children: ?ReactDOM.someElement( + React.jsx(FancyInput.make, {ref: input, children: {React.string("Click to focus")}}), + ), + }, + ): Jsx.element ) } let make = { diff --git a/tests/syntax_tests/data/ppx/react/expected/interfaceWithRef.res.txt b/tests/syntax_tests/data/ppx/react/expected/interfaceWithRef.res.txt index 46269f71d3..92d540f84a 100644 --- a/tests/syntax_tests/data/ppx/react/expected/interfaceWithRef.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/interfaceWithRef.res.txt @@ -2,9 +2,9 @@ let make = ( {x, _}: props, ref: Js.Nullable.t, -): Jsx.element => { +) => { let _ = ref->Js.Nullable.toOption->Belt.Option.map(ReactDOM.Ref.domRef) - React.string(x) + (React.string(x): Jsx.element) } let make = React.forwardRef({ let \"InterfaceWithRef" = (props: props<_>, ref) => make(props, ref) diff --git a/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt b/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt index d3524f22b2..87ec9259bd 100644 --- a/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt @@ -4,9 +4,9 @@ module V4A = { @res.jsxComponentProps type props<'a, 'b> = {a: 'a, b: 'b} - let make = ({a, b, _}: props<_, _>): Jsx.element => { + let make = ({a, b, _}: props<_, _>) => { Js.log("This function should be named 'TopLevel.react'") - ReactDOM.jsx("div", {}) + (ReactDOM.jsx("div", {}): Jsx.element) } let make = { let \"TopLevel$V4A" = (props: props<_>) => make(props) diff --git a/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt index 6770e5517a..b218cf52ca 100644 --- a/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt @@ -2,13 +2,12 @@ @res.jsxComponentProps type props<'a> = {a?: 'a} -let make = ({a: ?__a, _}: props unit>): Jsx.element => { +let make = ({a: ?__a, _}: props unit>) => { let a = switch __a { | Some(a) => a | None => () => () } - - React.null + (React.null: Jsx.element) } let make = { let \"UncurriedProps" = (props: props<_>) => make(props) @@ -30,15 +29,16 @@ module Foo = { @res.jsxComponentProps type props<'callback> = {callback?: 'callback} - let make = ({callback: ?__callback, _}: props<(string, bool, bool) => unit>): Jsx.element => { + let make = ({callback: ?__callback, _}: props<(string, bool, bool) => unit>) => { let callback = switch __callback { | Some(callback) => callback | None => (_, _, _) => () } - - { - React.null - } + ( + { + React.null + }: Jsx.element + ) } let make = { let \"UncurriedProps$Foo" = (props: props<_>) => make(props) From 8fd1296f4392822ec3937d46329dcbe80a87bd67 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 9 Oct 2025 20:19:45 +0200 Subject: [PATCH 9/9] Revert binding loc change --- compiler/syntax/src/jsx_v4.ml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index d84c6aaf2a..0da28c5a8d 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -811,7 +811,6 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = (Some props_record_type, binding, new_binding)) else if Jsx_common.has_attr_on_binding Jsx_common.has_attr_with_props binding then - let binding_loc = binding.pvb_loc in let modified_binding = { binding with @@ -907,7 +906,7 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = | Recursive -> None | Nonrecursive -> Some - (make_new_binding ~loc:binding_loc ~full_module_name modified_binding) + (make_new_binding ~loc:empty_loc ~full_module_name modified_binding) in let binding_expr = {