From a4b751310a0b1d17da78cca3c8010c3a32637a7b Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sat, 25 Apr 2026 11:26:48 +0200
Subject: [PATCH 01/16] Make Jsx.component abstract
---
compiler/ml/typecore.ml | 28 ++++
compiler/syntax/src/jsx_v4.ml | 141 +++++++++++------
packages/@rescript/runtime/Jsx.res | 6 +-
.../src/GenericJsx.res | 4 +-
.../deadcode/expected/deadcode.txt | 142 ++++++++++--------
.../tests-reanalyze/deadcode/src/Hooks.res | 7 +-
.../tests/src/expected/CodeLens.res.txt | 3 +
.../tests/src/expected/Hover.res.txt | 4 +-
tests/build_tests/react_ppx/src/React.res | 4 +-
.../react_ppx/src/abstract_component_test.res | 14 ++
.../src/abstract_component_test.res.js | 31 ++++
.../react_ppx/src/gpr_3987_test.res | 1 +
.../react_ppx/src/gpr_3987_test.res.js | 6 +-
.../src/recursive_component_test.res.js | 16 +-
...jsx_custom_component_children.res.expected | 10 +-
...ustom_component_optional_prop.res.expected | 10 +-
...ustom_component_type_mismatch.res.expected | 10 +-
.../jsx_maybe_missing_fragment.res.expected | 12 +-
...x_type_mismatch_array_element.res.expected | 10 +-
.../jsx_type_mismatch_array_raw.res.expected | 10 +-
.../jsx_type_mismatch_float.res.expected | 10 +-
.../jsx_type_mismatch_int.res.expected | 10 +-
.../jsx_type_mismatch_option.res.expected | 10 +-
.../jsx_type_mismatch_string.res.expected | 10 +-
.../jsx_custom_component_children.res | 1 +
.../jsx_custom_component_optional_prop.res | 1 +
.../jsx_custom_component_type_mismatch.res | 1 +
.../fixtures/jsx_maybe_missing_fragment.res | 1 +
.../jsx_type_mismatch_array_element.res | 1 +
.../fixtures/jsx_type_mismatch_array_raw.res | 1 +
.../fixtures/jsx_type_mismatch_float.res | 1 +
.../fixtures/jsx_type_mismatch_int.res | 1 +
.../fixtures/jsx_type_mismatch_option.res | 1 +
.../fixtures/jsx_type_mismatch_string.res | 1 +
.../typescript-react-example/src/Hooks.res | 8 +-
.../ppx/react/expected/aliasProps.res.txt | 28 ++--
.../ppx/react/expected/asyncAwait.res.txt | 8 +-
.../ppx/react/expected/commentAtTop.res.txt | 4 +-
.../react/expected/defaultPatternProp.res.txt | 8 +-
.../react/expected/defaultValueProp.res.txt | 16 +-
.../react/expected/fileLevelConfig.res.txt | 4 +-
.../ppx/react/expected/forwardRef.res.txt | 8 +-
.../data/ppx/react/expected/interface.res.txt | 8 +-
.../ppx/react/expected/mangleKeyword.res.txt | 4 +-
.../data/ppx/react/expected/nested.res.txt | 8 +-
.../data/ppx/react/expected/newtype.res.txt | 24 +--
.../ppx/react/expected/noPropsWithKey.res.txt | 8 +-
.../expected/optimizeAutomaticMode.res.txt | 4 +-
.../react/expected/returnConstraint.res.txt | 12 +-
.../ppx/react/expected/sharedProps.res.txt | 16 +-
.../expected/sharedPropsWithProps.res.txt | 28 ++--
.../data/ppx/react/expected/topLevel.res.txt | 4 +-
.../ppx/react/expected/typeConstraint.res.txt | 4 +-
.../ppx/react/expected/uncurriedProps.res.txt | 12 +-
.../data/ppx/react/expected/v4.res.txt | 64 ++++----
tests/tests/src/UncurriedAlways.res | 2 +-
tests/tests/src/jsx_preserve_test.mjs | 16 +-
tests/tests/src/jsx_preserve_test.res | 1 +
tests/tests/src/recursive_react_component.mjs | 10 +-
59 files changed, 498 insertions(+), 330 deletions(-)
create mode 100644 tests/build_tests/react_ppx/src/abstract_component_test.res
create mode 100644 tests/build_tests/react_ppx/src/abstract_component_test.res.js
diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml
index dc5c0d1d513..9f15b85a12f 100644
--- a/compiler/ml/typecore.ml
+++ b/compiler/ml/typecore.ml
@@ -1822,6 +1822,34 @@ let rec is_nonexpansive exp =
List.for_all (fun vb -> is_nonexpansive vb.vb_expr) pat_exp_list
&& is_nonexpansive body
| Texp_function _ -> true
+ (* `%identity` is a typed no-op coercion. Treating it like an ordinary
+ function call makes values such as `React.component(fn)` expansive, which
+ prevents generalization of polymorphic props:
+
+ @react.component
+ let make = (~x) =>
+ switch x {
+ | #a => React.string("A")
+ | #b => React.string("B")
+ | _ => React.string("other")
+ }
+
+ The JSX transform emits a function value and then coerces it through
+ `React.component`, whose implementation is `%identity`. Since no runtime
+ computation happens beyond evaluating the argument, the application is
+ non-expansive exactly when all supplied arguments are non-expansive. *)
+ | Texp_apply
+ {
+ funct =
+ {
+ exp_desc =
+ Texp_ident
+ (_, _, {val_kind = Val_prim {Primitive.prim_name = "%identity"}});
+ };
+ args;
+ _;
+ } ->
+ List.for_all is_nonexpansive_opt (List.map snd args)
| Texp_apply {partial = true; _} ->
(* ReScript partial applications (`foo(args, ...)`) lower to wrapper
functions in codegen, so creating the partial itself is nonexpansive
diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml
index 83bbb0dcdc4..272cebdf86a 100644
--- a/compiler/syntax/src/jsx_v4.ml
+++ b/compiler/syntax/src/jsx_v4.ml
@@ -49,6 +49,56 @@ let ref_type loc =
let jsx_element_type config ~loc =
Typ.constr ~loc {loc; txt = module_access_name config "element"} []
+let jsx_component_expr config ~loc expr =
+ Exp.apply ~loc
+ (Exp.ident ~loc {loc; txt = module_access_name config "component"})
+ [(Nolabel, expr)]
+
+let wrap_recursive_component_self_references ~config ~fn_name expr =
+ let jsx_module = String.capitalize_ascii config.Jsx_common.module_ in
+ let accepts_component = function
+ | {
+ pexp_desc =
+ Pexp_ident
+ {
+ txt =
+ Ldot
+ ( Lident module_name,
+ ( "createElement" | "createElementVariadic" | "jsx"
+ | "jsxKeyed" | "jsxs" | "jsxsKeyed" ) );
+ };
+ } ->
+ module_name = jsx_module
+ | _ -> false
+ in
+ let mapper =
+ {
+ Ast_mapper.default_mapper with
+ expr =
+ (fun mapper expr ->
+ match expr.pexp_desc with
+ | Pexp_apply ({funct; args} as apply) when accepts_component funct ->
+ let funct = mapper.expr mapper funct in
+ let args =
+ args
+ |> List.map (fun (label, arg) -> (label, mapper.expr mapper arg))
+ in
+ let args =
+ match args with
+ | ( Nolabel,
+ ({pexp_desc = Pexp_ident {txt = Lident name}; pexp_loc = loc}
+ as self_ref) )
+ :: rest
+ when name = fn_name ->
+ (Nolabel, jsx_component_expr config ~loc self_ref) :: rest
+ | _ -> args
+ in
+ {expr with pexp_desc = Pexp_apply {apply with funct; args}}
+ | _ -> Ast_mapper.default_mapper.expr mapper expr);
+ }
+ in
+ mapper.expr mapper expr
+
(* Helper method to filter out any attribute that isn't [@react.component] *)
let other_attrs_pure (loc, _) =
match loc.txt with
@@ -66,20 +116,6 @@ let rec get_fn_name binding =
Jsx_common.raise_error ~loc:ppat_loc
"JSX component calls cannot be destructured."
-let make_new_binding binding expression new_name =
- match binding with
- | {pvb_pat = {ppat_desc = Ppat_var ppat_var} as pvb_pat} ->
- {
- binding with
- pvb_pat =
- {pvb_pat with ppat_desc = Ppat_var {ppat_var with txt = new_name}};
- pvb_expr = expression;
- pvb_attributes = [];
- }
- | {pvb_loc} ->
- Jsx_common.raise_error ~loc:pvb_loc
- "JSX component calls cannot be destructured."
-
(* Lookup the filename from the location information on the AST node and turn it into a valid module identifier *)
let filename_from_loc (pstr_loc : Location.t) =
let file_name =
@@ -590,7 +626,6 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
}
in
let fn_name = get_fn_name binding.pvb_pat in
- let internal_fn_name = fn_name ^ "$Internal" in
let full_module_name =
make_module_name file_name config.nested_modules fn_name
in
@@ -611,12 +646,7 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
in
let inner_expression =
Exp.apply
- (Exp.ident
- (Location.mknoloc
- @@ Lident
- (match rec_flag with
- | Recursive -> internal_fn_name
- | Nonrecursive -> fn_name)))
+ (Exp.ident (Location.mknoloc @@ Lident fn_name))
([(Nolabel, Exp.ident (Location.mknoloc @@ Lident "props"))]
@
match has_forward_ref with
@@ -663,6 +693,23 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
]
(Exp.ident ~loc:pstr_loc {loc = empty_loc; txt = Lident txt})
in
+ (* Build a plain function first, then coerce that function to the abstract
+ component type:
+
+ let make = React.component({
+ let "File$Component" = props => make(props)
+ "File$Component"
+ })
+
+ Putting the coercion directly around the function argument, as in
+ `React.component(props => make(props))`, hides the function under an
+ application during typing and breaks inference for polymorphic props.
+ The typechecker treats this `%identity` coercion as non-expansive, so
+ the inferred prop type can still be generalized. *)
+ let full_expression =
+ if has_forward_ref then full_expression
+ else jsx_component_expr config ~loc:pstr_loc full_expression
+ in
let rec returned_expression patterns_with_label patterns_with_nolabel
({pexp_desc} as expr) =
match pexp_desc with
@@ -780,29 +827,30 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
newtypes
|> List.fold_left (fun e newtype -> Exp.newtype newtype e) expression
in
- (* let make = ({id, name, ...}: props<'id, 'name, ...>) => { ... } *)
- let binding, new_binding =
+ let expression =
match rec_flag with
+ | Nonrecursive -> expression
| Recursive ->
- ( binding_wrapper
- (Exp.let_ ~loc:empty_loc Nonrecursive
- [make_new_binding binding expression internal_fn_name]
- (Exp.let_ ~loc:empty_loc Nonrecursive
- [
- Vb.mk
- (Pat.var {loc = empty_loc; txt = fn_name})
- full_expression;
- ]
- (Exp.ident {loc = empty_loc; txt = Lident fn_name}))),
- None )
- | Nonrecursive ->
- ( {
- binding with
- pvb_expr = expression;
- pvb_pat = Pat.var {txt = fn_name; loc = Location.none};
- },
- Some (binding_wrapper full_expression) )
+ (* Inside `let rec make = ...`, `make` still has to be the callable
+ implementation so recursive calls like `make(props)` keep working.
+ But component-position APIs now expect the abstract component type:
+
+ React.createElement(make, props)
+
+ Wrap only those self-references in the `%identity` component
+ coercion. The generated JavaScript still receives the same function,
+ while the typed AST sees a `React.component<_>`. *)
+ wrap_recursive_component_self_references ~config ~fn_name expression
in
+ (* let make = ({id, name, ...}: props<'id, 'name, ...>) => { ... } *)
+ let binding =
+ {
+ binding with
+ pvb_expr = expression;
+ pvb_pat = Pat.var {txt = fn_name; loc = Location.none};
+ }
+ in
+ let new_binding = Some (binding_wrapper full_expression) in
(Some props_record_type, binding, new_binding))
else if Jsx_common.has_attr_on_binding Jsx_common.has_attr_with_props binding
then
@@ -860,7 +908,6 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
| _ -> Pat.var {txt = "props"; loc})
| _ -> Pat.var {txt = "props"; loc}
in
-
let applied_expression =
Exp.apply
(Exp.ident
@@ -887,6 +934,9 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
[Vb.mk (Pat.var {txt = full_module_name; loc}) wrapper_expr]
(Exp.ident {txt = Lident full_module_name; loc})
in
+ let internal_expression =
+ jsx_component_expr config ~loc internal_expression
+ in
Vb.mk ~attrs:modified_binding.pvb_attributes
(Pat.var {txt = fn_name; loc})
@@ -1027,7 +1077,12 @@ let transform_structure_item ~config item =
match new_bindings with
| [] -> []
| new_bindings ->
- [{pstr_loc = empty_loc; pstr_desc = Pstr_value (rec_flag, new_bindings)}])
+ [
+ {
+ pstr_loc = empty_loc;
+ pstr_desc = Pstr_value (Nonrecursive, new_bindings);
+ };
+ ])
| _ -> [item]
let transform_signature_item ~config item =
diff --git a/packages/@rescript/runtime/Jsx.res b/packages/@rescript/runtime/Jsx.res
index ec62d61644a..c2593003fd1 100644
--- a/packages/@rescript/runtime/Jsx.res
+++ b/packages/@rescript/runtime/Jsx.res
@@ -16,7 +16,11 @@ external array: array => element = "%identity"
external promise: promise => element = "%identity"
type componentLike<'props, 'return> = 'props => 'return
-type component<'props> = componentLike<'props, element>
+
+/* Components consume props. If one component can accept broader props, it can
+ safely stand in for a component that only needs narrower props, just like a
+ function argument type. That makes the props parameter contravariant. */
+type component<-'props>
/* this function exists to prepare for making `component` abstract */
external component: componentLike<'props, element> => component<'props> = "%identity"
diff --git a/tests/analysis_tests/tests-generic-jsx-transform/src/GenericJsx.res b/tests/analysis_tests/tests-generic-jsx-transform/src/GenericJsx.res
index d5f15273de1..b9e6341e62b 100644
--- a/tests/analysis_tests/tests-generic-jsx-transform/src/GenericJsx.res
+++ b/tests/analysis_tests/tests-generic-jsx-transform/src/GenericJsx.res
@@ -5,6 +5,8 @@ type component<'props> = Jsx.component<'props>
type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
+external component: componentLike<'props, element> => component<'props> = "%identity"
+
@module("preact")
external jsx: (component<'props>, 'props) => element = "jsx"
@@ -60,4 +62,4 @@ module Elements = {
external jsxsKeyed: (string, props, ~key: string=?, @ignore unit) => Jsx.element = "jsxs"
external someElement: element => option = "%identity"
-}
\ No newline at end of file
+}
diff --git a/tests/analysis_tests/tests-reanalyze/deadcode/expected/deadcode.txt b/tests/analysis_tests/tests-reanalyze/deadcode/expected/deadcode.txt
index 7ea5d1387e7..ba27040076b 100644
--- a/tests/analysis_tests/tests-reanalyze/deadcode/expected/deadcode.txt
+++ b/tests/analysis_tests/tests-reanalyze/deadcode/expected/deadcode.txt
@@ -31,6 +31,7 @@
addTypeReference _none_:1:-1 --> ComponentAsProp.res:6:12
addTypeReference _none_:1:-1 --> ComponentAsProp.res:6:20
addTypeReference _none_:1:-1 --> ComponentAsProp.res:6:34
+ addValueReference ComponentAsProp.res:6:4 --> React.res:15:0
Scanning CreateErrorHandler1.cmt Source:CreateErrorHandler1.res
addValueDeclaration +notification CreateErrorHandler1.res:3:6 path:+CreateErrorHandler1.Error1
addValueReference CreateErrorHandler1.res:3:6 --> CreateErrorHandler1.res:3:21
@@ -156,6 +157,7 @@
addValueReference DeadTest.res:117:32 --> DeadTest.res:117:12
addValueReference DeadTest.res:117:19 --> React.res:7:0
addTypeReference _none_:1:-1 --> DeadTest.res:117:12
+ addValueReference DeadTest.res:117:4 --> React.res:15:0
addValueReference DeadTest.res:119:16 --> DeadTest.res:117:4
addVariantCaseDeclaration A DeadTest.res:140:11 path:+DeadTest.WithInclude.t
addVariantCaseDeclaration A DeadTest.res:143:13 path:+DeadTest.WithInclude.T.t
@@ -297,8 +299,10 @@
addValueReference DynamicallyLoadedComponent.res:2:32 --> DynamicallyLoadedComponent.res:2:12
addValueReference DynamicallyLoadedComponent.res:2:19 --> React.res:7:0
addTypeReference _none_:1:-1 --> DynamicallyLoadedComponent.res:2:12
+ addValueReference DynamicallyLoadedComponent.res:2:4 --> React.res:15:0
Scanning EmptyArray.cmt Source:EmptyArray.res
addValueDeclaration +make EmptyArray.res:5:6 path:+EmptyArray.Z
+ addValueReference EmptyArray.res:5:6 --> React.res:15:0
addValueReference EmptyArray.res:10:9 --> EmptyArray.res:5:6
Scanning ErrorHandler.cmt Source:ErrorHandler.res
addValueDeclaration +notify ErrorHandler.res:7:6 path:+ErrorHandler.Make
@@ -364,12 +368,12 @@
addValueReference ForOf.res:10:4 --> ForOf.res:8:0
Scanning Hooks.cmt Source:Hooks.res
addValueDeclaration +make Hooks.res:4:4 path:+Hooks
- addValueDeclaration +default Hooks.res:25:4 path:+Hooks
- addValueDeclaration +make Hooks.res:29:6 path:+Hooks.Inner
- addValueDeclaration +make Hooks.res:33:8 path:+Hooks.Inner.Inner2
- addValueDeclaration +make Hooks.res:39:6 path:+Hooks.NoProps
- addValueDeclaration +functionWithRenamedArgs Hooks.res:45:4 path:+Hooks
- addValueDeclaration +make Hooks.res:63:6 path:+Hooks.RenderPropRequiresConversion
+ addValueDeclaration +default Hooks.res:28:4 path:+Hooks
+ addValueDeclaration +make Hooks.res:32:6 path:+Hooks.Inner
+ addValueDeclaration +make Hooks.res:36:8 path:+Hooks.Inner.Inner2
+ addValueDeclaration +make Hooks.res:42:6 path:+Hooks.NoProps
+ addValueDeclaration +functionWithRenamedArgs Hooks.res:48:4 path:+Hooks
+ addValueDeclaration +make Hooks.res:66:6 path:+Hooks.RenderPropRequiresConversion
addRecordLabelDeclaration name Hooks.res:1:16 path:+Hooks.vehicle
addRecordLabelDeclaration vehicle Hooks.res:4:12 path:+Hooks.props
addValueReference Hooks.res:5:26 --> React.res:134:0
@@ -386,52 +390,59 @@
addValueReference Hooks.res:13:40 --> Hooks.res:5:7
addValueReference Hooks.res:13:26 --> Hooks.res:5:14
addValueReference Hooks.res:14:5 --> ImportHooks.res:13:0
- addValueReference Hooks.res:15:7 --> React.res:7:0
- addValueReference Hooks.res:15:32 --> React.res:7:0
- addValueReference Hooks.res:14:76 --> Hooks.res:14:57
- addValueReference Hooks.res:14:63 --> React.res:7:0
- addValueReference Hooks.res:17:5 --> ImportHookDefault.res:6:0
- addValueReference Hooks.res:19:7 --> React.res:7:0
- addValueReference Hooks.res:19:32 --> React.res:7:0
- addValueReference Hooks.res:18:74 --> Hooks.res:18:55
- addValueReference Hooks.res:18:61 --> React.res:7:0
+ addValueReference Hooks.res:17:7 --> React.res:7:0
+ addValueReference Hooks.res:17:32 --> React.res:7:0
+ addValueReference Hooks.res:16:50 --> Hooks.res:16:32
+ addValueReference Hooks.res:16:37 --> React.res:7:0
+ addValueReference Hooks.res:16:16 --> React.res:15:0
+ addValueReference Hooks.res:19:5 --> ImportHookDefault.res:6:0
+ addValueReference Hooks.res:22:7 --> React.res:7:0
+ addValueReference Hooks.res:22:32 --> React.res:7:0
+ addValueReference Hooks.res:21:50 --> Hooks.res:21:32
+ addValueReference Hooks.res:21:37 --> React.res:7:0
+ addValueReference Hooks.res:21:16 --> React.res:15:0
addTypeReference _none_:1:-1 --> Hooks.res:4:12
- addValueReference Hooks.res:25:4 --> Hooks.res:4:4
- addRecordLabelDeclaration vehicle Hooks.res:29:14 path:+Hooks.Inner.props
- addRecordLabelDeclaration vehicle Hooks.res:33:16 path:+Hooks.Inner.Inner2.props
- addRecordLabelDeclaration vehicle Hooks.res:29:14 path:+Hooks.Inner.props
- addTypeReference Hooks.res:29:66 --> Hooks.res:1:16
- addValueReference Hooks.res:29:66 --> Hooks.res:29:14
- addValueReference Hooks.res:29:34 --> React.res:7:0
- addTypeReference Hooks.res:29:66 --> Hooks.res:1:16
- addValueReference Hooks.res:29:66 --> Hooks.res:29:14
- addValueReference Hooks.res:29:34 --> React.res:7:0
- addTypeReference _none_:1:-1 --> Hooks.res:29:14
- addRecordLabelDeclaration vehicle Hooks.res:33:16 path:+Hooks.Inner.Inner2.props
- addRecordLabelDeclaration vehicle Hooks.res:33:16 path:+Hooks.Inner.Inner2.props
- addTypeReference Hooks.res:33:68 --> Hooks.res:1:16
- addValueReference Hooks.res:33:68 --> Hooks.res:33:16
- addValueReference Hooks.res:33:36 --> React.res:7:0
- addTypeReference Hooks.res:33:68 --> Hooks.res:1:16
- addValueReference Hooks.res:33:68 --> Hooks.res:33:16
- addValueReference Hooks.res:33:36 --> React.res:7:0
- addTypeReference _none_:1:-1 --> Hooks.res:33:16
- addValueReference Hooks.res:39:25 --> React.res:3:0
- addValueReference Hooks.res:39:25 --> React.res:3:0
- addTypeReference Hooks.res:47:2 --> Hooks.res:1:16
- addValueReference Hooks.res:45:4 --> Hooks.res:45:31
- addTypeReference Hooks.res:47:14 --> Hooks.res:1:16
- addValueReference Hooks.res:45:4 --> Hooks.res:45:37
- addValueReference Hooks.res:45:4 --> Hooks.res:45:31
- addValueReference Hooks.res:45:4 --> Hooks.res:45:45
- addRecordLabelDeclaration x Hooks.res:50:10 path:+Hooks.r
- addRecordLabelDeclaration renderVehicle Hooks.res:63:14 path:+Hooks.RenderPropRequiresConversion.props
- addRecordLabelDeclaration renderVehicle Hooks.res:63:14 path:+Hooks.RenderPropRequiresConversion.props
- addValueDeclaration +car Hooks.res:64:8 path:+Hooks.RenderPropRequiresConversion
- addValueReference Hooks.res:65:30 --> Hooks.res:64:8
- addValueReference Hooks.res:65:18 --> Hooks.res:65:18
- addValueReference Hooks.res:65:4 --> Hooks.res:63:14
- addTypeReference _none_:1:-1 --> Hooks.res:63:14
+ addValueReference Hooks.res:4:4 --> React.res:15:0
+ addValueReference Hooks.res:28:4 --> Hooks.res:4:4
+ addRecordLabelDeclaration vehicle Hooks.res:32:14 path:+Hooks.Inner.props
+ addRecordLabelDeclaration vehicle Hooks.res:36:16 path:+Hooks.Inner.Inner2.props
+ addRecordLabelDeclaration vehicle Hooks.res:32:14 path:+Hooks.Inner.props
+ addTypeReference Hooks.res:32:66 --> Hooks.res:1:16
+ addValueReference Hooks.res:32:66 --> Hooks.res:32:14
+ addValueReference Hooks.res:32:34 --> React.res:7:0
+ addTypeReference Hooks.res:32:66 --> Hooks.res:1:16
+ addValueReference Hooks.res:32:66 --> Hooks.res:32:14
+ addValueReference Hooks.res:32:34 --> React.res:7:0
+ addTypeReference _none_:1:-1 --> Hooks.res:32:14
+ addValueReference Hooks.res:32:6 --> React.res:15:0
+ addRecordLabelDeclaration vehicle Hooks.res:36:16 path:+Hooks.Inner.Inner2.props
+ addRecordLabelDeclaration vehicle Hooks.res:36:16 path:+Hooks.Inner.Inner2.props
+ addTypeReference Hooks.res:36:68 --> Hooks.res:1:16
+ addValueReference Hooks.res:36:68 --> Hooks.res:36:16
+ addValueReference Hooks.res:36:36 --> React.res:7:0
+ addTypeReference Hooks.res:36:68 --> Hooks.res:1:16
+ addValueReference Hooks.res:36:68 --> Hooks.res:36:16
+ addValueReference Hooks.res:36:36 --> React.res:7:0
+ addTypeReference _none_:1:-1 --> Hooks.res:36:16
+ addValueReference Hooks.res:36:8 --> React.res:15:0
+ addValueReference Hooks.res:42:25 --> React.res:3:0
+ addValueReference Hooks.res:42:25 --> React.res:3:0
+ addValueReference Hooks.res:42:6 --> React.res:15:0
+ addTypeReference Hooks.res:50:2 --> Hooks.res:1:16
+ addValueReference Hooks.res:48:4 --> Hooks.res:48:31
+ addTypeReference Hooks.res:50:14 --> Hooks.res:1:16
+ addValueReference Hooks.res:48:4 --> Hooks.res:48:37
+ addValueReference Hooks.res:48:4 --> Hooks.res:48:31
+ addValueReference Hooks.res:48:4 --> Hooks.res:48:45
+ addRecordLabelDeclaration x Hooks.res:53:10 path:+Hooks.r
+ addRecordLabelDeclaration renderVehicle Hooks.res:66:14 path:+Hooks.RenderPropRequiresConversion.props
+ addRecordLabelDeclaration renderVehicle Hooks.res:66:14 path:+Hooks.RenderPropRequiresConversion.props
+ addValueDeclaration +car Hooks.res:67:8 path:+Hooks.RenderPropRequiresConversion
+ addValueReference Hooks.res:68:30 --> Hooks.res:67:8
+ addValueReference Hooks.res:68:18 --> Hooks.res:68:18
+ addValueReference Hooks.res:68:4 --> Hooks.res:66:14
+ addTypeReference _none_:1:-1 --> Hooks.res:66:14
+ addValueReference Hooks.res:66:6 --> React.res:15:0
Scanning IgnoreInterface.cmt Source:IgnoreInterface.res
Scanning IgnoreInterface.cmti Source:IgnoreInterface.resi
Scanning ImmutableArray.cmt Source:ImmutableArray.res
@@ -955,6 +966,7 @@
Scanning JsxV4.cmt Source:JsxV4.res
addValueDeclaration +make JsxV4.res:4:23 path:+JsxV4.C
addValueReference JsxV4.res:4:36 --> React.res:3:0
+ addValueReference JsxV4.res:4:23 --> React.res:15:0
addValueReference JsxV4.res:7:9 --> JsxV4.res:4:23
Scanning LetPrivate.cmt Source:LetPrivate.res
addValueDeclaration +y LetPrivate.res:7:4 path:+LetPrivate
@@ -1938,10 +1950,9 @@
Forward Liveness Analysis
decls: 698
- roots(external targets): 134
- decl-deps: decls_with_out=409 edges_to_decls=287
+ roots(external targets): 135
+ decl-deps: decls_with_out=410 edges_to_decls=287
- Root (annotated): Value +Hooks.+default
Root (external ref): Value +FirstClassModules.M.InnerModule2.+k
Root (external ref): VariantCase DeadRT.moduleAccessPath.Root
Root (external ref): Value +TypeReexport.VariantUseOriginal.+value
@@ -1965,7 +1976,6 @@ Forward Liveness Analysis
Root (annotated): Value +TypeParams3.+test
Root (annotated): Value +Variants.+sunday
Root (annotated): Value +NestedModules.Universe.Nested2.Nested3.+nested3Value
- Root (annotated): Value +Hooks.+functionWithRenamedArgs
Root (external ref): RecordLabel +Unison.t.doc
Root (annotated): Value +Tuples.+computeAreaWithIdent
Root (annotated): Value +LetPrivate.+y
@@ -1978,7 +1988,6 @@ Forward Liveness Analysis
Root (external ref): Value +Newton.+f
Root (external ref): RecordLabel +Records.record.v
Root (external ref): VariantCase +DeadTest.VariantUsedOnlyInImplementation.t.A
- Root (external ref): Value +Hooks.RenderPropRequiresConversion.+car
Root (external ref): RecordLabel +Records.person.address
Root (annotated): Value +Variants.+testConvert2
Root (annotated): Value +Tuples.+coord2d
@@ -2004,22 +2013,22 @@ Forward Liveness Analysis
Root (annotated): Value +ImportJsValue.+default
Root (annotated): Value +VariantsWithPayload.+testVariant1Int
Root (external ref): Value +DynamicallyLoadedComponent.+make
+ Root (annotated): Value +Hooks.RenderPropRequiresConversion.+make
Root (annotated): Value +Uncurried.+uncurried2
Root (annotated): Value +UseImportJsValue.+useTypeImportedInOtherModule
- Root (annotated): Value +Hooks.NoProps.+make
+ Root (annotated): Value +Hooks.Inner.+make
Root (external ref): Value +OptArg.+foo
Root (annotated): Value +Variants.+fortytwoOK
Root (external ref): Value OptArg.+bar
Root (annotated): Value +Records.+payloadValue
Root (external ref): RecordLabel +DeadTest.props.s
+ Root (annotated): Value +Hooks.+default
Root (external ref): VariantCase +Unison.break_.Never
Root (annotated): Value +TestEmitInnerModules.Inner.+y
Root (external ref): VariantCase InnerModuleTypes.I.t.Foo
Root (annotated): Value +Types.+selfRecursiveConverter
Root (external ref): Value +DeadTest.+thisIsUsedTwice
Root (annotated): Value +Opaque.+testConvertNestedRecordFromOtherFile
- Root (annotated): Value +Hooks.Inner.Inner2.+make
- Root (external ref): RecordLabel +Hooks.Inner.Inner2.props.vehicle
Root (external ref): Value +OptArg.+bar
Root (annotated): Value +TestFirstClassModules.+convertRecord
Root (external ref): VariantCase DeadTypeTest.deadType.OnlyInInterface
@@ -2036,6 +2045,7 @@ Forward Liveness Analysis
Root (external ref): Value +DeadTest.+deadIncorrect
Root (external ref): RecordLabel +TypeReexport.UseReexported.reexportedType.usedField
Root (annotated): Value +Docstrings.+signMessage
+ Root (external ref): RecordLabel +Hooks.Inner.Inner2.props.vehicle
Root (external ref): Value +DeadExn.+eInside
Root (external ref): RecordLabel +ComponentAsProp.props.button
Root (annotated): Value +ImportJsValue.+returnedFromHigherOrder
@@ -2059,6 +2069,7 @@ Forward Liveness Analysis
Root (annotated): Value +References.+get
Root (annotated): Value +ModuleAliases.+testNested
Root (external ref): Value +FirstClassModules.SomeFunctor.+ww
+ Root (annotated): Value +Hooks.Inner.Inner2.+make
Root (annotated): Value +ImportJsValue.+area
Root (annotated): Value +Records.+testMyRec
Root (annotated): Value +DeadTest.GloobLive.+globallyLive3
@@ -2075,13 +2086,13 @@ Forward Liveness Analysis
Root (annotated): Value +ScopedAnnotationsLiveVsDead.LiveScope.+root
Root (external ref): Value +Newton.+result
Root (annotated): Value +Records.+findAllAddresses
+ Root (annotated): Value +Hooks.NoProps.+make
Root (annotated): Value +ImportJsValue.+useGetProp
Root (annotated): Value +Variants.+id2
Root (annotated): Value +Uncurried.+sumU
Root (external ref): Value +TestOptArg.+bar
Root (external ref): RecordLabel +DeadTest.record.yyy
Root (annotated): Value +Docstrings.+one
- Root (annotated): Value +Hooks.Inner.+make
Root (external ref): Value +OptArg.+twoArgs
Root (annotated): Value +OcamlWarningSuppressToplevel.+suppressed1
Root (external ref): RecordLabel +Records.business2.address2
@@ -2102,7 +2113,6 @@ Forward Liveness Analysis
Root (annotated): Value +TestImmutableArray.+testImmutableArrayGet
Root (external ref): Value +TypeReexport.UseOriginal.+value
Root (annotated): Value +Docstrings.+unnamed2
- Root (annotated): Value +Hooks.RenderPropRequiresConversion.+make
Root (annotated): Value +LetPrivate.local_1.+x
Root (annotated): Value +TestImport.+make
Root (annotated): Value +DeadTest.GloobLive.+globallyLive1
@@ -2180,6 +2190,7 @@ Forward Liveness Analysis
Root (annotated): Value +Variants.+fortytwoBAD
Root (external ref): Value +DeadTest.+thisIsUsedOnce
Root (external ref): RecordLabel +Unison.t.break_
+ Root (external ref): RecordLabel +Hooks.Inner.props.vehicle
Root (external ref): Value ImmutableArray.+fromArray
Root (external ref): Value +RepeatedLabel.+userData
Root (annotated): Value +Variants.+testConvert2to3
@@ -2201,7 +2212,7 @@ Forward Liveness Analysis
Root (annotated): Value +ModuleAliases.+testInner2
Root (annotated): RecordLabel +ImportHooks.props.person
Root (external ref): Value DeadValueTest.+valueAlive
- Root (external ref): RecordLabel +Hooks.Inner.props.vehicle
+ Root (external ref): RecordLabel +Hooks.RenderPropRequiresConversion.props.renderVehicle
Root (annotated): Value +Shadow.M.+test
Root (annotated): Value +ComponentAsProp.+make
Root (annotated): Value +Records.+testMyRec2
@@ -2226,6 +2237,7 @@ Forward Liveness Analysis
Root (external ref): VariantCase +TypeReexport.VariantUseOriginal.reexportedType.A
Root (annotated): Value +Variants.+polyWithOpt
Root (annotated): Value +References.+destroysRefIdentity
+ Root (external ref): Value +Hooks.RenderPropRequiresConversion.+car
Root (external ref): Value +FirstClassModules.M.+x
Root (external ref): Value +TypeReexportCrossFileB.+recordValue
Root (annotated): Value +Records.+computeArea4
@@ -2262,11 +2274,10 @@ Forward Liveness Analysis
Root (annotated): Value +Tuples.+computeAreaNoConverters
Root (annotated): Value +Types.+mutuallyRecursiveConverter
Root (annotated): Value +UseImportJsValue.+useGetProp
- Root (external ref): RecordLabel +Hooks.RenderPropRequiresConversion.props.renderVehicle
+ Root (annotated): Value +Hooks.+functionWithRenamedArgs
322 roots found
- Propagate: +Hooks.+default -> +Hooks.+make
Propagate: DeadRT.moduleAccessPath.Root -> +DeadRT.moduleAccessPath.Root
Propagate: +TypeReexportCrossFileB.reexportedRecord.usedField -> +TypeReexportCrossFileA.originalRecord.usedField
Propagate: +DeadTypeTest.deadType.OnlyInImplementation -> DeadTypeTest.deadType.OnlyInImplementation
@@ -2277,6 +2288,7 @@ Forward Liveness Analysis
Propagate: +DeadTest.VariantUsedOnlyInImplementation.t.A -> +DeadTest.VariantUsedOnlyInImplementation.t.A
Propagate: +DeadTest.+thisIsMarkedLive -> +DeadTest.+thisIsKeptAlive
Propagate: +TypeReexport.UseOriginal.reexportedType.directlyUsed -> +TypeReexport.UseOriginal.originalType.directlyUsed
+ Propagate: +Hooks.+default -> +Hooks.+make
Propagate: InnerModuleTypes.I.t.Foo -> +InnerModuleTypes.I.t.Foo
Propagate: DeadTypeTest.deadType.OnlyInInterface -> +DeadTypeTest.deadType.OnlyInInterface
Propagate: +TypeReexport.UseReexported.reexportedType.usedField -> +TypeReexport.UseReexported.originalType.usedField
@@ -4239,7 +4251,7 @@ Forward Liveness Analysis
sideEffects is never used and could have side effects
Warning Dead Type
- Hooks.res:50:11-19
+ Hooks.res:53:11-19
r.x is a record label never used to read a value
Warning Dead Value
diff --git a/tests/analysis_tests/tests-reanalyze/deadcode/src/Hooks.res b/tests/analysis_tests/tests-reanalyze/deadcode/src/Hooks.res
index b3fd7dc7724..1ebc2d8e365 100644
--- a/tests/analysis_tests/tests-reanalyze/deadcode/src/Hooks.res
+++ b/tests/analysis_tests/tests-reanalyze/deadcode/src/Hooks.res
@@ -11,11 +11,14 @@ let make = (~vehicle) => {
)}
setCount(_ => count + 1)}> {React.string("Click me")}
- React.string(x["randomString"])}>
+ React.string(x["randomString"]))}>
{React.string("child1")} {React.string("child2")}
React.string(x["randomString"])}>
+ person={name: "DefaultImport", age: 42}
+ renderMe={React.component(x => React.string(x["randomString"]))}>
{React.string("child1")} {React.string("child2")}
diff --git a/tests/analysis_tests/tests/src/expected/CodeLens.res.txt b/tests/analysis_tests/tests/src/expected/CodeLens.res.txt
index e75a2865164..bc9288e8c95 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": "React.componentLike, React.element> => React.component>", "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 f9f1746e150..b5c1da0f91e 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\nReact.componentLike<\n props,\n React.element,\n> => React.component>\n```\n\n---\n\n```\n \n```\n```rescript\ntype React.componentLike<\n 'props,\n 'return,\n> = Jsx.componentLike<'props, 'return>\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22React.res%22%2C10%2C0%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype props<'name> = {name: 'name}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22Hover.res%22%2C36%2C0%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype React.element = Jsx.element\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22React.res%22%2C0%2C0%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype React.component<'props> = Jsx.component<'props>\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22React.res%22%2C12%2C0%5D)\n"}}
Hover src/Hover.res 42:15
-{"contents": {"kind": "markdown", "value": "```rescript\nstring\n```"}}
+{"contents": {"kind": "markdown", "value": "```rescript\nReact.componentLike<\n props,\n React.element,\n> => React.component>\n```\n\n---\n\n```\n \n```\n```rescript\ntype React.componentLike<\n 'props,\n 'return,\n> = Jsx.componentLike<'props, 'return>\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22React.res%22%2C10%2C0%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype props<'name> = {name: 'name}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22Hover.res%22%2C41%2C2%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype React.element = Jsx.element\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22React.res%22%2C0%2C0%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype React.component<'props> = Jsx.component<'props>\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22React.res%22%2C12%2C0%5D)\n"}}
Hover src/Hover.res 46:10
{"contents": {"kind": "markdown", "value": "```rescript\nint\n```"}}
diff --git a/tests/build_tests/react_ppx/src/React.res b/tests/build_tests/react_ppx/src/React.res
index 4350f6b2e54..6e16687642c 100644
--- a/tests/build_tests/react_ppx/src/React.res
+++ b/tests/build_tests/react_ppx/src/React.res
@@ -8,7 +8,9 @@ external array: array => element = "%identity"
type componentLike<'props, 'return> = 'props => 'return
-type component<'props> = componentLike<'props, element>
+type component<-'props>
+
+external component: componentLike<'props, element> => component<'props> = "%identity"
@module("react")
external createElement: (component<'props>, 'props) => element = "createElement"
diff --git a/tests/build_tests/react_ppx/src/abstract_component_test.res b/tests/build_tests/react_ppx/src/abstract_component_test.res
new file mode 100644
index 00000000000..067f75c2a35
--- /dev/null
+++ b/tests/build_tests/react_ppx/src/abstract_component_test.res
@@ -0,0 +1,14 @@
+module PolyVariantLowerBound = {
+ @react.component
+ let make = (~x) =>
+ switch x {
+ | #a => React.string("A")
+ | #b => React.string("B")
+ | _ => React.string("other")
+ }
+}
+
+module GenericRenderProp = {
+ @react.component
+ let make = (~x: 'x, ~render: 'x => React.element) => render(x)
+}
diff --git a/tests/build_tests/react_ppx/src/abstract_component_test.res.js b/tests/build_tests/react_ppx/src/abstract_component_test.res.js
new file mode 100644
index 00000000000..8863c77600f
--- /dev/null
+++ b/tests/build_tests/react_ppx/src/abstract_component_test.res.js
@@ -0,0 +1,31 @@
+// Generated by ReScript, PLEASE EDIT WITH CARE
+
+
+function Abstract_component_test$PolyVariantLowerBound(props) {
+ let x = props.x;
+ if (x === "a") {
+ return "A";
+ } else if (x === "b") {
+ return "B";
+ } else {
+ return "other";
+ }
+}
+
+let PolyVariantLowerBound = {
+ make: Abstract_component_test$PolyVariantLowerBound
+};
+
+function Abstract_component_test$GenericRenderProp(props) {
+ return props.render(props.x);
+}
+
+let GenericRenderProp = {
+ make: Abstract_component_test$GenericRenderProp
+};
+
+export {
+ PolyVariantLowerBound,
+ GenericRenderProp,
+}
+/* No side effect */
diff --git a/tests/build_tests/react_ppx/src/gpr_3987_test.res b/tests/build_tests/react_ppx/src/gpr_3987_test.res
index 6b73ce7a6c8..03a65d0e6d1 100644
--- a/tests/build_tests/react_ppx/src/gpr_3987_test.res
+++ b/tests/build_tests/react_ppx/src/gpr_3987_test.res
@@ -26,6 +26,7 @@ let makeContainer = text => {
module Gpr3987ReproOk = {
type props = {value: string, onChange: (string, int) => unit}
+ @react.componentWithProps
let make = (_props: props) => React.null
}
diff --git a/tests/build_tests/react_ppx/src/gpr_3987_test.res.js b/tests/build_tests/react_ppx/src/gpr_3987_test.res.js
index 378f9156caf..30bedfb06b7 100644
--- a/tests/build_tests/react_ppx/src/gpr_3987_test.res.js
+++ b/tests/build_tests/react_ppx/src/gpr_3987_test.res.js
@@ -16,15 +16,15 @@ function makeContainer(text) {
return content;
}
-function make(_props) {
+function Gpr_3987_test$Gpr3987ReproOk(props) {
return null;
}
let Gpr3987ReproOk = {
- make: make
+ make: Gpr_3987_test$Gpr3987ReproOk
};
-JsxRuntime.jsx(make, {
+JsxRuntime.jsx(Gpr_3987_test$Gpr3987ReproOk, {
value: "test",
onChange: (param, param$1) => {}
});
diff --git a/tests/build_tests/react_ppx/src/recursive_component_test.res.js b/tests/build_tests/react_ppx/src/recursive_component_test.res.js
index 4e3fd9951f4..8c8e26e1254 100644
--- a/tests/build_tests/react_ppx/src/recursive_component_test.res.js
+++ b/tests/build_tests/react_ppx/src/recursive_component_test.res.js
@@ -1,21 +1,23 @@
// Generated by ReScript, PLEASE EDIT WITH CARE
+function make(param) {
+ return mm({
+ b: param.b
+ });
+}
+
function mm(x) {
return make({
b: !x.b
});
}
-function make(props) {
- return mm({
- b: props.b
- });
-}
+let Recursive_component_test$Rec = make;
let Rec = {
- make: make,
- mm: mm
+ mm: mm,
+ make: Recursive_component_test$Rec
};
export {
diff --git a/tests/build_tests/super_errors/expected/jsx_custom_component_children.res.expected b/tests/build_tests/super_errors/expected/jsx_custom_component_children.res.expected
index 46b4d181884..a1eb83e9d5a 100644
--- a/tests/build_tests/super_errors/expected/jsx_custom_component_children.res.expected
+++ b/tests/build_tests/super_errors/expected/jsx_custom_component_children.res.expected
@@ -1,11 +1,11 @@
[1;31mWe've found a bug for you![0m
- [36m/.../fixtures/jsx_custom_component_children.res[0m:[2m24:28-29[0m
+ [36m/.../fixtures/jsx_custom_component_children.res[0m:[2m25:28-29[0m
- 22 [2m│[0m }
- 23 [2m│[0m
- [1;31m24[0m [2m│[0m let x = {[1;31m1.[0m}
- 25 [2m│[0m
+ 23 [2m│[0m }
+ 24 [2m│[0m
+ [1;31m25[0m [2m│[0m let x = {[1;31m1.[0m}
+ 26 [2m│[0m
This has type: [1;31mfloat[0m
But children passed to this component must be of type:
diff --git a/tests/build_tests/super_errors/expected/jsx_custom_component_optional_prop.res.expected b/tests/build_tests/super_errors/expected/jsx_custom_component_optional_prop.res.expected
index 334f245a49e..d2cdb5aae82 100644
--- a/tests/build_tests/super_errors/expected/jsx_custom_component_optional_prop.res.expected
+++ b/tests/build_tests/super_errors/expected/jsx_custom_component_optional_prop.res.expected
@@ -1,11 +1,11 @@
[1;31mWe've found a bug for you![0m
- [36m/.../fixtures/jsx_custom_component_optional_prop.res[0m:[2m33:34[0m
+ [36m/.../fixtures/jsx_custom_component_optional_prop.res[0m:[2m34:34[0m
- 31 [2m│[0m let o = Some(1.)
- 32 [2m│[0m
- [1;31m33[0m [2m│[0m let x =
- 34 [2m│[0m
+ 32 [2m│[0m let o = Some(1.)
+ 33 [2m│[0m
+ [1;31m34[0m [2m│[0m let x =
+ 35 [2m│[0m
This has type: [1;31moption[0m
But the component prop [1;33msomeOpt[0m is expected to have type: [1;33mfloat[0m
diff --git a/tests/build_tests/super_errors/expected/jsx_custom_component_type_mismatch.res.expected b/tests/build_tests/super_errors/expected/jsx_custom_component_type_mismatch.res.expected
index 9db22a386a8..04cbd8877f4 100644
--- a/tests/build_tests/super_errors/expected/jsx_custom_component_type_mismatch.res.expected
+++ b/tests/build_tests/super_errors/expected/jsx_custom_component_type_mismatch.res.expected
@@ -1,11 +1,11 @@
[1;31mWe've found a bug for you![0m
- [36m/.../fixtures/jsx_custom_component_type_mismatch.res[0m:[2m31:34-40[0m
+ [36m/.../fixtures/jsx_custom_component_type_mismatch.res[0m:[2m32:34-40[0m
- 29 [2m│[0m }
- 30 [2m│[0m
- [1;31m31[0m [2m│[0m let x =
- 32 [2m│[0m
+ 30 [2m│[0m }
+ 31 [2m│[0m
+ [1;31m32[0m [2m│[0m let x =
+ 33 [2m│[0m
This has type: [1;31mstring[0m
But the component prop [1;33msomeOpt[0m is expected to have type: [1;33mfloat[0m
diff --git a/tests/build_tests/super_errors/expected/jsx_maybe_missing_fragment.res.expected b/tests/build_tests/super_errors/expected/jsx_maybe_missing_fragment.res.expected
index a21dfbd5b99..9324ee38131 100644
--- a/tests/build_tests/super_errors/expected/jsx_maybe_missing_fragment.res.expected
+++ b/tests/build_tests/super_errors/expected/jsx_maybe_missing_fragment.res.expected
@@ -1,12 +1,12 @@
[1;31mWe've found a bug for you![0m
- [36m/.../fixtures/jsx_maybe_missing_fragment.res[0m:[2m18:3-8[0m
+ [36m/.../fixtures/jsx_maybe_missing_fragment.res[0m:[2m19:3-8[0m
- 16 [2m│[0m
- 17 [2m│[0m let x = {
- [1;31m18[0m [2m│[0m [1;31m<> >[0m
- 19 [2m│[0m <> >
- 20 [2m│[0m }
+ 17 [2m│[0m
+ 18 [2m│[0m let x = {
+ [1;31m19[0m [2m│[0m [1;31m<> >[0m
+ 20 [2m│[0m <> >
+ 21 [2m│[0m }
This has type: [1;31mReact.element[0m [2m(defined as[0m [1;31mJsx.element[0m[2m)[0m
But it's expected to have type: [1;33munit[0m
diff --git a/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_element.res.expected b/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_element.res.expected
index 1cf8426d78c..b83f56dc6d8 100644
--- a/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_element.res.expected
+++ b/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_element.res.expected
@@ -1,11 +1,11 @@
[1;31mWe've found a bug for you![0m
- [36m/.../fixtures/jsx_type_mismatch_array_element.res[0m:[2m19:13-30[0m
+ [36m/.../fixtures/jsx_type_mismatch_array_element.res[0m:[2m20:13-30[0m
- 17 [2m│[0m }
- 18 [2m│[0m
- [1;31m19[0m [2m│[0m let x = <> {[1;31m[React.string("")][0m} >
- 20 [2m│[0m
+ 18 [2m│[0m }
+ 19 [2m│[0m
+ [1;31m20[0m [2m│[0m let x = <> {[1;31m[React.string("")][0m} >
+ 21 [2m│[0m
This has type: [1;31marray<'a>[0m
But it's expected to have type: [1;33mReact.element[0m [2m(defined as[0m [1;33mJsx.element[0m[2m)[0m
diff --git a/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_raw.res.expected b/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_raw.res.expected
index 1eb1d4870d4..6524468d197 100644
--- a/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_raw.res.expected
+++ b/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_raw.res.expected
@@ -1,11 +1,11 @@
[1;31mWe've found a bug for you![0m
- [36m/.../fixtures/jsx_type_mismatch_array_raw.res[0m:[2m17:13-16[0m
+ [36m/.../fixtures/jsx_type_mismatch_array_raw.res[0m:[2m18:13-16[0m
- 15 [2m│[0m }
- 16 [2m│[0m
- [1;31m17[0m [2m│[0m let x = <> {[1;31m[""][0m} >
- 18 [2m│[0m
+ 16 [2m│[0m }
+ 17 [2m│[0m
+ [1;31m18[0m [2m│[0m let x = <> {[1;31m[""][0m} >
+ 19 [2m│[0m
This has type: [1;31marray<'a>[0m
But it's expected to have type: [1;33mReact.element[0m [2m(defined as[0m [1;33mJsx.element[0m[2m)[0m
diff --git a/tests/build_tests/super_errors/expected/jsx_type_mismatch_float.res.expected b/tests/build_tests/super_errors/expected/jsx_type_mismatch_float.res.expected
index 2244de59be7..6f709a191d9 100644
--- a/tests/build_tests/super_errors/expected/jsx_type_mismatch_float.res.expected
+++ b/tests/build_tests/super_errors/expected/jsx_type_mismatch_float.res.expected
@@ -1,11 +1,11 @@
[1;31mWe've found a bug for you![0m
- [36m/.../fixtures/jsx_type_mismatch_float.res[0m:[2m17:13-14[0m
+ [36m/.../fixtures/jsx_type_mismatch_float.res[0m:[2m18:13-14[0m
- 15 [2m│[0m }
- 16 [2m│[0m
- [1;31m17[0m [2m│[0m let x = <> {[1;31m1.[0m} >
- 18 [2m│[0m
+ 16 [2m│[0m }
+ 17 [2m│[0m
+ [1;31m18[0m [2m│[0m let x = <> {[1;31m1.[0m} >
+ 19 [2m│[0m
This has type: [1;31mfloat[0m
But children of JSX fragments must be of type:
diff --git a/tests/build_tests/super_errors/expected/jsx_type_mismatch_int.res.expected b/tests/build_tests/super_errors/expected/jsx_type_mismatch_int.res.expected
index 8bcf5e984bc..53ec9873d03 100644
--- a/tests/build_tests/super_errors/expected/jsx_type_mismatch_int.res.expected
+++ b/tests/build_tests/super_errors/expected/jsx_type_mismatch_int.res.expected
@@ -1,11 +1,11 @@
[1;31mWe've found a bug for you![0m
- [36m/.../fixtures/jsx_type_mismatch_int.res[0m:[2m17:13[0m
+ [36m/.../fixtures/jsx_type_mismatch_int.res[0m:[2m18:13[0m
- 15 [2m│[0m }
- 16 [2m│[0m
- [1;31m17[0m [2m│[0m let x = <> {[1;31m1[0m} >
- 18 [2m│[0m
+ 16 [2m│[0m }
+ 17 [2m│[0m
+ [1;31m18[0m [2m│[0m let x = <> {[1;31m1[0m} >
+ 19 [2m│[0m
This has type: [1;31mint[0m
But children of JSX fragments must be of type:
diff --git a/tests/build_tests/super_errors/expected/jsx_type_mismatch_option.res.expected b/tests/build_tests/super_errors/expected/jsx_type_mismatch_option.res.expected
index fee1ae03c6b..d68678a10af 100644
--- a/tests/build_tests/super_errors/expected/jsx_type_mismatch_option.res.expected
+++ b/tests/build_tests/super_errors/expected/jsx_type_mismatch_option.res.expected
@@ -1,11 +1,11 @@
[1;31mWe've found a bug for you![0m
- [36m/.../fixtures/jsx_type_mismatch_option.res[0m:[2m17:13-16[0m
+ [36m/.../fixtures/jsx_type_mismatch_option.res[0m:[2m18:13-16[0m
- 15 [2m│[0m }
- 16 [2m│[0m
- [1;31m17[0m [2m│[0m let x = <> {[1;31mNone[0m} >
- 18 [2m│[0m
+ 16 [2m│[0m }
+ 17 [2m│[0m
+ [1;31m18[0m [2m│[0m let x = <> {[1;31mNone[0m} >
+ 19 [2m│[0m
This has type: [1;31moption<'a>[0m
But it's expected to have type: [1;33mReact.element[0m [2m(defined as[0m [1;33mJsx.element[0m[2m)[0m
diff --git a/tests/build_tests/super_errors/expected/jsx_type_mismatch_string.res.expected b/tests/build_tests/super_errors/expected/jsx_type_mismatch_string.res.expected
index 63d11ba4f8c..42a24931952 100644
--- a/tests/build_tests/super_errors/expected/jsx_type_mismatch_string.res.expected
+++ b/tests/build_tests/super_errors/expected/jsx_type_mismatch_string.res.expected
@@ -1,11 +1,11 @@
[1;31mWe've found a bug for you![0m
- [36m/.../fixtures/jsx_type_mismatch_string.res[0m:[2m17:13-14[0m
+ [36m/.../fixtures/jsx_type_mismatch_string.res[0m:[2m18:13-14[0m
- 15 [2m│[0m }
- 16 [2m│[0m
- [1;31m17[0m [2m│[0m let x = <> {[1;31m""[0m} >
- 18 [2m│[0m
+ 16 [2m│[0m }
+ 17 [2m│[0m
+ [1;31m18[0m [2m│[0m let x = <> {[1;31m""[0m} >
+ 19 [2m│[0m
This has type: [1;31mstring[0m
But children of JSX fragments must be of type:
diff --git a/tests/build_tests/super_errors/fixtures/jsx_custom_component_children.res b/tests/build_tests/super_errors/fixtures/jsx_custom_component_children.res
index b4059e242b6..6dd8fc64321 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_custom_component_children.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_custom_component_children.res
@@ -6,6 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
+ external component: componentLike<'props, element> => component<'props> = "%identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_custom_component_optional_prop.res b/tests/build_tests/super_errors/fixtures/jsx_custom_component_optional_prop.res
index 3c3b220fdbc..613bc87c666 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_custom_component_optional_prop.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_custom_component_optional_prop.res
@@ -6,6 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
+ external component: componentLike<'props, element> => component<'props> = "%identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_custom_component_type_mismatch.res b/tests/build_tests/super_errors/fixtures/jsx_custom_component_type_mismatch.res
index 58759ac7e89..a03c235ccd1 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_custom_component_type_mismatch.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_custom_component_type_mismatch.res
@@ -6,6 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
+ external component: componentLike<'props, element> => component<'props> = "%identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_maybe_missing_fragment.res b/tests/build_tests/super_errors/fixtures/jsx_maybe_missing_fragment.res
index b0c101a4f0e..c148532b07d 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_maybe_missing_fragment.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_maybe_missing_fragment.res
@@ -6,6 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
+ external component: componentLike<'props, element> => component<'props> = "%identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_element.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_element.res
index cebe870397b..ec78b8038df 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_element.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_element.res
@@ -6,6 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
+ external component: componentLike<'props, element> => component<'props> = "%identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_raw.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_raw.res
index 3a3c531b503..5d681e9c904 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_raw.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_raw.res
@@ -6,6 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
+ external component: componentLike<'props, element> => component<'props> = "%identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_float.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_float.res
index 208eacac397..ea6a65c2eb1 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_float.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_float.res
@@ -6,6 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
+ external component: componentLike<'props, element> => component<'props> = "%identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_int.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_int.res
index 6836a4865de..587f55e912f 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_int.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_int.res
@@ -6,6 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
+ external component: componentLike<'props, element> => component<'props> = "%identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_option.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_option.res
index 334d11978c5..a0d833a9e39 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_option.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_option.res
@@ -6,6 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
+ external component: componentLike<'props, element> => component<'props> = "%identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_string.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_string.res
index 55ec5786e67..448c58c7b8d 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_string.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_string.res
@@ -6,6 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
+ external component: componentLike<'props, element> => component<'props> = "%identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/gentype_tests/typescript-react-example/src/Hooks.res b/tests/gentype_tests/typescript-react-example/src/Hooks.res
index b57ce241557..a8bcf090a0d 100644
--- a/tests/gentype_tests/typescript-react-example/src/Hooks.res
+++ b/tests/gentype_tests/typescript-react-example/src/Hooks.res
@@ -15,12 +15,16 @@ let make = (~vehicle) => {
)}
setCount(_ => count + 1)}> {React.string("Click me")}
- React.string(x["randomString"])}>
+ React.string(x["randomString"]))}
+ >
{React.string("child1")}
{React.string("child2")}
React.string(x["randomString"])}
+ person={name: "DefaultImport", age: 42}
+ renderMe={React.component(x => React.string(x["randomString"]))}
>
{React.string("child1")}
{React.string("child2")}
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 f2ae63a276f..cf1e0bc6ad1 100644
--- a/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt
@@ -15,11 +15,11 @@ module C0 = {
let text = __text_value
(React.string(text): React.element)
}
- let make = {
+ let make = React.component({
let \"AliasProps$C0" = (props: props<_>) => make(props)
\"AliasProps$C0"
- }
+ })
}
module C1 = {
@@ -37,11 +37,11 @@ module C1 = {
let text = __text_value
(React.string(p ++ text): React.element)
}
- let make = {
+ let make = React.component({
let \"AliasProps$C1" = (props: props<_>) => make(props)
\"AliasProps$C1"
- }
+ })
}
module C2 = {
@@ -58,11 +58,11 @@ module C2 = {
let bar = __foo_value
(React.string(bar): React.element)
}
- let make = {
+ let make = React.component({
let \"AliasProps$C2" = (props: props<_>) => make(props)
\"AliasProps$C2"
- }
+ })
}
module C3 = {
@@ -90,11 +90,11 @@ module C3 = {
}: React.element
)
}
- let make = {
+ let make = React.component({
let \"AliasProps$C3" = (props: props<_>) => make(props)
\"AliasProps$C3"
- }
+ })
}
module C4 = {
@@ -112,11 +112,11 @@ module C4 = {
let x = __x_value
(ReactDOM.jsx("div", {children: ?ReactDOM.someElement(b)}): React.element)
}
- let make = {
+ let make = React.component({
let \"AliasProps$C4" = (props: props<_>) => make(props)
\"AliasProps$C4"
- }
+ })
}
module C5 = {
@@ -134,11 +134,11 @@ module C5 = {
let z = __z_value
(x + y + z: React.element)
}
- let make = {
+ let make = React.component({
let \"AliasProps$C5" = (props: props<_>) => make(props)
\"AliasProps$C5"
- }
+ })
}
module C6 = {
@@ -156,9 +156,9 @@ module C6 = {
let make = ({comp: module(Comp: Comp), x: (a, b), _}: props<_, _>): React.element =>
React.jsx(Comp.make, {})
- let make = {
+ let make = React.component({
let \"AliasProps$C6" = (props: props<_>) => make(props)
\"AliasProps$C6"
- }
+ })
}
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 d77b11decd3..d78934b3c3d 100644
--- a/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt
@@ -10,11 +10,11 @@ module C0 = {
let a = await f(a)
(ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.int(a)})}): React.element)
}
- let make = {
+ let make = React.component({
let \"AsyncAwait$C0" = (props: props<_>) => Jsx.promise(make(props))
\"AsyncAwait$C0"
- }
+ })
}
module C1 = {
@@ -29,9 +29,9 @@ module C1 = {
| #off => React.string("off")
}
}
- let make = {
+ let make = React.component({
let \"AsyncAwait$C1" = (props: props<_>) => Jsx.promise(make(props))
\"AsyncAwait$C1"
- }
+ })
}
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 e8242d70cb3..033f9b418d6 100644
--- a/tests/syntax_tests/data/ppx/react/expected/commentAtTop.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/commentAtTop.res.txt
@@ -6,8 +6,8 @@ type props<'msg> = {
let make = ({msg, _}: props<_>): React.element => {
ReactDOM.jsx("div", {children: ?ReactDOM.someElement({msg->React.string})})
}
-let make = {
+let make = React.component({
let \"CommentAtTop" = (props: props<_>) => make(props)
\"CommentAtTop"
-}
+})
diff --git a/tests/syntax_tests/data/ppx/react/expected/defaultPatternProp.res.txt b/tests/syntax_tests/data/ppx/react/expected/defaultPatternProp.res.txt
index 97ed9f45567..b1ea9117691 100644
--- a/tests/syntax_tests/data/ppx/react/expected/defaultPatternProp.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/defaultPatternProp.res.txt
@@ -6,11 +6,11 @@ module C0 = {
type props = {}
let make = (_: props): React.element => React.null
- let make = {
+ let make = React.component({
let \"DefaultPatternProp$C0$M" = props => make(props)
\"DefaultPatternProp$C0$M"
- }
+ })
}
module type S = module type of M
@@ -27,9 +27,9 @@ module C0 = {
let module(C: S) = __component_value
(React.jsx(C.make, {}): React.element)
}
- let make = {
+ let make = React.component({
let \"DefaultPatternProp$C0" = (props: props<_>) => make(props)
\"DefaultPatternProp$C0"
- }
+ })
}
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 c8a7759839e..5406c7724d4 100644
--- a/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt
@@ -17,10 +17,10 @@ module C0 = {
let b = __b_value
(React.int(a + b): React.element)
}
- let make = {
+ let make = React.component({
let \"DefaultValueProp$C0" = (props: props<_>) => make(props)
\"DefaultValueProp$C0"
- }
+ })
}
module C1 = {
@@ -38,11 +38,11 @@ module C1 = {
let a = __a_value
(React.int(a + b): React.element)
}
- let make = {
+ let make = React.component({
let \"DefaultValueProp$C1" = (props: props<_>) => make(props)
\"DefaultValueProp$C1"
- }
+ })
}
module C2 = {
@@ -60,11 +60,11 @@ module C2 = {
let a = __a_value
(React.string(a): React.element)
}
- let make = {
+ let make = React.component({
let \"DefaultValueProp$C2" = (props: props<_>) => make(props)
\"DefaultValueProp$C2"
- }
+ })
}
module C3 = {
@@ -85,9 +85,9 @@ module C3 = {
}: React.element
)
}
- let make = {
+ let make = React.component({
let \"DefaultValueProp$C3" = (props: props<_>) => make(props)
\"DefaultValueProp$C3"
- }
+ })
}
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 77cde0caea1..c5566f8e46e 100644
--- a/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt
@@ -9,9 +9,9 @@ module V4A = {
let make = ({msg, _}: props<_>): React.element => {
ReactDOM.jsx("div", {children: ?ReactDOM.someElement({msg->React.string})})
}
- let make = {
+ let make = React.component({
let \"FileLevelConfig$V4A" = (props: props<_>) => make(props)
\"FileLevelConfig$V4A"
- }
+ })
}
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 8cd3bcff8ce..4d48136cc0f 100644
--- a/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt
@@ -51,11 +51,11 @@ module V4A = {
): React.element
)
}
- let make = {
+ let make = React.component({
let \"ForwardRef$V4A" = props => make(props)
\"ForwardRef$V4A"
- }
+ })
}
module V4AUncurried = {
@@ -109,9 +109,9 @@ module V4AUncurried = {
): React.element
)
}
- let make = {
+ let make = React.component({
let \"ForwardRef$V4AUncurried" = props => make(props)
\"ForwardRef$V4AUncurried"
- }
+ })
}
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 df0f7137fb5..77b6e77aec2 100644
--- a/tests/syntax_tests/data/ppx/react/expected/interface.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/interface.res.txt
@@ -4,10 +4,10 @@ module A = {
x: 'x,
}
let make = ({x, _}: props<_>): React.element => React.string(x)
- let make = {
+ let make = React.component({
let \"Interface$A" = (props: props<_>) => make(props)
\"Interface$A"
- }
+ })
}
module NoProps = {
@@ -15,9 +15,9 @@ module NoProps = {
type props = {}
let make = (_: props): React.element => ReactDOM.jsx("div", {})
- let make = {
+ let make = React.component({
let \"Interface$NoProps" = props => make(props)
\"Interface$NoProps"
- }
+ })
}
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 f1e40360ec5..fd905c590ba 100644
--- a/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt
@@ -9,11 +9,11 @@ module C4A0 = {
let make = ({@as("open") _open, @as("type") _type, _}: props<_, string>): React.element =>
React.string(_open)
- let make = {
+ let make = React.component({
let \"MangleKeyword$C4A0" = (props: props<_>) => make(props)
\"MangleKeyword$C4A0"
- }
+ })
}
module C4A1 = {
@res.jsxComponentProps @live
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 70f8efde0e3..4931436a500 100644
--- a/tests/syntax_tests/data/ppx/react/expected/nested.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/nested.res.txt
@@ -7,17 +7,17 @@ module Outer = {
type props = {}
let make = (_: props): React.element => ReactDOM.jsx("div", {})
- let make = {
+ let make = React.component({
let \"Nested$Outer" = props => make(props)
\"Nested$Outer"
- }
+ })
}
React.jsx(Inner.make, {})
}
- let make = {
+ let make = React.component({
let \"Nested$Outer" = props => make(props)
\"Nested$Outer"
- }
+ })
}
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 2fdecb86740..5873c71b31e 100644
--- a/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt
@@ -10,11 +10,11 @@ module V4A = {
let make = (type a, {a, b, c, _}: props>, 'a>): React.element =>
ReactDOM.jsx("div", {})
- let make = {
+ let make = React.component({
let \"Newtype$V4A" = (props: props<_>) => make(props)
\"Newtype$V4A"
- }
+ })
}
module V4A1 = {
@@ -27,11 +27,11 @@ module V4A1 = {
let make = (type x y, {a, b, c, _}: props, 'a>): React.element =>
ReactDOM.jsx("div", {})
- let make = {
+ let make = React.component({
let \"Newtype$V4A1" = (props: props<_>) => make(props)
\"Newtype$V4A1"
- }
+ })
}
module type T = {
@@ -48,11 +48,11 @@ module V4A2 = {
module T = unpack(foo)
ReactDOM.jsx("div", {})
}
- let make = {
+ let make = React.component({
let \"Newtype$V4A2" = (props: props<_>) => make(props)
\"Newtype$V4A2"
- }
+ })
}
module V4A3 = {
@@ -65,11 +65,11 @@ module V4A3 = {
module T = unpack(foo: T with type t = a)
foo
}
- let make = {
+ let make = React.component({
let \"Newtype$V4A3" = (props: props<_>) => make(props)
\"Newtype$V4A3"
- }
+ })
}
@res.jsxComponentProps
type props<'x, 'q> = {
@@ -78,11 +78,11 @@ type props<'x, 'q> = {
}
let make = ({x, q, _}: props<('a, 'b), 'a>): React.element => [fst(x), q]
-let make = {
+let make = React.component({
let \"Newtype" = (props: props<_>) => make(props)
\"Newtype"
-}
+})
@@uncurried
@@ -93,9 +93,9 @@ module Uncurried = {
}
let make = (type a, {?foo, _}: props<_>): React.element => React.null
- let make = {
+ let make = React.component({
let \"Newtype$Uncurried" = (props: props<_>) => make(props)
\"Newtype$Uncurried"
- }
+ })
}
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 aa8dc64f353..443bb57c62e 100644
--- a/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt
@@ -5,11 +5,11 @@ module V4CA = {
type props = {}
let make = (_: props): React.element => ReactDOM.jsx("div", {})
- let make = {
+ let make = React.component({
let \"NoPropsWithKey$V4CA" = props => make(props)
\"NoPropsWithKey$V4CA"
- }
+ })
}
module V4CB = {
@@ -34,9 +34,9 @@ module V4C = {
]),
},
)
- let make = {
+ let make = React.component({
let \"NoPropsWithKey$V4C" = props => make(props)
\"NoPropsWithKey$V4C"
- }
+ })
}
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 dfb3506590e..7ccf95dfb35 100644
--- a/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt
@@ -12,9 +12,9 @@ module User = {
let make = ({doctor, _}: props<_>): React.element => {
ReactDOM.jsx("h1", {id: "h1", children: ?ReactDOM.someElement({React.string(format(doctor))})})
}
- let make = {
+ let make = React.component({
let \"OptimizeAutomaticMode$User" = (props: props<_>) => make(props)
\"OptimizeAutomaticMode$User"
- }
+ })
}
diff --git a/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt b/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt
index 7d017b6d9b7..43e3ca14aef 100644
--- a/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt
@@ -5,11 +5,11 @@ module Standard = {
type props = {}
let make = (_: props): React.element => React.string("ok")
- let make = {
+ let make = React.component({
let \"ReturnConstraint$Standard" = props => make(props)
\"ReturnConstraint$Standard"
- }
+ })
}
module ForwardRef = {
@@ -29,10 +29,10 @@ module WithProps = {
let make = (props: props): React.element =>
ReactDOM.jsx("span", {children: ?ReactDOM.someElement({React.int(props.value)})})
- let make = {
+ let make = React.component({
let \"ReturnConstraint$WithProps" = (props: props): React.element => make(props)
\"ReturnConstraint$WithProps"
- }
+ })
}
module Async = {
@@ -41,9 +41,9 @@ module Async = {
let make = async (_: props): React.element =>
ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.string("async")})})
- let make = {
+ let make = React.component({
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 83c7a5fb041..34d29a757e4 100644
--- a/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt
@@ -4,44 +4,44 @@ module V4A1 = {
type props = sharedProps
let make = ({x, y, _}: props): React.element => React.string(x ++ y)
- let make = {
+ let make = React.component({
let \"SharedProps$V4A1" = props => make(props)
\"SharedProps$V4A1"
- }
+ })
}
module V4A2 = {
type props<'a> = sharedProps<'a>
let make = ({x, y, _}: props<_>): React.element => React.string(x ++ y)
- let make = {
+ let make = React.component({
let \"SharedProps$V4A2" = (props: props<_>) => make(props)
\"SharedProps$V4A2"
- }
+ })
}
module V4A3 = {
type props<'a> = sharedProps
let make = ({x, y, _}: props<_>): React.element => React.string(x ++ y)
- let make = {
+ let make = React.component({
let \"SharedProps$V4A3" = (props: props<_>) => make(props)
\"SharedProps$V4A3"
- }
+ })
}
module V4A4 = {
type props = sharedProps
let make = ({x, y, _}: props): React.element => React.string(x ++ y)
- let make = {
+ let make = React.component({
let \"SharedProps$V4A4" = props => make(props)
\"SharedProps$V4A4"
- }
+ })
}
module V4A5 = {
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 8abf4452b0f..c1a149f1c6e 100644
--- a/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt
@@ -5,37 +5,37 @@ let f = a => Js.Promise.resolve(a + a)
module V4A1 = {
type props = sharedProps
let make = (props): React.element => React.string(props.x ++ props.y)
- let make = {
+ let make = React.component({
let \"SharedPropsWithProps$V4A1" = (props): React.element => make(props)
\"SharedPropsWithProps$V4A1"
- }
+ })
}
module V4A2 = {
type props = sharedProps
let make = (props: props): React.element => React.string(props.x ++ props.y)
- let make = {
+ let make = React.component({
let \"SharedPropsWithProps$V4A2" = (props: props): React.element => make(props)
\"SharedPropsWithProps$V4A2"
- }
+ })
}
module V4A3 = {
type props<'a> = sharedProps<'a>
let make = ({x, y}: props<_>): React.element => React.string(x ++ y)
- let make = {
+ let make = React.component({
let \"SharedPropsWithProps$V4A3" = (props: props<_>): React.element => make(props)
\"SharedPropsWithProps$V4A3"
- }
+ })
}
module V4A4 = {
type props<'a> = sharedProps
let make = ({x, y}: props<_>): React.element => React.string(x ++ y)
- let make = {
+ let make = React.component({
let \"SharedPropsWithProps$V4A4" = (props: props<_>): React.element => make(props)
\"SharedPropsWithProps$V4A4"
- }
+ })
}
module V4A5 = {
@@ -44,10 +44,10 @@ module V4A5 = {
let a = await f(a)
(ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.int(a)})}): React.element)
}
- let make = {
+ let make = React.component({
let \"SharedPropsWithProps$V4A5" = (props: props<_>): React.element => Jsx.promise(make(props))
\"SharedPropsWithProps$V4A5"
- }
+ })
}
module V4A6 = {
@@ -58,10 +58,10 @@ module V4A6 = {
| #off => React.string("off")
}
}
- let make = {
+ let make = React.component({
let \"SharedPropsWithProps$V4A6" = (props: props<_>): React.element => Jsx.promise(make(props))
\"SharedPropsWithProps$V4A6"
- }
+ })
}
module V4A7 = {
@@ -70,9 +70,9 @@ module V4A7 = {
let make = (props): React.element => {
React.int(props.count)
}
- let make = {
+ let make = React.component({
let \"SharedPropsWithProps$V4A7" =
@directive("'use memo'") (props): React.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 9330346a961..619d35c0355 100644
--- a/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt
@@ -11,9 +11,9 @@ module V4A = {
Js.log("This function should be named 'TopLevel.react'")
(ReactDOM.jsx("div", {}): React.element)
}
- let make = {
+ let make = React.component({
let \"TopLevel$V4A" = (props: props<_>) => make(props)
\"TopLevel$V4A"
- }
+ })
}
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 68a3b0278b3..491f5c7d191 100644
--- a/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt
@@ -8,9 +8,9 @@ module V4A = {
}
let make = (type a, {a, b, _}: props<_, _>): React.element => ReactDOM.jsx("div", {})
- let make = {
+ let make = React.component({
let \"TypeConstraint$V4A" = (props: props<_>) => make(props)
\"TypeConstraint$V4A"
- }
+ })
}
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 9d81169bd1e..9e635bafdd6 100644
--- a/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt
@@ -12,11 +12,11 @@ let make = ({a: ?__a, _}: props unit>) => {
let a: unit => unit = __a_value
(React.null: React.element)
}
-let make = {
+let make = React.component({
let \"UncurriedProps" = (props: props<_>) => make(props)
\"UncurriedProps"
-}
+})
let func = (~callback: (string, bool, bool) => unit=(_, _, _) => (), ()) => {
let _ = callback
@@ -46,11 +46,11 @@ module Foo = {
}: React.element
)
}
- let make = {
+ let make = React.component({
let \"UncurriedProps$Foo" = (props: props<_>) => make(props)
\"UncurriedProps$Foo"
- }
+ })
}
module Bar = {
@@ -58,9 +58,9 @@ module Bar = {
type props = {}
let make = (_: props): React.element => React.jsx(Foo.make, {callback: {(_, _, _) => ()}})
- let make = {
+ let make = React.component({
let \"UncurriedProps$Bar" = props => make(props)
\"UncurriedProps$Bar"
- }
+ })
}
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 a6f05b0e17f..a9c093b3475 100644
--- a/tests/syntax_tests/data/ppx/react/expected/v4.res.txt
+++ b/tests/syntax_tests/data/ppx/react/expected/v4.res.txt
@@ -4,10 +4,10 @@ type props<'x, 'y> = {
y: 'y,
}
let make = ({x, y, _}: props): React.element => React.string(x ++ y)
-let make = {
+let make = React.component({
let \"V4" = (props: props<_>) => make(props)
\"V4"
-}
+})
module AnotherName = {
@res.jsxComponentProps
@@ -17,11 +17,11 @@ module AnotherName = {
}
let anotherName = ({x, _}: props<_>): React.element => React.string(x)
- let anotherName = {
+ let anotherName = React.component({
let \"V4$AnotherName$anotherName" = (props: props<_>) => anotherName(props)
\"V4$AnotherName$anotherName"
- }
+ })
}
module Uncurried = {
@@ -31,11 +31,11 @@ module Uncurried = {
}
let make = ({x, _}: props<_>): React.element => React.string(x)
- let make = {
+ let make = React.component({
let \"V4$Uncurried" = (props: props<_>) => make(props)
\"V4$Uncurried"
- }
+ })
}
module type TUncurried = {
@@ -69,50 +69,42 @@ module Rec = {
@res.jsxComponentProps
type props = {}
- let rec make = {
- let \"make$Internal" = (_: props): React.element => {
- make(({}: props))
- }
- let make = {
- let \"V4$Rec" = props => \"make$Internal"(props)
-
- \"V4$Rec"
- }
- make
+ let rec make = (_: props): React.element => {
+ make(({}: props))
}
+ let make = React.component({
+ let \"V4$Rec" = props => make(props)
+
+ \"V4$Rec"
+ })
}
module Rec1 = {
@res.jsxComponentProps
type props = {}
- let rec make = {
- let \"make$Internal" = (_: props): React.element => {
- React.null
- }
- let make = {
- let \"V4$Rec1" = props => \"make$Internal"(props)
-
- \"V4$Rec1"
- }
- make
+ let rec make = (_: props): React.element => {
+ React.null
}
+ let make = React.component({
+ let \"V4$Rec1" = props => make(props)
+
+ \"V4$Rec1"
+ })
}
module Rec2 = {
@res.jsxComponentProps
type props = {}
- let rec make = {
- let \"make$Internal" = (_: props): React.element => {
- mm(({}: props))
- }
- let make = {
- let \"V4$Rec2" = props => \"make$Internal"(props)
-
- \"V4$Rec2"
- }
- make
+ let rec make = (_: props): React.element => {
+ mm(({}: props))
}
+
and mm = x => make(x)
+ let make = React.component({
+ let \"V4$Rec2" = props => make(props)
+
+ \"V4$Rec2"
+ })
}
diff --git a/tests/tests/src/UncurriedAlways.res b/tests/tests/src/UncurriedAlways.res
index 8bb4a44cf15..68be69c634c 100644
--- a/tests/tests/src/UncurriedAlways.res
+++ b/tests/tests/src/UncurriedAlways.res
@@ -25,7 +25,7 @@ let foo3 = (x, y, z) => x + y + z
let bar3: _ => _ = foo3(_, 3, 4)
type cmp = Jsx.component
-let q: cmp = _ => Jsx.null // Check that subtyping works past type definitions
+let q: cmp = Jsx.component(_ => Jsx.null) // Check that subtyping works past type definitions
@inline
let inl = () => ()
diff --git a/tests/tests/src/jsx_preserve_test.mjs b/tests/tests/src/jsx_preserve_test.mjs
index 570ca4607cf..bcc16a2d3ce 100644
--- a/tests/tests/src/jsx_preserve_test.mjs
+++ b/tests/tests/src/jsx_preserve_test.mjs
@@ -208,12 +208,12 @@ let _youtube_iframe = ;
-function make(_props) {
+function Jsx_preserve_test$X(props) {
return null;
}
let X = {
- make: make
+ make: Jsx_preserve_test$X
};
;
@@ -222,21 +222,21 @@ function Jsx_preserve_test$Y(props) {
return null;
}
-let make$1 = React.memo(Jsx_preserve_test$Y);
+let make = React.memo(Jsx_preserve_test$Y);
let Y = {
x: 42,
- make: make$1
+ make: make
};
;
let context = React.createContext(0);
-let make$2 = context.Provider;
+let make$1 = context.Provider;
let ContextProvider = {
- make: make$2
+ make: make$1
};
function Jsx_preserve_test(props) {
@@ -247,7 +247,7 @@ function Jsx_preserve_test(props) {
;
}
-let make$3 = Jsx_preserve_test;
+let make$2 = Jsx_preserve_test;
export {
Icon,
@@ -280,6 +280,6 @@ export {
Y,
context,
ContextProvider,
- make$3 as make,
+ make$2 as make,
}
/* _single_element_child Not a pure module */
diff --git a/tests/tests/src/jsx_preserve_test.res b/tests/tests/src/jsx_preserve_test.res
index aeeaad15310..c824aab283c 100644
--- a/tests/tests/src/jsx_preserve_test.res
+++ b/tests/tests/src/jsx_preserve_test.res
@@ -142,6 +142,7 @@ let _youtube_iframe =
module X = {
type props = {}
+ @react.componentWithProps
let make = (_props: props) => React.null
}
diff --git a/tests/tests/src/recursive_react_component.mjs b/tests/tests/src/recursive_react_component.mjs
index 554e87ec77f..eda1ad70a8a 100644
--- a/tests/tests/src/recursive_react_component.mjs
+++ b/tests/tests/src/recursive_react_component.mjs
@@ -2,13 +2,17 @@
import * as React from "react";
-function make(props) {
+function make(param) {
return React.createElement(make, {
- foo: props.foo
+ foo: param.foo
});
}
+let Recursive_react_component = make;
+
+let make$1 = Recursive_react_component;
+
export {
- make,
+ make$1 as make,
}
/* react Not a pure module */
From 37f339560fa22669273d52e4a160ef7b059e657b Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sat, 25 Apr 2026 11:39:33 +0200
Subject: [PATCH 02/16] Fix shadowing issue
---
compiler/syntax/src/jsx_v4.ml | 34 +++++++++++++++++--
tests/tests/src/recursive_react_component.mjs | 20 +++++++++++
tests/tests/src/recursive_react_component.res | 13 +++++++
3 files changed, 65 insertions(+), 2 deletions(-)
diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml
index 272cebdf86a..64183f6206b 100644
--- a/compiler/syntax/src/jsx_v4.ml
+++ b/compiler/syntax/src/jsx_v4.ml
@@ -54,6 +54,27 @@ let jsx_component_expr config ~loc expr =
(Exp.ident ~loc {loc; txt = module_access_name config "component"})
[(Nolabel, expr)]
+let rec pattern_binds_name name pattern =
+ match pattern.ppat_desc with
+ | Ppat_var {txt} | Ppat_unpack {txt} -> txt = name
+ | Ppat_alias (pattern, {txt}) -> txt = name || pattern_binds_name name pattern
+ | Ppat_tuple patterns | Ppat_array patterns ->
+ List.exists (pattern_binds_name name) patterns
+ | Ppat_construct (_, pattern) | Ppat_variant (_, pattern) -> (
+ match pattern with
+ | Some pattern -> pattern_binds_name name pattern
+ | None -> false)
+ | Ppat_exception pattern -> pattern_binds_name name pattern
+ | Ppat_record (fields, _) ->
+ List.exists (fun {x = pattern} -> pattern_binds_name name pattern) fields
+ | Ppat_or (left, right) ->
+ pattern_binds_name name left || pattern_binds_name name right
+ | Ppat_constraint (pattern, _) | Ppat_open (_, pattern) ->
+ pattern_binds_name name pattern
+ | Ppat_any | Ppat_constant _ | Ppat_interval _ | Ppat_type _
+ | Ppat_extension _ ->
+ false
+
let wrap_recursive_component_self_references ~config ~fn_name expr =
let jsx_module = String.capitalize_ascii config.Jsx_common.module_ in
let accepts_component = function
@@ -804,6 +825,9 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
| [] -> Pat.any ()
| _ -> Pat.record (List.rev patterns_with_label) Open
in
+ let self_reference_is_shadowed_by_props =
+ pattern_binds_name fn_name record_pattern
+ in
let expression =
(* Shape internal implementation to match wrapper: uncurried when using forwardRef. *)
let total_arity = if has_forward_ref then 2 else 1 in
@@ -839,8 +863,14 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
Wrap only those self-references in the `%identity` component
coercion. The generated JavaScript still receives the same function,
- while the typed AST sees a `React.component<_>`. *)
- wrap_recursive_component_self_references ~config ~fn_name expression
+ while the typed AST sees a `React.component<_>`.
+
+ If the generated props pattern itself binds `make`, then references
+ inside the body point at that prop, not at the recursive function. In
+ that case there is no self-reference to coerce. *)
+ if self_reference_is_shadowed_by_props then expression
+ else
+ wrap_recursive_component_self_references ~config ~fn_name expression
in
(* let make = ({id, name, ...}: props<'id, 'name, ...>) => { ... } *)
let binding =
diff --git a/tests/tests/src/recursive_react_component.mjs b/tests/tests/src/recursive_react_component.mjs
index eda1ad70a8a..07a343d1094 100644
--- a/tests/tests/src/recursive_react_component.mjs
+++ b/tests/tests/src/recursive_react_component.mjs
@@ -10,9 +10,29 @@ function make(param) {
let Recursive_react_component = make;
+function Recursive_react_component$ShadowedSelfReference$Child(props) {
+ return props.foo;
+}
+
+let Child = {
+ make: Recursive_react_component$ShadowedSelfReference$Child
+};
+
+function Recursive_react_component$ShadowedSelfReference(props) {
+ return React.createElement(props.make, {
+ foo: props.foo
+ });
+}
+
+let ShadowedSelfReference = {
+ Child: Child,
+ make: Recursive_react_component$ShadowedSelfReference
+};
+
let make$1 = Recursive_react_component;
export {
make$1 as make,
+ ShadowedSelfReference,
}
/* react Not a pure module */
diff --git a/tests/tests/src/recursive_react_component.res b/tests/tests/src/recursive_react_component.res
index e2d7e3cdf00..a8a83bbd66c 100644
--- a/tests/tests/src/recursive_react_component.res
+++ b/tests/tests/src/recursive_react_component.res
@@ -9,3 +9,16 @@
@react.component
let rec make = (~foo, ()) => React.createElement(make, {foo: foo})
+
+module ShadowedSelfReference = {
+ type childProps = {foo: int}
+
+ module Child = {
+ @react.component
+ let make = (~foo) => React.int(foo)
+ }
+
+ @react.component
+ let rec make = (~make: React.component, ~foo, ()) =>
+ React.createElement(make, {foo: foo})
+}
From fe82d34a10d6af66990393f02dec059b3cf777a0 Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sat, 25 Apr 2026 20:44:48 +0200
Subject: [PATCH 03/16] Clean up
---
compiler/ml/typecore.ml | 26 ++++++-------------
compiler/syntax/src/jsx_v4.ml | 8 ++++++
.../src/identity_generalization_test.res | 8 ++++++
.../src/identity_generalization_test.res.js | 17 ++++++++++++
tests/tests/src/recursive_react_component.mjs | 9 -------
tests/tests/src/recursive_react_component.res | 10 ++-----
6 files changed, 43 insertions(+), 35 deletions(-)
create mode 100644 tests/build_tests/react_ppx/src/identity_generalization_test.res
create mode 100644 tests/build_tests/react_ppx/src/identity_generalization_test.res.js
diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml
index 9f15b85a12f..c7d08068496 100644
--- a/compiler/ml/typecore.ml
+++ b/compiler/ml/typecore.ml
@@ -1808,6 +1808,12 @@ let rec final_subexpression sexp =
(* Generalization criterion for expressions *)
+let is_identity = function
+ | Texp_ident (_, _, {val_kind = Val_prim {Primitive.prim_name = "%identity"}})
+ ->
+ true
+ | _ -> false
+
let rec is_nonexpansive exp =
List.exists
(function
@@ -1838,17 +1844,7 @@ let rec is_nonexpansive exp =
`React.component`, whose implementation is `%identity`. Since no runtime
computation happens beyond evaluating the argument, the application is
non-expansive exactly when all supplied arguments are non-expansive. *)
- | Texp_apply
- {
- funct =
- {
- exp_desc =
- Texp_ident
- (_, _, {val_kind = Val_prim {Primitive.prim_name = "%identity"}});
- };
- args;
- _;
- } ->
+ | Texp_apply {funct = {exp_desc}; args; _} when is_identity exp_desc ->
List.for_all is_nonexpansive_opt (List.map snd args)
| Texp_apply {partial = true; _} ->
(* ReScript partial applications (`foo(args, ...)`) lower to wrapper
@@ -2266,12 +2262,6 @@ let is_ignore ~env ~arity funct =
with Unify _ -> false)
| _ -> false
-let not_identity = function
- | Texp_ident (_, _, {val_kind = Val_prim {Primitive.prim_name = "%identity"}})
- ->
- false
- | _ -> true
-
let rec lower_args env seen ty_fun =
let ty = expand_head env ty_fun in
if List.memq ty seen then ()
@@ -3856,7 +3846,7 @@ and type_application ~context total_app env funct (sargs : sargs) :
(* This is a total application when the toplevel type is a polymorphic variable,
so the function type including arity can be inferred. *)
let t1 = newvar () and t2 = newvar () in
- if ty_fun.level >= t1.level && not_identity funct.exp_desc then
+ if ty_fun.level >= t1.level && not (is_identity funct.exp_desc) then
Location.prerr_warning sarg1.pexp_loc Warnings.Unused_argument;
unify env ty_fun
(newty
diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml
index 64183f6206b..b28deb9156f 100644
--- a/compiler/syntax/src/jsx_v4.ml
+++ b/compiler/syntax/src/jsx_v4.ml
@@ -54,6 +54,10 @@ let jsx_component_expr config ~loc expr =
(Exp.ident ~loc {loc; txt = module_access_name config "component"})
[(Nolabel, expr)]
+(* Check whether a generated props pattern introduces a binding with this name.
+ This is intentionally narrower than full lexical scope tracking: it only
+ guards the recursive component rewrite from confusing a prop named `make`
+ with the recursive function named `make`. *)
let rec pattern_binds_name name pattern =
match pattern.ppat_desc with
| Ppat_var {txt} | Ppat_unpack {txt} -> txt = name
@@ -75,6 +79,10 @@ let rec pattern_binds_name name pattern =
| Ppat_extension _ ->
false
+(* In a recursive component, [fn_name] still refers to the implementation
+ function so recursive calls like `make(props)` keep working. But React APIs
+ such as `React.createElement(make, props)` now expect an abstract component,
+ so wrap that direct self-reference as `React.component(make)`. *)
let wrap_recursive_component_self_references ~config ~fn_name expr =
let jsx_module = String.capitalize_ascii config.Jsx_common.module_ in
let accepts_component = function
diff --git a/tests/build_tests/react_ppx/src/identity_generalization_test.res b/tests/build_tests/react_ppx/src/identity_generalization_test.res
new file mode 100644
index 00000000000..34d31469844
--- /dev/null
+++ b/tests/build_tests/react_ppx/src/identity_generalization_test.res
@@ -0,0 +1,8 @@
+external id: 'a => 'a = "%identity"
+
+// `%identity` applications should generalize like the wrapped expression.
+// Otherwise, `f` becomes monomorphic and one of these calls fails to typecheck.
+let f = id(x => x)
+
+let intValue = f(1)
+let stringValue = f("one")
diff --git a/tests/build_tests/react_ppx/src/identity_generalization_test.res.js b/tests/build_tests/react_ppx/src/identity_generalization_test.res.js
new file mode 100644
index 00000000000..68936de442b
--- /dev/null
+++ b/tests/build_tests/react_ppx/src/identity_generalization_test.res.js
@@ -0,0 +1,17 @@
+// Generated by ReScript, PLEASE EDIT WITH CARE
+
+
+function f(x) {
+ return x;
+}
+
+let intValue = 1;
+
+let stringValue = "one";
+
+export {
+ f,
+ intValue,
+ stringValue,
+}
+/* No side effect */
diff --git a/tests/tests/src/recursive_react_component.mjs b/tests/tests/src/recursive_react_component.mjs
index 07a343d1094..6d4ad345e07 100644
--- a/tests/tests/src/recursive_react_component.mjs
+++ b/tests/tests/src/recursive_react_component.mjs
@@ -10,14 +10,6 @@ function make(param) {
let Recursive_react_component = make;
-function Recursive_react_component$ShadowedSelfReference$Child(props) {
- return props.foo;
-}
-
-let Child = {
- make: Recursive_react_component$ShadowedSelfReference$Child
-};
-
function Recursive_react_component$ShadowedSelfReference(props) {
return React.createElement(props.make, {
foo: props.foo
@@ -25,7 +17,6 @@ function Recursive_react_component$ShadowedSelfReference(props) {
}
let ShadowedSelfReference = {
- Child: Child,
make: Recursive_react_component$ShadowedSelfReference
};
diff --git a/tests/tests/src/recursive_react_component.res b/tests/tests/src/recursive_react_component.res
index a8a83bbd66c..3200aadabac 100644
--- a/tests/tests/src/recursive_react_component.res
+++ b/tests/tests/src/recursive_react_component.res
@@ -8,17 +8,11 @@
})
@react.component
-let rec make = (~foo, ()) => React.createElement(make, {foo: foo})
+let rec make = (~foo) => React.createElement(make, {foo: foo})
module ShadowedSelfReference = {
type childProps = {foo: int}
- module Child = {
- @react.component
- let make = (~foo) => React.int(foo)
- }
-
@react.component
- let rec make = (~make: React.component, ~foo, ()) =>
- React.createElement(make, {foo: foo})
+ let rec make = (~make: React.component, ~foo) => React.createElement(make, {foo: foo})
}
From caec980917d5afdff1314ebf8f35971272d01b7f Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sat, 25 Apr 2026 21:23:40 +0200
Subject: [PATCH 04/16] More recursive binding fixes
---
compiler/syntax/src/jsx_v4.ml | 124 ++++++++++++++++--
tests/tests/src/recursive_react_component.mjs | 31 +++++
tests/tests/src/recursive_react_component.res | 22 ++++
3 files changed, 164 insertions(+), 13 deletions(-)
diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml
index b28deb9156f..04fecb1300c 100644
--- a/compiler/syntax/src/jsx_v4.ml
+++ b/compiler/syntax/src/jsx_v4.ml
@@ -80,9 +80,15 @@ let rec pattern_binds_name name pattern =
false
(* In a recursive component, [fn_name] still refers to the implementation
- function so recursive calls like `make(props)` keep working. But React APIs
- such as `React.createElement(make, props)` now expect an abstract component,
- so wrap that direct self-reference as `React.component(make)`. *)
+ function, so direct recursive calls like `make(props)` keep working. React
+ APIs such as `React.createElement(make, props)` now expect an abstract
+ component, so wrap only that direct self-reference as
+ `React.component(make)`.
+
+ The rewrite is scope-aware. If a nested binding or pattern introduces the
+ same name, references below that binder point at the local value, not at the
+ recursive component. In that shadowed scope we keep traversing normally but
+ disable the self-reference coercion. *)
let wrap_recursive_component_self_references ~config ~fn_name expr =
let jsx_module = String.capitalize_ascii config.Jsx_common.module_ in
let accepts_component = function
@@ -100,32 +106,124 @@ let wrap_recursive_component_self_references ~config ~fn_name expr =
module_name = jsx_module
| _ -> false
in
- let mapper =
+ (* [shadowed] means the current lexical scope has rebound [fn_name]. The
+ custom cases below are only the binders that can change that state; every
+ other expression shape can use the default mapper. *)
+ let rec mapper shadowed =
{
Ast_mapper.default_mapper with
expr =
- (fun mapper expr ->
+ (fun ast_mapper expr ->
match expr.pexp_desc with
+ | Pexp_let (rec_flag, bindings, body) ->
+ (* Non-recursive bindings are not in scope for their own RHS, while
+ recursive bindings are. The let body is shadowed by any binding
+ in the group either way. *)
+ let bindings_shadow_name =
+ List.exists
+ (fun binding -> pattern_binds_name fn_name binding.pvb_pat)
+ bindings
+ in
+ let bindings =
+ bindings
+ |> List.map (fun binding ->
+ let binding_shadowed =
+ shadowed
+ ||
+ match rec_flag with
+ | Recursive -> bindings_shadow_name
+ | Nonrecursive -> false
+ in
+ let binding_mapper = mapper binding_shadowed in
+ {
+ binding with
+ pvb_expr =
+ binding_mapper.expr binding_mapper binding.pvb_expr;
+ })
+ in
+ let body_mapper = mapper (shadowed || bindings_shadow_name) in
+ let body =
+ body_mapper.expr body_mapper body
+ in
+ {expr with pexp_desc = Pexp_let (rec_flag, bindings, body)}
+ | Pexp_fun ({default; lhs; rhs} as desc) ->
+ (* Optional default expressions are evaluated outside the argument
+ pattern's scope; the function body is inside it. *)
+ let default =
+ match default with
+ | Some default -> Some (ast_mapper.expr ast_mapper default)
+ | None -> None
+ in
+ let rhs_mapper =
+ mapper (shadowed || pattern_binds_name fn_name lhs)
+ in
+ let rhs =
+ rhs_mapper.expr rhs_mapper rhs
+ in
+ {expr with pexp_desc = Pexp_fun {desc with default; rhs}}
| Pexp_apply ({funct; args} as apply) when accepts_component funct ->
- let funct = mapper.expr mapper funct in
+ let funct = ast_mapper.expr ast_mapper funct in
let args =
args
- |> List.map (fun (label, arg) -> (label, mapper.expr mapper arg))
+ |> List.map (fun (label, arg) ->
+ (label, ast_mapper.expr ast_mapper arg))
in
let args =
- match args with
- | ( Nolabel,
- ({pexp_desc = Pexp_ident {txt = Lident name}; pexp_loc = loc}
- as self_ref) )
- :: rest
+ match (shadowed, args) with
+ | ( false,
+ ( Nolabel,
+ ({
+ pexp_desc = Pexp_ident {txt = Lident name};
+ pexp_loc = loc;
+ } as self_ref) )
+ :: rest )
when name = fn_name ->
(Nolabel, jsx_component_expr config ~loc self_ref) :: rest
| _ -> args
in
{expr with pexp_desc = Pexp_apply {apply with funct; args}}
- | _ -> Ast_mapper.default_mapper.expr mapper expr);
+ | Pexp_match (scrutinee, cases) ->
+ let scrutinee = ast_mapper.expr ast_mapper scrutinee in
+ let cases =
+ cases
+ |> List.map (fun ({pc_lhs; pc_guard; pc_rhs} as case) ->
+ let case_shadowed =
+ shadowed || pattern_binds_name fn_name pc_lhs
+ in
+ let case_mapper = mapper case_shadowed in
+ {
+ case with
+ pc_guard =
+ Option.map
+ (fun guard -> case_mapper.expr case_mapper guard)
+ pc_guard;
+ pc_rhs = case_mapper.expr case_mapper pc_rhs;
+ })
+ in
+ {expr with pexp_desc = Pexp_match (scrutinee, cases)}
+ | Pexp_try (body, cases) ->
+ let body = ast_mapper.expr ast_mapper body in
+ let cases =
+ cases
+ |> List.map (fun ({pc_lhs; pc_guard; pc_rhs} as case) ->
+ let case_shadowed =
+ shadowed || pattern_binds_name fn_name pc_lhs
+ in
+ let case_mapper = mapper case_shadowed in
+ {
+ case with
+ pc_guard =
+ Option.map
+ (fun guard -> case_mapper.expr case_mapper guard)
+ pc_guard;
+ pc_rhs = case_mapper.expr case_mapper pc_rhs;
+ })
+ in
+ {expr with pexp_desc = Pexp_try (body, cases)}
+ | _ -> Ast_mapper.default_mapper.expr ast_mapper expr);
}
in
+ let mapper = mapper false in
mapper.expr mapper expr
(* Helper method to filter out any attribute that isn't [@react.component] *)
diff --git a/tests/tests/src/recursive_react_component.mjs b/tests/tests/src/recursive_react_component.mjs
index 6d4ad345e07..d7c3005004b 100644
--- a/tests/tests/src/recursive_react_component.mjs
+++ b/tests/tests/src/recursive_react_component.mjs
@@ -20,10 +20,41 @@ let ShadowedSelfReference = {
make: Recursive_react_component$ShadowedSelfReference
};
+function Recursive_react_component$Leaf(props) {
+ return props.foo;
+}
+
+let Leaf = {
+ make: Recursive_react_component$Leaf
+};
+
+function Recursive_react_component$ShadowedByLocalLet(props) {
+ return React.createElement(Recursive_react_component$Leaf, {
+ foo: props.foo
+ });
+}
+
+let ShadowedByLocalLet = {
+ make: Recursive_react_component$ShadowedByLocalLet
+};
+
+function Recursive_react_component$ShadowedByNestedParameter(props) {
+ return React.createElement(Recursive_react_component$Leaf, {
+ foo: props.foo
+ });
+}
+
+let ShadowedByNestedParameter = {
+ make: Recursive_react_component$ShadowedByNestedParameter
+};
+
let make$1 = Recursive_react_component;
export {
make$1 as make,
ShadowedSelfReference,
+ Leaf,
+ ShadowedByLocalLet,
+ ShadowedByNestedParameter,
}
/* react Not a pure module */
diff --git a/tests/tests/src/recursive_react_component.res b/tests/tests/src/recursive_react_component.res
index 3200aadabac..82f4ed56bdc 100644
--- a/tests/tests/src/recursive_react_component.res
+++ b/tests/tests/src/recursive_react_component.res
@@ -16,3 +16,25 @@ module ShadowedSelfReference = {
@react.component
let rec make = (~make: React.component, ~foo) => React.createElement(make, {foo: foo})
}
+
+module Leaf = {
+ @react.component
+ let make = (~foo) => React.int(foo)
+}
+
+module ShadowedByLocalLet = {
+ @react.component
+ let rec make = (~foo) => {
+ let make = Leaf.make
+ React.createElement(make, {foo: foo})
+ }
+}
+
+module ShadowedByNestedParameter = {
+ @react.component
+ let rec make = (~foo) => {
+ let render = (make: React.component>) =>
+ React.createElement(make, ({foo: foo}: Leaf.props))
+ render(Leaf.make)
+ }
+}
From 26a5f4cb097c79db95bc0c730dcb841295169b92 Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sat, 25 Apr 2026 21:24:26 +0200
Subject: [PATCH 05/16] CHANGELOG
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 579b2508fda..016da97df83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,8 @@
#### :boom: Breaking Change
+- Make Jsx.component abstract. https://github.com/rescript-lang/rescript/pull/8390
+
#### :eyeglasses: Spec Compliance
#### :rocket: New Feature
From fdcc4c007fa5952be1999531ce117bfa17a61577 Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sat, 25 Apr 2026 21:26:39 +0200
Subject: [PATCH 06/16] Format
---
compiler/syntax/src/jsx_v4.ml | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml
index 04fecb1300c..dd93fe381ef 100644
--- a/compiler/syntax/src/jsx_v4.ml
+++ b/compiler/syntax/src/jsx_v4.ml
@@ -142,9 +142,7 @@ let wrap_recursive_component_self_references ~config ~fn_name expr =
})
in
let body_mapper = mapper (shadowed || bindings_shadow_name) in
- let body =
- body_mapper.expr body_mapper body
- in
+ let body = body_mapper.expr body_mapper body in
{expr with pexp_desc = Pexp_let (rec_flag, bindings, body)}
| Pexp_fun ({default; lhs; rhs} as desc) ->
(* Optional default expressions are evaluated outside the argument
@@ -157,9 +155,7 @@ let wrap_recursive_component_self_references ~config ~fn_name expr =
let rhs_mapper =
mapper (shadowed || pattern_binds_name fn_name lhs)
in
- let rhs =
- rhs_mapper.expr rhs_mapper rhs
- in
+ let rhs = rhs_mapper.expr rhs_mapper rhs in
{expr with pexp_desc = Pexp_fun {desc with default; rhs}}
| Pexp_apply ({funct; args} as apply) when accepts_component funct ->
let funct = ast_mapper.expr ast_mapper funct in
From 7364fa05b15df00c67f8733943c210d2a5ab53dc Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sat, 25 Apr 2026 21:45:31 +0200
Subject: [PATCH 07/16] Simplify
---
compiler/syntax/src/jsx_v4.ml | 192 ------------------
.../src/recursive_explicit_component_test.res | 22 ++
.../recursive_explicit_component_test.res.js | 51 +++++
...te_element_requires_component.res.expected | 12 ++
...nent_create_element_requires_component.res | 13 ++
tests/tests/src/recursive_react_component.res | 2 +-
6 files changed, 99 insertions(+), 193 deletions(-)
create mode 100644 tests/build_tests/react_ppx/src/recursive_explicit_component_test.res
create mode 100644 tests/build_tests/react_ppx/src/recursive_explicit_component_test.res.js
create mode 100644 tests/build_tests/super_errors/expected/recursive_component_create_element_requires_component.res.expected
create mode 100644 tests/build_tests/super_errors/fixtures/recursive_component_create_element_requires_component.res
diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml
index dd93fe381ef..89fd372c20f 100644
--- a/compiler/syntax/src/jsx_v4.ml
+++ b/compiler/syntax/src/jsx_v4.ml
@@ -54,174 +54,6 @@ let jsx_component_expr config ~loc expr =
(Exp.ident ~loc {loc; txt = module_access_name config "component"})
[(Nolabel, expr)]
-(* Check whether a generated props pattern introduces a binding with this name.
- This is intentionally narrower than full lexical scope tracking: it only
- guards the recursive component rewrite from confusing a prop named `make`
- with the recursive function named `make`. *)
-let rec pattern_binds_name name pattern =
- match pattern.ppat_desc with
- | Ppat_var {txt} | Ppat_unpack {txt} -> txt = name
- | Ppat_alias (pattern, {txt}) -> txt = name || pattern_binds_name name pattern
- | Ppat_tuple patterns | Ppat_array patterns ->
- List.exists (pattern_binds_name name) patterns
- | Ppat_construct (_, pattern) | Ppat_variant (_, pattern) -> (
- match pattern with
- | Some pattern -> pattern_binds_name name pattern
- | None -> false)
- | Ppat_exception pattern -> pattern_binds_name name pattern
- | Ppat_record (fields, _) ->
- List.exists (fun {x = pattern} -> pattern_binds_name name pattern) fields
- | Ppat_or (left, right) ->
- pattern_binds_name name left || pattern_binds_name name right
- | Ppat_constraint (pattern, _) | Ppat_open (_, pattern) ->
- pattern_binds_name name pattern
- | Ppat_any | Ppat_constant _ | Ppat_interval _ | Ppat_type _
- | Ppat_extension _ ->
- false
-
-(* In a recursive component, [fn_name] still refers to the implementation
- function, so direct recursive calls like `make(props)` keep working. React
- APIs such as `React.createElement(make, props)` now expect an abstract
- component, so wrap only that direct self-reference as
- `React.component(make)`.
-
- The rewrite is scope-aware. If a nested binding or pattern introduces the
- same name, references below that binder point at the local value, not at the
- recursive component. In that shadowed scope we keep traversing normally but
- disable the self-reference coercion. *)
-let wrap_recursive_component_self_references ~config ~fn_name expr =
- let jsx_module = String.capitalize_ascii config.Jsx_common.module_ in
- let accepts_component = function
- | {
- pexp_desc =
- Pexp_ident
- {
- txt =
- Ldot
- ( Lident module_name,
- ( "createElement" | "createElementVariadic" | "jsx"
- | "jsxKeyed" | "jsxs" | "jsxsKeyed" ) );
- };
- } ->
- module_name = jsx_module
- | _ -> false
- in
- (* [shadowed] means the current lexical scope has rebound [fn_name]. The
- custom cases below are only the binders that can change that state; every
- other expression shape can use the default mapper. *)
- let rec mapper shadowed =
- {
- Ast_mapper.default_mapper with
- expr =
- (fun ast_mapper expr ->
- match expr.pexp_desc with
- | Pexp_let (rec_flag, bindings, body) ->
- (* Non-recursive bindings are not in scope for their own RHS, while
- recursive bindings are. The let body is shadowed by any binding
- in the group either way. *)
- let bindings_shadow_name =
- List.exists
- (fun binding -> pattern_binds_name fn_name binding.pvb_pat)
- bindings
- in
- let bindings =
- bindings
- |> List.map (fun binding ->
- let binding_shadowed =
- shadowed
- ||
- match rec_flag with
- | Recursive -> bindings_shadow_name
- | Nonrecursive -> false
- in
- let binding_mapper = mapper binding_shadowed in
- {
- binding with
- pvb_expr =
- binding_mapper.expr binding_mapper binding.pvb_expr;
- })
- in
- let body_mapper = mapper (shadowed || bindings_shadow_name) in
- let body = body_mapper.expr body_mapper body in
- {expr with pexp_desc = Pexp_let (rec_flag, bindings, body)}
- | Pexp_fun ({default; lhs; rhs} as desc) ->
- (* Optional default expressions are evaluated outside the argument
- pattern's scope; the function body is inside it. *)
- let default =
- match default with
- | Some default -> Some (ast_mapper.expr ast_mapper default)
- | None -> None
- in
- let rhs_mapper =
- mapper (shadowed || pattern_binds_name fn_name lhs)
- in
- let rhs = rhs_mapper.expr rhs_mapper rhs in
- {expr with pexp_desc = Pexp_fun {desc with default; rhs}}
- | Pexp_apply ({funct; args} as apply) when accepts_component funct ->
- let funct = ast_mapper.expr ast_mapper funct in
- let args =
- args
- |> List.map (fun (label, arg) ->
- (label, ast_mapper.expr ast_mapper arg))
- in
- let args =
- match (shadowed, args) with
- | ( false,
- ( Nolabel,
- ({
- pexp_desc = Pexp_ident {txt = Lident name};
- pexp_loc = loc;
- } as self_ref) )
- :: rest )
- when name = fn_name ->
- (Nolabel, jsx_component_expr config ~loc self_ref) :: rest
- | _ -> args
- in
- {expr with pexp_desc = Pexp_apply {apply with funct; args}}
- | Pexp_match (scrutinee, cases) ->
- let scrutinee = ast_mapper.expr ast_mapper scrutinee in
- let cases =
- cases
- |> List.map (fun ({pc_lhs; pc_guard; pc_rhs} as case) ->
- let case_shadowed =
- shadowed || pattern_binds_name fn_name pc_lhs
- in
- let case_mapper = mapper case_shadowed in
- {
- case with
- pc_guard =
- Option.map
- (fun guard -> case_mapper.expr case_mapper guard)
- pc_guard;
- pc_rhs = case_mapper.expr case_mapper pc_rhs;
- })
- in
- {expr with pexp_desc = Pexp_match (scrutinee, cases)}
- | Pexp_try (body, cases) ->
- let body = ast_mapper.expr ast_mapper body in
- let cases =
- cases
- |> List.map (fun ({pc_lhs; pc_guard; pc_rhs} as case) ->
- let case_shadowed =
- shadowed || pattern_binds_name fn_name pc_lhs
- in
- let case_mapper = mapper case_shadowed in
- {
- case with
- pc_guard =
- Option.map
- (fun guard -> case_mapper.expr case_mapper guard)
- pc_guard;
- pc_rhs = case_mapper.expr case_mapper pc_rhs;
- })
- in
- {expr with pexp_desc = Pexp_try (body, cases)}
- | _ -> Ast_mapper.default_mapper.expr ast_mapper expr);
- }
- in
- let mapper = mapper false in
- mapper.expr mapper expr
-
(* Helper method to filter out any attribute that isn't [@react.component] *)
let other_attrs_pure (loc, _) =
match loc.txt with
@@ -927,9 +759,6 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
| [] -> Pat.any ()
| _ -> Pat.record (List.rev patterns_with_label) Open
in
- let self_reference_is_shadowed_by_props =
- pattern_binds_name fn_name record_pattern
- in
let expression =
(* Shape internal implementation to match wrapper: uncurried when using forwardRef. *)
let total_arity = if has_forward_ref then 2 else 1 in
@@ -953,27 +782,6 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
newtypes
|> List.fold_left (fun e newtype -> Exp.newtype newtype e) expression
in
- let expression =
- match rec_flag with
- | Nonrecursive -> expression
- | Recursive ->
- (* Inside `let rec make = ...`, `make` still has to be the callable
- implementation so recursive calls like `make(props)` keep working.
- But component-position APIs now expect the abstract component type:
-
- React.createElement(make, props)
-
- Wrap only those self-references in the `%identity` component
- coercion. The generated JavaScript still receives the same function,
- while the typed AST sees a `React.component<_>`.
-
- If the generated props pattern itself binds `make`, then references
- inside the body point at that prop, not at the recursive function. In
- that case there is no self-reference to coerce. *)
- if self_reference_is_shadowed_by_props then expression
- else
- wrap_recursive_component_self_references ~config ~fn_name expression
- in
(* let make = ({id, name, ...}: props<'id, 'name, ...>) => { ... } *)
let binding =
{
diff --git a/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res b/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res
new file mode 100644
index 00000000000..f361ad45e8a
--- /dev/null
+++ b/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res
@@ -0,0 +1,22 @@
+module SelfCreateElement = {
+ @react.component
+ let rec make = (~foo) => React.createElement(React.component(make), {foo: foo - 1})
+}
+
+module RawSiblingCreateElement = {
+ @react.component
+ let rec make = (~foo) => React.createElement(React.component(other), {foo: foo})
+ and other = ({foo}) => React.string(foo->Int.toString)
+}
+
+module ComponentWithProps = {
+ type props = {foo: int}
+
+ @react.componentWithProps
+ let rec make = (props: props) =>
+ if props.foo <= 0 {
+ React.null
+ } else {
+ React.createElement(React.component(make), {foo: props.foo - 1})
+ }
+}
diff --git a/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res.js b/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res.js
new file mode 100644
index 00000000000..038e9c8e8cd
--- /dev/null
+++ b/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res.js
@@ -0,0 +1,51 @@
+// Generated by ReScript, PLEASE EDIT WITH CARE
+
+import * as React from "react";
+
+function make(param) {
+ return React.createElement(make, {
+ foo: param.foo - 1 | 0
+ });
+}
+
+let Recursive_explicit_component_test$SelfCreateElement = make;
+
+let SelfCreateElement = {
+ make: Recursive_explicit_component_test$SelfCreateElement
+};
+
+function other(param) {
+ return param.foo.toString();
+}
+
+function Recursive_explicit_component_test$RawSiblingCreateElement(props) {
+ return React.createElement(other, {
+ foo: props.foo
+ });
+}
+
+let RawSiblingCreateElement = {
+ other: other,
+ make: Recursive_explicit_component_test$RawSiblingCreateElement
+};
+
+function make$1(props) {
+ if (props.foo <= 0) {
+ return null;
+ } else {
+ return React.createElement(make$1, {
+ foo: props.foo - 1 | 0
+ });
+ }
+}
+
+let ComponentWithProps = {
+ make: make$1
+};
+
+export {
+ SelfCreateElement,
+ RawSiblingCreateElement,
+ ComponentWithProps,
+}
+/* react Not a pure module */
diff --git a/tests/build_tests/super_errors/expected/recursive_component_create_element_requires_component.res.expected b/tests/build_tests/super_errors/expected/recursive_component_create_element_requires_component.res.expected
new file mode 100644
index 00000000000..de2816d050f
--- /dev/null
+++ b/tests/build_tests/super_errors/expected/recursive_component_create_element_requires_component.res.expected
@@ -0,0 +1,12 @@
+
+ [1;31mWe've found a bug for you![0m
+ [36m/.../fixtures/recursive_component_create_element_requires_component.res[0m:[2m13:46-49[0m
+
+ 11 [2m│[0m
+ 12 [2m│[0m @react.component
+ [1;31m13[0m [2m│[0m let rec make = (~foo) => React.createElement([1;31mmake[0m, {foo: foo})
+ 14 [2m│[0m
+
+ This has type: [1;31mprops<'a> => React.element[0m
+ But this function argument is expecting:
+ [1;33mReact.component<'b>[0m [2m(defined as[0m [1;33mJsx.component<'b>[0m[2m)[0m
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/fixtures/recursive_component_create_element_requires_component.res b/tests/build_tests/super_errors/fixtures/recursive_component_create_element_requires_component.res
new file mode 100644
index 00000000000..f01c5177ac4
--- /dev/null
+++ b/tests/build_tests/super_errors/fixtures/recursive_component_create_element_requires_component.res
@@ -0,0 +1,13 @@
+module React = {
+ type element = Jsx.element
+ @val external null: element = "null"
+ external string: string => element = "%identity"
+ type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
+ type component<'props> = Jsx.component<'props>
+ external component: componentLike<'props, element> => component<'props> = "%identity"
+ @module("react")
+ external createElement: (component<'props>, 'props) => element = "createElement"
+}
+
+@react.component
+let rec make = (~foo) => React.createElement(make, {foo: foo})
diff --git a/tests/tests/src/recursive_react_component.res b/tests/tests/src/recursive_react_component.res
index 82f4ed56bdc..a3af8f1640f 100644
--- a/tests/tests/src/recursive_react_component.res
+++ b/tests/tests/src/recursive_react_component.res
@@ -8,7 +8,7 @@
})
@react.component
-let rec make = (~foo) => React.createElement(make, {foo: foo})
+let rec make = (~foo) => React.createElement(React.component(make), {foo: foo})
module ShadowedSelfReference = {
type childProps = {foo: int}
From 0f33de2f003158544da93e01f47062bfad69122c Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sat, 25 Apr 2026 21:50:02 +0200
Subject: [PATCH 08/16] Fix playground test
---
packages/playground/playground_test.cjs | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/playground/playground_test.cjs b/packages/playground/playground_test.cjs
index b38acc7f112..051cb9efd6f 100644
--- a/packages/playground/playground_test.cjs
+++ b/packages/playground/playground_test.cjs
@@ -29,6 +29,7 @@ const result = compiler.rescript.compile(`
module B = {
type props = { a: string }
+ @react.componentWithProps
let make = ({a}) => {
}
From 54a5b2bc6d43a2ab3ae7706dbcd52f3f400c4ab7 Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sat, 25 Apr 2026 21:53:47 +0200
Subject: [PATCH 09/16] Fix recursive @react.componentWithProps
---
compiler/syntax/src/jsx_v4.ml | 21 ++++---------------
.../src/recursive_explicit_component_test.res | 2 ++
.../recursive_explicit_component_test.res.js | 11 ++++++++--
3 files changed, 15 insertions(+), 19 deletions(-)
diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml
index 89fd372c20f..7f8bd768fac 100644
--- a/compiler/syntax/src/jsx_v4.ml
+++ b/compiler/syntax/src/jsx_v4.ml
@@ -531,7 +531,7 @@ let vb_match_expr named_arg_list expr =
in
aux (List.rev named_arg_list)
-let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
+let map_binding ~config ~empty_loc ~pstr_loc ~file_name binding =
(* 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
@@ -801,7 +801,6 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
}
in
let fn_name = get_fn_name modified_binding.pvb_pat in
- let internal_fn_name = fn_name ^ "$Internal" in
let full_module_name =
make_module_name file_name config.nested_modules fn_name
in
@@ -850,15 +849,7 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
in
let applied_expression =
Exp.apply
- (Exp.ident
- {
- txt =
- Lident
- (match rec_flag with
- | Recursive -> internal_fn_name
- | Nonrecursive -> fn_name);
- loc;
- })
+ (Exp.ident {txt = Lident fn_name; loc})
[(Nolabel, Exp.ident {txt = Lident "props"; loc})]
in
let applied_expression =
@@ -884,11 +875,7 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
in
let new_binding =
- match rec_flag with
- | Recursive -> None
- | Nonrecursive ->
- Some
- (make_new_binding ~loc:empty_loc ~full_module_name modified_binding)
+ Some (make_new_binding ~loc:empty_loc ~full_module_name modified_binding)
in
let binding_expr =
{
@@ -994,7 +981,7 @@ let transform_structure_item ~config item =
let empty_loc = Location.in_file file_name in
let process_binding binding (new_items, bindings, new_bindings) =
let new_item, binding, new_binding =
- map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding
+ map_binding ~config ~empty_loc ~pstr_loc ~file_name binding
in
let new_items =
match new_item with
diff --git a/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res b/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res
index f361ad45e8a..a0d4baf6b54 100644
--- a/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res
+++ b/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res
@@ -20,3 +20,5 @@ module ComponentWithProps = {
React.createElement(React.component(make), {foo: props.foo - 1})
}
}
+
+let componentWithPropsElement = React.createElement(ComponentWithProps.make, {foo: 1})
diff --git a/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res.js b/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res.js
index 038e9c8e8cd..9577ffd28ab 100644
--- a/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res.js
+++ b/tests/build_tests/react_ppx/src/recursive_explicit_component_test.res.js
@@ -39,13 +39,20 @@ function make$1(props) {
}
}
+let Recursive_explicit_component_test$ComponentWithProps = make$1;
+
let ComponentWithProps = {
- make: make$1
+ make: Recursive_explicit_component_test$ComponentWithProps
};
+let componentWithPropsElement = React.createElement(Recursive_explicit_component_test$ComponentWithProps, {
+ foo: 1
+});
+
export {
SelfCreateElement,
RawSiblingCreateElement,
ComponentWithProps,
+ componentWithPropsElement,
}
-/* react Not a pure module */
+/* componentWithPropsElement Not a pure module */
From 3d2a55171954450e4265a043cb1d74db595c0ff6 Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sat, 25 Apr 2026 22:03:01 +0200
Subject: [PATCH 10/16] Introduce %component_identity
---
compiler/frontend/bs_ast_invariant.ml | 7 +++--
compiler/ml/translcore.ml | 1 +
compiler/ml/typecore.ml | 29 ++++++++++++++-----
packages/@rescript/runtime/Jsx.res | 2 +-
.../src/GenericJsx.res | 2 +-
.../@rescript/react/src/React.res | 2 +-
tests/build_tests/react_ppx/src/React.res | 2 +-
.../src/identity_generalization_test.res | 6 ++--
.../identity_does_not_generalize.res.expected | 13 +++++++++
.../fixtures/identity_does_not_generalize.res | 6 ++++
.../jsx_custom_component_children.res | 2 +-
.../jsx_custom_component_optional_prop.res | 2 +-
.../jsx_custom_component_type_mismatch.res | 2 +-
.../jsx_invalid_prop_ast0_conversion.res | 2 +-
.../fixtures/jsx_maybe_missing_fragment.res | 2 +-
.../jsx_type_mismatch_array_element.res | 2 +-
.../fixtures/jsx_type_mismatch_array_raw.res | 2 +-
.../fixtures/jsx_type_mismatch_float.res | 2 +-
.../fixtures/jsx_type_mismatch_int.res | 2 +-
.../fixtures/jsx_type_mismatch_option.res | 2 +-
.../fixtures/jsx_type_mismatch_string.res | 2 +-
.../fixtures/missing_required_prop.res | 2 +-
.../missing_required_prop_when_children.res | 2 +-
...issing_required_prop_when_single_child.res | 2 +-
...nent_create_element_requires_component.res | 2 +-
.../fixtures/wrong_type_prop_punning.res | 2 +-
.../dependencies/rescript-react/src/React.res | 2 +-
tests/tests/src/react.res | 2 +-
28 files changed, 71 insertions(+), 35 deletions(-)
create mode 100644 tests/build_tests/super_errors/expected/identity_does_not_generalize.res.expected
create mode 100644 tests/build_tests/super_errors/fixtures/identity_does_not_generalize.res
diff --git a/compiler/frontend/bs_ast_invariant.ml b/compiler/frontend/bs_ast_invariant.ml
index cbe5a4432ee..60e1ad285d0 100644
--- a/compiler/frontend/bs_ast_invariant.ml
+++ b/compiler/frontend/bs_ast_invariant.ml
@@ -124,10 +124,11 @@ let emit_external_warnings : iterator =
| ({pval_loc; pval_prim = [byte_name]; pval_type} :
Parsetree.value_description) -> (
match byte_name with
- | "%identity" when not (Ast_core_type.is_arity_one pval_type) ->
+ | ("%identity" | "%component_identity")
+ when not (Ast_core_type.is_arity_one pval_type) ->
Location.raise_errorf ~loc:pval_loc
- "%%identity expects a function type of the form 'a => 'b (arity \
- 1)"
+ "%s expects a function type of the form 'a => 'b (arity 1)"
+ byte_name
| _ ->
if byte_name <> "" then
let c = String.unsafe_get byte_name 0 in
diff --git a/compiler/ml/translcore.ml b/compiler/ml/translcore.ml
index 44006f4dad3..ecf3a73528d 100644
--- a/compiler/ml/translcore.ml
+++ b/compiler/ml/translcore.ml
@@ -236,6 +236,7 @@ let primitives_table =
create_hashtable
[|
("%identity", Pidentity);
+ ("%component_identity", Pidentity);
("%ignore", Pignore);
("%revapply", Prevapply);
("%apply", Pdirapply);
diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml
index c7d08068496..7c79f3162a4 100644
--- a/compiler/ml/typecore.ml
+++ b/compiler/ml/typecore.ml
@@ -1808,12 +1808,24 @@ let rec final_subexpression sexp =
(* Generalization criterion for expressions *)
-let is_identity = function
- | Texp_ident (_, _, {val_kind = Val_prim {Primitive.prim_name = "%identity"}})
+let is_component_identity = function
+ | Texp_ident
+ (_, _, {val_kind = Val_prim {Primitive.prim_name = "%component_identity"}})
->
true
| _ -> false
+let is_identity_coercion = function
+ | Texp_ident
+ ( _,
+ _,
+ {
+ val_kind =
+ Val_prim {Primitive.prim_name = "%identity" | "%component_identity"};
+ } ) ->
+ true
+ | _ -> false
+
let rec is_nonexpansive exp =
List.exists
(function
@@ -1828,7 +1840,7 @@ let rec is_nonexpansive exp =
List.for_all (fun vb -> is_nonexpansive vb.vb_expr) pat_exp_list
&& is_nonexpansive body
| Texp_function _ -> true
- (* `%identity` is a typed no-op coercion. Treating it like an ordinary
+ (* `%component_identity` is a typed no-op coercion. Treating it like an ordinary
function call makes values such as `React.component(fn)` expansive, which
prevents generalization of polymorphic props:
@@ -1841,10 +1853,11 @@ let rec is_nonexpansive exp =
}
The JSX transform emits a function value and then coerces it through
- `React.component`, whose implementation is `%identity`. Since no runtime
+ `React.component`, whose implementation is `%component_identity`. Since no runtime
computation happens beyond evaluating the argument, the application is
non-expansive exactly when all supplied arguments are non-expansive. *)
- | Texp_apply {funct = {exp_desc}; args; _} when is_identity exp_desc ->
+ | Texp_apply {funct = {exp_desc}; args; _} when is_component_identity exp_desc
+ ->
List.for_all is_nonexpansive_opt (List.map snd args)
| Texp_apply {partial = true; _} ->
(* ReScript partial applications (`foo(args, ...)`) lower to wrapper
@@ -3846,8 +3859,10 @@ and type_application ~context total_app env funct (sargs : sargs) :
(* This is a total application when the toplevel type is a polymorphic variable,
so the function type including arity can be inferred. *)
let t1 = newvar () and t2 = newvar () in
- if ty_fun.level >= t1.level && not (is_identity funct.exp_desc) then
- Location.prerr_warning sarg1.pexp_loc Warnings.Unused_argument;
+ if
+ ty_fun.level >= t1.level
+ && not (is_identity_coercion funct.exp_desc)
+ then Location.prerr_warning sarg1.pexp_loc Warnings.Unused_argument;
unify env ty_fun
(newty
(Tarrow
diff --git a/packages/@rescript/runtime/Jsx.res b/packages/@rescript/runtime/Jsx.res
index c2593003fd1..90b4a9fa158 100644
--- a/packages/@rescript/runtime/Jsx.res
+++ b/packages/@rescript/runtime/Jsx.res
@@ -23,4 +23,4 @@ type componentLike<'props, 'return> = 'props => 'return
type component<-'props>
/* this function exists to prepare for making `component` abstract */
-external component: componentLike<'props, element> => component<'props> = "%identity"
+external component: componentLike<'props, element> => component<'props> = "%component_identity"
diff --git a/tests/analysis_tests/tests-generic-jsx-transform/src/GenericJsx.res b/tests/analysis_tests/tests-generic-jsx-transform/src/GenericJsx.res
index b9e6341e62b..e984928dea1 100644
--- a/tests/analysis_tests/tests-generic-jsx-transform/src/GenericJsx.res
+++ b/tests/analysis_tests/tests-generic-jsx-transform/src/GenericJsx.res
@@ -5,7 +5,7 @@ type component<'props> = Jsx.component<'props>
type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
-external component: componentLike<'props, element> => component<'props> = "%identity"
+external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("preact")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/jsx_settings_inheritance/node_modules/@rescript/react/src/React.res b/tests/build_tests/jsx_settings_inheritance/node_modules/@rescript/react/src/React.res
index 01f4ebc91ab..33d33e2f313 100644
--- a/tests/build_tests/jsx_settings_inheritance/node_modules/@rescript/react/src/React.res
+++ b/tests/build_tests/jsx_settings_inheritance/node_modules/@rescript/react/src/React.res
@@ -12,7 +12,7 @@ type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
type component<'props> = Jsx.component<'props>
-external component: componentLike<'props, element> => component<'props> = "%identity"
+external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react")
external createElement: (component<'props>, 'props) => element = "createElement"
diff --git a/tests/build_tests/react_ppx/src/React.res b/tests/build_tests/react_ppx/src/React.res
index 6e16687642c..38a5649b159 100644
--- a/tests/build_tests/react_ppx/src/React.res
+++ b/tests/build_tests/react_ppx/src/React.res
@@ -10,7 +10,7 @@ type componentLike<'props, 'return> = 'props => 'return
type component<-'props>
-external component: componentLike<'props, element> => component<'props> = "%identity"
+external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react")
external createElement: (component<'props>, 'props) => element = "createElement"
diff --git a/tests/build_tests/react_ppx/src/identity_generalization_test.res b/tests/build_tests/react_ppx/src/identity_generalization_test.res
index 34d31469844..edf5dce2b7d 100644
--- a/tests/build_tests/react_ppx/src/identity_generalization_test.res
+++ b/tests/build_tests/react_ppx/src/identity_generalization_test.res
@@ -1,8 +1,8 @@
-external id: 'a => 'a = "%identity"
+external componentId: 'a => 'a = "%component_identity"
-// `%identity` applications should generalize like the wrapped expression.
+// `%component_identity` applications should generalize like the wrapped expression.
// Otherwise, `f` becomes monomorphic and one of these calls fails to typecheck.
-let f = id(x => x)
+let f = componentId(x => x)
let intValue = f(1)
let stringValue = f("one")
diff --git a/tests/build_tests/super_errors/expected/identity_does_not_generalize.res.expected b/tests/build_tests/super_errors/expected/identity_does_not_generalize.res.expected
new file mode 100644
index 00000000000..eccd98daabd
--- /dev/null
+++ b/tests/build_tests/super_errors/expected/identity_does_not_generalize.res.expected
@@ -0,0 +1,13 @@
+
+ [1;31mWe've found a bug for you![0m
+ [36m/.../fixtures/identity_does_not_generalize.res[0m:[2m6:21-25[0m
+
+ 4 [2m│[0m
+ 5 [2m│[0m let intValue = f(1)
+ [1;31m6[0m [2m│[0m let stringValue = f([1;31m"one"[0m)
+ 7 [2m│[0m
+
+ This has type: [1;31mstring[0m
+ But this function argument is expecting: [1;33mint[0m
+
+ You can convert [1;33mstring[0m to [1;33mint[0m with [1;33mInt.fromString[0m.
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/fixtures/identity_does_not_generalize.res b/tests/build_tests/super_errors/fixtures/identity_does_not_generalize.res
new file mode 100644
index 00000000000..b84499be542
--- /dev/null
+++ b/tests/build_tests/super_errors/fixtures/identity_does_not_generalize.res
@@ -0,0 +1,6 @@
+external id: 'a => 'a = "%identity"
+
+let f = id(x => x)
+
+let intValue = f(1)
+let stringValue = f("one")
diff --git a/tests/build_tests/super_errors/fixtures/jsx_custom_component_children.res b/tests/build_tests/super_errors/fixtures/jsx_custom_component_children.res
index 6dd8fc64321..77f1a0c8df3 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_custom_component_children.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_custom_component_children.res
@@ -6,7 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_custom_component_optional_prop.res b/tests/build_tests/super_errors/fixtures/jsx_custom_component_optional_prop.res
index 613bc87c666..0a2113562cb 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_custom_component_optional_prop.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_custom_component_optional_prop.res
@@ -6,7 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_custom_component_type_mismatch.res b/tests/build_tests/super_errors/fixtures/jsx_custom_component_type_mismatch.res
index a03c235ccd1..975318989c2 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_custom_component_type_mismatch.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_custom_component_type_mismatch.res
@@ -6,7 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_invalid_prop_ast0_conversion.res b/tests/build_tests/super_errors/fixtures/jsx_invalid_prop_ast0_conversion.res
index 8ded1f8566e..3876d99228d 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_invalid_prop_ast0_conversion.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_invalid_prop_ast0_conversion.res
@@ -5,7 +5,7 @@ module React = {
@val external null: element = "null"
type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
}
diff --git a/tests/build_tests/super_errors/fixtures/jsx_maybe_missing_fragment.res b/tests/build_tests/super_errors/fixtures/jsx_maybe_missing_fragment.res
index c148532b07d..c9efce42f1c 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_maybe_missing_fragment.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_maybe_missing_fragment.res
@@ -6,7 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_element.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_element.res
index ec78b8038df..d3b70571e32 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_element.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_element.res
@@ -6,7 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_raw.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_raw.res
index 5d681e9c904..7bbd491da15 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_raw.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_raw.res
@@ -6,7 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_float.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_float.res
index ea6a65c2eb1..cc76e13f962 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_float.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_float.res
@@ -6,7 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_int.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_int.res
index 587f55e912f..7ef46cf1ca4 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_int.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_int.res
@@ -6,7 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_option.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_option.res
index a0d833a9e39..9d73eb095a8 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_option.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_option.res
@@ -6,7 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_string.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_string.res
index 448c58c7b8d..8fc85fb38f7 100644
--- a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_string.res
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_string.res
@@ -6,7 +6,7 @@ module React = {
type element = Jsx.element
type componentLike<'props, 'return> = 'props => 'return
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
diff --git a/tests/build_tests/super_errors/fixtures/missing_required_prop.res b/tests/build_tests/super_errors/fixtures/missing_required_prop.res
index 3faa09aaa61..78f2489adc5 100644
--- a/tests/build_tests/super_errors/fixtures/missing_required_prop.res
+++ b/tests/build_tests/super_errors/fixtures/missing_required_prop.res
@@ -3,7 +3,7 @@ module React = {
@val external null: element = "null"
type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
external string: string => element = "%identity"
diff --git a/tests/build_tests/super_errors/fixtures/missing_required_prop_when_children.res b/tests/build_tests/super_errors/fixtures/missing_required_prop_when_children.res
index ab468eda49a..41e61b50db3 100644
--- a/tests/build_tests/super_errors/fixtures/missing_required_prop_when_children.res
+++ b/tests/build_tests/super_errors/fixtures/missing_required_prop_when_children.res
@@ -3,7 +3,7 @@ module React = {
@val external null: element = "null"
type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
@module("react/jsx-runtime")
diff --git a/tests/build_tests/super_errors/fixtures/missing_required_prop_when_single_child.res b/tests/build_tests/super_errors/fixtures/missing_required_prop_when_single_child.res
index 97b0438f3f4..eaa02221a48 100644
--- a/tests/build_tests/super_errors/fixtures/missing_required_prop_when_single_child.res
+++ b/tests/build_tests/super_errors/fixtures/missing_required_prop_when_single_child.res
@@ -3,7 +3,7 @@ module React = {
@val external null: element = "null"
type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
external string: string => element = "%identity"
diff --git a/tests/build_tests/super_errors/fixtures/recursive_component_create_element_requires_component.res b/tests/build_tests/super_errors/fixtures/recursive_component_create_element_requires_component.res
index f01c5177ac4..542db6fdb99 100644
--- a/tests/build_tests/super_errors/fixtures/recursive_component_create_element_requires_component.res
+++ b/tests/build_tests/super_errors/fixtures/recursive_component_create_element_requires_component.res
@@ -4,7 +4,7 @@ module React = {
external string: string => element = "%identity"
type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react")
external createElement: (component<'props>, 'props) => element = "createElement"
}
diff --git a/tests/build_tests/super_errors/fixtures/wrong_type_prop_punning.res b/tests/build_tests/super_errors/fixtures/wrong_type_prop_punning.res
index 797539e1720..d2f81ba7a34 100644
--- a/tests/build_tests/super_errors/fixtures/wrong_type_prop_punning.res
+++ b/tests/build_tests/super_errors/fixtures/wrong_type_prop_punning.res
@@ -3,7 +3,7 @@ module React = {
@val external null: element = "null"
type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
type component<'props> = Jsx.component<'props>
- external component: componentLike<'props, element> => component<'props> = "%identity"
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"
}
diff --git a/tests/dependencies/rescript-react/src/React.res b/tests/dependencies/rescript-react/src/React.res
index 0beaa590807..a62d8501460 100644
--- a/tests/dependencies/rescript-react/src/React.res
+++ b/tests/dependencies/rescript-react/src/React.res
@@ -12,7 +12,7 @@ type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
type component<'props> = Jsx.component<'props>
-external component: componentLike<'props, element> => component<'props> = "%identity"
+external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react")
external createElement: (component<'props>, 'props) => element = "createElement"
diff --git a/tests/tests/src/react.res b/tests/tests/src/react.res
index f1f68315186..18b7e8475ae 100644
--- a/tests/tests/src/react.res
+++ b/tests/tests/src/react.res
@@ -13,7 +13,7 @@ type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
type component<'props> = Jsx.component<'props>
-external component: componentLike<'props, element> => component<'props> = "%identity"
+external component: componentLike<'props, element> => component<'props> = "%component_identity"
@module("react")
external createElement: (component<'props>, 'props) => element = "createElement"
From a1892c4459d9c13ea24bdaa6d23700331a6b37a6 Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sat, 25 Apr 2026 22:12:04 +0200
Subject: [PATCH 11/16] Fix gentype tests
---
.../@rescript-react-npm-0.14.0-e462ba0c5d.patch | 13 +++++++++++++
packages/playground/package.json | 2 +-
.../typescript-react-example/package.json | 2 +-
yarn.lock | 16 +++++++++++++---
4 files changed, 28 insertions(+), 5 deletions(-)
create mode 100644 .yarn/patches/@rescript-react-npm-0.14.0-e462ba0c5d.patch
diff --git a/.yarn/patches/@rescript-react-npm-0.14.0-e462ba0c5d.patch b/.yarn/patches/@rescript-react-npm-0.14.0-e462ba0c5d.patch
new file mode 100644
index 00000000000..4a313a4570b
--- /dev/null
+++ b/.yarn/patches/@rescript-react-npm-0.14.0-e462ba0c5d.patch
@@ -0,0 +1,13 @@
+diff --git a/src/React.res b/src/React.res
+index 0676012234c8a9cdd93030d732274b5ed3914b11..1ff7a036a06a69e76a680b0396a3f8a67a59c328 100644
+--- a/src/React.res
++++ b/src/React.res
+@@ -13,7 +13,7 @@ type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
+
+ type component<'props> = Jsx.component<'props>
+
+-external component: componentLike<'props, element> => component<'props> = "%identity"
++external component: componentLike<'props, element> => component<'props> = "%component_identity"
+
+ @module("react")
+ external createElement: (component<'props>, 'props) => element = "createElement"
diff --git a/packages/playground/package.json b/packages/playground/package.json
index f5946f261b5..75e45a7627b 100644
--- a/packages/playground/package.json
+++ b/packages/playground/package.json
@@ -10,7 +10,7 @@
"serve-bundle": "node serve-bundle.mjs"
},
"dependencies": {
- "@rescript/react": "^0.14.0",
+ "@rescript/react": "patch:@rescript/react@npm%3A0.14.0#~/.yarn/patches/@rescript-react-npm-0.14.0-e462ba0c5d.patch",
"rescript": "workspace:^"
},
"devDependencies": {
diff --git a/tests/gentype_tests/typescript-react-example/package.json b/tests/gentype_tests/typescript-react-example/package.json
index 72a93675b29..00e5b605552 100644
--- a/tests/gentype_tests/typescript-react-example/package.json
+++ b/tests/gentype_tests/typescript-react-example/package.json
@@ -8,7 +8,7 @@
"typecheck": "tsc"
},
"dependencies": {
- "@rescript/react": "^0.14.0",
+ "@rescript/react": "patch:@rescript/react@npm%3A0.14.0#~/.yarn/patches/@rescript-react-npm-0.14.0-e462ba0c5d.patch",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rescript": "workspace:^"
diff --git a/yarn.lock b/yarn.lock
index 9ec62921179..fa47abcd915 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -476,7 +476,7 @@ __metadata:
languageName: node
linkType: soft
-"@rescript/react@npm:^0.14.0":
+"@rescript/react@npm:0.14.0":
version: 0.14.0
resolution: "@rescript/react@npm:0.14.0"
peerDependencies:
@@ -486,6 +486,16 @@ __metadata:
languageName: node
linkType: hard
+"@rescript/react@patch:@rescript/react@npm%3A0.14.0#~/.yarn/patches/@rescript-react-npm-0.14.0-e462ba0c5d.patch":
+ version: 0.14.0
+ resolution: "@rescript/react@patch:@rescript/react@npm%3A0.14.0#~/.yarn/patches/@rescript-react-npm-0.14.0-e462ba0c5d.patch::version=0.14.0&hash=fe24f5"
+ peerDependencies:
+ react: ">=19.0.0"
+ react-dom: ">=19.0.0"
+ checksum: 10c0/f55f81dca3908901df7232fd499d048d12ea9165c1d4a273d130205286b98c5c2050058be8295a05363638914a897236e8716debdd7bb13a69996b15b9a90b3f
+ languageName: node
+ linkType: hard
+
"@rescript/react@workspace:tests/dependencies/rescript-react":
version: 0.0.0-use.local
resolution: "@rescript/react@workspace:tests/dependencies/rescript-react"
@@ -745,7 +755,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@tests/gentype-react-example@workspace:tests/gentype_tests/typescript-react-example"
dependencies:
- "@rescript/react": "npm:^0.14.0"
+ "@rescript/react": "patch:@rescript/react@npm%3A0.14.0#~/.yarn/patches/@rescript-react-npm-0.14.0-e462ba0c5d.patch"
"@types/react": "npm:^18.3.3"
"@types/react-dom": "npm:^18.3.0"
react: "npm:^18.3.1"
@@ -2326,7 +2336,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "playground@workspace:packages/playground"
dependencies:
- "@rescript/react": "npm:^0.14.0"
+ "@rescript/react": "patch:@rescript/react@npm%3A0.14.0#~/.yarn/patches/@rescript-react-npm-0.14.0-e462ba0c5d.patch"
"@rollup/plugin-node-resolve": "npm:^16.0.0"
h3: "npm:2.0.1-rc.20"
rescript: "workspace:^"
From 97045f716ca6ade53b375e3c688142a6d3d274a2 Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sat, 25 Apr 2026 22:23:20 +0200
Subject: [PATCH 12/16] Fix LSP hover regression from JSX component coercion
---
compiler/syntax/src/jsx_v4.ml | 4 ++--
tests/analysis_tests/tests/src/expected/CodeLens.res.txt | 3 ---
tests/analysis_tests/tests/src/expected/Hover.res.txt | 4 ++--
3 files changed, 4 insertions(+), 7 deletions(-)
diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml
index 7f8bd768fac..5b8e83bedb8 100644
--- a/compiler/syntax/src/jsx_v4.ml
+++ b/compiler/syntax/src/jsx_v4.ml
@@ -659,11 +659,11 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name binding =
Putting the coercion directly around the function argument, as in
`React.component(props => make(props))`, hides the function under an
application during typing and breaks inference for polymorphic props.
- The typechecker treats this `%identity` coercion as non-expansive, so
+ The typechecker treats this `%component_identity` coercion as non-expansive, so
the inferred prop type can still be generalized. *)
let full_expression =
if has_forward_ref then full_expression
- else jsx_component_expr config ~loc:pstr_loc full_expression
+ else jsx_component_expr config ~loc:empty_loc full_expression
in
let rec returned_expression patterns_with_label patterns_with_nolabel
({pexp_desc} as expr) =
diff --git a/tests/analysis_tests/tests/src/expected/CodeLens.res.txt b/tests/analysis_tests/tests/src/expected/CodeLens.res.txt
index bc9288e8c95..e75a2865164 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": "React.componentLike, React.element> => React.component>", "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 b5c1da0f91e..f9f1746e150 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\nReact.componentLike<\n props,\n React.element,\n> => React.component>\n```\n\n---\n\n```\n \n```\n```rescript\ntype React.componentLike<\n 'props,\n 'return,\n> = Jsx.componentLike<'props, 'return>\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22React.res%22%2C10%2C0%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype props<'name> = {name: 'name}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22Hover.res%22%2C36%2C0%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype React.element = Jsx.element\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22React.res%22%2C0%2C0%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype React.component<'props> = Jsx.component<'props>\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22React.res%22%2C12%2C0%5D)\n"}}
+{"contents": {"kind": "markdown", "value": "```rescript\nstring\n```"}}
Hover src/Hover.res 42:15
-{"contents": {"kind": "markdown", "value": "```rescript\nReact.componentLike<\n props,\n React.element,\n> => React.component>\n```\n\n---\n\n```\n \n```\n```rescript\ntype React.componentLike<\n 'props,\n 'return,\n> = Jsx.componentLike<'props, 'return>\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22React.res%22%2C10%2C0%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype props<'name> = {name: 'name}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22Hover.res%22%2C41%2C2%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype React.element = Jsx.element\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22React.res%22%2C0%2C0%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype React.component<'props> = Jsx.component<'props>\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22React.res%22%2C12%2C0%5D)\n"}}
+{"contents": {"kind": "markdown", "value": "```rescript\nstring\n```"}}
Hover src/Hover.res 46:10
{"contents": {"kind": "markdown", "value": "```rescript\nint\n```"}}
From 765b113face16afab4a9cef6df10c3fbcd50dab1 Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sun, 26 Apr 2026 07:51:11 +0200
Subject: [PATCH 13/16] Improve error message for plain functions used as JSX
components
---
compiler/ml/error_message_utils.ml | 15 +++++++++++
compiler/ml/typecore.ml | 19 +++++++++-----
.../jsx_plain_function_component.res.expected | 19 ++++++++++++++
.../fixtures/jsx_plain_function_component.res | 26 +++++++++++++++++++
4 files changed, 73 insertions(+), 6 deletions(-)
create mode 100644 tests/build_tests/super_errors/expected/jsx_plain_function_component.res.expected
create mode 100644 tests/build_tests/super_errors/fixtures/jsx_plain_function_component.res
diff --git a/compiler/ml/error_message_utils.ml b/compiler/ml/error_message_utils.ml
index 1805844fd96..24518cc0fee 100644
--- a/compiler/ml/error_message_utils.ml
+++ b/compiler/ml/error_message_utils.ml
@@ -107,6 +107,7 @@ type type_clash_context =
is_constant: string option;
}
| FunctionArgument of {optional: bool; name: string option}
+ | JsxComponent
| BracedIdent
| Statement of type_clash_statement
| ForLoopCondition
@@ -127,6 +128,7 @@ let context_to_string = function
| Some TryReturn -> "TryReturn"
| Some StringConcat -> "StringConcat"
| Some (FunctionArgument _) -> "FunctionArgument"
+ | Some JsxComponent -> "JsxComponent"
| Some ComparisonOperator -> "ComparisonOperator"
| Some IfReturn -> "IfReturn"
| Some TernaryReturn -> "TernaryReturn"
@@ -145,6 +147,7 @@ let error_type_text ppf type_clash_context =
| Some ArrayValue -> "This array item has type:"
| Some (SetRecordField _) ->
"You're assigning something to this field that has type:"
+ | Some JsxComponent -> "This JSX tag resolves to:"
| _ -> "This has type:"
in
fprintf ppf "%s" text
@@ -162,6 +165,7 @@ let error_expected_type_text ppf type_clash_context =
| None -> ());
fprintf ppf " is expecting:"
+ | Some JsxComponent -> fprintf ppf "But JSX component positions require:"
| Some ComparisonOperator ->
fprintf ppf "But it's being compared to something of type:"
| Some SwitchReturn -> fprintf ppf "But this switch is expected to return:"
@@ -396,6 +400,17 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
\ - Remove the @{await@} if this is not expected to be a promise\n\
\ - Wrap the expression in @{Promise.resolve@} to convert the \
expression to a promise"
+ | Some JsxComponent, _ ->
+ fprintf ppf
+ "\n\n\
+ \ JSX tags must resolve to a React component, not a plain function.\n\n\
+ \ Possible solutions:\n\
+ \ - If this function takes labeled props, annotate it with \
+ @{@react.component@}\n\
+ \ - If this function takes a single props record, annotate it with \
+ @{@react.componentWithProps@}\n\
+ \ - If this is already a valid component-like value, wrap it with \
+ @{React.component(...)@}"
| Some IfReturn, _ ->
fprintf ppf
"\n\n\
diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml
index 7c79f3162a4..97e7373e842 100644
--- a/compiler/ml/typecore.ml
+++ b/compiler/ml/typecore.ml
@@ -2525,7 +2525,10 @@ and type_expect_ ?deprecated_context ~context ?in_function ?(recarg = Rejected)
wrap_trace_gadt_instances env (lower_args env []) ty;
begin_def ();
let total_app = not partial in
- let context = type_clash_context_from_function sexp sfunct in
+ let context =
+ if transformed_jsx then Some JsxComponent
+ else type_clash_context_from_function sexp sfunct
+ in
let args, ty_res, fully_applied =
match translate_unified_ops env funct sargs with
| Some (targs, result_type) -> (targs, result_type, true)
@@ -3925,15 +3928,19 @@ and type_application ~context total_app env funct (sargs : sargs) :
if (not optional) && is_optional l' then
Location.prerr_warning sarg0.pexp_loc
(Warnings.Nonoptional_label (Printtyp.string_of_label l));
+ let argument_context =
+ match (context, args) with
+ | Some JsxComponent, [] -> Some JsxComponent
+ | Some JsxComponent, _ ->
+ type_clash_context_for_function_argument ~label:l' None sarg0
+ | _ ->
+ type_clash_context_for_function_argument ~label:l' context sarg0
+ in
( sargs,
omitted,
Some
(if (not optional) || is_optional l' then fun () ->
- type_argument
- ~context:
- (type_clash_context_for_function_argument ~label:l' context
- sarg0)
- env sarg0 ty ty0
+ type_argument ~context:argument_context env sarg0 ty ty0
else fun () ->
option_some
(type_argument
diff --git a/tests/build_tests/super_errors/expected/jsx_plain_function_component.res.expected b/tests/build_tests/super_errors/expected/jsx_plain_function_component.res.expected
new file mode 100644
index 00000000000..92802a08da4
--- /dev/null
+++ b/tests/build_tests/super_errors/expected/jsx_plain_function_component.res.expected
@@ -0,0 +1,19 @@
+
+ [1;31mWe've found a bug for you![0m
+ [36m/.../fixtures/jsx_plain_function_component.res[0m:[2m26:10-16[0m
+
+ 24 [2m│[0m }
+ 25 [2m│[0m
+ [1;31m26[0m [2m│[0m let _ = <[1;31mWrapper[0m value="hello" />
+ 27 [2m│[0m
+
+ This JSX tag resolves to: [1;31mWrapper.props => Jsx.element[0m
+ But JSX component positions require:
+ [1;33mReact.component<'a>[0m [2m(defined as[0m [1;33mJsx.component<'a>[0m[2m)[0m
+
+ JSX tags must resolve to a React component, not a plain function.
+
+ Possible solutions:
+ - If this function takes labeled props, annotate it with [1;33m@react.component[0m
+ - If this function takes a single props record, annotate it with [1;33m@react.componentWithProps[0m
+ - If this is already a valid component-like value, wrap it with [1;33mReact.component(...)[0m
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/fixtures/jsx_plain_function_component.res b/tests/build_tests/super_errors/fixtures/jsx_plain_function_component.res
new file mode 100644
index 00000000000..b7cfc238fb6
--- /dev/null
+++ b/tests/build_tests/super_errors/fixtures/jsx_plain_function_component.res
@@ -0,0 +1,26 @@
+module React = {
+ type element = Jsx.element
+ type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
+ type component<'props> = Jsx.component<'props>
+
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
+ external string: string => element = "%identity"
+ @module("react/jsx-runtime")
+ external jsx: (component<'props>, 'props) => element = "jsx"
+
+ @module("react/jsx-runtime")
+ external jsxs: (component<'props>, 'props) => element = "jsxs"
+}
+
+module ReactDOM = {
+ external someElement: React.element => option = "%identity"
+ @module("react/jsx-runtime")
+ external jsx: (string, JsxDOM.domProps) => Jsx.element = "jsx"
+}
+
+module Wrapper = {
+ type props = {value: string}
+ let make = (props: props) => {React.string(props.value)}
+}
+
+let _ =
From e6f1cbab142bbe03311d76dd69828500f51de1d2 Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sun, 26 Apr 2026 09:05:40 +0200
Subject: [PATCH 14/16] Another error message improvement
---
compiler/ml/error_message_utils.ml | 17 +++++++++++++++++
...component_prop_plain_function.res.expected | 19 +++++++++++++++++++
...te_element_requires_component.res.expected | 8 +++++++-
.../jsx_component_prop_plain_function.res | 19 +++++++++++++++++++
4 files changed, 62 insertions(+), 1 deletion(-)
create mode 100644 tests/build_tests/super_errors/expected/jsx_component_prop_plain_function.res.expected
create mode 100644 tests/build_tests/super_errors/fixtures/jsx_component_prop_plain_function.res
diff --git a/compiler/ml/error_message_utils.ml b/compiler/ml/error_message_utils.ml
index 24518cc0fee..187d66feb34 100644
--- a/compiler/ml/error_message_utils.ml
+++ b/compiler/ml/error_message_utils.ml
@@ -228,6 +228,12 @@ let is_variant_type ~(extract_concrete_typedecl : extract_concrete_typedecl)
| _ -> false
with _ -> false
+let is_jsx_component_type ~env ty =
+ match Ctype.expand_head env ty with
+ | {desc = Tconstr (Pdot (Pident {name = "Jsx"}, "component", _), _, _)} ->
+ true
+ | _ -> false
+
let get_variant_constructors
~(extract_concrete_typedecl : extract_concrete_typedecl) ~env ty =
match extract_concrete_typedecl env ty with
@@ -438,6 +444,17 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
\ - Use a tuple, if your array is of fixed length. Tuples can mix types \
freely, and compiles to a JavaScript array. Example of a tuple: `let \
myTuple = (10, \"hello\", 15.5, true)"
+ | _, Some ({desc = Tarrow _}, expected)
+ when is_jsx_component_type ~env expected ->
+ fprintf ppf
+ "\n\n\
+ \ A React component is expected here, but this expression is a plain \
+ function.\n\n\
+ \ Possible solutions:\n\
+ \ - Extract it to a component annotated with @{@react.component@} \
+ or @{@react.componentWithProps@}\n\
+ \ - If this is already a valid component-like value, wrap it with \
+ @{React.component(...)@}"
| _, Some (_, {desc = Tconstr (p2, _, _)}) when Path.same Predef.path_dict p2
->
fprintf ppf
diff --git a/tests/build_tests/super_errors/expected/jsx_component_prop_plain_function.res.expected b/tests/build_tests/super_errors/expected/jsx_component_prop_plain_function.res.expected
new file mode 100644
index 00000000000..9258619424b
--- /dev/null
+++ b/tests/build_tests/super_errors/expected/jsx_component_prop_plain_function.res.expected
@@ -0,0 +1,19 @@
+
+ [1;31mWe've found a bug for you![0m
+ [36m/.../fixtures/jsx_component_prop_plain_function.res[0m:[2m19:46-58[0m
+
+ 17 [2m│[0m }
+ 18 [2m│[0m
+ [1;31m19[0m [2m│[0m let _ = React.null[0m} />
+ 20 [2m│[0m
+
+ This has type: [1;31m'a => 'b[0m
+ But it's expected to have type:
+ [1;33mReact.component[0m [2m(defined as[0m
+ [1;33mJsx.component[0m[2m)[0m
+
+ A React component is expected here, but this expression is a plain function.
+
+ Possible solutions:
+ - Extract it to a component annotated with [1;33m@react.component[0m or [1;33m@react.componentWithProps[0m
+ - If this is already a valid component-like value, wrap it with [1;33mReact.component(...)[0m
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/expected/recursive_component_create_element_requires_component.res.expected b/tests/build_tests/super_errors/expected/recursive_component_create_element_requires_component.res.expected
index de2816d050f..f531ae3fbb0 100644
--- a/tests/build_tests/super_errors/expected/recursive_component_create_element_requires_component.res.expected
+++ b/tests/build_tests/super_errors/expected/recursive_component_create_element_requires_component.res.expected
@@ -9,4 +9,10 @@
This has type: [1;31mprops<'a> => React.element[0m
But this function argument is expecting:
- [1;33mReact.component<'b>[0m [2m(defined as[0m [1;33mJsx.component<'b>[0m[2m)[0m
\ No newline at end of file
+ [1;33mReact.component<'b>[0m [2m(defined as[0m [1;33mJsx.component<'b>[0m[2m)[0m
+
+ A React component is expected here, but this expression is a plain function.
+
+ Possible solutions:
+ - Extract it to a component annotated with [1;33m@react.component[0m or [1;33m@react.componentWithProps[0m
+ - If this is already a valid component-like value, wrap it with [1;33mReact.component(...)[0m
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/fixtures/jsx_component_prop_plain_function.res b/tests/build_tests/super_errors/fixtures/jsx_component_prop_plain_function.res
new file mode 100644
index 00000000000..39b4e47d5fb
--- /dev/null
+++ b/tests/build_tests/super_errors/fixtures/jsx_component_prop_plain_function.res
@@ -0,0 +1,19 @@
+module React = {
+ type element = Jsx.element
+ @val external null: element = "null"
+ type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
+ type component<'props> = Jsx.component<'props>
+
+ external component: componentLike<'props, element> => component<'props> = "%component_identity"
+ @module("react/jsx-runtime")
+ external jsx: (component<'props>, 'props) => element = "jsx"
+}
+
+module List = {
+ type separatorProps = {index: int}
+
+ @react.component
+ let make = (~itemSeparatorComponent: React.component) => React.null
+}
+
+let _ = React.null} />
From 88c46051b25094ed4c67de1821580ef239ce9a1a Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Sun, 26 Apr 2026 09:12:54 +0200
Subject: [PATCH 15/16] Improve comment
---
compiler/ml/typecore.ml | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml
index 97e7373e842..4f0b3be38e4 100644
--- a/compiler/ml/typecore.ml
+++ b/compiler/ml/typecore.ml
@@ -1840,9 +1840,10 @@ let rec is_nonexpansive exp =
List.for_all (fun vb -> is_nonexpansive vb.vb_expr) pat_exp_list
&& is_nonexpansive body
| Texp_function _ -> true
- (* `%component_identity` is a typed no-op coercion. Treating it like an ordinary
- function call makes values such as `React.component(fn)` expansive, which
- prevents generalization of polymorphic props:
+ (* `%component_identity` is a typed no-op coercion that lets generated
+ component wrappers keep the same generalization behavior as their
+ underlying function values. This preserves polymorphic props for
+ components such as:
@react.component
let make = (~x) =>
@@ -1853,9 +1854,9 @@ let rec is_nonexpansive exp =
}
The JSX transform emits a function value and then coerces it through
- `React.component`, whose implementation is `%component_identity`. Since no runtime
- computation happens beyond evaluating the argument, the application is
- non-expansive exactly when all supplied arguments are non-expansive. *)
+ `React.component`, whose implementation is `%component_identity`. Since no
+ runtime computation happens beyond evaluating the argument, the application
+ is non-expansive exactly when all supplied arguments are non-expansive. *)
| Texp_apply {funct = {exp_desc}; args; _} when is_component_identity exp_desc
->
List.for_all is_nonexpansive_opt (List.map snd args)
From 214280b90088be673aaed00458b4bc7f95e7d598 Mon Sep 17 00:00:00 2001
From: Christoph Knittel
Date: Mon, 27 Apr 2026 08:17:44 +0200
Subject: [PATCH 16/16] Clearer error messages
---
compiler/ml/error_message_utils.ml | 4 ++--
.../expected/jsx_plain_function_component.res.expected | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/compiler/ml/error_message_utils.ml b/compiler/ml/error_message_utils.ml
index 187d66feb34..19995a6145c 100644
--- a/compiler/ml/error_message_utils.ml
+++ b/compiler/ml/error_message_utils.ml
@@ -147,7 +147,7 @@ let error_type_text ppf type_clash_context =
| Some ArrayValue -> "This array item has type:"
| Some (SetRecordField _) ->
"You're assigning something to this field that has type:"
- | Some JsxComponent -> "This JSX tag resolves to:"
+ | Some JsxComponent -> "This JSX tag has type:"
| _ -> "This has type:"
in
fprintf ppf "%s" text
@@ -409,7 +409,7 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
| Some JsxComponent, _ ->
fprintf ppf
"\n\n\
- \ JSX tags must resolve to a React component, not a plain function.\n\n\
+ \ JSX tags must be React components, not plain functions.\n\n\
\ Possible solutions:\n\
\ - If this function takes labeled props, annotate it with \
@{@react.component@}\n\
diff --git a/tests/build_tests/super_errors/expected/jsx_plain_function_component.res.expected b/tests/build_tests/super_errors/expected/jsx_plain_function_component.res.expected
index 92802a08da4..c4c1c500e29 100644
--- a/tests/build_tests/super_errors/expected/jsx_plain_function_component.res.expected
+++ b/tests/build_tests/super_errors/expected/jsx_plain_function_component.res.expected
@@ -7,11 +7,11 @@
[1;31m26[0m [2m│[0m let _ = <[1;31mWrapper[0m value="hello" />
27 [2m│[0m
- This JSX tag resolves to: [1;31mWrapper.props => Jsx.element[0m
+ This JSX tag has type: [1;31mWrapper.props => Jsx.element[0m
But JSX component positions require:
[1;33mReact.component<'a>[0m [2m(defined as[0m [1;33mJsx.component<'a>[0m[2m)[0m
- JSX tags must resolve to a React component, not a plain function.
+ JSX tags must be React components, not plain functions.
Possible solutions:
- If this function takes labeled props, annotate it with [1;33m@react.component[0m