Skip to content

Commit f83f216

Browse files
authored
Remove raise annotations and fix locations on errors (#863)
* Enable ref in ppx * Add jest test for ref * Add test for error on key * Add locations into key * Change message on key * Fix I#843 which improves error message on react.component wrongly placed * Apply same message on similar fn, update snapshot * Add test for record-props and record-props-error * Use caller to genreate uppercase * Fix callee being always make * Remove Invalid_arguments * Add broken make_fn test and keep logic as before * Printinf on snapshot and errors has breakline * Remove test that comes with 19
1 parent f95c8c6 commit f83f216

File tree

13 files changed

+231
-57
lines changed

13 files changed

+231
-57
lines changed

ppx/reason_react_ppx.ml

Lines changed: 36 additions & 57 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 *)
@@ -518,7 +506,6 @@ let makeExternalDecl fnName loc namedArgListWithKeyAndRef namedTypeList =
518506
makePropsExternal ~component_is_external:false fnName loc
519507
(List.map pluckLabelDefaultLocType namedArgListWithKeyAndRef)
520508
(makePropsType ~loc namedTypeList)
521-
[@@raises Invalid_argument]
522509

523510
(* TODO: some line number might still be wrong *)
524511
let jsxMapper =
@@ -529,7 +516,7 @@ let jsxMapper =
529516
let argsForMake = argsWithLabels in
530517
let keyProps, otherProps =
531518
List.partition
532-
(fun (arg_label, _) -> "key" = getLabel arg_label)
519+
(fun (arg_label, _) -> "key" = getLabelOrEmpty arg_label)
533520
argsForMake
534521
in
535522
let jsxExpr, key, childrenProp =
@@ -543,10 +530,12 @@ let jsxMapper =
543530
(label, mapper#expression ctxt expression))
544531
in
545532
let isCap str =
546-
let first = String.sub str 0 1 [@@raises Invalid_argument] in
547-
let capped = String.uppercase_ascii first in
548-
first = capped
549-
[@@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
550539
in
551540
let ident =
552541
match modulePath with
@@ -608,7 +597,7 @@ let jsxMapper =
608597
let componentNameExpr = constantString ~loc:callerLoc id in
609598
let keyProps, nonChildrenProps =
610599
List.partition
611-
(fun (arg_label, _) -> "key" = getLabel arg_label)
600+
(fun (arg_label, _) -> "key" = getLabelOrEmpty arg_label)
612601
nonChildrenProps
613602
in
614603

@@ -657,17 +646,9 @@ let jsxMapper =
657646
let rec recursivelyTransformNamedArgsForMake ~ctxt mapper expr list =
658647
let expr = mapper#expression ctxt expr in
659648
match expr.pexp_desc with
660-
(* TODO: make this show up with a loc. *)
661649
| Pexp_fun (Labelled "key", _, _, _) | Pexp_fun (Optional "key", _, _, _) ->
662-
raise
663-
(Invalid_argument
664-
"Key cannot be accessed inside of a component. Don't worry - you \
665-
can always key a component from its parent!")
666-
| Pexp_fun (Labelled "ref", _, _, _) | Pexp_fun (Optional "ref", _, _, _) ->
667-
raise
668-
(Invalid_argument
669-
"Ref cannot be passed as a normal prop. Please use `forwardRef` \
670-
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.")
671652
| Pexp_fun
672653
( ((Optional label | Labelled label) as arg),
673654
default,
@@ -714,7 +695,6 @@ let jsxMapper =
714695
"reason-react-ppx: react.component refs only support plain arguments \
715696
and type annotations."
716697
| _ -> (list, None)
717-
[@@raises Invalid_argument]
718698
in
719699

720700
let argToType types (name, default, _noLabelName, _alias, loc, type_) =
@@ -736,7 +716,7 @@ let jsxMapper =
736716
} )
737717
:: types
738718
| Some type_, name, Some _default ->
739-
( getLabel name,
719+
( getLabelOrEmpty name,
740720
[],
741721
{
742722
ptyp_desc = Ptyp_constr ({ loc; txt = optionIdent }, [ type_ ]);
@@ -745,7 +725,7 @@ let jsxMapper =
745725
ptyp_attributes = [];
746726
} )
747727
:: types
748-
| Some type_, name, _ -> (getLabel name, [], type_) :: types
728+
| Some type_, name, _ -> (getLabelOrEmpty name, [], type_) :: types
749729
| None, Optional label, _ ->
750730
( label,
751731
[],
@@ -777,7 +757,6 @@ let jsxMapper =
777757
} )
778758
:: types
779759
| _ -> types
780-
[@@raises Invalid_argument]
781760
in
782761

783762
let argToConcreteType types (name, loc, type_) =
@@ -1110,7 +1089,7 @@ let jsxMapper =
11101089
in
11111090
let pluckArg (label, _, _, alias, loc, _) =
11121091
( label,
1113-
match getLabel label with
1092+
match getLabelOrEmpty label with
11141093
| "" -> Builder.pexp_ident ~loc { txt = Lident alias; loc }
11151094
| labelString ->
11161095
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=?) => {

ppx/test/component.t/run.t

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,27 @@ We need to output ML syntax here, otherwise refmt could not parse it.
6868
make ~buttonRef:(Props ## buttonRef) ~children:(Props ## children) in
6969
Output$Forward_Ref)
7070
end
71+
module Ref_as_prop =
72+
struct
73+
external makeProps :
74+
children:'children ->
75+
ref:'ref ->
76+
?key:string -> unit -> < children: 'children ;ref: 'ref > Js.t
77+
= ""[@@mel.obj ]
78+
let make =
79+
((fun ~children ->
80+
((fun ~ref ->
81+
ReactDOM.jsx "button"
82+
(((ReactDOM.domProps)[@merlin.hide ]) ~children ~ref
83+
~className:"FancyButton" ()))
84+
[@warning "-16"]))
85+
[@warning "-16"])
86+
let make =
87+
let Output$Ref_as_prop
88+
(Props : < children: 'children ;ref: 'ref > Js.t) =
89+
make ~ref:(Props ## ref) ~children:(Props ## children) in
90+
Output$Ref_as_prop
91+
end
7192
module Onclick_handler_button =
7293
struct
7394
external makeProps :
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[@react.component]
2+
let (pageState, setPageState) = React.useState(_ => 0);
3+
let make = (~children, ()) => <div> children </div>;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Test some locations in reason-react components
2+
3+
$ cat >dune-project <<EOF
4+
> (lang dune 3.8)
5+
> (using melange 0.1)
6+
> EOF
7+
8+
$ cat >dune <<EOF
9+
> (melange.emit
10+
> (alias foo)
11+
> (target foo)
12+
> (libraries reason-react)
13+
> (preprocess
14+
> (pps melange.ppx reason-react-ppx)))
15+
> EOF
16+
17+
$ dune build
18+
File "component.re", lines 1-2, characters 0-54:
19+
1 | [@react.component]
20+
2 | let (pageState, setPageState) = React.useState(_ => 0).
21+
Error: [@react.component] cannot be used with a destructured binding. Please use it on a `let make = ...` binding instead.
22+
[1]

0 commit comments

Comments
 (0)