Skip to content

Commit b8fdd38

Browse files
authored
Make Jsx.component abstract (#8390)
* Make Jsx.component abstract * Fix shadowing issue * Clean up * More recursive binding fixes * CHANGELOG * Format * Simplify * Fix playground test * Fix recursive @react.componentWithProps * Introduce %component_identity * Fix gentype tests * Fix LSP hover regression from JSX component coercion * Improve error message for plain functions used as JSX components * Another error message improvement * Improve comment * Clearer error messages
1 parent 4d93364 commit b8fdd38

87 files changed

Lines changed: 853 additions & 377 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
diff --git a/src/React.res b/src/React.res
2+
index 0676012234c8a9cdd93030d732274b5ed3914b11..1ff7a036a06a69e76a680b0396a3f8a67a59c328 100644
3+
--- a/src/React.res
4+
+++ b/src/React.res
5+
@@ -13,7 +13,7 @@ type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
6+
7+
type component<'props> = Jsx.component<'props>
8+
9+
-external component: componentLike<'props, element> => component<'props> = "%identity"
10+
+external component: componentLike<'props, element> => component<'props> = "%component_identity"
11+
12+
@module("react")
13+
external createElement: (component<'props>, 'props) => element = "createElement"

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
#### :boom: Breaking Change
1616

17+
- Make Jsx.component abstract. https://github.com/rescript-lang/rescript/pull/8390
18+
1719
#### :eyeglasses: Spec Compliance
1820

1921
#### :rocket: New Feature

compiler/frontend/bs_ast_invariant.ml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,11 @@ let emit_external_warnings : iterator =
124124
| ({pval_loc; pval_prim = [byte_name]; pval_type} :
125125
Parsetree.value_description) -> (
126126
match byte_name with
127-
| "%identity" when not (Ast_core_type.is_arity_one pval_type) ->
127+
| ("%identity" | "%component_identity")
128+
when not (Ast_core_type.is_arity_one pval_type) ->
128129
Location.raise_errorf ~loc:pval_loc
129-
"%%identity expects a function type of the form 'a => 'b (arity \
130-
1)"
130+
"%s expects a function type of the form 'a => 'b (arity 1)"
131+
byte_name
131132
| _ ->
132133
if byte_name <> "" then
133134
let c = String.unsafe_get byte_name 0 in

compiler/ml/error_message_utils.ml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ type type_clash_context =
107107
is_constant: string option;
108108
}
109109
| FunctionArgument of {optional: bool; name: string option}
110+
| JsxComponent
110111
| BracedIdent
111112
| Statement of type_clash_statement
112113
| ForLoopCondition
@@ -127,6 +128,7 @@ let context_to_string = function
127128
| Some TryReturn -> "TryReturn"
128129
| Some StringConcat -> "StringConcat"
129130
| Some (FunctionArgument _) -> "FunctionArgument"
131+
| Some JsxComponent -> "JsxComponent"
130132
| Some ComparisonOperator -> "ComparisonOperator"
131133
| Some IfReturn -> "IfReturn"
132134
| Some TernaryReturn -> "TernaryReturn"
@@ -145,6 +147,7 @@ let error_type_text ppf type_clash_context =
145147
| Some ArrayValue -> "This array item has type:"
146148
| Some (SetRecordField _) ->
147149
"You're assigning something to this field that has type:"
150+
| Some JsxComponent -> "This JSX tag has type:"
148151
| _ -> "This has type:"
149152
in
150153
fprintf ppf "%s" text
@@ -162,6 +165,7 @@ let error_expected_type_text ppf type_clash_context =
162165
| None -> ());
163166
164167
fprintf ppf " is expecting:"
168+
| Some JsxComponent -> fprintf ppf "But JSX component positions require:"
165169
| Some ComparisonOperator ->
166170
fprintf ppf "But it's being compared to something of type:"
167171
| Some SwitchReturn -> fprintf ppf "But this switch is expected to return:"
@@ -224,6 +228,12 @@ let is_variant_type ~(extract_concrete_typedecl : extract_concrete_typedecl)
224228
| _ -> false
225229
with _ -> false
226230
231+
let is_jsx_component_type ~env ty =
232+
match Ctype.expand_head env ty with
233+
| {desc = Tconstr (Pdot (Pident {name = "Jsx"}, "component", _), _, _)} ->
234+
true
235+
| _ -> false
236+
227237
let get_variant_constructors
228238
~(extract_concrete_typedecl : extract_concrete_typedecl) ~env ty =
229239
match extract_concrete_typedecl env ty with
@@ -396,6 +406,17 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
396406
\ - Remove the @{<info>await@} if this is not expected to be a promise\n\
397407
\ - Wrap the expression in @{<info>Promise.resolve@} to convert the \
398408
expression to a promise"
409+
| Some JsxComponent, _ ->
410+
fprintf ppf
411+
"\n\n\
412+
\ JSX tags must be React components, not plain functions.\n\n\
413+
\ Possible solutions:\n\
414+
\ - If this function takes labeled props, annotate it with \
415+
@{<info>@react.component@}\n\
416+
\ - If this function takes a single props record, annotate it with \
417+
@{<info>@react.componentWithProps@}\n\
418+
\ - If this is already a valid component-like value, wrap it with \
419+
@{<info>React.component(...)@}"
399420
| Some IfReturn, _ ->
400421
fprintf ppf
401422
"\n\n\
@@ -423,6 +444,17 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
423444
\ - Use a tuple, if your array is of fixed length. Tuples can mix types \
424445
freely, and compiles to a JavaScript array. Example of a tuple: `let \
425446
myTuple = (10, \"hello\", 15.5, true)"
447+
| _, Some ({desc = Tarrow _}, expected)
448+
when is_jsx_component_type ~env expected ->
449+
fprintf ppf
450+
"\n\n\
451+
\ A React component is expected here, but this expression is a plain \
452+
function.\n\n\
453+
\ Possible solutions:\n\
454+
\ - Extract it to a component annotated with @{<info>@react.component@} \
455+
or @{<info>@react.componentWithProps@}\n\
456+
\ - If this is already a valid component-like value, wrap it with \
457+
@{<info>React.component(...)@}"
426458
| _, Some (_, {desc = Tconstr (p2, _, _)}) when Path.same Predef.path_dict p2
427459
->
428460
fprintf ppf

compiler/ml/translcore.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ let primitives_table =
236236
create_hashtable
237237
[|
238238
("%identity", Pidentity);
239+
("%component_identity", Pidentity);
239240
("%ignore", Pignore);
240241
("%revapply", Prevapply);
241242
("%apply", Pdirapply);

compiler/ml/typecore.ml

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1808,6 +1808,24 @@ let rec final_subexpression sexp =
18081808
18091809
(* Generalization criterion for expressions *)
18101810
1811+
let is_component_identity = function
1812+
| Texp_ident
1813+
(_, _, {val_kind = Val_prim {Primitive.prim_name = "%component_identity"}})
1814+
->
1815+
true
1816+
| _ -> false
1817+
1818+
let is_identity_coercion = function
1819+
| Texp_ident
1820+
( _,
1821+
_,
1822+
{
1823+
val_kind =
1824+
Val_prim {Primitive.prim_name = "%identity" | "%component_identity"};
1825+
} ) ->
1826+
true
1827+
| _ -> false
1828+
18111829
let rec is_nonexpansive exp =
18121830
List.exists
18131831
(function
@@ -1822,6 +1840,26 @@ let rec is_nonexpansive exp =
18221840
List.for_all (fun vb -> is_nonexpansive vb.vb_expr) pat_exp_list
18231841
&& is_nonexpansive body
18241842
| Texp_function _ -> true
1843+
(* `%component_identity` is a typed no-op coercion that lets generated
1844+
component wrappers keep the same generalization behavior as their
1845+
underlying function values. This preserves polymorphic props for
1846+
components such as:
1847+
1848+
@react.component
1849+
let make = (~x) =>
1850+
switch x {
1851+
| #a => React.string("A")
1852+
| #b => React.string("B")
1853+
| _ => React.string("other")
1854+
}
1855+
1856+
The JSX transform emits a function value and then coerces it through
1857+
`React.component`, whose implementation is `%component_identity`. Since no
1858+
runtime computation happens beyond evaluating the argument, the application
1859+
is non-expansive exactly when all supplied arguments are non-expansive. *)
1860+
| Texp_apply {funct = {exp_desc}; args; _} when is_component_identity exp_desc
1861+
->
1862+
List.for_all is_nonexpansive_opt (List.map snd args)
18251863
| Texp_apply {partial = true; _} ->
18261864
(* ReScript partial applications (`foo(args, ...)`) lower to wrapper
18271865
functions in codegen, so creating the partial itself is nonexpansive
@@ -2238,12 +2276,6 @@ let is_ignore ~env ~arity funct =
22382276
with Unify _ -> false)
22392277
| _ -> false
22402278
2241-
let not_identity = function
2242-
| Texp_ident (_, _, {val_kind = Val_prim {Primitive.prim_name = "%identity"}})
2243-
->
2244-
false
2245-
| _ -> true
2246-
22472279
let rec lower_args env seen ty_fun =
22482280
let ty = expand_head env ty_fun in
22492281
if List.memq ty seen then ()
@@ -2494,7 +2526,10 @@ and type_expect_ ?deprecated_context ~context ?in_function ?(recarg = Rejected)
24942526
wrap_trace_gadt_instances env (lower_args env []) ty;
24952527
begin_def ();
24962528
let total_app = not partial in
2497-
let context = type_clash_context_from_function sexp sfunct in
2529+
let context =
2530+
if transformed_jsx then Some JsxComponent
2531+
else type_clash_context_from_function sexp sfunct
2532+
in
24982533
let args, ty_res, fully_applied =
24992534
match translate_unified_ops env funct sargs with
25002535
| Some (targs, result_type) -> (targs, result_type, true)
@@ -3828,8 +3863,10 @@ and type_application ~context total_app env funct (sargs : sargs) :
38283863
(* This is a total application when the toplevel type is a polymorphic variable,
38293864
so the function type including arity can be inferred. *)
38303865
let t1 = newvar () and t2 = newvar () in
3831-
if ty_fun.level >= t1.level && not_identity funct.exp_desc then
3832-
Location.prerr_warning sarg1.pexp_loc Warnings.Unused_argument;
3866+
if
3867+
ty_fun.level >= t1.level
3868+
&& not (is_identity_coercion funct.exp_desc)
3869+
then Location.prerr_warning sarg1.pexp_loc Warnings.Unused_argument;
38333870
unify env ty_fun
38343871
(newty
38353872
(Tarrow
@@ -3892,15 +3929,19 @@ and type_application ~context total_app env funct (sargs : sargs) :
38923929
if (not optional) && is_optional l' then
38933930
Location.prerr_warning sarg0.pexp_loc
38943931
(Warnings.Nonoptional_label (Printtyp.string_of_label l));
3932+
let argument_context =
3933+
match (context, args) with
3934+
| Some JsxComponent, [] -> Some JsxComponent
3935+
| Some JsxComponent, _ ->
3936+
type_clash_context_for_function_argument ~label:l' None sarg0
3937+
| _ ->
3938+
type_clash_context_for_function_argument ~label:l' context sarg0
3939+
in
38953940
( sargs,
38963941
omitted,
38973942
Some
38983943
(if (not optional) || is_optional l' then fun () ->
3899-
type_argument
3900-
~context:
3901-
(type_clash_context_for_function_argument ~label:l' context
3902-
sarg0)
3903-
env sarg0 ty ty0
3944+
type_argument ~context:argument_context env sarg0 ty ty0
39043945
else fun () ->
39053946
option_some
39063947
(type_argument

0 commit comments

Comments
 (0)