Skip to content

Hint in error for string constants matching expected variant #7711

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

- Configuration fields `bs-dependencies`, `bs-dev-dependencies` and `bsc-flags` are now deprecated in favor of `dependencies`, `dev-dependencies` and `compiler-flags`. https://github.com/rescript-lang/rescript/pull/7658
- Better error message if platform binaries package is not found. https://github.com/rescript-lang/rescript/pull/7698
- Hint in error for string constants matching expected variant/polyvariant constructor. https://github.com/rescript-lang/rescript/pull/7711
- Polish arity mismatch error message a bit. https://github.com/rescript-lang/rescript/pull/7709

#### :house: Internal
Expand Down
114 changes: 113 additions & 1 deletion compiler/ml/error_message_utils.ml
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,40 @@ let error_expected_type_text ppf type_clash_context =
| Some MaybeUnwrapOption | None ->
fprintf ppf "But it's expected to have type:"

let is_record_type ~extract_concrete_typedecl ~env ty =
let is_record_type ~(extract_concrete_typedecl : extract_concrete_typedecl) ~env
ty =
try
match extract_concrete_typedecl env ty with
| _, _, {Types.type_kind = Type_record _; _} -> true
| _ -> false
with _ -> false

let is_variant_type ~(extract_concrete_typedecl : extract_concrete_typedecl)
~env ty =
try
match extract_concrete_typedecl env ty with
| _, _, {Types.type_kind = Type_variant _; _} -> true
| _ -> false
with _ -> false

let get_variant_constructors
~(extract_concrete_typedecl : extract_concrete_typedecl) ~env ty =
match extract_concrete_typedecl env ty with
| _, _, {Types.type_kind = Type_variant constructors; _} -> constructors
| _ -> []

let extract_string_constant text =
match !Parser.parse_source text with
| ( [
{
Parsetree.pstr_desc =
Pstr_eval ({pexp_desc = Pexp_constant (Pconst_string (s, _))}, _);
};
],
_ ) ->
Some s
| _ -> None

let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
(bottom_aliases : (Types.type_expr * Types.type_expr) option)
type_clash_context =
Expand Down Expand Up @@ -496,6 +523,91 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
with @{<info>?@} to show you want to pass the option, like: \
@{<info>?%s@}"
(Parser.extract_text_at_loc loc)
| _, Some ({Types.desc = Tconstr (p1, _, _)}, {desc = Tvariant row_desc})
when Path.same Predef.path_string p1 -> (
(* Check if we have a string constant that could be a polymorphic variant constructor *)
let target_expr_text = Parser.extract_text_at_loc loc in
match extract_string_constant target_expr_text with
| Some string_value -> (
let variant_constructors = List.map fst row_desc.row_fields in
let reprinted =
Parser.reprint_expr_at_loc loc ~mapper:(fun exp ->
match exp.Parsetree.pexp_desc with
| Pexp_constant (Pconst_string (s, _)) ->
Some {exp with Parsetree.pexp_desc = Pexp_variant (s, None)}
| _ -> None)
in
match (reprinted, List.mem string_value variant_constructors) with
| Some reprinted, true ->
fprintf ppf
"\n\n\
\ Possible solutions:\n\
\ - The constant passed matches one of the expected polymorphic \
variant constructors. Did you mean to pass this as a polymorphic \
variant? If so, rewrite @{<info>\"%s\"@} to @{<info>%s@}"
string_value reprinted
| _ -> ())
| None -> ())
| _, Some ({Types.desc = Tconstr (p1, _, _)}, ({desc = Tconstr _} as t2))
when Path.same Predef.path_string p1
&& is_variant_type ~extract_concrete_typedecl ~env t2 -> (
(* Check if we have a string constant that could be a regular variant constructor *)
let target_expr_text = Parser.extract_text_at_loc loc in
match extract_string_constant target_expr_text with
| Some string_value -> (
let constructors =
get_variant_constructors ~extract_concrete_typedecl ~env t2
in
(* Extract runtime representations from constructor declarations *)
let constructor_mappings =
List.filter_map
(fun (cd : Types.constructor_declaration) ->
let constructor_name = Ident.name cd.cd_id in
let runtime_repr =
match Ast_untagged_variants.process_tag_type cd.cd_attributes with
| Some (String s) -> Some s (* @as("string_value") *)
| Some _ -> None (* @as with non-string values *)
| None -> Some constructor_name (* No @as, use constructor name *)
in
match runtime_repr with
| Some repr -> Some (repr, constructor_name)
| None -> None)
constructors
in
let matching_constructor =
List.find_opt
(fun (runtime_repr, _) -> runtime_repr = string_value)
constructor_mappings
in
match matching_constructor with
| Some (_, constructor_name) -> (
let reprinted =
Parser.reprint_expr_at_loc loc ~mapper:(fun exp ->
match exp.Parsetree.pexp_desc with
| Pexp_constant (Pconst_string (_, _)) ->
Some
{
exp with
Parsetree.pexp_desc =
Pexp_construct
( {txt = Lident constructor_name; loc = exp.pexp_loc},
None );
}
| _ -> None)
in
match reprinted with
| Some reprinted ->
fprintf ppf
"\n\n\
\ Possible solutions:\n\
\ - The constant passed matches the runtime representation of one \
of the expected variant constructors. Did you mean to pass this \
as a variant constructor? If so, rewrite @{<info>\"%s\"@} to \
@{<info>%s@}"
string_value reprinted
| None -> ())
| None -> ())
| _ -> ())
| _, Some (t1, t2) ->
let can_show_coercion_message =
match (t1.desc, t2.desc) with
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

We've found a bug for you!
/.../fixtures/string_constant_to_polyvariant.res:8:20-24

6 │ }
7 │
8 │ let x = doStuff(1, "ONE")
9 │

This has type: string
But this function argument is expecting: [#ONE | #TWO]

Possible solutions:
- The constant passed matches one of the expected polymorphic variant constructors. Did you mean to pass this as a polymorphic variant? If so, rewrite "ONE" to #ONE

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

We've found a bug for you!
/.../fixtures/string_constant_to_variant.res:11:28-35

9 │ }
10 │
11 │ let result = processStatus("Active")
12 │

This has type: string
But this function argument is expecting: status

Possible solutions:
- The constant passed matches the runtime representation of one of the expected variant constructors. Did you mean to pass this as a variant constructor? If so, rewrite "Active" to Active

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
let doStuff = (a: int, b: [#ONE | #TWO]) => {
switch b {
| #ONE => a + 1
| #TWO => a + 2
}
}

let x = doStuff(1, "ONE")
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
type status = Active | Inactive | Pending

let processStatus = (s: status) => {
switch s {
| Active => "active"
| Inactive => "inactive"
| Pending => "pending"
}
}

let result = processStatus("Active")