Skip to content

Commit 2193869

Browse files
authored
Improve various error messages (#7500)
* improve array <-> tuple error message by looking at what the user actually wrote * refactor * example of detecting and providing rewrite for ReScript object -> record * remove Belt prefix * add dedicated JSX element type mismatch errors * note that you need to unwrap options in JSX * fix * add simple error message indicating dict syntax
1 parent e2e1e3c commit 2193869

27 files changed

+456
-26
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
- Suggest awaiting promise before using it when types mismatch. https://github.com/rescript-lang/rescript/pull/7498
2626
- Complete from `RegExp` stdlib module for regexes. https://github.com/rescript-lang/rescript/pull/7425
2727
- Allow oneliner formatting when including module with single type alias. https://github.com/rescript-lang/rescript/pull/7502
28+
- Improve error messages for JSX type mismatches, passing objects where record is expected, passing array literal where tuple is expected, and more. https://github.com/rescript-lang/rescript/pull/7500
2829

2930
# 12.0.0-alpha.13
3031

compiler/bsc/rescript_compiler_main.ml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,34 @@
1212

1313
let absname = ref false
1414

15+
module Error_message_utils_support = struct
16+
external to_comment : Res_comment.t -> Error_message_utils.Parser.comment
17+
= "%identity"
18+
external from_comment : Error_message_utils.Parser.comment -> Res_comment.t
19+
= "%identity"
20+
21+
let setup () =
22+
(Error_message_utils.Parser.parse_source :=
23+
fun source ->
24+
let res =
25+
Res_driver.parse_implementation_from_source ~for_printer:false
26+
~display_filename:"<none>" ~source
27+
in
28+
(res.parsetree, res.comments |> List.map to_comment));
29+
30+
(Error_message_utils.Parser.reprint_source :=
31+
fun parsetree comments ->
32+
Res_printer.print_implementation parsetree
33+
~comments:(comments |> List.map from_comment)
34+
~width:80);
35+
36+
Error_message_utils.configured_jsx_module :=
37+
Some
38+
(match !Js_config.jsx_module with
39+
| React -> "React"
40+
| Generic {module_name} -> module_name)
41+
end
42+
1543
let set_abs_input_name sourcefile =
1644
let sourcefile =
1745
if !absname && Filename.is_relative sourcefile then
@@ -41,6 +69,7 @@ let process_file sourcefile ?kind ppf =
4169
properly
4270
*)
4371
setup_outcome_printer ();
72+
Error_message_utils_support.setup ();
4473
let kind =
4574
match kind with
4675
| None ->

compiler/ml/error_message_utils.ml

Lines changed: 171 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,64 @@
11
type extract_concrete_typedecl =
22
Env.t -> Types.type_expr -> Path.t * Path.t * Types.type_declaration
33

4+
let configured_jsx_module : string option ref = ref None
5+
6+
let with_configured_jsx_module s =
7+
match !configured_jsx_module with
8+
| None -> s
9+
| Some module_name -> module_name ^ "." ^ s
10+
11+
module Parser : sig
12+
type comment
13+
14+
val parse_source : (string -> Parsetree.structure * comment list) ref
15+
16+
val reprint_source : (Parsetree.structure -> comment list -> string) ref
17+
18+
val parse_expr_at_loc :
19+
Warnings.loc -> (Parsetree.expression * comment list) option
20+
21+
val reprint_expr_at_loc :
22+
?mapper:(Parsetree.expression -> Parsetree.expression option) ->
23+
Warnings.loc ->
24+
string option
25+
end = struct
26+
type comment
27+
28+
let parse_source : (string -> Parsetree.structure * comment list) ref =
29+
ref (fun _ -> ([], []))
30+
31+
let reprint_source : (Parsetree.structure -> comment list -> string) ref =
32+
ref (fun _ _ -> "")
33+
34+
let extract_location_string ~src (loc : Location.t) =
35+
let start_pos = loc.loc_start in
36+
let end_pos = loc.loc_end in
37+
let start_offset = start_pos.pos_cnum in
38+
let end_offset = end_pos.pos_cnum in
39+
String.sub src start_offset (end_offset - start_offset)
40+
41+
let parse_expr_at_loc loc =
42+
(* TODO: Maybe cache later on *)
43+
let src = Ext_io.load_file loc.Location.loc_start.pos_fname in
44+
let sub_src = extract_location_string ~src loc in
45+
let parsed, comments = !parse_source sub_src in
46+
match parsed with
47+
| [{Parsetree.pstr_desc = Pstr_eval (exp, _)}] -> Some (exp, comments)
48+
| _ -> None
49+
50+
let wrap_in_structure exp =
51+
[{Parsetree.pstr_desc = Pstr_eval (exp, []); pstr_loc = Location.none}]
52+
53+
let reprint_expr_at_loc ?(mapper = fun _ -> None) loc =
54+
match parse_expr_at_loc loc with
55+
| Some (exp, comments) -> (
56+
match mapper exp with
57+
| Some exp -> Some (!reprint_source (wrap_in_structure exp) comments)
58+
| None -> None)
59+
| None -> None
60+
end
61+
462
type type_clash_statement = FunctionCall
563
type type_clash_context =
664
| SetRecordField
@@ -62,7 +120,7 @@ let is_record_type ~extract_concrete_typedecl ~env ty =
62120
| _ -> false
63121
with _ -> false
64122
65-
let print_extra_type_clash_help ~extract_concrete_typedecl ~env ppf
123+
let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
66124
(bottom_aliases : (Types.type_expr * Types.type_expr) option)
67125
type_clash_context =
68126
match (type_clash_context, bottom_aliases) with
@@ -103,9 +161,9 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env ppf
103161
\ Floats and ints have their own mathematical operators. This means \
104162
you cannot %s a float and an int without converting between the two.\n\n\
105163
\ Possible solutions:\n\
106-
\ - Ensure all values in this calculation has the type @{<info>%s@}. \
107-
You can convert between floats and ints via \
108-
@{<info>Belt.Float.toInt@} and @{<info>Belt.Int.fromFloat@}."
164+
\ - Ensure all values in this calculation have the type @{<info>%s@}. \
165+
You can convert between floats and ints via @{<info>Float.toInt@} and \
166+
@{<info>Int.fromFloat@}."
109167
operator_text
110168
(if for_float then "float" else "int"));
111169
match (is_constant, bottom_aliases) with
@@ -153,7 +211,7 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env ppf
153211
"\n\n\
154212
\ Possible solutions:\n\
155213
\ - Unwrap the option to its underlying value using \
156-
`yourValue->Belt.Option.getWithDefault(someDefaultValue)`"
214+
`yourValue->Option.getOr(someDefaultValue)`"
157215
| Some ComparisonOperator, _ ->
158216
fprintf ppf "\n\n You can only compare things of the same type."
159217
| Some ArrayValue, _ ->
@@ -165,6 +223,10 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env ppf
165223
\ - Use a tuple, if your array is of fixed length. Tuples can mix types \
166224
freely, and compiles to a JavaScript array. Example of a tuple: `let \
167225
myTuple = (10, \"hello\", 15.5, true)"
226+
| _, Some (_, {desc = Tconstr (p2, _, _)}) when Path.same Predef.path_dict p2
227+
->
228+
fprintf ppf
229+
"@,@,Dicts are written like: @{<info>dict{\"a\": 1, \"b\": 2}@}@,"
168230
| _, Some ({Types.desc = Tconstr (_p1, _, _)}, {desc = Tconstr (p2, _, _)})
169231
when Path.same Predef.path_unit p2 ->
170232
fprintf ppf
@@ -176,15 +238,113 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env ppf
176238
| _, Some ({desc = Tobject _}, ({Types.desc = Tconstr _} as t1))
177239
when is_record_type ~extract_concrete_typedecl ~env t1 ->
178240
fprintf ppf
179-
"\n\n\
180-
\ You're passing a @{<error>ReScript object@} where a @{<info>record@} \
181-
is expected. \n\n\
182-
\ - Did you mean to pass a record instead of an object? Objects are \
183-
written with quoted keys, and records with unquoted keys. Remove the \
184-
quotes from the object keys to pass it as a record instead of object. \n\n"
241+
"@,\
242+
@,\
243+
You're passing a @{<error>ReScript object@} where a @{<info>record@} is \
244+
expected. Objects are written with quoted keys, and records with \
245+
unquoted keys.";
246+
247+
let suggested_rewrite =
248+
Parser.reprint_expr_at_loc loc ~mapper:(fun exp ->
249+
match exp.Parsetree.pexp_desc with
250+
| Pexp_extension
251+
( {txt = "obj"},
252+
PStr
253+
[
254+
{
255+
pstr_desc =
256+
Pstr_eval (({pexp_desc = Pexp_record _} as record), _);
257+
};
258+
] ) ->
259+
Some record
260+
| _ -> None)
261+
in
262+
fprintf ppf
263+
"@,\
264+
@,\
265+
Possible solutions: @,\
266+
- Rewrite the object to a record%s@{<info>%s@}@,"
267+
(match suggested_rewrite with
268+
| Some _ -> ", like: "
269+
| None -> "")
270+
(match suggested_rewrite with
271+
| Some rewrite -> rewrite
272+
| None -> "")
185273
| _, Some ({Types.desc = Tconstr (p1, _, _)}, _)
186274
when Path.same p1 Predef.path_promise ->
187275
fprintf ppf "\n\n - Did you mean to await this promise before using it?\n"
276+
| _, Some ({Types.desc = Tconstr (p1, _, _)}, {Types.desc = Ttuple _})
277+
when Path.same p1 Predef.path_array ->
278+
let suggested_rewrite =
279+
Parser.reprint_expr_at_loc loc ~mapper:(fun exp ->
280+
match exp.Parsetree.pexp_desc with
281+
| Pexp_array items ->
282+
Some {exp with Parsetree.pexp_desc = Pexp_tuple items}
283+
| _ -> None)
284+
in
285+
fprintf ppf
286+
"\n\n - Fix this by passing a tuple instead of an array%s@{<info>%s@}\n"
287+
(match suggested_rewrite with
288+
| Some _ -> ", like: "
289+
| None -> "")
290+
(match suggested_rewrite with
291+
| Some rewrite -> rewrite
292+
| None -> "")
293+
| ( _,
294+
Some
295+
( {desc = Tconstr (p, type_params, _)},
296+
{desc = Tconstr (Pdot (Pident {name = "Jsx"}, "element", _), _, _)} )
297+
) -> (
298+
(* Looking for a JSX element but got something else *)
299+
let is_jsx_element ty =
300+
match Ctype.expand_head env ty with
301+
| {desc = Tconstr (Pdot (Pident {name = "Jsx"}, "element", _), _, _)} ->
302+
true
303+
| _ -> false
304+
in
305+
306+
let print_jsx_msg ?(extra = "") name target_fn =
307+
fprintf ppf
308+
"@,\
309+
@,\
310+
In JSX, all content must be JSX elements. You can convert %s to a JSX \
311+
element with @{<info>%s@}%s.@,"
312+
name target_fn extra
313+
in
314+
315+
match type_params with
316+
| _ when Path.same p Predef.path_int ->
317+
print_jsx_msg "int" (with_configured_jsx_module "int")
318+
| _ when Path.same p Predef.path_string ->
319+
print_jsx_msg "string" (with_configured_jsx_module "string")
320+
| _ when Path.same p Predef.path_float ->
321+
print_jsx_msg "float" (with_configured_jsx_module "float")
322+
| [_] when Path.same p Predef.path_option ->
323+
fprintf ppf
324+
"@,\
325+
@,\
326+
You need to unwrap this option to its underlying value first, then \
327+
turn that value into a JSX element.@,\
328+
For @{<info>None@}, you can use @{<info>%s@} to output nothing into \
329+
JSX.@,"
330+
(with_configured_jsx_module "null")
331+
| [tp] when Path.same p Predef.path_array && is_jsx_element tp ->
332+
print_jsx_msg
333+
~extra:
334+
(" (for example by using a pipe: ->"
335+
^ with_configured_jsx_module "array"
336+
^ ".")
337+
"array"
338+
(with_configured_jsx_module "array")
339+
| [_] when Path.same p Predef.path_array ->
340+
fprintf ppf
341+
"@,\
342+
@,\
343+
You need to convert each item in this array to a JSX element first, \
344+
then use @{<info>%s@} to convert the array of JSX elements into a \
345+
single JSX element.@,"
346+
(with_configured_jsx_module "array")
347+
| _ -> ())
188348
| _ -> ()
189349
190350
let type_clash_context_from_function sexp sfunct =

compiler/ml/typecore.ml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,7 @@ let rec collect_missing_arguments env type1 type2 =
728728
| None -> None)
729729
| _ -> None
730730

731-
let print_expr_type_clash ?type_clash_context env trace ppf =
731+
let print_expr_type_clash ?type_clash_context env loc trace ppf =
732732
(* this is the most frequent error. We should do whatever we can to provide
733733
specific guidance to this generic error before giving up *)
734734
let bottom_aliases_result = bottom_aliases trace in
@@ -785,7 +785,7 @@ let print_expr_type_clash ?type_clash_context env trace ppf =
785785
(function
786786
| ppf -> error_type_text ppf type_clash_context)
787787
(function ppf -> error_expected_type_text ppf type_clash_context);
788-
print_extra_type_clash_help ~extract_concrete_typedecl ~env ppf
788+
print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
789789
bottom_aliases_result type_clash_context;
790790
show_extra_help ppf env trace
791791

@@ -4173,7 +4173,7 @@ let type_expr ppf typ =
41734173
Printtyp.reset_and_mark_loops typ;
41744174
Printtyp.type_expr ppf typ
41754175
4176-
let report_error env ppf error =
4176+
let report_error env loc ppf error =
41774177
match error with
41784178
| Polymorphic_label lid ->
41794179
fprintf ppf "@[The record field %a is polymorphic.@ %s@]" longident lid
@@ -4237,7 +4237,7 @@ let report_error env ppf error =
42374237
| Expr_type_clash (trace, type_clash_context) ->
42384238
(* modified *)
42394239
fprintf ppf "@[<v>";
4240-
print_expr_type_clash ?type_clash_context env trace ppf;
4240+
print_expr_type_clash ?type_clash_context env loc trace ppf;
42414241
fprintf ppf "@]"
42424242
| Apply_non_function typ -> (
42434243
(* modified *)
@@ -4542,13 +4542,13 @@ let report_error env ppf error =
45424542
fprintf ppf
45434543
"Direct field access on a dict is not supported. Use Dict.get instead."
45444544
4545-
let report_error env ppf err =
4546-
Printtyp.wrap_printing_env env (fun () -> report_error env ppf err)
4545+
let report_error env loc ppf err =
4546+
Printtyp.wrap_printing_env env (fun () -> report_error env loc ppf err)
45474547
45484548
let () =
45494549
Location.register_error_of_exn (function
45504550
| Error (loc, env, err) ->
4551-
Some (Location.error_of_printer loc (report_error env) err)
4551+
Some (Location.error_of_printer loc (report_error env loc) err)
45524552
| Error_forward err -> Some err
45534553
| _ -> None)
45544554

compiler/ml/typecore.mli

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ type error =
108108
exception Error of Location.t * Env.t * error
109109
exception Error_forward of Location.error
110110

111-
val report_error : Env.t -> formatter -> error -> unit
111+
val report_error : Env.t -> Location.t -> formatter -> error -> unit
112112
(* Deprecated. Use Location.{error_of_exn, report_error}. *)
113113

114114
(* Forward declaration, to be filled in by Typemod.type_module *)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/array_literal_passed_to_tuple.res:5:17-34
4+
5+
3 │ }
6+
4 │
7+
5 │ let x = doStuff(["hello", "world"])
8+
6 │
9+
10+
This has type: array<'a>
11+
But it's expected to have type: (string, string)
12+
13+
- Fix this by passing a tuple instead of an array, like: ("hello", "world")
14+

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/array_var_passed_to_tuple.res:7:17-18
4+
5+
5 │ let xx = ["hello", "world"]
6+
6 │
7+
7 │ let x = doStuff(xx)
8+
8 │
9+
10+
This has type: array<string>
11+
But this function argument is expecting: (string, string)
12+
13+
- Fix this by passing a tuple instead of an array
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/dict_helper.res:3:11-23
4+
5+
1 │ external takesDict: dict<string> => unit = "takesDict"
6+
2 │
7+
3 │ takesDict({"test": "1"})
8+
4 │
9+
10+
This has type: {"test": string}
11+
But it's expected to have type: dict<string>
12+
13+
Dicts are written like: dict{"a": 1, "b": 2}

0 commit comments

Comments
 (0)