Skip to content

Commit b2ce0f7

Browse files
authored
JSX PPX: add Jsx.element return constraint (#7939)
* JSX PPX: add Jsx.element return constraint * Extract collect_prop_types helper * Simplify collect_prop_types * Cleanup * Remove merlin.focus attribute * CHANGELOG # Conflicts: # CHANGELOG.md * Update JSXV4 docs. * Constrain return expression; do not use outer loc * Revert binding loc change
1 parent bac94a4 commit b2ce0f7

File tree

53 files changed

+408
-261
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+408
-261
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#### :nail_care: Polish
2626

2727
- Improve circular dependency errors, and make sure they end up in the compiler log so the editor tooling can surface them. https://github.com/rescript-lang/rescript/pull/7940
28+
- JSX PPX: add Jsx.element return constraint. https://github.com/rescript-lang/rescript/pull/7939
2829

2930
#### :house: Internal
3031

compiler/syntax/src/jsx_v4.ml

Lines changed: 70 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,33 @@ let ref_type loc =
4646
{loc; txt = Ldot (Ldot (Lident "Js", "Nullable"), "t")}
4747
[ref_type_var loc]
4848

49-
let merlin_focus = ({loc = Location.none; txt = "merlin.focus"}, PStr [])
49+
let jsx_element_type ~loc =
50+
Typ.constr ~loc {loc; txt = Ldot (Lident "Jsx", "element")} []
51+
52+
let jsx_element_constraint expr =
53+
Exp.constraint_ expr (jsx_element_type ~loc:expr.pexp_loc)
54+
55+
(* Traverse the component body and force every reachable return expression to
56+
be annotated as `Jsx.element`. This walks through the wrapper constructs the
57+
PPX introduces (fun/newtype/let/sequence) so that the constraint ends up on
58+
the real return position even after we rewrite the function. *)
59+
let rec constrain_jsx_return expr =
60+
match expr.pexp_desc with
61+
| Pexp_fun ({rhs} as desc) ->
62+
{expr with pexp_desc = Pexp_fun {desc with rhs = constrain_jsx_return rhs}}
63+
| Pexp_newtype (param, inner) ->
64+
{expr with pexp_desc = Pexp_newtype (param, constrain_jsx_return inner)}
65+
| Pexp_constraint (inner, _) ->
66+
let constrained_inner = constrain_jsx_return inner in
67+
jsx_element_constraint constrained_inner
68+
| Pexp_let (rec_flag, bindings, body) ->
69+
{
70+
expr with
71+
pexp_desc = Pexp_let (rec_flag, bindings, constrain_jsx_return body);
72+
}
73+
| Pexp_sequence (first, second) ->
74+
{expr with pexp_desc = Pexp_sequence (first, constrain_jsx_return second)}
75+
| _ -> jsx_element_constraint expr
5076

5177
(* Helper method to filter out any attribute that isn't [@react.component] *)
5278
let other_attrs_pure (loc, _) =
@@ -73,7 +99,7 @@ let make_new_binding binding expression new_name =
7399
pvb_pat =
74100
{pvb_pat with ppat_desc = Ppat_var {ppat_var with txt = new_name}};
75101
pvb_expr = expression;
76-
pvb_attributes = [merlin_focus];
102+
pvb_attributes = [];
77103
}
78104
| {pvb_loc} ->
79105
Jsx_common.raise_error ~loc:pvb_loc
@@ -713,6 +739,7 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
713739
vb_match_expr named_arg_list expression
714740
else expression
715741
in
742+
let expression = constrain_jsx_return expression in
716743
(* (ref) => expr *)
717744
let expression =
718745
List.fold_left
@@ -839,21 +866,26 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
839866
| _ -> Pat.var {txt = "props"; loc}
840867
in
841868

869+
let applied_expression =
870+
Exp.apply
871+
(Exp.ident
872+
{
873+
txt =
874+
Lident
875+
(match rec_flag with
876+
| Recursive -> internal_fn_name
877+
| Nonrecursive -> fn_name);
878+
loc;
879+
})
880+
[(Nolabel, Exp.ident {txt = Lident "props"; loc})]
881+
in
882+
let applied_expression =
883+
Jsx_common.async_component ~async:is_async applied_expression
884+
in
885+
let applied_expression = constrain_jsx_return applied_expression in
842886
let wrapper_expr =
843887
Exp.fun_ ~arity:None Nolabel None props_pattern
844-
~attrs:binding.pvb_expr.pexp_attributes
845-
(Jsx_common.async_component ~async:is_async
846-
(Exp.apply
847-
(Exp.ident
848-
{
849-
txt =
850-
Lident
851-
(match rec_flag with
852-
| Recursive -> internal_fn_name
853-
| Nonrecursive -> fn_name);
854-
loc;
855-
})
856-
[(Nolabel, Exp.ident {txt = Lident "props"; loc})]))
888+
~attrs:binding.pvb_expr.pexp_attributes applied_expression
857889
in
858890

859891
let wrapper_expr = Ast_uncurried.uncurried_fun ~arity:1 wrapper_expr in
@@ -876,20 +908,33 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
876908
Some
877909
(make_new_binding ~loc:empty_loc ~full_module_name modified_binding)
878910
in
911+
let binding_expr =
912+
{
913+
binding.pvb_expr with
914+
(* moved to wrapper_expr *)
915+
pexp_attributes = [];
916+
}
917+
in
879918
( None,
880919
{
881920
binding with
882921
pvb_attributes = binding.pvb_attributes |> List.filter other_attrs_pure;
883-
pvb_expr =
884-
{
885-
binding.pvb_expr with
886-
(* moved to wrapper_expr *)
887-
pexp_attributes = [];
888-
};
922+
pvb_expr = binding_expr |> constrain_jsx_return;
889923
},
890924
new_binding )
891925
else (None, binding, None)
892926

927+
let rec collect_prop_types types {ptyp_loc; ptyp_desc} =
928+
match ptyp_desc with
929+
| Ptyp_arrow {arg; ret = {ptyp_desc = Ptyp_arrow _} as rest}
930+
when is_labelled arg.lbl || is_optional arg.lbl ->
931+
collect_prop_types ((arg.lbl, arg.attrs, ptyp_loc, arg.typ) :: types) rest
932+
| Ptyp_arrow {arg = {lbl = Nolabel}; ret} -> collect_prop_types types ret
933+
| Ptyp_arrow {arg; ret = return_value}
934+
when is_labelled arg.lbl || is_optional arg.lbl ->
935+
(arg.lbl, arg.attrs, return_value.ptyp_loc, arg.typ) :: types
936+
| _ -> types
937+
893938
let transform_structure_item ~config item =
894939
match item with
895940
(* external *)
@@ -922,19 +967,7 @@ let transform_structure_item ~config item =
922967
|> Option.map Jsx_common.typ_vars_of_core_type
923968
|> Option.value ~default:[]
924969
in
925-
let rec get_prop_types types ({ptyp_loc; ptyp_desc} as full_type) =
926-
match ptyp_desc with
927-
| Ptyp_arrow {arg; ret = {ptyp_desc = Ptyp_arrow _} as typ2}
928-
when is_labelled arg.lbl || is_optional arg.lbl ->
929-
get_prop_types ((arg.lbl, arg.attrs, ptyp_loc, arg.typ) :: types) typ2
930-
| Ptyp_arrow {arg = {lbl = Nolabel}; ret} -> get_prop_types types ret
931-
| Ptyp_arrow {arg; ret = return_value}
932-
when is_labelled arg.lbl || is_optional arg.lbl ->
933-
( return_value,
934-
(arg.lbl, arg.attrs, return_value.ptyp_loc, arg.typ) :: types )
935-
| _ -> (full_type, types)
936-
in
937-
let inner_type, prop_types = get_prop_types [] pval_type in
970+
let prop_types = collect_prop_types [] pval_type in
938971
let named_type_list = List.fold_left arg_to_concrete_type [] prop_types in
939972
let ret_props_type =
940973
Typ.constr ~loc:pstr_loc
@@ -955,7 +988,7 @@ let transform_structure_item ~config item =
955988
let new_external_type =
956989
Ptyp_constr
957990
( {loc = pstr_loc; txt = module_access_name config "componentLike"},
958-
[ret_props_type; inner_type] )
991+
[ret_props_type; jsx_element_type ~loc:pstr_loc] )
959992
in
960993
let new_structure =
961994
{
@@ -1023,30 +1056,7 @@ let transform_signature_item ~config item =
10231056
|> Option.map Jsx_common.typ_vars_of_core_type
10241057
|> Option.value ~default:[]
10251058
in
1026-
let rec get_prop_types types ({ptyp_loc; ptyp_desc} as full_type) =
1027-
match ptyp_desc with
1028-
| Ptyp_arrow {arg; ret = {ptyp_desc = Ptyp_arrow _} as rest}
1029-
when is_optional arg.lbl || is_labelled arg.lbl ->
1030-
get_prop_types ((arg.lbl, arg.attrs, ptyp_loc, arg.typ) :: types) rest
1031-
| Ptyp_arrow
1032-
{
1033-
arg =
1034-
{
1035-
lbl = Nolabel;
1036-
typ = {ptyp_desc = Ptyp_constr ({txt = Lident "unit"}, _)};
1037-
};
1038-
ret = rest;
1039-
} ->
1040-
get_prop_types types rest
1041-
| Ptyp_arrow {arg = {lbl = Nolabel}; ret = rest} ->
1042-
get_prop_types types rest
1043-
| Ptyp_arrow {arg; ret = return_value}
1044-
when is_optional arg.lbl || is_labelled arg.lbl ->
1045-
( return_value,
1046-
(arg.lbl, arg.attrs, return_value.ptyp_loc, arg.typ) :: types )
1047-
| _ -> (full_type, types)
1048-
in
1049-
let inner_type, prop_types = get_prop_types [] pval_type in
1059+
let prop_types = collect_prop_types [] pval_type in
10501060
let named_type_list = List.fold_left arg_to_concrete_type [] prop_types in
10511061
let ret_props_type =
10521062
Typ.constr
@@ -1067,7 +1077,7 @@ let transform_signature_item ~config item =
10671077
let new_external_type =
10681078
Ptyp_constr
10691079
( {loc = psig_loc; txt = module_access_name config "componentLike"},
1070-
[ret_props_type; inner_type] )
1080+
[ret_props_type; jsx_element_type ~loc:psig_loc] )
10711081
in
10721082
let new_structure =
10731083
{

docs/JSXV4.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ is transformed to
272272
```rescript
273273
type props<'x, 'y, 'z> = {x: 'x, y?: 'y, z?: 'z}
274274
275-
({x, ?y, ?z}: props<_, _, _>) => {
275+
({x, ?y, ?z}: props<_, _, _>): Jsx.element => {
276276
let y = switch y {
277277
| None => 3 + x
278278
| Some(y) => y
@@ -281,7 +281,9 @@ type props<'x, 'y, 'z> = {x: 'x, y?: 'y, z?: 'z}
281281
}
282282
```
283283

284-
> Note: this implicit definition of type `props` means that there cannot be other type definitions of `props` in the same scope, or it will be a compiler error about multiple definitions of the type name.
284+
> Note:
285+
> - This implicit definition of type `props` means that there cannot be other type definitions of `props` in the same scope, or it will be a compiler error about multiple definitions of the type name.
286+
> - JSX V4 automatically adds a `Jsx.element` return constraint to all component functions. This ensures that components always return valid JSX elements and provides better type safety. If a component returns a non-JSX value, you'll get a helpful error message suggesting how to convert it (e.g., use `React.int` for integers, `React.string` for strings, etc.).
285287
286288
### Transformation for Component Application
287289

tests/analysis_tests/tests/src/CompletionJsx.res

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ module MultiPropComp = {
6767
@react.component
6868
let make = (~name, ~age, ~time: time) => {
6969
ignore(time)
70-
name ++ age
70+
React.string(name ++ age)
7171
}
7272
}
7373

tests/analysis_tests/tests/src/CompletionPattern.res

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ let make = (~thing: result<someVariant, unit>) => {
229229
// ^com
230230
| _ => ()
231231
}
232+
React.null
232233
}
233234

234235
type results = {

tests/analysis_tests/tests/src/CreateInterface.res

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,27 +109,27 @@ module type OptT = {
109109

110110
module Opt = {
111111
@react.component
112-
let withOpt1 = (~x=3, ~y) => x + y
112+
let withOpt1 = (~x=3, ~y) => React.int(x + y)
113113

114114
module Opt2 = {
115115
@react.component
116116
let withOpt2 = (~x: option<int>=?, ~y: int) =>
117-
switch x {
117+
React.int(switch x {
118118
| None => 0
119119
| Some(x) => x
120120
} +
121-
y
121+
y)
122122
}
123123
module type Opt2 = module type of Opt2
124124

125125
module Opt3 = {
126126
@react.component
127127
let withOpt3 = (~x: option<int>, ~y: int) =>
128-
switch x {
128+
React.int(switch x {
129129
| None => 0
130130
| Some(x) => x
131131
} +
132-
y
132+
y)
133133
}
134134
module type Opt3 = module type of Opt3
135135
}

tests/analysis_tests/tests/src/expected/CompletionPattern.res.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,7 +1178,8 @@ Path res
11781178
}]
11791179

11801180
Complete src/CompletionPattern.res 227:25
1181-
posCursor:[227:25] posNoWhite:[227:24] Found expr:[223:11->231:1]
1181+
posCursor:[227:25] posNoWhite:[227:24] Found expr:[223:11->232:1]
1182+
posCursor:[227:25] posNoWhite:[227:24] Found expr:[224:2->231:12]
11821183
posCursor:[227:25] posNoWhite:[227:24] Found expr:[226:4->227:28]
11831184
posCursor:[227:25] posNoWhite:[227:24] Found pattern:[227:18->227:27]
11841185
Completable: Cpattern Value[r]->recordBody
@@ -1206,8 +1207,8 @@ Path r
12061207
"documentation": {"kind": "markdown", "value": "```rescript\nnest: nestedRecord\n```\n\n```rescript\ntype someRecord = {first: int, second: (bool, option<someRecord>), optThird: option<[#first | #second(someRecord)]>, nest: nestedRecord}\n```"}
12071208
}]
12081209

1209-
Complete src/CompletionPattern.res 242:33
1210-
posCursor:[242:33] posNoWhite:[242:32] Found pattern:[242:7->242:35]
1210+
Complete src/CompletionPattern.res 243:33
1211+
posCursor:[243:33] posNoWhite:[243:32] Found pattern:[243:7->243:35]
12111212
Completable: Cpattern Value[hitsUse](Nolabel)->recordBody
12121213
Package opens Stdlib.place holder Pervasives.JsxModules.place holder
12131214
Resolved opens 1 Stdlib

tests/analysis_tests/tests/src/expected/CreateInterface.res.txt

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ Create Interface src/CreateInterface.res
22
type r = {name: string, age: int}
33
let add: (~x: int, ~y: int) => int
44
@react.component
5-
let make: (~name: string) => React.element
5+
let make: (~name: string) => Jsx.element
66
module Other: {
77
@react.component
8-
let otherComponentName: (~name: string) => React.element
8+
let otherComponentName: (~name: string) => Jsx.element
99
}
1010
module Mod: {
1111
@react.component
12-
let make: (~name: string) => React.element
12+
let make: (~name: string) => Jsx.element
1313
}
1414
module type ModTyp = {
1515
@react.component
16-
let make: (~name: string) => React.element
16+
let make: (~name: string) => Jsx.element
1717
}
1818
@module("path") external dirname: string => string = "dirname"
1919
@module("path") @variadic
@@ -49,21 +49,21 @@ module RFS: {
4949
module Functor: () =>
5050
{
5151
@react.component
52-
let make: unit => React.element
52+
let make: unit => Jsx.element
5353
}
5454
module type FT = {
5555
module Functor: (
5656
X: {
5757
let a: int
5858
@react.component
59-
let make: (~name: string) => React.element
59+
let make: (~name: string) => Jsx.element
6060
let b: int
6161
},
6262
Y: ModTyp,
6363
) =>
6464
{
6565
@react.component
66-
let make: (~name: string) => React.element
66+
let make: (~name: string) => Jsx.element
6767
}
6868
}
6969
module NormaList = List
@@ -73,34 +73,34 @@ module rec RM: ModTyp
7373
and D: ModTyp
7474
module type OptT = {
7575
@react.component
76-
let withOpt1: (~x: int=?, ~y: int) => int
76+
let withOpt1: (~x: int=?, ~y: int) => Jsx.element
7777
module type Opt2 = {
7878
@react.component
79-
let withOpt2: (~x: int=?, ~y: int) => int
79+
let withOpt2: (~x: int=?, ~y: int) => Jsx.element
8080
}
8181
module type Opt3 = {
8282
@react.component
83-
let withOpt3: (~x: option<int>, ~y: int) => int
83+
let withOpt3: (~x: option<int>, ~y: int) => Jsx.element
8484
}
8585
}
8686
module Opt: {
8787
@react.component
88-
let withOpt1: (~x: int=?, ~y: int) => int
88+
let withOpt1: (~x: int=?, ~y: int) => Jsx.element
8989
module Opt2: {
9090
@react.component
91-
let withOpt2: (~x: int=?, ~y: int) => int
91+
let withOpt2: (~x: int=?, ~y: int) => Jsx.element
9292
}
9393
module type Opt2 = {
9494
@react.component
95-
let withOpt2: (~x: int=?, ~y: int) => int
95+
let withOpt2: (~x: int=?, ~y: int) => Jsx.element
9696
}
9797
module Opt3: {
9898
@react.component
99-
let withOpt3: (~x: option<int>, ~y: int) => int
99+
let withOpt3: (~x: option<int>, ~y: int) => Jsx.element
100100
}
101101
module type Opt3 = {
102102
@react.component
103-
let withOpt3: (~x: option<int>, ~y: int) => int
103+
let withOpt3: (~x: option<int>, ~y: int) => Jsx.element
104104
}
105105
}
106106
module Opt2: OptT

tests/analysis_tests/tests/src/expected/Jsx2.resi.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Hover src/Jsx2.resi 1:4
2-
{"contents": {"kind": "markdown", "value": "```rescript\nprops<string>\n```\n\n---\n\n```\n \n```\n```rescript\ntype props<'first> = {first: 'first}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22Jsx2.resi%22%2C0%2C0%5D)\n"}}
2+
{"contents": {"kind": "markdown", "value": "```rescript\nJsx.element\n```\n\n---\n\n```\n \n```\n```rescript\ntype Jsx.element\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22Jsx.res%22%2C25%2C0%5D)\n"}}
33

44
Hover src/Jsx2.resi 4:4
55
{"contents": {"kind": "markdown", "value": "```rescript\nint\n```"}}

0 commit comments

Comments
 (0)