Repository: rescript-lang/rescript
Pull request #7500: Improve various error messages
Author: login: zth
Created: 2025-05-22T09:16:56Z | Merged: 2025-05-22T16:36:40Z

This explores how we can re-parse the relevant parts of the source where the error is, to give the viewer more concrete error messages (and suggestions for fixes) that we can't derive just from the type errors we see. It also improves a bunch of specific error messages:

* Better JSX element mismatch messages (suggest how to convert int/float/string/array into Jsx.element)
* Full rewrite suggestion for when passing an object to where a record is expected
* Full rewrite suggestion when passing an array literal where a tuple is expected


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a5998097a5..b44e383e830 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@
 - Suggest awaiting promise before using it when types mismatch. https://github.com/rescript-lang/rescript/pull/7498
 - Complete from `RegExp` stdlib module for regexes. https://github.com/rescript-lang/rescript/pull/7425
 - Allow oneliner formatting when including module with single type alias. https://github.com/rescript-lang/rescript/pull/7502
+- 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
 
 # 12.0.0-alpha.13
 
diff --git a/compiler/bsc/rescript_compiler_main.ml b/compiler/bsc/rescript_compiler_main.ml
index f9113c40b6a..52f4b856514 100644
--- a/compiler/bsc/rescript_compiler_main.ml
+++ b/compiler/bsc/rescript_compiler_main.ml
@@ -12,6 +12,34 @@
 
 let absname = ref false
 
+module Error_message_utils_support = struct
+  external to_comment : Res_comment.t -> Error_message_utils.Parser.comment
+    = "%identity"
+  external from_comment : Error_message_utils.Parser.comment -> Res_comment.t
+    = "%identity"
+
+  let setup () =
+    (Error_message_utils.Parser.parse_source :=
+       fun source ->
+         let res =
+           Res_driver.parse_implementation_from_source ~for_printer:false
+             ~display_filename:"<none>" ~source
+         in
+         (res.parsetree, res.comments |> List.map to_comment));
+
+    (Error_message_utils.Parser.reprint_source :=
+       fun parsetree comments ->
+         Res_printer.print_implementation parsetree
+           ~comments:(comments |> List.map from_comment)
+           ~width:80);
+
+    Error_message_utils.configured_jsx_module :=
+      Some
+        (match !Js_config.jsx_module with
+        | React -> "React"
+        | Generic {module_name} -> module_name)
+end
+
 let set_abs_input_name sourcefile =
   let sourcefile =
     if !absname && Filename.is_relative sourcefile then
@@ -41,6 +69,7 @@ let process_file sourcefile ?kind ppf =
      properly
   *)
   setup_outcome_printer ();
+  Error_message_utils_support.setup ();
   let kind =
     match kind with
     | None ->
diff --git a/compiler/ml/error_message_utils.ml b/compiler/ml/error_message_utils.ml
index 31d8cafc3bc..ebc138284fe 100644
--- a/compiler/ml/error_message_utils.ml
+++ b/compiler/ml/error_message_utils.ml
@@ -1,6 +1,64 @@
 type extract_concrete_typedecl =
   Env.t -> Types.type_expr -> Path.t * Path.t * Types.type_declaration
 
+let configured_jsx_module : string option ref = ref None
+
+let with_configured_jsx_module s =
+  match !configured_jsx_module with
+  | None -> s
+  | Some module_name -> module_name ^ "." ^ s
+
+module Parser : sig
+  type comment
+
+  val parse_source : (string -> Parsetree.structure * comment list) ref
+
+  val reprint_source : (Parsetree.structure -> comment list -> string) ref
+
+  val parse_expr_at_loc :
+    Warnings.loc -> (Parsetree.expression * comment list) option
+
+  val reprint_expr_at_loc :
+    ?mapper:(Parsetree.expression -> Parsetree.expression option) ->
+    Warnings.loc ->
+    string option
+end = struct
+  type comment
+
+  let parse_source : (string -> Parsetree.structure * comment list) ref =
+    ref (fun _ -> ([], []))
+
+  let reprint_source : (Parsetree.structure -> comment list -> string) ref =
+    ref (fun _ _ -> "")
+
+  let extract_location_string ~src (loc : Location.t) =
+    let start_pos = loc.loc_start in
+    let end_pos = loc.loc_end in
+    let start_offset = start_pos.pos_cnum in
+    let end_offset = end_pos.pos_cnum in
+    String.sub src start_offset (end_offset - start_offset)
+
+  let parse_expr_at_loc loc =
+    (* TODO: Maybe cache later on *)
+    let src = Ext_io.load_file loc.Location.loc_start.pos_fname in
+    let sub_src = extract_location_string ~src loc in
+    let parsed, comments = !parse_source sub_src in
+    match parsed with
+    | [{Parsetree.pstr_desc = Pstr_eval (exp, _)}] -> Some (exp, comments)
+    | _ -> None
+
+  let wrap_in_structure exp =
+    [{Parsetree.pstr_desc = Pstr_eval (exp, []); pstr_loc = Location.none}]
+
+  let reprint_expr_at_loc ?(mapper = fun _ -> None) loc =
+    match parse_expr_at_loc loc with
+    | Some (exp, comments) -> (
+      match mapper exp with
+      | Some exp -> Some (!reprint_source (wrap_in_structure exp) comments)
+      | None -> None)
+    | None -> None
+end
+
 type type_clash_statement = FunctionCall
 type type_clash_context =
   | SetRecordField
@@ -62,7 +120,7 @@ let is_record_type ~extract_concrete_typedecl ~env ty =
     | _ -> false
   with _ -> false
 
-let print_extra_type_clash_help ~extract_concrete_typedecl ~env ppf
+let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
     (bottom_aliases : (Types.type_expr * Types.type_expr) option)
     type_clash_context =
   match (type_clash_context, bottom_aliases) with
@@ -103,9 +161,9 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env ppf
         \  Floats and ints have their own mathematical operators. This means \
          you cannot %s a float and an int without converting between the two.\n\n\
         \  Possible solutions:\n\
-        \  - Ensure all values in this calculation has the type @{<info>%s@}. \
-         You can convert between floats and ints via \
-         @{<info>Belt.Float.toInt@} and @{<info>Belt.Int.fromFloat@}."
+        \  - Ensure all values in this calculation have the type @{<info>%s@}. \
+         You can convert between floats and ints via @{<info>Float.toInt@} and \
+         @{<info>Int.fromFloat@}."
         operator_text
         (if for_float then "float" else "int"));
     match (is_constant, bottom_aliases) with
@@ -153,7 +211,7 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env ppf
       "\n\n\
       \  Possible solutions:\n\
       \  - Unwrap the option to its underlying value using \
-       `yourValue->Belt.Option.getWithDefault(someDefaultValue)`"
+       `yourValue->Option.getOr(someDefaultValue)`"
   | Some ComparisonOperator, _ ->
     fprintf ppf "\n\n  You can only compare things of the same type."
   | Some ArrayValue, _ ->
@@ -165,6 +223,10 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env ppf
       \  - Use a tuple, if your array is of fixed length. Tuples can mix types \
        freely, and compiles to a JavaScript array. Example of a tuple: `let \
        myTuple = (10, \"hello\", 15.5, true)"
+  | _, Some (_, {desc = Tconstr (p2, _, _)}) when Path.same Predef.path_dict p2
+    ->
+    fprintf ppf
+      "@,@,Dicts are written like: @{<info>dict{\"a\": 1, \"b\": 2}@}@,"
   | _, Some ({Types.desc = Tconstr (_p1, _, _)}, {desc = Tconstr (p2, _, _)})
     when Path.same Predef.path_unit p2 ->
     fprintf ppf
@@ -176,15 +238,113 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env ppf
   | _, Some ({desc = Tobject _}, ({Types.desc = Tconstr _} as t1))
     when is_record_type ~extract_concrete_typedecl ~env t1 ->
     fprintf ppf
-      "\n\n\
-      \  You're passing a @{<error>ReScript object@} where a @{<info>record@} \
-       is expected. \n\n\
-      \  - Did you mean to pass a record instead of an object? Objects are \
-       written with quoted keys, and records with unquoted keys. Remove the \
-       quotes from the object keys to pass it as a record instead of object. \n\n"
+      "@,\
+       @,\
+       You're passing a @{<error>ReScript object@} where a @{<info>record@} is \
+       expected. Objects are written with quoted keys, and records with \
+       unquoted keys.";
+
+    let suggested_rewrite =
+      Parser.reprint_expr_at_loc loc ~mapper:(fun exp ->
+          match exp.Parsetree.pexp_desc with
+          | Pexp_extension
+              ( {txt = "obj"},
+                PStr
+                  [
+                    {
+                      pstr_desc =
+                        Pstr_eval (({pexp_desc = Pexp_record _} as record), _);
+                    };
+                  ] ) ->
+            Some record
+          | _ -> None)
+    in
+    fprintf ppf
+      "@,\
+       @,\
+       Possible solutions: @,\
+       - Rewrite the object to a record%s@{<info>%s@}@,"
+      (match suggested_rewrite with
+      | Some _ -> ", like: "
+      | None -> "")
+      (match suggested_rewrite with
+      | Some rewrite -> rewrite
+      | None -> "")
   | _, Some ({Types.desc = Tconstr (p1, _, _)}, _)
     when Path.same p1 Predef.path_promise ->
     fprintf ppf "\n\n  - Did you mean to await this promise before using it?\n"
+  | _, Some ({Types.desc = Tconstr (p1, _, _)}, {Types.desc = Ttuple _})
+    when Path.same p1 Predef.path_array ->
+    let suggested_rewrite =
+      Parser.reprint_expr_at_loc loc ~mapper:(fun exp ->
+          match exp.Parsetree.pexp_desc with
+          | Pexp_array items ->
+            Some {exp with Parsetree.pexp_desc = Pexp_tuple items}
+          | _ -> None)
+    in
+    fprintf ppf
+      "\n\n  - Fix this by passing a tuple instead of an array%s@{<info>%s@}\n"
+      (match suggested_rewrite with
+      | Some _ -> ", like: "
+      | None -> "")
+      (match suggested_rewrite with
+      | Some rewrite -> rewrite
+      | None -> "")
+  | ( _,
+      Some
+        ( {desc = Tconstr (p, type_params, _)},
+          {desc = Tconstr (Pdot (Pident {name = "Jsx"}, "element", _), _, _)} )
+    ) -> (
+    (* Looking for a JSX element but got something else *)
+    let is_jsx_element ty =
+      match Ctype.expand_head env ty with
+      | {desc = Tconstr (Pdot (Pident {name = "Jsx"}, "element", _), _, _)} ->
+        true
+      | _ -> false
+    in
+
+    let print_jsx_msg ?(extra = "") name target_fn =
+      fprintf ppf
+        "@,\
+         @,\
+         In JSX, all content must be JSX elements. You can convert %s to a JSX \
+         element with @{<info>%s@}%s.@,"
+        name target_fn extra
+    in
+
+    match type_params with
+    | _ when Path.same p Predef.path_int ->
+      print_jsx_msg "int" (with_configured_jsx_module "int")
+    | _ when Path.same p Predef.path_string ->
+      print_jsx_msg "string" (with_configured_jsx_module "string")
+    | _ when Path.same p Predef.path_float ->
+      print_jsx_msg "float" (with_configured_jsx_module "float")
+    | [_] when Path.same p Predef.path_option ->
+      fprintf ppf
+        "@,\
+         @,\
+         You need to unwrap this option to its underlying value first, then \
+         turn that value into a JSX element.@,\
+         For @{<info>None@}, you can use @{<info>%s@} to output nothing into \
+         JSX.@,"
+        (with_configured_jsx_module "null")
+    | [tp] when Path.same p Predef.path_array && is_jsx_element tp ->
+      print_jsx_msg
+        ~extra:
+          (" (for example by using a pipe: ->"
+          ^ with_configured_jsx_module "array"
+          ^ ".")
+        "array"
+        (with_configured_jsx_module "array")
+    | [_] when Path.same p Predef.path_array ->
+      fprintf ppf
+        "@,\
+         @,\
+         You need to convert each item in this array to a JSX element first, \
+         then use @{<info>%s@} to convert the array of JSX elements into a \
+         single JSX element.@,"
+        (with_configured_jsx_module "array")
+    | _ -> ())
   | _ -> ()
 
 let type_clash_context_from_function sexp sfunct =
diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml
index 7fb9472b9f4..de48f4b2c74 100644
--- a/compiler/ml/typecore.ml
+++ b/compiler/ml/typecore.ml
@@ -728,7 +728,7 @@ let rec collect_missing_arguments env type1 type2 =
     | None -> None)
   | _ -> None
 
-let print_expr_type_clash ?type_clash_context env trace ppf =
+let print_expr_type_clash ?type_clash_context env loc trace ppf =
   (* this is the most frequent error. We should do whatever we can to provide
       specific guidance to this generic error before giving up *)
   let bottom_aliases_result = bottom_aliases trace in
@@ -785,7 +785,7 @@ let print_expr_type_clash ?type_clash_context env trace ppf =
       (function
         | ppf -> error_type_text ppf type_clash_context)
       (function ppf -> error_expected_type_text ppf type_clash_context);
-    print_extra_type_clash_help ~extract_concrete_typedecl ~env ppf
+    print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
       bottom_aliases_result type_clash_context;
     show_extra_help ppf env trace
 
@@ -4173,7 +4173,7 @@ let type_expr ppf typ =
   Printtyp.reset_and_mark_loops typ;
   Printtyp.type_expr ppf typ
 
-let report_error env ppf error =
+let report_error env loc ppf error =
   match error with
   | Polymorphic_label lid ->
     fprintf ppf "@[The record field %a is polymorphic.@ %s@]" longident lid
@@ -4237,7 +4237,7 @@ let report_error env ppf error =
   | Expr_type_clash (trace, type_clash_context) ->
     (* modified *)
     fprintf ppf "@[<v>";
-    print_expr_type_clash ?type_clash_context env trace ppf;
+    print_expr_type_clash ?type_clash_context env loc trace ppf;
     fprintf ppf "@]"
   | Apply_non_function typ -> (
     (* modified *)
@@ -4542,13 +4542,13 @@ let report_error env ppf error =
     fprintf ppf
       "Direct field access on a dict is not supported. Use Dict.get instead."
 
-let report_error env ppf err =
-  Printtyp.wrap_printing_env env (fun () -> report_error env ppf err)
+let report_error env loc ppf err =
+  Printtyp.wrap_printing_env env (fun () -> report_error env loc ppf err)
 
 let () =
   Location.register_error_of_exn (function
     | Error (loc, env, err) ->
-      Some (Location.error_of_printer loc (report_error env) err)
+      Some (Location.error_of_printer loc (report_error env loc) err)
     | Error_forward err -> Some err
     | _ -> None)
 
diff --git a/compiler/ml/typecore.mli b/compiler/ml/typecore.mli
index c11f557a2e6..fef96830c2b 100644
--- a/compiler/ml/typecore.mli
+++ b/compiler/ml/typecore.mli
@@ -108,7 +108,7 @@ type error =
 exception Error of Location.t * Env.t * error
 exception Error_forward of Location.error
 
-val report_error : Env.t -> formatter -> error -> unit
+val report_error : Env.t -> Location.t -> formatter -> error -> unit
 (* Deprecated.  Use Location.{error_of_exn, report_error}. *)
 
 (* Forward declaration, to be filled in by Typemod.type_module *)
diff --git a/tests/build_tests/super_errors/expected/array_literal_passed_to_tuple.res.expected b/tests/build_tests/super_errors/expected/array_literal_passed_to_tuple.res.expected
new file mode 100644
index 00000000000..5fe60abda35
--- /dev/null
+++ b/tests/build_tests/super_errors/expected/array_literal_passed_to_tuple.res.expected
@@ -0,0 +1,14 @@
+
+  [1;31mWe've found a bug for you![0m
+  [36m/.../fixtures/array_literal_passed_to_tuple.res[0m:[2m5:17-34[0m
+
+  3 [2m│[0m }
+  4 [2m│[0m 
+  [1;31m5[0m [2m│[0m let x = doStuff([1;31m["hello", "world"][0m)
+  6 [2m│[0m 
+
+  This has type: [1;31marray<'a>[0m
+  But it's expected to have type: [1;33m(string, string)[0m
+
+  - Fix this by passing a tuple instead of an array, like: [1;33m("hello", "world")
+[0m
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/expected/array_var_passed_to_tuple.res.expected b/tests/build_tests/super_errors/expected/array_var_passed_to_tuple.res.expected
new file mode 100644
index 00000000000..e75a50f376a
--- /dev/null
+++ b/tests/build_tests/super_errors/expected/array_var_passed_to_tuple.res.expected
@@ -0,0 +1,13 @@
+
+  [1;31mWe've found a bug for you![0m
+  [36m/.../fixtures/array_var_passed_to_tuple.res[0m:[2m7:17-18[0m
+
+  5 [2m│[0m let xx = ["hello", "world"]
+  6 [2m│[0m 
+  [1;31m7[0m [2m│[0m let x = doStuff([1;31mxx[0m)
+  8 [2m│[0m 
+
+  This has type: [1;31marray<string>[0m
+  But this function argument is expecting: [1;33m(string, string)[0m
+
+  - Fix this by passing a tuple instead of an array[1;33m[0m
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/expected/dict_helper.res.expected b/tests/build_tests/super_errors/expected/dict_helper.res.expected
new file mode 100644
index 00000000000..b2e35e89b7d
--- /dev/null
+++ b/tests/build_tests/super_errors/expected/dict_helper.res.expected
@@ -0,0 +1,13 @@
+
+  [1;31mWe've found a bug for you![0m
+  [36m/.../fixtures/dict_helper.res[0m:[2m3:11-23[0m
+
+  1 [2m│[0m external takesDict: dict<string> => unit = "takesDict"
+  2 [2m│[0m 
+  [1;31m3[0m [2m│[0m takesDict([1;31m{"test": "1"}[0m)
+  4 [2m│[0m 
+
+  This has type: [1;31m{"test": string}[0m
+  But it's expected to have type: [1;33mdict<string>[0m
+  
+  Dicts are written like: [1;33mdict{"a": 1, "b": 2}[0m
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_element.res.expected b/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_element.res.expected
new file mode 100644
index 00000000000..1cf8426d78c
--- /dev/null
+++ b/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_element.res.expected
@@ -0,0 +1,13 @@
+
+  [1;31mWe've found a bug for you![0m
+  [36m/.../fixtures/jsx_type_mismatch_array_element.res[0m:[2m19:13-30[0m
+
+  17 [2m│[0m }
+  18 [2m│[0m 
+  [1;31m19[0m [2m│[0m let x = <> {[1;31m[React.string("")][0m} </>
+  20 [2m│[0m 
+
+  This has type: [1;31marray<'a>[0m
+  But it's expected to have type: [1;33mReact.element[0m [2m(defined as[0m [1;33mJsx.element[0m[2m)[0m
+  
+  You need to convert each item in this array to a JSX element first, then use [1;33mReact.array[0m to convert the array of JSX elements into a single JSX element.
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_raw.res.expected b/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_raw.res.expected
new file mode 100644
index 00000000000..1eb1d4870d4
--- /dev/null
+++ b/tests/build_tests/super_errors/expected/jsx_type_mismatch_array_raw.res.expected
@@ -0,0 +1,13 @@
+
+  [1;31mWe've found a bug for you![0m
+  [36m/.../fixtures/jsx_type_mismatch_array_raw.res[0m:[2m17:13-16[0m
+
+  15 [2m│[0m }
+  16 [2m│[0m 
+  [1;31m17[0m [2m│[0m let x = <> {[1;31m[""][0m} </>
+  18 [2m│[0m 
+
+  This has type: [1;31marray<'a>[0m
+  But it's expected to have type: [1;33mReact.element[0m [2m(defined as[0m [1;33mJsx.element[0m[2m)[0m
+  
+  You need to convert each item in this array to a JSX element first, then use [1;33mReact.array[0m to convert the array of JSX elements into a single JSX element.
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/expected/jsx_type_mismatch_float.res.expected b/tests/build_tests/super_errors/expected/jsx_type_mismatch_float.res.expected
new file mode 100644
index 00000000000..af4279bd526
--- /dev/null
+++ b/tests/build_tests/super_errors/expected/jsx_type_mismatch_float.res.expected
@@ -0,0 +1,13 @@
+
+  [1;31mWe've found a bug for you![0m
+  [36m/.../fixtures/jsx_type_mismatch_float.res[0m:[2m17:13-14[0m
+
+  15 [2m│[0m }
+  16 [2m│[0m 
+  [1;31m17[0m [2m│[0m let x = <> {[1;31m1.[0m} </>
+  18 [2m│[0m 
+
+  This has type: [1;31mfloat[0m
+  But it's expected to have type: [1;33mReact.element[0m [2m(defined as[0m [1;33mJsx.element[0m[2m)[0m
+  
+  In JSX, all content must be JSX elements. You can convert float to a JSX element with [1;33mReact.float[0m.
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/expected/jsx_type_mismatch_int.res.expected b/tests/build_tests/super_errors/expected/jsx_type_mismatch_int.res.expected
new file mode 100644
index 00000000000..5a43157d034
--- /dev/null
+++ b/tests/build_tests/super_errors/expected/jsx_type_mismatch_int.res.expected
@@ -0,0 +1,13 @@
+
+  [1;31mWe've found a bug for you![0m
+  [36m/.../fixtures/jsx_type_mismatch_int.res[0m:[2m17:13[0m
+
+  15 [2m│[0m }
+  16 [2m│[0m 
+  [1;31m17[0m [2m│[0m let x = <> {[1;31m1[0m} </>
+  18 [2m│[0m 
+
+  This has type: [1;31mint[0m
+  But it's expected to have type: [1;33mReact.element[0m [2m(defined as[0m [1;33mJsx.element[0m[2m)[0m
+  
+  In JSX, all content must be JSX elements. You can convert int to a JSX element with [1;33mReact.int[0m.
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/expected/jsx_type_mismatch_option.res.expected b/tests/build_tests/super_errors/expected/jsx_type_mismatch_option.res.expected
new file mode 100644
index 00000000000..fee1ae03c6b
--- /dev/null
+++ b/tests/build_tests/super_errors/expected/jsx_type_mismatch_option.res.expected
@@ -0,0 +1,14 @@
+
+  [1;31mWe've found a bug for you![0m
+  [36m/.../fixtures/jsx_type_mismatch_option.res[0m:[2m17:13-16[0m
+
+  15 [2m│[0m }
+  16 [2m│[0m 
+  [1;31m17[0m [2m│[0m let x = <> {[1;31mNone[0m} </>
+  18 [2m│[0m 
+
+  This has type: [1;31moption<'a>[0m
+  But it's expected to have type: [1;33mReact.element[0m [2m(defined as[0m [1;33mJsx.element[0m[2m)[0m
+  
+  You need to unwrap this option to its underlying value first, then turn that value into a JSX element.
+  For [1;33mNone[0m, you can use [1;33mReact.null[0m to output nothing into JSX.
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/expected/jsx_type_mismatch_string.res.expected b/tests/build_tests/super_errors/expected/jsx_type_mismatch_string.res.expected
new file mode 100644
index 00000000000..10d1d64dc7f
--- /dev/null
+++ b/tests/build_tests/super_errors/expected/jsx_type_mismatch_string.res.expected
@@ -0,0 +1,13 @@
+
+  [1;31mWe've found a bug for you![0m
+  [36m/.../fixtures/jsx_type_mismatch_string.res[0m:[2m17:13-14[0m
+
+  15 [2m│[0m }
+  16 [2m│[0m 
+  [1;31m17[0m [2m│[0m let x = <> {[1;31m""[0m} </>
+  18 [2m│[0m 
+
+  This has type: [1;31mstring[0m
+  But it's expected to have type: [1;33mReact.element[0m [2m(defined as[0m [1;33mJsx.element[0m[2m)[0m
+  
+  In JSX, all content must be JSX elements. You can convert string to a JSX element with [1;33mReact.string[0m.
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/expected/math_operator_float.res.expected b/tests/build_tests/super_errors/expected/math_operator_float.res.expected
index 640e57a5007..fe5df9c95c2 100644
--- a/tests/build_tests/super_errors/expected/math_operator_float.res.expected
+++ b/tests/build_tests/super_errors/expected/math_operator_float.res.expected
@@ -13,7 +13,7 @@
   Floats and ints have their own mathematical operators. This means you cannot add a float and an int without converting between the two.
 
   Possible solutions:
-  - Ensure all values in this calculation has the type [1;33mfloat[0m. You can convert between floats and ints via [1;33mBelt.Float.toInt[0m and [1;33mBelt.Int.fromFloat[0m.
+  - Ensure all values in this calculation have the type [1;33mfloat[0m. You can convert between floats and ints via [1;33mFloat.toInt[0m and [1;33mInt.fromFloat[0m.
   - Change the operator to [1;33m+[0m, which works on [1;33mint[0m
   
   You can convert [1;33mint[0m to [1;33mfloat[0m with [1;33mInt.toFloat[0m.
diff --git a/tests/build_tests/super_errors/expected/object_passed_when_record_expected.res.expected b/tests/build_tests/super_errors/expected/object_literal_passed_when_record_expected.res.expected
similarity index 51%
rename from tests/build_tests/super_errors/expected/object_passed_when_record_expected.res.expected
rename to tests/build_tests/super_errors/expected/object_literal_passed_when_record_expected.res.expected
index be090672024..b69288a6869 100644
--- a/tests/build_tests/super_errors/expected/object_passed_when_record_expected.res.expected
+++ b/tests/build_tests/super_errors/expected/object_literal_passed_when_record_expected.res.expected
@@ -1,6 +1,6 @@
 
   [1;31mWe've found a bug for you![0m
-  [36m/.../fixtures/object_passed_when_record_expected.res[0m:[2m4:14-26[0m
+  [36m/.../fixtures/object_literal_passed_when_record_expected.res[0m:[2m4:14-26[0m
 
   2 [2m│[0m type xx = array<x>
   3 [2m│[0m 
@@ -9,7 +9,9 @@
 
   This has type: [1;31m{"one": bool}[0m
   But it's expected to have type: [1;33mx[0m
-
-  You're passing a [1;31mReScript object[0m where a [1;33mrecord[0m is expected. 
-
-  - Did you mean to pass a record instead of an object? Objects are written with quoted keys, and records with unquoted keys. Remove the quotes from the object keys to pass it as a record instead of object.
\ No newline at end of file
+  
+  You're passing a [1;31mReScript object[0m where a [1;33mrecord[0m is expected. Objects are written with quoted keys, and records with unquoted keys.
+  
+  Possible solutions: 
+  - Rewrite the object to a record, like: [1;33m{one: true}
+[0m
\ No newline at end of file
diff --git a/tests/build_tests/super_errors/expected/primitives7.res.expected b/tests/build_tests/super_errors/expected/primitives7.res.expected
index a6fe42b2236..bfe3cce7c02 100644
--- a/tests/build_tests/super_errors/expected/primitives7.res.expected
+++ b/tests/build_tests/super_errors/expected/primitives7.res.expected
@@ -13,7 +13,7 @@
   Floats and ints have their own mathematical operators. This means you cannot add a float and an int without converting between the two.
 
   Possible solutions:
-  - Ensure all values in this calculation has the type [1;33mfloat[0m. You can convert between floats and ints via [1;33mBelt.Float.toInt[0m and [1;33mBelt.Int.fromFloat[0m.
+  - Ensure all values in this calculation have the type [1;33mfloat[0m. You can convert between floats and ints via [1;33mFloat.toInt[0m and [1;33mInt.fromFloat[0m.
   - Change the operator to [1;33m+[0m, which works on [1;33mint[0m
   
   You can convert [1;33mint[0m to [1;33mfloat[0m with [1;33mInt.toFloat[0m.
diff --git a/tests/build_tests/super_errors/fixtures/array_literal_passed_to_tuple.res b/tests/build_tests/super_errors/fixtures/array_literal_passed_to_tuple.res
new file mode 100644
index 00000000000..4b203b92cee
--- /dev/null
+++ b/tests/build_tests/super_errors/fixtures/array_literal_passed_to_tuple.res
@@ -0,0 +1,5 @@
+let doStuff = ((one, two)) => {
+  one ++ two
+}
+
+let x = doStuff(["hello", "world"])
diff --git a/tests/build_tests/super_errors/fixtures/array_var_passed_to_tuple.res b/tests/build_tests/super_errors/fixtures/array_var_passed_to_tuple.res
new file mode 100644
index 00000000000..7fee1bbc8bd
--- /dev/null
+++ b/tests/build_tests/super_errors/fixtures/array_var_passed_to_tuple.res
@@ -0,0 +1,7 @@
+let doStuff = ((one, two)) => {
+  one ++ two
+}
+
+let xx = ["hello", "world"]
+
+let x = doStuff(xx)
diff --git a/tests/build_tests/super_errors/fixtures/dict_helper.res b/tests/build_tests/super_errors/fixtures/dict_helper.res
new file mode 100644
index 00000000000..85e2ba4fea5
--- /dev/null
+++ b/tests/build_tests/super_errors/fixtures/dict_helper.res
@@ -0,0 +1,3 @@
+external takesDict: dict<string> => unit = "takesDict"
+
+takesDict({"test": "1"})
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_element.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_element.res
new file mode 100644
index 00000000000..cebe870397b
--- /dev/null
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_element.res
@@ -0,0 +1,19 @@
+@@config({
+  flags: ["-bs-jsx", "4"],
+})
+
+module React = {
+  type element = Jsx.element
+  type componentLike<'props, 'return> = 'props => 'return
+  type component<'props> = Jsx.component<'props>
+
+  @module("react/jsx-runtime")
+  external jsx: (component<'props>, 'props) => element = "jsx"
+
+  type fragmentProps = {children?: element}
+  @module("react/jsx-runtime") external jsxFragment: component<fragmentProps> = "Fragment"
+
+  external string: string => element = "%identity"
+}
+
+let x = <> {[React.string("")]} </>
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_raw.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_raw.res
new file mode 100644
index 00000000000..3a3c531b503
--- /dev/null
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_array_raw.res
@@ -0,0 +1,17 @@
+@@config({
+  flags: ["-bs-jsx", "4"],
+})
+
+module React = {
+  type element = Jsx.element
+  type componentLike<'props, 'return> = 'props => 'return
+  type component<'props> = Jsx.component<'props>
+
+  @module("react/jsx-runtime")
+  external jsx: (component<'props>, 'props) => element = "jsx"
+
+  type fragmentProps = {children?: element}
+  @module("react/jsx-runtime") external jsxFragment: component<fragmentProps> = "Fragment"
+}
+
+let x = <> {[""]} </>
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_float.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_float.res
new file mode 100644
index 00000000000..208eacac397
--- /dev/null
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_float.res
@@ -0,0 +1,17 @@
+@@config({
+  flags: ["-bs-jsx", "4"],
+})
+
+module React = {
+  type element = Jsx.element
+  type componentLike<'props, 'return> = 'props => 'return
+  type component<'props> = Jsx.component<'props>
+
+  @module("react/jsx-runtime")
+  external jsx: (component<'props>, 'props) => element = "jsx"
+
+  type fragmentProps = {children?: element}
+  @module("react/jsx-runtime") external jsxFragment: component<fragmentProps> = "Fragment"
+}
+
+let x = <> {1.} </>
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_int.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_int.res
new file mode 100644
index 00000000000..6836a4865de
--- /dev/null
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_int.res
@@ -0,0 +1,17 @@
+@@config({
+  flags: ["-bs-jsx", "4"],
+})
+
+module React = {
+  type element = Jsx.element
+  type componentLike<'props, 'return> = 'props => 'return
+  type component<'props> = Jsx.component<'props>
+
+  @module("react/jsx-runtime")
+  external jsx: (component<'props>, 'props) => element = "jsx"
+
+  type fragmentProps = {children?: element}
+  @module("react/jsx-runtime") external jsxFragment: component<fragmentProps> = "Fragment"
+}
+
+let x = <> {1} </>
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_option.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_option.res
new file mode 100644
index 00000000000..334d11978c5
--- /dev/null
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_option.res
@@ -0,0 +1,17 @@
+@@config({
+  flags: ["-bs-jsx", "4"],
+})
+
+module React = {
+  type element = Jsx.element
+  type componentLike<'props, 'return> = 'props => 'return
+  type component<'props> = Jsx.component<'props>
+
+  @module("react/jsx-runtime")
+  external jsx: (component<'props>, 'props) => element = "jsx"
+
+  type fragmentProps = {children?: element}
+  @module("react/jsx-runtime") external jsxFragment: component<fragmentProps> = "Fragment"
+}
+
+let x = <> {None} </>
diff --git a/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_string.res b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_string.res
new file mode 100644
index 00000000000..55ec5786e67
--- /dev/null
+++ b/tests/build_tests/super_errors/fixtures/jsx_type_mismatch_string.res
@@ -0,0 +1,17 @@
+@@config({
+  flags: ["-bs-jsx", "4"],
+})
+
+module React = {
+  type element = Jsx.element
+  type componentLike<'props, 'return> = 'props => 'return
+  type component<'props> = Jsx.component<'props>
+
+  @module("react/jsx-runtime")
+  external jsx: (component<'props>, 'props) => element = "jsx"
+
+  type fragmentProps = {children?: element}
+  @module("react/jsx-runtime") external jsxFragment: component<fragmentProps> = "Fragment"
+}
+
+let x = <> {""} </>
diff --git a/tests/build_tests/super_errors/fixtures/object_passed_when_record_expected.res b/tests/build_tests/super_errors/fixtures/object_literal_passed_when_record_expected.res
similarity index 100%
rename from tests/build_tests/super_errors/fixtures/object_passed_when_record_expected.res
rename to tests/build_tests/super_errors/fixtures/object_literal_passed_when_record_expected.res


Reviews:
user: login: zth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T09:18:30Z
user: login: zth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T09:18:54Z
user: login: zth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T11:10:38Z
user: login: zth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T12:39:21Z
user: login: tsnobip
body: great work really @zth. We could also do the same thing for dicts now that we have dict literals that are kinda close to objects syntactically.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-22T14:04:35Z
user: login: cknitt
body: Awesome stuff!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T14:27:16Z

Review comments:
diff_hunk: @@ -0,0 +1,14 @@
+
+  [1;31mWe've found a bug for you![0m
+  [36m/.../fixtures/array_literal_passed_to_tuple.res[0m:[2m5:17-34[0m
+
+  3 [2m│[0m }
+  4 [2m│[0m 
+  [1;31m5[0m [2m│[0m let x = doStuff([1;31m["hello", "world"][0m)
+  6 [2m│[0m 
+
+  This has type: [1;31marray<'a>[0m
+  But it's expected to have type: [1;33m(string, string)[0m
+
+  - Fix this by passing a tuple instead of an array, like: [1;33m("hello", "world")
+[0m
path: tests/build_tests/super_errors/expected/array_literal_passed_to_tuple.res.expected
user: login: zth
body: When the user wrote an actual array literal, we can suggest the exact code for turning it into a tuple.
created_at: 2025-05-22T09:18:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,13 @@
+
+  [1;31mWe've found a bug for you![0m
+  [36m/.../fixtures/array_var_passed_to_tuple.res[0m:[2m7:17-18[0m
+
+  5 [2m│[0m let xx = ["hello", "world"]
+  6 [2m│[0m 
+  [1;31m7[0m [2m│[0m let x = doStuff([1;31mxx[0m)
+  8 [2m│[0m 
+
+  This has type: [1;31marray<string>[0m
+  But this function argument is expecting: [1;33m(string, string)[0m
+
+  - Fix this by passing a tuple instead of an array[1;33m[0m
path: tests/build_tests/super_errors/expected/array_var_passed_to_tuple.res.expected
user: login: zth
body: When it's not an actual array literal, we just say that you need to pass a tuple instead.
created_at: 2025-05-22T09:18:54Z
author_association: MEMBER
diff_hunk: @@ -9,7 +9,9 @@
 
   This has type: [1;31m{"one": bool}[0m
   But it's expected to have type: [1;33mx[0m
-
-  You're passing a [1;31mReScript object[0m where a [1;33mrecord[0m is expected. 
-
-  - Did you mean to pass a record instead of an object? Objects are written with quoted keys, and records with unquoted keys. Remove the quotes from the object keys to pass it as a record instead of object.
\ No newline at end of file
+  
+  You're passing a [1;31mReScript object[0m where a [1;33mrecord[0m is expected. Objects are written with quoted keys, and records with unquoted keys.
+  
+  Possible solutions: 
+  - Rewrite the object to a record, like: [1;33m{one: true}
+[0m
path: tests/build_tests/super_errors/expected/object_literal_passed_when_record_expected.res.expected
user: login: zth
body: Another example of how we can suggest an actual and correct rewrite here, since we now have the relevant expression to look at.
created_at: 2025-05-22T11:10:38Z
author_association: MEMBER
diff_hunk: @@ -41,6 +69,7 @@ let process_file sourcefile ?kind ppf =
      properly
   *)
   setup_outcome_printer ();
+  Error_message_utils_support.setup ();
path: compiler/bsc/rescript_compiler_main.ml
user: login: zth
body: I think `process_file` is the appropriate place to run this, but maybe there's a better place, idk.
created_at: 2025-05-22T12:39:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,14 @@
+
+  [1;31mWe've found a bug for you![0m
+  [36m/.../fixtures/array_literal_passed_to_tuple.res[0m:[2m5:17-34[0m
+
+  3 [2m│[0m }
+  4 [2m│[0m 
+  [1;31m5[0m [2m│[0m let x = doStuff([1;31m["hello", "world"][0m)
+  6 [2m│[0m 
+
+  This has type: [1;31marray<'a>[0m
+  But it's expected to have type: [1;33m(string, string)[0m
+
+  - Fix this by passing a tuple instead of an array, like: [1;33m("hello", "world")
+[0m
path: tests/build_tests/super_errors/expected/array_literal_passed_to_tuple.res.expected
user: login: tsnobip
body: awesome!
created_at: 2025-05-22T14:01:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,14 @@
+
+  [1;31mWe've found a bug for you![0m
+  [36m/.../fixtures/jsx_type_mismatch_option.res[0m:[2m17:13-16[0m
+
+  15 [2m│[0m }
+  16 [2m│[0m 
+  [1;31m17[0m [2m│[0m let x = <> {[1;31mNone[0m} </>
+  18 [2m│[0m 
+
+  This has type: [1;31moption<'a>[0m
+  But it's expected to have type: [1;33mReact.element[0m [2m(defined as[0m [1;33mJsx.element[0m[2m)[0m
+  
+  You need to unwrap this option to its underlying value first, then turn that value into a JSX element.
+  For [1;33mNone[0m, you can use [1;33mReact.null[0m to output nothing into JSX.
path: tests/build_tests/super_errors/expected/jsx_type_mismatch_option.res.expected
user: login: tsnobip
body: this is nice because not obvious for newcomers!
created_at: 2025-05-22T14:02:27Z
author_association: MEMBER
diff_hunk: @@ -9,7 +9,9 @@
 
   This has type: [1;31m{"one": bool}[0m
   But it's expected to have type: [1;33mx[0m
-
-  You're passing a [1;31mReScript object[0m where a [1;33mrecord[0m is expected. 
-
-  - Did you mean to pass a record instead of an object? Objects are written with quoted keys, and records with unquoted keys. Remove the quotes from the object keys to pass it as a record instead of object.
\ No newline at end of file
+  
+  You're passing a [1;31mReScript object[0m where a [1;33mrecord[0m is expected. Objects are written with quoted keys, and records with unquoted keys.
+  
+  Possible solutions: 
+  - Rewrite the object to a record, like: [1;33m{one: true}
+[0m
path: tests/build_tests/super_errors/expected/object_literal_passed_when_record_expected.res.expected
user: login: tsnobip
body: this is great too! We could also do the same thing for mismatch of objects vs dicts now that we have dict literals, they could become quite common for configs.
created_at: 2025-05-22T14:03:28Z
author_association: MEMBER
diff_hunk: @@ -13,7 +13,7 @@
   Floats and ints have their own mathematical operators. This means you cannot add a float and an int without converting between the two.
 
   Possible solutions:
-  - Ensure all values in this calculation has the type [1;33mfloat[0m. You can convert between floats and ints via [1;33mBelt.Float.toInt[0m and [1;33mBelt.Int.fromFloat[0m.
+  - Ensure all values in this calculation has the type [1;33mfloat[0m. You can convert between floats and ints via [1;33mFloat.toInt[0m and [1;33mInt.fromFloat[0m.
path: tests/build_tests/super_errors/expected/math_operator_float.res.expected
user: login: cknitt
body: ```suggestion
  - Ensure all values in this calculation have the type [1;33mfloat[0m. You can convert between floats and ints via [1;33mFloat.toInt[0m and [1;33mInt.fromFloat[0m.
```
created_at: 2025-05-22T14:25:50Z
author_association: MEMBER

PR comments:
user: login: zth
created_at: 2025-05-22T11:11:46Z
body: @mediremi @nojaf FYI, the work in this PR can probably serve as a good foundation for "compiler error message driven code actions" that we've been exploring before. Just need to figure out how to integrate it in a good way.
author_association: MEMBER
user: login: pkg-pr-new[bot]
created_at: 2025-05-22T11:25:47Z
body: 
[Open in StackBlitz](https://pkg.pr.new/template/72ace126-2a2d-485a-8b4c-d2d1c1e3695a)


<details><summary><b>rescript</b></summary><p>

```
npm i https://pkg.pr.new/rescript-lang/rescript@7500
```

</p></details>
      
    

<details><summary><b>@rescript/darwin-x64</b></summary><p>

```
npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-x64@7500
```

</p></details>
      
    

<details><summary><b>@rescript/linux-arm64</b></summary><p>

```
npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-arm64@7500
```

</p></details>
      
    

<details><summary><b>@rescript/darwin-arm64</b></summary><p>

```
npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-arm64@7500
```

</p></details>
      
    

<details><summary><b>@rescript/win32-x64</b></summary><p>

```
npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/win32-x64@7500
```

</p></details>
      
    

<details><summary><b>@rescript/linux-x64</b></summary><p>

```
npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-x64@7500
```

</p></details>
      
    

_commit: <a href="https://github.com/rescript-lang/rescript/runs/42726933987"><code>2f2dcb7</code></a>_

author_association: NONE
user: login: zth
created_at: 2025-05-22T12:47:20Z
body: @cknitt @tsnobip another round of error message feedback please! 😄 
author_association: MEMBER
user: login: zth
created_at: 2025-05-22T14:12:14Z
body: > great work really @zth. We could also do the same thing for dicts now that we have dict literals that are kinda close to objects syntactically.

Oh, great idea.
author_association: MEMBER
user: login: zth
created_at: 2025-05-22T14:41:29Z
body: @tsnobip what error messages would you add for dicts?
author_association: MEMBER
user: login: tsnobip
created_at: 2025-05-22T15:02:52Z
body: > @tsnobip what error messages would you add for dicts?

@zth Something like this when an object literal is provided but it should actually be a dict:

```
 You're passing a [1;31mReScript object[0m where a [1;dict[0m is expected. Dict literals start with dict{.

  Possible solutions: 
  - Rewrite the object to a dict, like: [1;33mdict{"one": true}
```
author_association: MEMBER
user: login: zth
created_at: 2025-05-22T16:02:35Z
body: > @zth Something like this when an object literal is provided but it should actually be a dict:
> 
> ```
>  You're passing a �[1;31mReScript object�[0m where a �[1;dict�[0m is expected. Dict literals start with dict{.
> 
>   Possible solutions: 
>   - Rewrite the object to a dict, like: �[1;33mdict{"one": true}
> ```

The rewrite part is harder with dict since we need to make sure that they'd be compatible. But I guess some variant of it could be done. Might do it in a follow up though.

author_association: MEMBER
user: login: zth
created_at: 2025-05-22T16:06:07Z
body: @tsnobip did a basic one in: https://github.com/rescript-lang/rescript/pull/7500/commits/d2cac6720768c020330a1d1da8267b1832718650

We can refine it later.
author_association: MEMBER