Skip to content

Commit 7953354

Browse files
authored
Hint in error for string constants matching expected variant (#7711)
* add dedicated error message for when passing a string constant to something that expects a variant/polyvariant, and that string constant matches one of the constructors * changelog
1 parent e8b4764 commit 7953354

File tree

6 files changed

+163
-1
lines changed

6 files changed

+163
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
- 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
2727
- Better error message if platform binaries package is not found. https://github.com/rescript-lang/rescript/pull/7698
28+
- Hint in error for string constants matching expected variant/polyvariant constructor. https://github.com/rescript-lang/rescript/pull/7711
2829
- Polish arity mismatch error message a bit. https://github.com/rescript-lang/rescript/pull/7709
2930

3031
#### :house: Internal

compiler/ml/error_message_utils.ml

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,13 +198,40 @@ let error_expected_type_text ppf type_clash_context =
198198
| Some MaybeUnwrapOption | None ->
199199
fprintf ppf "But it's expected to have type:"
200200
201-
let is_record_type ~extract_concrete_typedecl ~env ty =
201+
let is_record_type ~(extract_concrete_typedecl : extract_concrete_typedecl) ~env
202+
ty =
202203
try
203204
match extract_concrete_typedecl env ty with
204205
| _, _, {Types.type_kind = Type_record _; _} -> true
205206
| _ -> false
206207
with _ -> false
207208
209+
let is_variant_type ~(extract_concrete_typedecl : extract_concrete_typedecl)
210+
~env ty =
211+
try
212+
match extract_concrete_typedecl env ty with
213+
| _, _, {Types.type_kind = Type_variant _; _} -> true
214+
| _ -> false
215+
with _ -> false
216+
217+
let get_variant_constructors
218+
~(extract_concrete_typedecl : extract_concrete_typedecl) ~env ty =
219+
match extract_concrete_typedecl env ty with
220+
| _, _, {Types.type_kind = Type_variant constructors; _} -> constructors
221+
| _ -> []
222+
223+
let extract_string_constant text =
224+
match !Parser.parse_source text with
225+
| ( [
226+
{
227+
Parsetree.pstr_desc =
228+
Pstr_eval ({pexp_desc = Pexp_constant (Pconst_string (s, _))}, _);
229+
};
230+
],
231+
_ ) ->
232+
Some s
233+
| _ -> None
234+
208235
let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
209236
(bottom_aliases : (Types.type_expr * Types.type_expr) option)
210237
type_clash_context =
@@ -496,6 +523,91 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
496523
with @{<info>?@} to show you want to pass the option, like: \
497524
@{<info>?%s@}"
498525
(Parser.extract_text_at_loc loc)
526+
| _, Some ({Types.desc = Tconstr (p1, _, _)}, {desc = Tvariant row_desc})
527+
when Path.same Predef.path_string p1 -> (
528+
(* Check if we have a string constant that could be a polymorphic variant constructor *)
529+
let target_expr_text = Parser.extract_text_at_loc loc in
530+
match extract_string_constant target_expr_text with
531+
| Some string_value -> (
532+
let variant_constructors = List.map fst row_desc.row_fields in
533+
let reprinted =
534+
Parser.reprint_expr_at_loc loc ~mapper:(fun exp ->
535+
match exp.Parsetree.pexp_desc with
536+
| Pexp_constant (Pconst_string (s, _)) ->
537+
Some {exp with Parsetree.pexp_desc = Pexp_variant (s, None)}
538+
| _ -> None)
539+
in
540+
match (reprinted, List.mem string_value variant_constructors) with
541+
| Some reprinted, true ->
542+
fprintf ppf
543+
"\n\n\
544+
\ Possible solutions:\n\
545+
\ - The constant passed matches one of the expected polymorphic \
546+
variant constructors. Did you mean to pass this as a polymorphic \
547+
variant? If so, rewrite @{<info>\"%s\"@} to @{<info>%s@}"
548+
string_value reprinted
549+
| _ -> ())
550+
| None -> ())
551+
| _, Some ({Types.desc = Tconstr (p1, _, _)}, ({desc = Tconstr _} as t2))
552+
when Path.same Predef.path_string p1
553+
&& is_variant_type ~extract_concrete_typedecl ~env t2 -> (
554+
(* Check if we have a string constant that could be a regular variant constructor *)
555+
let target_expr_text = Parser.extract_text_at_loc loc in
556+
match extract_string_constant target_expr_text with
557+
| Some string_value -> (
558+
let constructors =
559+
get_variant_constructors ~extract_concrete_typedecl ~env t2
560+
in
561+
(* Extract runtime representations from constructor declarations *)
562+
let constructor_mappings =
563+
List.filter_map
564+
(fun (cd : Types.constructor_declaration) ->
565+
let constructor_name = Ident.name cd.cd_id in
566+
let runtime_repr =
567+
match Ast_untagged_variants.process_tag_type cd.cd_attributes with
568+
| Some (String s) -> Some s (* @as("string_value") *)
569+
| Some _ -> None (* @as with non-string values *)
570+
| None -> Some constructor_name (* No @as, use constructor name *)
571+
in
572+
match runtime_repr with
573+
| Some repr -> Some (repr, constructor_name)
574+
| None -> None)
575+
constructors
576+
in
577+
let matching_constructor =
578+
List.find_opt
579+
(fun (runtime_repr, _) -> runtime_repr = string_value)
580+
constructor_mappings
581+
in
582+
match matching_constructor with
583+
| Some (_, constructor_name) -> (
584+
let reprinted =
585+
Parser.reprint_expr_at_loc loc ~mapper:(fun exp ->
586+
match exp.Parsetree.pexp_desc with
587+
| Pexp_constant (Pconst_string (_, _)) ->
588+
Some
589+
{
590+
exp with
591+
Parsetree.pexp_desc =
592+
Pexp_construct
593+
( {txt = Lident constructor_name; loc = exp.pexp_loc},
594+
None );
595+
}
596+
| _ -> None)
597+
in
598+
match reprinted with
599+
| Some reprinted ->
600+
fprintf ppf
601+
"\n\n\
602+
\ Possible solutions:\n\
603+
\ - The constant passed matches the runtime representation of one \
604+
of the expected variant constructors. Did you mean to pass this \
605+
as a variant constructor? If so, rewrite @{<info>\"%s\"@} to \
606+
@{<info>%s@}"
607+
string_value reprinted
608+
| None -> ())
609+
| None -> ())
610+
| _ -> ())
499611
| _, Some (t1, t2) ->
500612
let can_show_coercion_message =
501613
match (t1.desc, t2.desc) with
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/string_constant_to_polyvariant.res:8:20-24
4+
5+
6 │ }
6+
7 │
7+
8 │ let x = doStuff(1, "ONE")
8+
9 │
9+
10+
This has type: string
11+
But this function argument is expecting: [#ONE | #TWO]
12+
13+
Possible solutions:
14+
- 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
15+

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/string_constant_to_variant.res:11:28-35
4+
5+
9 │ }
6+
10 │
7+
11 │ let result = processStatus("Active")
8+
12 │
9+
10+
This has type: string
11+
But this function argument is expecting: status
12+
13+
Possible solutions:
14+
- 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
15+

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
let doStuff = (a: int, b: [#ONE | #TWO]) => {
2+
switch b {
3+
| #ONE => a + 1
4+
| #TWO => a + 2
5+
}
6+
}
7+
8+
let x = doStuff(1, "ONE")
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
type status = Active | Inactive | Pending
2+
3+
let processStatus = (s: status) => {
4+
switch s {
5+
| Active => "active"
6+
| Inactive => "inactive"
7+
| Pending => "pending"
8+
}
9+
}
10+
11+
let result = processStatus("Active")

0 commit comments

Comments
 (0)