Skip to content

Commit 127c693

Browse files
authored
Merge pull request #2969 from chenglou/refmt
Upstream refmt and jsx ppx 193cec5eb39b23009fe2697abbc81e943039b833
2 parents b9bd13d + ffd0a87 commit 127c693

File tree

4 files changed

+476
-395
lines changed

4 files changed

+476
-395
lines changed

jscomp/bin/jsoo_reactjs_jsx_ppx_v2.ml

+153-125
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@
1212
(*
1313
The actual transform:
1414
15-
transform `div props1::a props2::b children::[foo, bar] () [@JSX]` into
16-
`ReactDOMRe.createElement "div" props::[%bs.obj {props1: 1, props2: b}] [|foo,
17-
bar|]`.
15+
transform `[@JSX] div(~props1=a, ~props2=b, ~children=[foo, bar], ())` into
16+
`ReactDOMRe.createElement("div", ~props={"props1": 1, "props2": b}, [|foo,
17+
bar|])`.
18+
19+
transform `[@JSX] div(~props1=a, ~props2=b, ~children=foo, ())` into
20+
`ReactDOMRe.createElementVariadic("div", ~props={"props1": 1, "props2": b}, foo)`.
1821
1922
transform the upper-cased case
20-
`Foo.createElement key::a ref::b foo::bar children::[] () [@JSX]` into
21-
`ReasonReact.element key::a ref::b (Foo.make foo::bar [||] [@JSX])`
23+
`[@JSX] Foo.createElement(~key=a, ~ref=b, ~foo=bar, ~children=[], ())` into
24+
`ReasonReact.element(~key=a, ~ref=b, Foo.make(~foo=bar, [||]))`
25+
26+
transform `[@JSX] [foo]` into
27+
`ReactDOMRe.createElement(ReasonReact.fragment, [|foo|])`
2228
*)
2329

2430
(*
@@ -51,8 +57,8 @@ open Parsetree
5157
open Longident
5258

5359
(* if children is a list, convert it to an array while mapping each element. If not, just map over it, as usual *)
54-
let transformChildren ~loc ~mapper theList =
55-
let rec transformChildren' theList accum =
60+
let transformChildrenIfList ~loc ~mapper theList =
61+
let rec transformChildren_ theList accum =
5662
(* not in the sense of converting a list to an array; convert the AST
5763
reprensentation of a list to the AST reprensentation of an array *)
5864
match theList with
@@ -62,12 +68,12 @@ let transformChildren ~loc ~mapper theList =
6268
{txt = Lident "::"},
6369
Some {pexp_desc = Pexp_tuple (v::acc::[])}
6470
)} ->
65-
transformChildren' acc ((mapper.expr mapper v)::accum)
71+
transformChildren_ acc ((mapper.expr mapper v)::accum)
6672
| notAList -> mapper.expr mapper notAList
6773
in
68-
transformChildren' theList []
74+
transformChildren_ theList []
6975

70-
let extractChildrenForDOMElements ?(removeLastPositionUnit=false) ~loc propsAndChildren =
76+
let extractChildren ?(removeLastPositionUnit=false) ~loc propsAndChildren =
7177
let rec allButLast_ lst acc = match lst with
7278
| [] -> []
7379

@@ -78,56 +84,25 @@ let extractChildrenForDOMElements ?(removeLastPositionUnit=false) ~loc propsAndC
7884
in
7985
let allButLast lst = allButLast_ lst [] |> List.rev in
8086
match (List.partition (fun (label, expr) -> label = labelled "children") propsAndChildren) with
81-
| ((label, childrenExpr)::[], props) ->
82-
(childrenExpr, if removeLastPositionUnit then allButLast props else props)
8387
| ([], props) ->
84-
(* no children provided? Place a placeholder list (don't forgot we're talking about DOM element conversion here only) *)
88+
(* no children provided? Place a placeholder list *)
8589
(Exp.construct ~loc {loc; txt = Lident "[]"} None, if removeLastPositionUnit then allButLast props else props)
90+
| ([(label, childrenExpr)], props) ->
91+
(childrenExpr, if removeLastPositionUnit then allButLast props else props)
8692
| (moreThanOneChild, props) -> raise (Invalid_argument "JSX: somehow there's more than one `children` label")
8793

8894
(* TODO: some line number might still be wrong *)
8995
let jsxMapper () =
9096

91-
let jsxTransformV3 modulePath mapper loc attrs callExpression callArguments =
92-
let (children, argsWithLabels) =
93-
extractChildrenForDOMElements ~loc ~removeLastPositionUnit:true callArguments in
94-
let (argsKeyRef, argsForMake) = List.partition argIsKeyRef argsWithLabels in
95-
let childrenExpr = match children with
96-
(* if it's a single, non-jsx item, keep it so (remove the list wrapper, don't add the array wrapper) *)
97-
| {pexp_desc = Pexp_construct (
98-
{txt = Lident "::"; loc},
99-
Some {pexp_desc = Pexp_tuple [
100-
({pexp_attributes} as singleItem);
101-
{pexp_desc = Pexp_construct ({txt = Lident "[]"}, None)}
102-
]}
103-
)} when List.for_all (fun (attribute, _) -> attribute.txt <> "JSX") pexp_attributes ->
104-
mapper.expr mapper singleItem
105-
(* if it's a single jsx item, or multiple items, turn list into an array *)
106-
| nonEmptyChildren -> transformChildren ~loc ~mapper nonEmptyChildren
107-
in
108-
let recursivelyTransformedArgsForMake = argsForMake |> List.map (fun (label, expression) -> (label, mapper.expr mapper expression)) in
109-
let args = recursivelyTransformedArgsForMake @ [ (nolabel, childrenExpr) ] in
110-
let wrapWithReasonReactElement e = (* ReasonReact.element ::key ::ref (...) *)
111-
Exp.apply
112-
~loc
113-
(Exp.ident ~loc {loc; txt = Ldot (Lident "ReasonReact", "element")})
114-
(argsKeyRef @ [(nolabel, e)]) in
115-
Exp.apply
116-
~loc
117-
~attrs
118-
(* Foo.make *)
119-
(Exp.ident ~loc {loc; txt = Ldot (modulePath, "make")})
120-
args
121-
|> wrapWithReasonReactElement in
97+
let jsxVersion = ref None in
12298

123-
let jsxTransformV2 modulePath mapper loc attrs callExpression callArguments =
124-
let (children, argsWithLabels) =
125-
extractChildrenForDOMElements ~loc ~removeLastPositionUnit:true callArguments in
99+
let transformUppercaseCall modulePath mapper loc attrs callExpression callArguments =
100+
let (children, argsWithLabels) = extractChildren ~loc ~removeLastPositionUnit:true callArguments in
126101
let (argsKeyRef, argsForMake) = List.partition argIsKeyRef argsWithLabels in
127-
let childrenExpr = transformChildren ~loc ~mapper children in
102+
let childrenExpr = transformChildrenIfList ~loc ~mapper children in
128103
let recursivelyTransformedArgsForMake = argsForMake |> List.map (fun (label, expression) -> (label, mapper.expr mapper expression)) in
129104
let args = recursivelyTransformedArgsForMake @ [ (nolabel, childrenExpr) ] in
130-
let wrapWithReasonReactElement e = (* ReasonReact.element ::key ::ref (...) *)
105+
let wrapWithReasonReactElement e = (* ReasonReact.element(~key, ~ref, ...) *)
131106
Exp.apply
132107
~loc
133108
(Exp.ident ~loc {loc; txt = Ldot (Lident "ReasonReact", "element")})
@@ -140,13 +115,36 @@ let jsxMapper () =
140115
args
141116
|> wrapWithReasonReactElement in
142117

143-
let lowercaseCaller mapper loc attrs callArguments id =
144-
let (children, propsWithLabels) =
145-
extractChildrenForDOMElements ~loc callArguments in
118+
let transformLowercaseCall mapper loc attrs callArguments id =
119+
let (children, nonChildrenProps) = extractChildren ~loc callArguments in
146120
let componentNameExpr = constantString ~loc id in
147-
let childrenExpr = transformChildren ~loc ~mapper children in
148-
let args = match propsWithLabels with
149-
| [theUnitArgumentAtEnd] ->
121+
let childrenExpr = transformChildrenIfList ~loc ~mapper children in
122+
let createElementCall = match children with
123+
(* [@JSX] div(~children=[a]), coming from <div> a </div> *)
124+
| {
125+
pexp_desc =
126+
Pexp_construct ({txt = Lident "::"; loc}, Some {pexp_desc = Pexp_tuple _ })
127+
| Pexp_construct ({txt = Lident "[]"; loc}, None);
128+
pexp_attributes
129+
} -> "createElement"
130+
(* [@JSX] div(~children=[|a|]), coming from <div> ...[|a|] </div> *)
131+
| {
132+
pexp_desc = (Pexp_array _);
133+
pexp_attributes
134+
} ->
135+
raise (Invalid_argument "A spread + an array literal as a DOM element's \
136+
children would cancel each other out, and thus don't make sense written \
137+
together. You can simply remove the spread and the array literal.")
138+
(* [@JSX] div(~children= <div />), coming from <div> ...<div/> </div> *)
139+
| {
140+
pexp_attributes
141+
} when pexp_attributes |> List.exists (fun (attribute, _) -> attribute.txt = "JSX") ->
142+
raise (Invalid_argument "A spread + a JSX literal as a DOM element's \
143+
children don't make sense written together. You can simply remove the spread.")
144+
| notAList -> "createElementVariadic"
145+
in
146+
let args = match nonChildrenProps with
147+
| [_justTheUnitArgumentAtEnd] ->
150148
[
151149
(* "div" *)
152150
(nolabel, componentNameExpr);
@@ -163,7 +161,7 @@ let jsxMapper () =
163161
[
164162
(* "div" *)
165163
(nolabel, componentNameExpr);
166-
(* ReactDOMRe.props className:blabla foo::bar () *)
164+
(* ReactDOMRe.props(~className=blabla, ~foo=bar, ()) *)
167165
(labelled "props", propsCall);
168166
(* [|moreCreateElementCallsHere|] *)
169167
(nolabel, childrenExpr)
@@ -172,82 +170,31 @@ let jsxMapper () =
172170
~loc
173171
(* throw away the [@JSX] attribute and keep the others, if any *)
174172
~attrs
175-
(* ReactDOMRe.createDOMElement *)
176-
(Exp.ident ~loc {loc; txt = Ldot (Lident "ReactDOMRe", "createElement")})
177-
args in
178-
179-
let jsxVersion = ref None in
180-
181-
let structure =
182-
(fun mapper structure -> match structure with
183-
(*
184-
match against [@@@bs.config {foo, jsx: ...}] at the file-level. This
185-
indicates which version of JSX we're using. This code stays here because
186-
we used to have 2 versions of JSX PPX (and likely will again in the
187-
future when JSX PPX changes). So the architecture for switching between
188-
JSX behavior stayed here. To create a new JSX ppx, copy paste this
189-
entire file and change the relevant parts.
190-
191-
Description of architecture: in bucklescript's bsconfig.json, you can
192-
specify a project-wide JSX version. You can also specify a file-level
193-
JSX version. This degree of freedom allows a person to convert a project
194-
one file at time onto the new JSX, when it was released. It also enabled
195-
a project to depend on a third-party which is still using an old version
196-
of JSX
197-
*)
198-
| {
199-
pstr_loc;
200-
pstr_desc = Pstr_attribute (
201-
({txt = "bs.config"} as bsConfigLabel),
202-
PStr [{pstr_desc = Pstr_eval ({pexp_desc = Pexp_record (recordFields, b)} as innerConfigRecord, a)} as configRecord]
203-
)
204-
}::restOfStructure -> begin
205-
let (jsxField, recordFieldsWithoutJsx) = recordFields |> List.partition (fun ({txt}, _) -> txt = Lident "jsx") in
206-
match (jsxField, recordFieldsWithoutJsx) with
207-
(* no file-level jsx config found *)
208-
| ([], _) -> default_mapper.structure mapper structure
209-
(* {jsx: 2 | 3} *)
210-
211-
| ((_, {pexp_desc = Pexp_constant (Const_int version)})::rest, recordFieldsWithoutJsx) -> begin
212-
(match version with
213-
| 2 -> jsxVersion := Some 2
214-
| 3 -> jsxVersion := Some 3
215-
| _ -> raise (Invalid_argument "JSX: the file-level bs.config's jsx version must be either 2 or 3"));
216-
217-
match recordFieldsWithoutJsx with
218-
(* record empty now, remove the whole bs.config attribute *)
219-
| [] -> default_mapper.structure mapper restOfStructure
220-
| fields -> default_mapper.structure mapper ({
221-
pstr_loc;
222-
pstr_desc = Pstr_attribute (
223-
bsConfigLabel,
224-
PStr [{configRecord with pstr_desc = Pstr_eval ({innerConfigRecord with pexp_desc = Pexp_record (fields, b)}, a)}]
225-
)
226-
}::restOfStructure)
227-
end
228-
| (_, recordFieldsWithoutJsx) -> raise (Invalid_argument "JSX: the file-level bs.config's {jsx: ...} config accepts only a version number")
229-
end
230-
| _ -> default_mapper.structure mapper structure
231-
) in
173+
(* ReactDOMRe.createElement *)
174+
(Exp.ident ~loc {loc; txt = Ldot (Lident "ReactDOMRe", createElementCall)})
175+
args
176+
in
232177

233178
let transformJsxCall mapper callExpression callArguments attrs =
234179
(match callExpression.pexp_desc with
235180
| Pexp_ident caller ->
236181
(match caller with
237182
| {txt = Lident "createElement"} ->
238183
raise (Invalid_argument "JSX: `createElement` should be preceeded by a module name.")
239-
(* Foo.createElement prop1::foo prop2:bar children::[] () *)
184+
185+
(* Foo.createElement(~prop1=foo, ~prop2=bar, ~children=[], ()) *)
240186
| {loc; txt = Ldot (modulePath, ("createElement" | "make"))} ->
241-
let f = match !jsxVersion with
242-
| Some 2 -> jsxTransformV2 modulePath
243-
| Some 3 -> jsxTransformV3 modulePath
244-
| Some _ -> raise (Invalid_argument "JSX: the JSX version must be either 2 or 3")
245-
| None -> jsxTransformV2 modulePath
246-
in f mapper loc attrs callExpression callArguments
247-
(* div prop1::foo prop2:bar children::[bla] () *)
248-
(* turn that into ReactDOMRe.createElement props::(ReactDOMRe.props props1::foo props2::bar ()) [|bla|] *)
187+
(match !jsxVersion with
188+
| None
189+
| Some 2 -> transformUppercaseCall modulePath mapper loc attrs callExpression callArguments
190+
| Some _ -> raise (Invalid_argument "JSX: the JSX version must be 2"))
191+
192+
(* div(~prop1=foo, ~prop2=bar, ~children=[bla], ()) *)
193+
(* turn that into
194+
ReactDOMRe.createElement(~props=ReactDOMRe.props(~props1=foo, ~props2=bar, ()), [|bla|]) *)
249195
| {loc; txt = Lident id} ->
250-
lowercaseCaller mapper loc attrs callArguments id
196+
transformLowercaseCall mapper loc attrs callArguments id
197+
251198
| {txt = Ldot (_, anythingNotCreateElementOrMake)} ->
252199
raise (
253200
Invalid_argument
@@ -256,6 +203,7 @@ let jsxMapper () =
256203
^ "` instead"
257204
)
258205
)
206+
259207
| {txt = Lapply _} ->
260208
(* don't think there's ever a case where this is reached *)
261209
raise (
@@ -268,11 +216,61 @@ let jsxMapper () =
268216
)
269217
) in
270218

219+
let structure =
220+
(fun mapper structure -> match structure with
221+
(*
222+
match against [@bs.config {foo, jsx: ...}] at the file-level. This
223+
indicates which version of JSX we're using. This code stays here because
224+
we used to have 2 versions of JSX PPX (and likely will again in the
225+
future when JSX PPX changes). So the architecture for switching between
226+
JSX behavior stayed here. To create a new JSX ppx, copy paste this
227+
entire file and change the relevant parts.
228+
229+
Description of architecture: in bucklescript's bsconfig.json, you can
230+
specify a project-wide JSX version. You can also specify a file-level
231+
JSX version. This degree of freedom allows a person to convert a project
232+
one file at time onto the new JSX, when it was released. It also enabled
233+
a project to depend on a third-party which is still using an old version
234+
of JSX
235+
*)
236+
| {
237+
pstr_loc;
238+
pstr_desc = Pstr_attribute (
239+
({txt = "bs.config"} as bsConfigLabel),
240+
PStr [{pstr_desc = Pstr_eval ({pexp_desc = Pexp_record (recordFields, b)} as innerConfigRecord, a)} as configRecord]
241+
)
242+
}::restOfStructure -> begin
243+
let (jsxField, recordFieldsWithoutJsx) = recordFields |> List.partition (fun ({txt}, _) -> txt = Lident "jsx") in
244+
match (jsxField, recordFieldsWithoutJsx) with
245+
(* no file-level jsx config found *)
246+
| ([], _) -> default_mapper.structure mapper structure
247+
(* {jsx: 2} *)
248+
249+
| ((_, {pexp_desc = Pexp_constant (Const_int version)})::rest, recordFieldsWithoutJsx) -> begin
250+
(match version with
251+
| 2 -> jsxVersion := Some 2
252+
| _ -> raise (Invalid_argument "JSX: the file-level bs.config's jsx version must be 2"));
253+
254+
match recordFieldsWithoutJsx with
255+
(* record empty now, remove the whole bs.config attribute *)
256+
| [] -> default_mapper.structure mapper restOfStructure
257+
| fields -> default_mapper.structure mapper ({
258+
pstr_loc;
259+
pstr_desc = Pstr_attribute (
260+
bsConfigLabel,
261+
PStr [{configRecord with pstr_desc = Pstr_eval ({innerConfigRecord with pexp_desc = Pexp_record (fields, b)}, a)}]
262+
)
263+
}::restOfStructure)
264+
end
265+
| (_, recordFieldsWithoutJsx) -> raise (Invalid_argument "JSX: the file-level bs.config's {jsx: ...} config accepts only a version number")
266+
end
267+
| _ -> default_mapper.structure mapper structure
268+
) in
269+
271270
let expr =
272271
(fun mapper expression -> match expression with
273272
(* Does the function application have the @JSX attribute? *)
274-
|
275-
{
273+
| {
276274
pexp_desc = Pexp_apply (callExpression, callArguments);
277275
pexp_attributes
278276
} ->
@@ -281,6 +279,35 @@ let jsxMapper () =
281279
(* no JSX attribute *)
282280
| ([], _) -> default_mapper.expr mapper expression
283281
| (_, nonJSXAttributes) -> transformJsxCall mapper callExpression callArguments nonJSXAttributes)
282+
283+
(* is it a list with jsx attribute? Reason <>foo</> desugars to [@JSX][foo]*)
284+
| {
285+
pexp_desc =
286+
Pexp_construct ({txt = Lident "::"; loc}, Some {pexp_desc = Pexp_tuple _ })
287+
| Pexp_construct ({txt = Lident "[]"; loc}, None);
288+
pexp_attributes
289+
} as listItems ->
290+
let (jsxAttribute, nonJSXAttributes) = List.partition (fun (attribute, _) -> attribute.txt = "JSX") pexp_attributes in
291+
(match (jsxAttribute, nonJSXAttributes) with
292+
(* no JSX attribute *)
293+
| ([], _) -> default_mapper.expr mapper expression
294+
| (_, nonJSXAttributes) ->
295+
let fragment = Exp.ident ~loc {loc; txt = Ldot (Lident "ReasonReact", "fragment")} in
296+
let childrenExpr = transformChildrenIfList ~loc ~mapper listItems in
297+
let args = [
298+
(* "div" *)
299+
(nolabel, fragment);
300+
(* [|moreCreateElementCallsHere|] *)
301+
(nolabel, childrenExpr)
302+
] in
303+
Exp.apply
304+
~loc
305+
(* throw away the [@JSX] attribute and keep the others, if any *)
306+
~attrs:nonJSXAttributes
307+
(* ReactDOMRe.createElement *)
308+
(Exp.ident ~loc {loc; txt = Ldot (Lident "ReactDOMRe", "createElement")})
309+
args
310+
)
284311
(* Delegate to the default mapper, a deep identity traversal *)
285312
| e -> default_mapper.expr mapper e) in
286313

@@ -357,3 +384,4 @@ let make_ppx name =
357384

358385
let () = make_ppx "jsxv2"
359386

387+

0 commit comments

Comments
 (0)