Skip to content

Commit ea1f768

Browse files
committed
Merge branch 'main' of github.com:/reasonml/reason-react into 19
* 'main' of github.com:/reasonml/reason-react: Remove raise annotations and fix locations on errors (#863) ppx: support "custom children" in uppercase components without having to wrap in array literal (#823)
2 parents f5f5579 + f83f216 commit ea1f768

File tree

16 files changed

+288
-74
lines changed

16 files changed

+288
-74
lines changed

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# Unreleased
2+
3+
* BREAKING, ppx: Allow passing an array of custom children to a component
4+
without having to wrap in array literal ([@jchavarri in #748](https://github.com/reasonml/reason-react/pull/823))
5+
16
# 0.15.0
27

38
* Add `isValidElement` (@r17x in

ppx/reason_react_ppx.ml

Lines changed: 50 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ let labelled str = Labelled str
4747
let optional str = Optional str
4848

4949
module Binding = struct
50-
(* Binding is the interface that the ppx uses to interact with the bindings.
51-
Here we define the same APIs as the bindings but it generates Parsetree *)
50+
(* Binding is the interface that the ppx relies on to interact with the react bindings.
51+
Here we define the same APIs as the bindings but it generates Parsetree nodes *)
5252
module ReactDOM = struct
5353
let domProps ~applyLoc ~loc props =
5454
Builder.pexp_apply ~loc:applyLoc
@@ -58,9 +58,6 @@ module Binding = struct
5858
end
5959

6060
module React = struct
61-
let null ~loc =
62-
Builder.pexp_ident ~loc { loc; txt = Ldot (Lident "React", "null") }
63-
6461
let array ~loc children =
6562
Builder.pexp_apply ~loc
6663
(Builder.pexp_ident ~loc
@@ -98,18 +95,22 @@ let rec find_opt p = function
9895
| [] -> None
9996
| x :: l -> if p x then Some x else find_opt p l
10097

101-
let getLabel str =
98+
let getLabelOrEmpty str =
10299
match str with Optional str | Labelled str -> str | Nolabel -> ""
103100

101+
let getLabel str =
102+
match str with Optional str | Labelled str -> Some str | Nolabel -> None
103+
104104
let optionIdent = Lident "option"
105105

106106
let constantString ~loc str =
107107
Builder.pexp_constant ~loc (Pconst_string (str, Location.none, None))
108108

109109
let safeTypeFromValue valueStr =
110-
let valueStr = getLabel valueStr in
111-
match String.sub valueStr 0 1 with "_" -> "T" ^ valueStr | _ -> valueStr
112-
[@@raises Invalid_argument]
110+
match getLabel valueStr with
111+
| Some valueStr when String.sub valueStr 0 1 = "_" -> ("T" ^ valueStr)
112+
| Some valueStr -> valueStr
113+
| None -> ""
113114

114115
let keyType loc = Builder.ptyp_constr ~loc { loc; txt = Lident "string" } []
115116

@@ -224,14 +225,12 @@ let otherAttrsPure { attr_name = loc; _ } = loc.txt <> "react.component"
224225
let hasAttrOnBinding { pvb_attributes; _ } =
225226
find_opt hasAttr pvb_attributes <> None
226227

227-
(* Finds the name of the variable the binding is assigned to, otherwise raises
228-
Invalid_argument *)
228+
(* Finds the name of the variable the binding is assigned to, otherwise raises an error *)
229229
let getFnName binding =
230230
match binding with
231231
| { pvb_pat = { ppat_desc = Ppat_var { txt; _ }; _ }; _ } -> txt
232-
| _ ->
233-
raise (Invalid_argument "react.component calls cannot be destructured.")
234-
[@@raises Invalid_argument]
232+
| { pvb_loc; _} ->
233+
Location.raise_errorf ~loc:pvb_loc "[@react.component] cannot be used with a destructured binding. Please use it on a `let make = ...` binding instead."
235234

236235
let makeNewBinding binding expression newName =
237236
match binding with
@@ -243,22 +242,17 @@ let makeNewBinding binding expression newName =
243242
pvb_expr = expression;
244243
pvb_attributes = [ merlinFocus ];
245244
}
246-
| _ ->
247-
raise (Invalid_argument "react.component calls cannot be destructured.")
248-
[@@raises Invalid_argument]
245+
| { pvb_loc; _ } ->
246+
Location.raise_errorf ~loc:pvb_loc "[@react.component] cannot be used with a destructured binding. Please use it on a `let make = ...` binding instead."
249247

250-
(* Lookup the value of `props` otherwise raise Invalid_argument error *)
251-
let getPropsNameValue _acc (loc, exp) =
252-
match (loc, exp) with
248+
(* Lookup the value of `props` otherwise raise errorf *)
249+
let getPropsNameValue _acc (loc, expr) =
250+
match (loc, expr) with
253251
| ( { txt = Lident "props"; _ },
254252
{ pexp_desc = Pexp_ident { txt = Lident str; _ }; _ } ) ->
255253
{ propsName = str }
256-
| { txt; _ }, _ ->
257-
raise
258-
(Invalid_argument
259-
("react.component only accepts props as an option, given: "
260-
^ Longident.last_exn txt))
261-
[@@raises Invalid_argument]
254+
| { txt; loc }, _ ->
255+
Location.raise_errorf ~loc "[@react.component] only accepts 'props' as a field, given: %s" (Longident.last_exn txt)
262256

263257
(* Lookup the `props` record or string as part of [@react.component] and store
264258
the name for use when rewriting *)
@@ -284,12 +278,10 @@ let getPropsAttr payload =
284278
}
285279
:: _rest)) ->
286280
{ propsName = "props" }
287-
| Some (PStr ({ pstr_desc = Pstr_eval (_, _); _ } :: _rest)) ->
288-
raise
289-
(Invalid_argument
290-
"react.component accepts a record config with props as an options.")
281+
| Some (PStr ({ pstr_desc = Pstr_eval (_, _); pstr_loc; _ } :: _rest)) ->
282+
Location.raise_errorf ~loc:pstr_loc
283+
"[@react.component] accepts a record config with 'props' as a field."
291284
| _ -> defaultProps
292-
[@@raises Invalid_argument]
293285

294286
(* Plucks the label, loc, and type_ from an AST node *)
295287
let pluckLabelDefaultLocType (label, default, _, _, loc, type_) =
@@ -370,7 +362,6 @@ let rec recursivelyMakeNamedArgsForExternal ~types_come_from_signature list args
370362
| _label, Some type_, _ -> type_)
371363
args)
372364
| [] -> args
373-
[@@raises Invalid_argument]
374365

375366
(* Build an AST node for the [@bs.obj] representing props for a component *)
376367
let makePropsValue fnName ~types_come_from_signature loc
@@ -400,7 +391,6 @@ let makePropsValue fnName ~types_come_from_signature loc
400391
];
401392
pval_loc = loc;
402393
}
403-
[@@raises Invalid_argument]
404394

405395
(* Build an AST node representing an `external` with the definition of the
406396
[@bs.obj] *)
@@ -413,7 +403,6 @@ let makePropsExternal fnName loc ~component_is_external
413403
(makePropsValue ~types_come_from_signature:component_is_external fnName
414404
loc namedArgListWithKeyAndRef propsType);
415405
}
416-
[@@raises Invalid_argument]
417406

418407
(* Build an AST node for the signature of the `external` definition *)
419408
let makePropsExternalSig fnName loc namedArgListWithKeyAndRef propsType =
@@ -424,7 +413,6 @@ let makePropsExternalSig fnName loc namedArgListWithKeyAndRef propsType =
424413
(makePropsValue ~types_come_from_signature:true fnName loc
425414
namedArgListWithKeyAndRef propsType);
426415
}
427-
[@@raises Invalid_argument]
428416

429417
(* Build an AST node for the props name when converted to an object inside the
430418
function signature *)
@@ -458,7 +446,14 @@ let makePropsType ~loc namedTypeList =
458446
};
459447
]
460448

461-
let jsxExprAndChildren ~ident ~loc ~ctxt mapper ~keyProps children =
449+
type component_type = Uppercase | Lowercase
450+
451+
let jsxExprAndChildren ~component_type ~loc ~ctxt mapper ~keyProps children =
452+
let ident =
453+
match component_type with
454+
| Uppercase -> Lident "React"
455+
| Lowercase -> Lident "ReactDOM"
456+
in
462457
let childrenExpr =
463458
Option.map (transformChildrenIfListUpper ~loc ~mapper ~ctxt) children
464459
in
@@ -492,23 +487,25 @@ let jsxExprAndChildren ~ident ~loc ~ctxt mapper ~keyProps children =
492487
children *)
493488
( Builder.pexp_ident ~loc { loc; txt = Ldot (ident, "jsxs") },
494489
None,
495-
Some (Binding.React.array ~loc children) )
490+
Some
491+
(match component_type with
492+
| Uppercase -> children
493+
| Lowercase -> Binding.React.array ~loc children) )
496494
| None, (label, key) :: _ ->
497495
( Builder.pexp_ident ~loc { loc; txt = Ldot (ident, "jsxKeyed") },
498496
Some (label, key),
499497
None )
500498
| None, [] ->
501499
(Builder.pexp_ident ~loc { loc; txt = Ldot (ident, "jsx") }, None, None)
502500

503-
let reactJsxExprAndChildren = jsxExprAndChildren ~ident:(Lident "React")
504-
let reactDomJsxExprAndChildren = jsxExprAndChildren ~ident:(Lident "ReactDOM")
501+
let reactJsxExprAndChildren = jsxExprAndChildren ~component_type:Uppercase
502+
let reactDomJsxExprAndChildren = jsxExprAndChildren ~component_type:Lowercase
505503

506504
(* Builds an AST node for the entire `external` definition of props *)
507505
let makeExternalDecl fnName loc namedArgListWithKeyAndRef namedTypeList =
508506
makePropsExternal ~component_is_external:false fnName loc
509507
(List.map pluckLabelDefaultLocType namedArgListWithKeyAndRef)
510508
(makePropsType ~loc namedTypeList)
511-
[@@raises Invalid_argument]
512509

513510
(* TODO: some line number might still be wrong *)
514511
let jsxMapper =
@@ -519,7 +516,7 @@ let jsxMapper =
519516
let argsForMake = argsWithLabels in
520517
let keyProps, otherProps =
521518
List.partition
522-
(fun (arg_label, _) -> "key" = getLabel arg_label)
519+
(fun (arg_label, _) -> "key" = getLabelOrEmpty arg_label)
523520
argsForMake
524521
in
525522
let jsxExpr, key, childrenProp =
@@ -533,10 +530,12 @@ let jsxMapper =
533530
(label, mapper#expression ctxt expression))
534531
in
535532
let isCap str =
536-
let first = String.sub str 0 1 [@@raises Invalid_argument] in
537-
let capped = String.uppercase_ascii first in
538-
first = capped
539-
[@@raises Invalid_argument]
533+
match String.length str with
534+
| 0 -> false
535+
| _ ->
536+
let first = String.sub str 0 1 in
537+
let capped = String.uppercase_ascii first in
538+
first = capped
540539
in
541540
let ident =
542541
match modulePath with
@@ -598,7 +597,7 @@ let jsxMapper =
598597
let componentNameExpr = constantString ~loc:callerLoc id in
599598
let keyProps, nonChildrenProps =
600599
List.partition
601-
(fun (arg_label, _) -> "key" = getLabel arg_label)
600+
(fun (arg_label, _) -> "key" = getLabelOrEmpty arg_label)
602601
nonChildrenProps
603602
in
604603

@@ -647,17 +646,9 @@ let jsxMapper =
647646
let rec recursivelyTransformNamedArgsForMake ~ctxt mapper expr list =
648647
let expr = mapper#expression ctxt expr in
649648
match expr.pexp_desc with
650-
(* TODO: make this show up with a loc. *)
651649
| Pexp_fun (Labelled "key", _, _, _) | Pexp_fun (Optional "key", _, _, _) ->
652-
raise
653-
(Invalid_argument
654-
"Key cannot be accessed inside of a component. Don't worry - you \
655-
can always key a component from its parent!")
656-
| Pexp_fun (Labelled "ref", _, _, _) | Pexp_fun (Optional "ref", _, _, _) ->
657-
raise
658-
(Invalid_argument
659-
"Ref cannot be passed as a normal prop. Please use `forwardRef` \
660-
API instead.")
650+
Location.raise_errorf ~loc:expr.pexp_loc
651+
("~key cannot be accessed from the component props. Please set the key where the component is being used.")
661652
| Pexp_fun
662653
( ((Optional label | Labelled label) as arg),
663654
default,
@@ -704,7 +695,6 @@ let jsxMapper =
704695
"reason-react-ppx: react.component refs only support plain arguments \
705696
and type annotations."
706697
| _ -> (list, None)
707-
[@@raises Invalid_argument]
708698
in
709699

710700
let argToType types (name, default, _noLabelName, _alias, loc, type_) =
@@ -726,7 +716,7 @@ let jsxMapper =
726716
} )
727717
:: types
728718
| Some type_, name, Some _default ->
729-
( getLabel name,
719+
( getLabelOrEmpty name,
730720
[],
731721
{
732722
ptyp_desc = Ptyp_constr ({ loc; txt = optionIdent }, [ type_ ]);
@@ -735,7 +725,7 @@ let jsxMapper =
735725
ptyp_attributes = [];
736726
} )
737727
:: types
738-
| Some type_, name, _ -> (getLabel name, [], type_) :: types
728+
| Some type_, name, _ -> (getLabelOrEmpty name, [], type_) :: types
739729
| None, Optional label, _ ->
740730
( label,
741731
[],
@@ -767,7 +757,6 @@ let jsxMapper =
767757
} )
768758
:: types
769759
| _ -> types
770-
[@@raises Invalid_argument]
771760
in
772761

773762
let argToConcreteType types (name, loc, type_) =
@@ -1100,7 +1089,7 @@ let jsxMapper =
11001089
in
11011090
let pluckArg (label, _, _, alias, loc, _) =
11021091
( label,
1103-
match getLabel label with
1092+
match getLabelOrEmpty label with
11041093
| "" -> Builder.pexp_ident ~loc { txt = Lident alias; loc }
11051094
| labelString ->
11061095
Builder.pexp_apply ~loc
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module X_as_main_function = {
2+
[@react.component]
3+
let x = () => <div />;
4+
};
5+
6+
module Create_element_as_main_function = {
7+
[@react.component]
8+
let createElement = (~lola) => <div> {React.string(lola)} </div>;
9+
};
10+
11+
/* This isn't valid running code, since Foo gets transformed into Foo.make, not createElement. */
12+
module Invalid_case = {
13+
[@react.component]
14+
let make = (~lola) => {
15+
<Create_element_as_main_function lola />;
16+
};
17+
};
18+
19+
/* If main function is not make, neither createElement, then it can be explicitly annotated */
20+
/* NOTE: If you use `createElement` refmt removes it */
21+
module Valid_case = {
22+
[@react.component]
23+
let make = () => {
24+
<Component_with_x_as_main_function.x />;
25+
};
26+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
Since we generate invalid syntax for the argument of the make fn `(Props : <>)`
2+
We need to output ML syntax here, otherwise refmt could not parse it.
3+
$ ../ppx.sh --output ml input.re
4+
module X_as_main_function =
5+
struct
6+
external xProps : ?key:string -> unit -> < > Js.t = ""[@@mel.obj ]
7+
let x () = ReactDOM.jsx "div" (((ReactDOM.domProps)[@merlin.hide ]) ())
8+
let x =
9+
let Output$X_as_main_function$x (Props : < > Js.t) = x () in
10+
Output$X_as_main_function$x
11+
end
12+
module Create_element_as_main_function =
13+
struct
14+
external createElementProps :
15+
lola:'lola -> ?key:string -> unit -> < lola: 'lola > Js.t = ""
16+
[@@mel.obj ]
17+
let createElement =
18+
((fun ~lola ->
19+
ReactDOM.jsx "div"
20+
(((ReactDOM.domProps)[@merlin.hide ])
21+
~children:(React.string lola) ()))
22+
[@warning "-16"])
23+
let createElement =
24+
let Output$Create_element_as_main_function$createElement
25+
(Props : < lola: 'lola > Js.t) =
26+
createElement ~lola:(Props ## lola) in
27+
Output$Create_element_as_main_function$createElement
28+
end
29+
module Invalid_case =
30+
struct
31+
external makeProps :
32+
lola:'lola -> ?key:string -> unit -> < lola: 'lola > Js.t = ""
33+
[@@mel.obj ]
34+
let make =
35+
((fun ~lola ->
36+
React.jsx Create_element_as_main_function.make
37+
(Create_element_as_main_function.makeProps ~lola ()))
38+
[@warning "-16"])
39+
let make =
40+
let Output$Invalid_case (Props : < lola: 'lola > Js.t) =
41+
make ~lola:(Props ## lola) in
42+
Output$Invalid_case
43+
end
44+
module Valid_case =
45+
struct
46+
external makeProps : ?key:string -> unit -> < > Js.t = ""[@@mel.obj ]
47+
let make () =
48+
React.jsx Component_with_x_as_main_function.x
49+
(Component_with_x_as_main_function.xProps ())
50+
let make =
51+
let Output$Valid_case (Props : < > Js.t) = make () in
52+
Output$Valid_case
53+
end

ppx/test/component.t/input.re

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ module Forward_Ref = {
4141
});
4242
};
4343

44+
module Ref_as_prop = {
45+
[@react.component]
46+
let make = (~children, ~ref) => {
47+
<button ref className="FancyButton"> children </button>;
48+
};
49+
};
50+
4451
module Onclick_handler_button = {
4552
[@react.component]
4653
let make = (~name, ~isDisabled=?) => {

0 commit comments

Comments
 (0)