Skip to content

Commit fba122f

Browse files
authored
JSX component error message improvements (#7038)
* improve component prop not existing error * improve JSX component missing props error message * improve component prop passed multiple times error * add attribute in interface files too * make function * add tests * changelog * tweak error message * fix location and message of JSX component prop passed multiple times * sync syntax roundtrip tests * try pointing to label instead of expr for error highlight
1 parent a7a1f25 commit fba122f

37 files changed

+261
-57
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
- Improve error messages for pattern matching on option vs non-option, and vice versa. https://github.com/rescript-lang/rescript-compiler/pull/7035
2626
- Improve bigint literal comparison. https://github.com/rescript-lang/rescript-compiler/pull/7029
2727
- Improve output of `@variadic` bindings. https://github.com/rescript-lang/rescript-compiler/pull/7030
28+
- Improve error messages around JSX components. https://github.com/rescript-lang/rescript-compiler/pull/7038
2829

2930
# 12.0.0-alpha.3
3031

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/component_invalid_prop.res:7:5-8
4+
5+
5 │
6+
6 │ let make = (): props<'name> => {
7+
7 │ test: false,
8+
8 │ }
9+
9 │ }
10+
11+
The prop test does not belong to the JSX component
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11

22
We've found a bug for you!
3-
/.../fixtures/component_missing_prop.res:5:34-35
3+
/.../fixtures/component_missing_prop.res:6:34-35
44

5-
3 │ type props<'name> = {name: 'name}
6-
4 │
7-
5 │ let make = (): props<'name> => {}
8-
6 │ }
9-
7 │
5+
4 │ type props<'name> = {name: 'name}
6+
5 │
7+
6 │ let make = (): props<'name> => {}
8+
7 │ }
9+
8 │
1010

11-
Some required record fields are missing:
12-
name. If this is a component, add the missing props.
11+
The component is missing these required props:
12+
name
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/component_prop_passed_multiple_times.res:8:5-8
4+
5+
6 │ let make = (): props<'name> => {
6+
7 │ name: "hello",
7+
8 │ name: "world",
8+
9 │ }
9+
10 │ }
10+
11+
The prop name has already been passed to the component
12+
You can't pass the same prop more than once.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Since the React transform isn't active in the tests, mimic what the transform outputs.
2+
module Component = {
3+
@res.jsxComponentProps
4+
type props<'name> = {name: 'name}
5+
6+
let make = (): props<'name> => {
7+
test: false,
8+
}
9+
}

jscomp/build_tests/super_errors/fixtures/component_missing_prop.res

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Since the React transform isn't active in the tests, mimic what the transform outputs.
22
module Component = {
3+
@res.jsxComponentProps
34
type props<'name> = {name: 'name}
45

56
let make = (): props<'name> => {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Since the React transform isn't active in the tests, mimic what the transform outputs.
2+
module Component = {
3+
@res.jsxComponentProps
4+
type props<'name> = {name: 'name}
5+
6+
let make = (): props<'name> => {
7+
name: "hello",
8+
name: "world",
9+
}
10+
}

jscomp/ml/error_message_utils.ml

+60-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
type extract_concrete_typedecl =
2+
Env.t -> Types.type_expr -> Path.t * Path.t * Types.type_declaration
3+
14
type type_clash_statement = FunctionCall
25
type type_clash_context =
36
| SetRecordField
@@ -236,4 +239,60 @@ let print_contextual_unification_error ppf t1 t2 =
236239
@[<v 0>The value you're pattern matching on here is wrapped in an \
237240
@{<info>option@}, but you're trying to match on the actual value.@ Wrap \
238241
the highlighted pattern in @{<info>Some()@} to make it work.@]"
239-
| _ -> ()
242+
| _ -> ()
243+
244+
type jsx_prop_error_info = {
245+
fields: Types.label_declaration list;
246+
props_record_path: Path.t;
247+
}
248+
249+
let attributes_include_jsx_component_props (attrs : Parsetree.attributes) =
250+
attrs
251+
|> List.exists (fun ({Location.txt}, _) -> txt = "res.jsxComponentProps")
252+
253+
let path_to_jsx_component_name p =
254+
match p |> Path.name |> String.split_on_char '.' |> List.rev with
255+
| "props" :: component_name :: _ -> Some component_name
256+
| _ -> None
257+
258+
let get_jsx_component_props
259+
~(extract_concrete_typedecl : extract_concrete_typedecl) env ty p =
260+
match Path.last p with
261+
| "props" -> (
262+
try
263+
match extract_concrete_typedecl env ty with
264+
| ( _p0,
265+
_p,
266+
{Types.type_kind = Type_record (fields, _repr); type_attributes} )
267+
when attributes_include_jsx_component_props type_attributes ->
268+
Some {props_record_path = p; fields}
269+
| _ -> None
270+
with _ -> None)
271+
| _ -> None
272+
273+
let print_component_name ppf (p : Path.t) =
274+
match path_to_jsx_component_name p with
275+
| Some component_name -> fprintf ppf "@{<info><%s />@} " component_name
276+
| None -> ()
277+
278+
let print_component_wrong_prop_error ppf (p : Path.t)
279+
(_fields : Types.label_declaration list) name =
280+
fprintf ppf "@[<v>";
281+
fprintf ppf
282+
"@[<2>The prop @{<error>%s@} does not belong to the JSX component " name;
283+
print_component_name ppf p;
284+
fprintf ppf "@]@,@,"
285+
286+
let print_component_labels_missing_error ppf labels
287+
(error_info : jsx_prop_error_info) =
288+
fprintf ppf "@[<hov>The component ";
289+
print_component_name ppf error_info.props_record_path;
290+
fprintf ppf "is missing these required props:@\n";
291+
labels |> List.iter (fun lbl -> fprintf ppf "@ %s" lbl);
292+
fprintf ppf "@]"
293+
294+
let get_jsx_component_error_info ~extract_concrete_typedecl opath env ty_record () =
295+
match opath with
296+
| Some (p, _) ->
297+
get_jsx_component_props ~extract_concrete_typedecl env ty_record p
298+
| None -> None

jscomp/ml/typecore.ml

+47-27
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ type error =
3535
| Expr_type_clash of (type_expr * type_expr) list * (type_clash_context option)
3636
| Apply_non_function of type_expr
3737
| Apply_wrong_label of arg_label * type_expr
38-
| Label_multiply_defined of string
39-
| Labels_missing of string list * bool
38+
| Label_multiply_defined of {label: string; jsx_component_info: jsx_prop_error_info option}
39+
| Labels_missing of {labels: string list; jsx_component_info: jsx_prop_error_info option}
4040
| Label_not_mutable of Longident.t
4141
| Wrong_name of string * type_expr * string * Path.t * string * string list
4242
| Name_type_mismatch of
@@ -960,15 +960,18 @@ let type_label_a_list ?labels loc closed env type_lbl_a opath lid_a_list k =
960960
(* Checks over the labels mentioned in a record pattern:
961961
no duplicate definitions (error); properly closed (warning) *)
962962

963-
let check_recordpat_labels loc lbl_pat_list closed =
963+
let check_recordpat_labels ~get_jsx_component_error_info loc lbl_pat_list closed =
964964
match lbl_pat_list with
965965
| [] -> () (* should not happen *)
966-
| (_, label1, _) :: _ ->
966+
| ((l: Longident.t loc), label1, _) :: _ ->
967967
let all = label1.lbl_all in
968968
let defined = Array.make (Array.length all) false in
969969
let check_defined (_, label, _) =
970970
if defined.(label.lbl_pos)
971-
then raise(Error(loc, Env.empty, Label_multiply_defined label.lbl_name))
971+
then raise(Error(l.loc, Env.empty, Label_multiply_defined {
972+
label = label.lbl_name;
973+
jsx_component_info = get_jsx_component_error_info ();
974+
}))
972975
else defined.(label.lbl_pos) <- true in
973976
List.iter check_defined lbl_pat_list;
974977
if closed = Closed
@@ -1292,6 +1295,7 @@ and type_pat_aux ~constrs ~labels ~no_existentials ~mode ~explode ~env
12921295
Some (p0, p), expected_ty
12931296
with Not_found -> None, newvar ()
12941297
in
1298+
let get_jsx_component_error_info = get_jsx_component_error_info ~extract_concrete_typedecl opath !env record_ty in
12951299
let process_optional_label (ld, pat) =
12961300
let exp_optional_attr = check_optional_attr !env ld pat.ppat_attributes pat.ppat_loc in
12971301
let is_from_pamatch = match pat.ppat_desc with
@@ -1330,7 +1334,7 @@ and type_pat_aux ~constrs ~labels ~no_existentials ~mode ~explode ~env
13301334
k (label_lid, label, arg))
13311335
in
13321336
let k' k lbl_pat_list =
1333-
check_recordpat_labels loc lbl_pat_list closed;
1337+
check_recordpat_labels ~get_jsx_component_error_info loc lbl_pat_list closed;
13341338
unify_pat_types loc !env record_ty expected_ty;
13351339
rp k {
13361340
pat_desc = Tpat_record (lbl_pat_list, closed);
@@ -1897,11 +1901,14 @@ let duplicate_ident_types caselist env =
18971901
(* type_label_a_list returns a list of labels sorted by lbl_pos *)
18981902
(* note: check_duplicates would better be implemented in
18991903
type_label_a_list directly *)
1900-
let rec check_duplicates loc env = function
1901-
| (_, lbl1, _) :: (_, lbl2, _) :: _ when lbl1.lbl_pos = lbl2.lbl_pos ->
1902-
raise(Error(loc, env, Label_multiply_defined lbl1.lbl_name))
1904+
let rec check_duplicates ~get_jsx_component_error_info loc env = function
1905+
| (_, lbl1, _) :: ((l: Longident.t loc), lbl2, _) :: _ when lbl1.lbl_pos = lbl2.lbl_pos ->
1906+
raise(Error(l.loc, env, Label_multiply_defined {
1907+
label = lbl1.lbl_name;
1908+
jsx_component_info = get_jsx_component_error_info();
1909+
}))
19031910
| _ :: rem ->
1904-
check_duplicates loc env rem
1911+
check_duplicates ~get_jsx_component_error_info loc env rem
19051912
| [] -> ()
19061913
(* Getting proper location of already typed expressions.
19071914
@@ -1974,11 +1981,6 @@ let rec lower_args env seen ty_fun =
19741981
let not_function env ty =
19751982
let ls, tvar = list_labels env ty in
19761983
ls = [] && not tvar
1977-
1978-
let check_might_be_component env ty_record =
1979-
match (expand_head env ty_record).desc with
1980-
| Tconstr (path, _, _) when path |> Path.last = "props" -> true
1981-
| _ -> false
19821984

19831985
type lazy_args =
19841986
(Asttypes.arg_label * (unit -> Typedtree.expression) option) list
@@ -2279,6 +2281,10 @@ and type_expect_ ?type_clash_context ?in_function ?(recarg=Rejected) env sexp ty
22792281
| exception Not_found ->
22802282
newvar (), None, [], None
22812283

2284+
in
2285+
let get_jsx_component_error_info () = (match opath with
2286+
| Some (p, _) -> get_jsx_component_props ~extract_concrete_typedecl env ty_record p
2287+
| None -> None)
22822288
in
22832289
let lbl_exp_list =
22842290
wrap_disambiguate "This record expression is expected to have" ty_record
@@ -2288,7 +2294,7 @@ and type_expect_ ?type_clash_context ?in_function ?(recarg=Rejected) env sexp ty
22882294
(fun x -> x)
22892295
in
22902296
unify_exp_types loc env ty_record (instance env ty_expected);
2291-
check_duplicates loc env lbl_exp_list;
2297+
check_duplicates ~get_jsx_component_error_info loc env lbl_exp_list;
22922298
let label_descriptions, representation = match lbl_exp_list, repr_opt with
22932299
| (_, { lbl_all = label_descriptions; lbl_repres = representation}, _) :: _, _ -> label_descriptions, representation
22942300
| [], Some (representation) when lid_sexp_list = [] ->
@@ -2304,8 +2310,10 @@ and type_expect_ ?type_clash_context ?in_function ?(recarg=Rejected) env sexp ty
23042310
Some name in
23052311
let labels_missing = fields |> List.filter_map filter_missing in
23062312
if labels_missing <> [] then (
2307-
let might_be_component = check_might_be_component env ty_record in
2308-
raise(Error(loc, env, Labels_missing (labels_missing, might_be_component))));
2313+
raise(Error(loc, env, Labels_missing {
2314+
labels = labels_missing;
2315+
jsx_component_info = get_jsx_component_error_info ();
2316+
})));
23092317
[||], representation
23102318
| [], _ ->
23112319
if fields = [] && repr_opt <> None then
@@ -2330,8 +2338,10 @@ and type_expect_ ?type_clash_context ?in_function ?(recarg=Rejected) env sexp ty
23302338
label_descriptions
23312339
in
23322340
if !labels_missing <> [] then (
2333-
let might_be_component = check_might_be_component env ty_record in
2334-
raise(Error(loc, env, Labels_missing ((List.rev !labels_missing), might_be_component))));
2341+
raise(Error(loc, env, Labels_missing {
2342+
labels=(List.rev !labels_missing);
2343+
jsx_component_info = get_jsx_component_error_info ();
2344+
})));
23352345
let fields =
23362346
Array.map2 (fun descr def -> descr, def)
23372347
label_descriptions label_definitions
@@ -2372,6 +2382,7 @@ and type_expect_ ?type_clash_context ?in_function ?(recarg=Rejected) env sexp ty
23722382
end
23732383
| op -> ty_expected, op
23742384
in
2385+
let get_jsx_component_error_info = get_jsx_component_error_info ~extract_concrete_typedecl opath env ty_record in
23752386
let closed = false in
23762387
let lbl_exp_list =
23772388
wrap_disambiguate "This record expression is expected to have" ty_record
@@ -2381,7 +2392,7 @@ and type_expect_ ?type_clash_context ?in_function ?(recarg=Rejected) env sexp ty
23812392
(fun x -> x)
23822393
in
23832394
unify_exp_types loc env ty_record (instance env ty_expected);
2384-
check_duplicates loc env lbl_exp_list;
2395+
check_duplicates ~get_jsx_component_error_info loc env lbl_exp_list;
23852396
let opt_exp, label_definitions =
23862397
let (_lid, lbl, _lbl_exp) = List.hd lbl_exp_list in
23872398
let matching_label lbl =
@@ -3846,17 +3857,25 @@ let report_error env ppf = function
38463857
"@[<v>@[<2>The function applied to this argument has type@ %a@]@.\
38473858
This argument cannot be applied %a@]"
38483859
type_expr ty print_label l
3849-
| Label_multiply_defined s ->
3850-
fprintf ppf "The record field label %s is defined several times" s
3851-
| Labels_missing (labels, might_be_component) ->
3860+
| Label_multiply_defined {label; jsx_component_info = Some jsx_component_info} ->
3861+
fprintf ppf "The prop @{<info>%s@} has already been passed to the component " label;
3862+
print_component_name ppf jsx_component_info.props_record_path;
3863+
fprintf ppf "@,@,You can't pass the same prop more than once.";
3864+
| Label_multiply_defined {label} ->
3865+
fprintf ppf "The record field label %s is defined several times" label
3866+
| Labels_missing {labels; jsx_component_info = Some jsx_component_info} ->
3867+
print_component_labels_missing_error ppf labels jsx_component_info
3868+
| Labels_missing {labels} ->
38523869
let print_labels ppf =
38533870
List.iter (fun lbl -> fprintf ppf "@ %s" ( lbl)) in
3854-
let component_text = if might_be_component then " If this is a component, add the missing props." else "" in
3855-
fprintf ppf "@[<hov>Some required record fields are missing:%a.%s@]"
3856-
print_labels labels component_text
3871+
fprintf ppf "@[<hov>Some required record fields are missing:%a.@]"
3872+
print_labels labels
38573873
| Label_not_mutable lid ->
38583874
fprintf ppf "The record field %a is not mutable" longident lid
38593875
| Wrong_name (eorp, ty, kind, p, name, valid_names) ->
3876+
(match get_jsx_component_props ~extract_concrete_typedecl env ty p with
3877+
| Some {fields} -> print_component_wrong_prop_error ppf p fields name; spellcheck ppf name valid_names;
3878+
| None ->
38603879
(* modified *)
38613880
if Path.is_constructor_typath p then begin
38623881
fprintf ppf "@[The field %s is not part of the record \
@@ -3876,6 +3895,7 @@ let report_error env ppf = function
38763895
fprintf ppf "@]";
38773896
end;
38783897
spellcheck ppf name valid_names;
3898+
)
38793899
| Name_type_mismatch (kind, lid, tp, tpl) ->
38803900
let name = label_of_kind kind in
38813901
report_ambiguous_type_error ppf env tp tpl

jscomp/ml/typecore.mli

+2-2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ type error =
6969
| Expr_type_clash of (type_expr * type_expr) list * (Error_message_utils.type_clash_context option)
7070
| Apply_non_function of type_expr
7171
| Apply_wrong_label of arg_label * type_expr
72-
| Label_multiply_defined of string
73-
| Labels_missing of string list * bool
72+
| Label_multiply_defined of {label: string; jsx_component_info: Error_message_utils.jsx_prop_error_info option}
73+
| Labels_missing of {labels: string list; jsx_component_info: Error_message_utils.jsx_prop_error_info option}
7474
| Label_not_mutable of Longident.t
7575
| Wrong_name of string * type_expr * string * Path.t * string * string list
7676
| Name_type_mismatch of

jscomp/syntax/src/jsx_v4.ml

+12-3
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,8 @@ let make_label_decls named_type_list =
310310
| hd :: tl ->
311311
if mem_label hd tl then
312312
let _, label, _, loc, _ = hd in
313-
Jsx_common.raise_error ~loc "JSX: found the duplicated prop `%s`" label
313+
Jsx_common.raise_error ~loc
314+
"The prop `%s` is defined several times in this component." label
314315
else check_duplicated_label tl
315316
in
316317
let () = named_type_list |> List.rev |> check_duplicated_label in
@@ -347,11 +348,16 @@ let make_type_decls_with_core_type props_name loc core_type typ_vars =
347348
]
348349

349350
let live_attr = ({txt = "live"; loc = Location.none}, PStr [])
351+
let jsx_component_props_attr =
352+
({txt = "res.jsxComponentProps"; loc = Location.none}, PStr [])
350353

351354
(* type props<'x, 'y, ...> = { x: 'x, y?: 'y, ... } *)
352355
let make_props_record_type ~core_type_of_attr ~external_ ~typ_vars_of_core_type
353356
props_name loc named_type_list =
354-
let attrs = if external_ then [live_attr] else [] in
357+
let attrs =
358+
if external_ then [jsx_component_props_attr; live_attr]
359+
else [jsx_component_props_attr]
360+
in
355361
Str.type_ Nonrecursive
356362
(match core_type_of_attr with
357363
| None -> make_type_decls ~attrs props_name loc named_type_list
@@ -362,7 +368,10 @@ let make_props_record_type ~core_type_of_attr ~external_ ~typ_vars_of_core_type
362368
(* type props<'x, 'y, ...> = { x: 'x, y?: 'y, ... } *)
363369
let make_props_record_type_sig ~core_type_of_attr ~external_
364370
~typ_vars_of_core_type props_name loc named_type_list =
365-
let attrs = if external_ then [live_attr] else [] in
371+
let attrs =
372+
if external_ then [jsx_component_props_attr; live_attr]
373+
else [jsx_component_props_attr]
374+
in
366375
Sig.type_ Nonrecursive
367376
(match core_type_of_attr with
368377
| None -> make_type_decls ~attrs props_name loc named_type_list

0 commit comments

Comments
 (0)