Skip to content
This repository was archived by the owner on Jun 15, 2023. It is now read-only.

Commit abb5189

Browse files
committed
add parsing & jsx mapper for spread props
with the general type checking the structural typings `@obj`
1 parent b693237 commit abb5189

File tree

4 files changed

+179
-96
lines changed

4 files changed

+179
-96
lines changed

cli/reactjs_jsx_ppx_v3.ml

Lines changed: 147 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -191,31 +191,108 @@ let rec recursivelyMakeNamedArgsForExternal list args =
191191
| [] -> args
192192
[@@raises Invalid_argument]
193193

194-
(* Build an AST node for the [@bs.obj] representing props for a component *)
195-
let makePropsValue fnName loc namedArgListWithKeyAndRef propsType =
196-
let propsName = fnName ^ "Props" in
197-
{
198-
pval_name = { txt = propsName; loc };
199-
pval_type =
200-
recursivelyMakeNamedArgsForExternal namedArgListWithKeyAndRef
201-
(Typ.arrow nolabel
202-
{ ptyp_desc = Ptyp_constr ({ txt = Lident "unit"; loc }, []); ptyp_loc = loc; ptyp_attributes = [] }
203-
propsType);
204-
pval_prim = [ "" ];
205-
pval_attributes = [ ({ txt = "bs.obj"; loc }, PStr []) ];
206-
pval_loc = loc;
207-
}
208-
[@@raises Invalid_argument]
209-
210-
(* Build an AST node representing an `external` with the definition of the [@bs.obj] *)
211-
let makePropsExternal fnName loc namedArgListWithKeyAndRef propsType =
212-
{ pstr_loc = loc; pstr_desc = Pstr_primitive (makePropsValue fnName loc namedArgListWithKeyAndRef propsType) }
213-
[@@raises Invalid_argument]
214-
215-
(* Build an AST node for the signature of the `external` definition *)
216-
let makePropsExternalSig fnName loc namedArgListWithKeyAndRef propsType =
217-
{ psig_loc = loc; psig_desc = Psig_value (makePropsValue fnName loc namedArgListWithKeyAndRef propsType) }
218-
[@@raises Invalid_argument]
194+
(* List.filter_map in 4.08.0 *)
195+
let filterMap f =
196+
let rec aux accu = function
197+
| [] -> List.rev accu
198+
| x :: l -> (
199+
match f x with None -> aux accu l | Some v -> aux (v :: accu) l )
200+
in
201+
aux []
202+
203+
(* make record from props and spread props if exists *)
204+
let recordFromProps { pexp_loc } callArguments =
205+
let rec removeLastPositionUnitAux props acc =
206+
match props with
207+
| [] -> acc
208+
| [ (Nolabel, { pexp_desc = Pexp_construct ({ txt = Lident "()" }, None) }) ] -> acc
209+
| (Nolabel, _) :: _rest -> raise (Invalid_argument "JSX: found non-labelled argument before the last position")
210+
| prop :: rest -> removeLastPositionUnitAux rest (prop :: acc)
211+
in
212+
let props, propsToSpread = removeLastPositionUnitAux callArguments []
213+
|> List.rev
214+
|> List.partition (fun (label, _) -> label <> labelled "spreadProps") in
215+
let fields = props |> List.map (fun (arg_label, ({ pexp_loc } as expr) ) ->
216+
({ txt = (Longident.parse (getLabel arg_label)); loc = pexp_loc} , expr))
217+
in
218+
let spreadFields = propsToSpread |> List.map (fun (_, expression) -> expression) in
219+
match spreadFields with
220+
| [] -> { pexp_desc=Pexp_record (fields, None); pexp_loc; pexp_attributes=[]}
221+
| [ spreadProps] -> { pexp_desc=Pexp_record (fields, Some spreadProps); pexp_loc; pexp_attributes=[] }
222+
| spreadProps :: _ -> { pexp_desc=Pexp_record (fields, Some spreadProps); pexp_loc; pexp_attributes=[] }
223+
224+
(* @obj type props = { id: option<string>, key: ... } *)
225+
let makePropsRecordType fnName loc namedTypeList =
226+
let labelDeclList =
227+
namedTypeList
228+
|> List.map (fun (label, _, interiorType) ->
229+
Type.field ~loc { txt = label; loc } interiorType)
230+
in
231+
(* 'id, 'className, ... *)
232+
let params =
233+
namedTypeList
234+
|> filterMap (fun (_, _, interiorType) ->
235+
match interiorType with
236+
| { ptyp_desc = Ptyp_var _ } as param -> Some (param, Invariant)
237+
| _ -> None)
238+
in
239+
Str.type_ Nonrecursive
240+
[
241+
Type.mk ~loc
242+
~attrs:[ ({ txt = "bs.obj"; loc }, PStr []) ]
243+
~params
244+
{ txt = fnName ^ "Props"; loc }
245+
~kind:(Ptype_record labelDeclList);
246+
]
247+
248+
(* let makeProps = (props: makeProps) => props *)
249+
let makePropsFn fnName loc =
250+
Str.value Nonrecursive
251+
[
252+
Vb.mk
253+
(Pat.var { txt = fnName ^ "Props"; loc })
254+
(Exp.fun_ Nolabel None
255+
(Pat.constraint_
256+
(Pat.var { txt = "props"; loc })
257+
(Typ.constr ~loc
258+
{ txt = Longident.parse @@ fnName ^ "Props"; loc }
259+
[]))
260+
(Exp.ident { txt = Longident.parse "props"; loc }));
261+
]
262+
263+
(* @obj type props = { id: option<string>, key: ... } *)
264+
let makePropsRecordTypeSig fnName loc namedTypeList =
265+
let labelDeclList =
266+
namedTypeList
267+
|> List.map (fun (label, _, interiorType) ->
268+
Type.field ~loc { txt = label; loc } interiorType)
269+
in
270+
let params =
271+
namedTypeList
272+
|> filterMap (fun (_, _, interiorType) ->
273+
match interiorType with
274+
| { ptyp_desc = Ptyp_var _ } as param -> Some (param, Invariant)
275+
| _ -> None)
276+
in
277+
Sig.type_ Nonrecursive
278+
[
279+
Type.mk ~loc
280+
~attrs:[ ({ txt = "bs.obj"; loc }, PStr []) ]
281+
~params
282+
{ txt = fnName ^ "Props"; loc }
283+
~kind:(Ptype_record labelDeclList);
284+
]
285+
286+
(* let makeProps: makeProps => props *)
287+
let makePropsFnSig fnName loc =
288+
Sig.value ~loc
289+
(Val.mk
290+
{ txt = fnName ^ "Props"; loc }
291+
(Typ.arrow Nolabel
292+
(Typ.constr
293+
(Location.mkloc (Longident.parse (fnName ^ "Props")) loc)
294+
[])
295+
(Typ.constr (Location.mkloc (Longident.parse @@ fnName ^ "Props") loc) [])))
219296

220297
(* Build an AST node for the props name when converted to an object inside the function signature *)
221298
let makePropsName ~loc name = { ppat_desc = Ppat_var { txt = name; loc }; ppat_loc = loc; ppat_attributes = [] }
@@ -226,13 +303,6 @@ let makeObjectField loc (str, attrs, type_) = Otag ({ loc; txt = str }, attrs, t
226303
let makePropsType ~loc namedTypeList =
227304
Typ.mk ~loc (Ptyp_object (List.map (makeObjectField loc) namedTypeList, Closed))
228305

229-
(* Builds an AST node for the entire `external` definition of props *)
230-
let makeExternalDecl fnName loc namedArgListWithKeyAndRef namedTypeList =
231-
makePropsExternal fnName loc
232-
(List.map pluckLabelDefaultLocType namedArgListWithKeyAndRef)
233-
(makePropsType ~loc namedTypeList)
234-
[@@raises Invalid_argument]
235-
236306
let newtypeToVar newtype type_ =
237307
let var_desc = Ptyp_var ("type-" ^ newtype) in
238308
let typ (mapper : Ast_mapper.mapper) typ =
@@ -248,7 +318,7 @@ let newtypeToVar newtype type_ =
248318
let jsxMapper () =
249319
let jsxVersion = ref None in
250320

251-
let transformUppercaseCall3 modulePath mapper loc attrs _ callArguments =
321+
let transformUppercaseCall3 modulePath mapper loc attrs callExpression callArguments =
252322
let children, argsWithLabels = extractChildren ~loc ~removeLastPositionUnit:true callArguments in
253323
let argsForMake = argsWithLabels in
254324
let childrenExpr = transformChildrenIfListUpper ~loc ~mapper children in
@@ -265,8 +335,8 @@ let jsxMapper () =
265335
(* this is a hack to support react components that introspect into their children *)
266336
childrenArg := Some expression;
267337
[ (labelled "children", Exp.ident ~loc { loc; txt = Ldot (Lident "React", "null") }) ] )
268-
@ [ (nolabel, Exp.construct ~loc { loc; txt = Lident "()" } None) ]
269338
in
339+
let record = recordFromProps callExpression args in
270340
let isCap str =
271341
let first = String.sub str 0 1 [@@raises Invalid_argument] in
272342
let capped = String.uppercase_ascii first in
@@ -285,7 +355,7 @@ let jsxMapper () =
285355
| Ldot (ident, path) -> Ldot (ident, path ^ "Props")
286356
| _ -> raise (Invalid_argument "JSX name can't be the result of function applications")
287357
in
288-
let props = Exp.apply ~attrs ~loc (Exp.ident ~loc { loc; txt = propsIdent }) args in
358+
let props = Exp.apply ~attrs ~loc (Exp.ident ~loc { loc; txt = propsIdent }) [ (nolabel, record) ] in
289359
(* handle key, ref, children *)
290360
(* React.createElement(Component.make, props, ...children) *)
291361
match !childrenArg with
@@ -300,8 +370,9 @@ let jsxMapper () =
300370
[@@raises Invalid_argument]
301371
in
302372

303-
let transformLowercaseCall3 mapper loc attrs callArguments id =
373+
let transformLowercaseCall3 mapper loc attrs callExpression callArguments id =
304374
let children, nonChildrenProps = extractChildren ~loc callArguments in
375+
let record = recordFromProps callExpression nonChildrenProps in
305376
let componentNameExpr = constantString ~loc id in
306377
let childrenExpr = transformChildrenIfList ~loc ~mapper children in
307378
let createElementCall =
@@ -324,17 +395,12 @@ let jsxMapper () =
324395
match nonChildrenProps with
325396
| [ _justTheUnitArgumentAtEnd ] ->
326397
[ (* "div" *) (nolabel, componentNameExpr); (* [|moreCreateElementCallsHere|] *) (nolabel, childrenExpr) ]
327-
| nonEmptyProps ->
328-
let propsCall =
329-
Exp.apply ~loc
330-
(Exp.ident ~loc { loc; txt = Ldot (Lident "ReactDOMRe", "domProps") })
331-
(nonEmptyProps |> List.map (fun (label, expression) -> (label, mapper.expr mapper expression)))
332-
in
398+
| _nonEmptyProps ->
333399
[
334400
(* "div" *)
335401
(nolabel, componentNameExpr);
336-
(* ReactDOMRe.props(~className=blabla, ~foo=bar, ()) *)
337-
(labelled "props", propsCall);
402+
(* ReactDOMRe.props({className=blabla, foo=bar}) *)
403+
(labelled "props", record);
338404
(* [|moreCreateElementCallsHere|] *)
339405
(nolabel, childrenExpr);
340406
]
@@ -469,12 +535,15 @@ let jsxMapper () =
469535
in
470536
let innerType, propTypes = getPropTypes [] pval_type in
471537
let namedTypeList = List.fold_left argToConcreteType [] propTypes in
472-
let pluckLabelAndLoc (label, loc, type_) = (label, None (* default *), loc, Some type_) in
473-
let retPropsType = makePropsType ~loc:pstr_loc namedTypeList in
474-
let externalPropsDecl =
475-
makePropsExternal fnName pstr_loc
476-
((optional "key", None, pstr_loc, Some (keyType pstr_loc)) :: List.map pluckLabelAndLoc propTypes)
477-
retPropsType
538+
let retPropsType = (Typ.constr ~loc:pstr_loc (Location.mkloc (Longident.parse (fnName ^ "Props")) pstr_loc) []) in
539+
(* @obj type makeProps = { ... } *)
540+
let propsRecordType =
541+
makePropsRecordType fnName pstr_loc
542+
(("key", [], keyType pstr_loc) :: namedTypeList)
543+
in
544+
(* let makeProps = (props: makeProps) => props *)
545+
let propsFn =
546+
makePropsFn fnName pstr_loc
478547
in
479548
(* can't be an arrow because it will defensively uncurry *)
480549
let newExternalType =
@@ -492,7 +561,7 @@ let jsxMapper () =
492561
};
493562
}
494563
in
495-
externalPropsDecl :: newStructure :: returnStructures
564+
propsRecordType :: propsFn :: newStructure :: returnStructures
496565
| _ -> raise (Invalid_argument "Only one react.component call can exist on a component at one time") )
497566
(* let component = ... *)
498567
| { pstr_loc; pstr_desc = Pstr_value (recFlag, valueBindings) } ->
@@ -616,17 +685,6 @@ let jsxMapper () =
616685
let namedArgList, newtypes, forwardRef =
617686
recursivelyTransformNamedArgsForMake mapper (modifiedBindingOld binding) [] []
618687
in
619-
let namedArgListWithKeyAndRef =
620-
(optional "key", None, Pat.var { txt = "key"; loc = emptyLoc }, "key", emptyLoc, Some (keyType emptyLoc))
621-
:: namedArgList
622-
in
623-
let namedArgListWithKeyAndRef =
624-
match forwardRef with
625-
| Some _ ->
626-
(optional "ref", None, Pat.var { txt = "key"; loc = emptyLoc }, "ref", emptyLoc, None)
627-
:: namedArgListWithKeyAndRef
628-
| None -> namedArgListWithKeyAndRef
629-
in
630688
let namedArgListWithKeyAndRefForNew =
631689
match forwardRef with
632690
| Some txt -> namedArgList @ [ (nolabel, None, Pat.var { txt; loc = emptyLoc }, txt, emptyLoc, None) ]
@@ -649,25 +707,22 @@ let jsxMapper () =
649707
in
650708
let namedTypeList = List.fold_left argToType [] namedArgList in
651709
let loc = emptyLoc in
652-
let externalArgs = (* translate newtypes to type variables *)
653-
List.fold_left
654-
(fun args newtype ->
655-
List.map (fun (a, b, c, d, e, maybeTyp) ->
656-
match maybeTyp with
657-
| Some typ -> (a, b, c, d, e, Some (newtypeToVar newtype.txt typ))
658-
| None -> (a, b, c, d, e, None))
659-
args)
660-
namedArgListWithKeyAndRef
661-
newtypes
662-
in
663710
let externalTypes = (* translate newtypes to type variables *)
664711
List.fold_left
665712
(fun args newtype ->
666713
List.map (fun (a, b, typ) -> (a, b, newtypeToVar newtype.txt typ)) args)
667714
namedTypeList
668715
newtypes
669716
in
670-
let externalDecl = makeExternalDecl fnName loc externalArgs externalTypes in
717+
(* @obj type makeProps = { ... } *)
718+
let propsRecordType =
719+
makePropsRecordType fnName pstr_loc
720+
(("key", [], keyType pstr_loc) :: namedTypeList)
721+
in
722+
(* let makeProps = (props: makeProps) => props *)
723+
let propsFn =
724+
makePropsFn fnName pstr_loc
725+
in
671726
let innerExpressionArgs =
672727
List.map pluckArg namedArgListWithKeyAndRefForNew
673728
@ if hasUnit then [ (Nolabel, Exp.construct { loc; txt = Lident "()" } None) ] else []
@@ -727,20 +782,22 @@ let jsxMapper () =
727782
| Nonrecursive ->
728783
([ { binding with pvb_expr = expression; pvb_attributes = [] } ], Some (bindingWrapper fullExpression))
729784
in
730-
(Some externalDecl, bindings, newBinding)
731-
else (None, [ binding ], None)
785+
(Some propsRecordType, Some propsFn, bindings, newBinding)
786+
else (None, None, [ binding ], None)
732787
[@@raises Invalid_argument]
733788
in
734789
let structuresAndBinding = List.map mapBinding valueBindings in
735-
let otherStructures (extern, binding, newBinding) (externs, bindings, newBindings) =
736-
let externs = match extern with Some extern -> extern :: externs | None -> externs in
790+
let otherStructures (type_, propsFn, binding, newBinding) (types, propsFns, bindings, newBindings) =
791+
let types = match type_ with Some type_ -> type_ :: types | None -> types in
792+
let propsFns = match propsFn with Some propsFn -> propsFn :: propsFns | None -> propsFns in
737793
let newBindings =
738794
match newBinding with Some newBinding -> newBinding :: newBindings | None -> newBindings
739795
in
740-
(externs, binding @ bindings, newBindings)
796+
(types, propsFns, binding @ bindings, newBindings)
741797
in
742-
let externs, bindings, newBindings = List.fold_right otherStructures structuresAndBinding ([], [], []) in
743-
externs
798+
let types, propsFns, bindings, newBindings = List.fold_right otherStructures structuresAndBinding ([], [], [], []) in
799+
types
800+
@ propsFns
744801
@ [ { pstr_loc; pstr_desc = Pstr_value (recFlag, bindings) } ]
745802
@ ( match newBindings with
746803
| [] -> []
@@ -774,12 +831,13 @@ let jsxMapper () =
774831
in
775832
let innerType, propTypes = getPropTypes [] pval_type in
776833
let namedTypeList = List.fold_left argToConcreteType [] propTypes in
777-
let pluckLabelAndLoc (label, loc, type_) = (label, None, loc, Some type_) in
778-
let retPropsType = makePropsType ~loc:psig_loc namedTypeList in
779-
let externalPropsDecl =
780-
makePropsExternalSig fnName psig_loc
781-
((optional "key", None, psig_loc, Some (keyType psig_loc)) :: List.map pluckLabelAndLoc propTypes)
782-
retPropsType
834+
let retPropsType = (Typ.constr (Location.mkloc (Longident.parse (fnName ^ "Props")) psig_loc) []) in
835+
let propsRecordType =
836+
makePropsRecordTypeSig fnName psig_loc
837+
(("key", [], keyType psig_loc) :: namedTypeList)
838+
in
839+
let propsFnSig =
840+
makePropsFnSig fnName psig_loc
783841
in
784842
(* can't be an arrow because it will defensively uncurry *)
785843
let newExternalType =
@@ -797,7 +855,7 @@ let jsxMapper () =
797855
};
798856
}
799857
in
800-
externalPropsDecl :: newStructure :: returnSignatures
858+
propsRecordType :: propsFnSig :: newStructure :: returnSignatures
801859
| _ -> raise (Invalid_argument "Only one react.component call can exist on a component at one time") )
802860
| signature -> signature :: returnSignatures
803861
[@@raises Invalid_argument]
@@ -824,7 +882,7 @@ let jsxMapper () =
824882
ReactDOMRe.createElement(~props=ReactDOMRe.props(~props1=foo, ~props2=bar, ()), [|bla|]) *)
825883
| { loc; txt = Lident id } -> (
826884
match !jsxVersion with
827-
| None | Some 3 -> transformLowercaseCall3 mapper loc attrs callArguments id
885+
| None | Some 3 -> transformLowercaseCall3 mapper loc attrs callExpression callArguments id
828886
| Some _ -> raise (Invalid_argument "JSX: the JSX version must be 3") )
829887
| { txt = Ldot (_, anythingNotCreateElementOrMake) } ->
830888
raise

cli/reactjs_jsx_ppx_v3.mli

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@
2222
transform `[@JSX] [foo]` into
2323
`ReactDOMRe.createElement(ReasonReact.fragment, [|foo|])`
2424
v3:
25-
transform `[@JSX] div(~props1=a, ~props2=b, ~children=[foo, bar], ())` into
26-
`ReactDOMRe.createDOMElementVariadic("div", ReactDOMRe.domProps(~props1=1, ~props2=b), [|foo, bar|])`.
25+
transform `[@JSX] div(~props1=a, ~props2=b, ~spreadProps=props3 ~children=[foo, bar], ())` into
26+
`ReactDOMRe.createDOMElementVariadic("div", ~props=({...props3, props1: a, props2: b}), [|foo, bar|])`.
2727
transform the upper-cased case
28-
`[@JSX] Foo.createElement(~key=a, ~ref=b, ~foo=bar, ~children=[], ())` into
29-
`React.createElement(Foo.make, Foo.makeProps(~key=a, ~ref=b, ~foo=bar, ()))`
28+
`[@JSX] Foo.createElement(~key=a, ~ref=b, ~foo=bar, ~spreadProps=baz ~children=[], ())` into
29+
`React.createElement(Foo.make, Foo.makeProps({...baz, key: a, ref: b, foo: bar}))`
3030
transform the upper-cased case
31-
`[@JSX] Foo.createElement(~foo=bar, ~children=[foo, bar], ())` into
32-
`React.createElementVariadic(Foo.make, Foo.makeProps(~foo=bar, ~children=React.null, ()), [|foo, bar|])`
31+
`[@JSX] Foo.createElement(~foo=bar, ~spreadProps=baz, ~children=[foo, bar], ())` into
32+
`React.createElementVariadic(Foo.make, Foo.makeProps({...baz, foo: bar, children: React.null}), [|foo, bar|])`
3333
transform `[@JSX] [foo]` into
3434
`ReactDOMRe.createElement(ReasonReact.fragment, [|foo|])`
3535
*)

0 commit comments

Comments
 (0)