Skip to content

Fix missing checks for duplicate literals in variants with payloads. #7441

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 1 commit into from
May 8, 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 @@ -32,6 +32,7 @@
- Fix leading comments removed when braces inside JSX contains `let` assignment. https://github.com/rescript-lang/rescript/pull/7424
- Fix JSON escaping in code editor analysis: JSON was not always escaped properly, which prevented code actions from being available in certain situations https://github.com/rescript-lang/rescript/pull/7435
- Fix regression in pattern matching for optional fields containing variants. https://github.com/rescript-lang/rescript/pull/7440
- Fix missing checks for duplicate literals in variants with payloads. https://github.com/rescript-lang/rescript/pull/7441

#### :house: Internal

Expand Down
57 changes: 36 additions & 21 deletions compiler/ml/ast_untagged_variants.ml
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,10 @@ let is_nullary_variant (x : Types.constructor_arguments) =
let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list)
~(blocks : (Location.t * block) list) =
let module StringSet = Set.Make (String) in
let string_literals = ref StringSet.empty in
let nonstring_literals = ref StringSet.empty in
let string_literals_consts = ref StringSet.empty in
let string_literals_blocks = ref StringSet.empty in
let nonstring_literals_consts = ref StringSet.empty in
let nonstring_literals_blocks = ref StringSet.empty in
let instance_types = Hashtbl.create 1 in
let function_types = ref 0 in
let object_types = ref 0 in
Expand All @@ -268,15 +270,21 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list)
let bigint_types = ref 0 in
let boolean_types = ref 0 in
let unknown_types = ref 0 in
let add_string_literal ~loc s =
if StringSet.mem s !string_literals then
let add_string_literal ~is_const ~loc s =
let set =
if is_const then string_literals_consts else string_literals_blocks
in
if StringSet.mem s !set then
raise (Error (loc, InvalidUntaggedVariantDefinition (DuplicateLiteral s)));
string_literals := StringSet.add s !string_literals
set := StringSet.add s !set
in
let add_nonstring_literal ~loc s =
if StringSet.mem s !nonstring_literals then
let add_nonstring_literal ~is_const ~loc s =
let set =
if is_const then nonstring_literals_consts else nonstring_literals_blocks
in
if StringSet.mem s !set then
raise (Error (loc, InvalidUntaggedVariantDefinition (DuplicateLiteral s)));
nonstring_literals := StringSet.add s !nonstring_literals
set := StringSet.add s !set
in
let invariant loc name =
if !unknown_types <> 0 && List.length blocks <> 1 then
Expand All @@ -302,23 +310,27 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list)
raise (Error (loc, InvalidUntaggedVariantDefinition AtMostOneBoolean));
if
!boolean_types > 0
&& (StringSet.mem "true" !nonstring_literals
|| StringSet.mem "false" !nonstring_literals)
&& (StringSet.mem "true" !nonstring_literals_consts
|| StringSet.mem "false" !nonstring_literals_consts)
then raise (Error (loc, InvalidUntaggedVariantDefinition AtMostOneBoolean));
()
in
let check_literal ~is_const ~loc (literal : tag) =
match literal.tag_type with
| Some (String s) -> add_string_literal ~is_const ~loc s
| Some (Int i) -> add_nonstring_literal ~is_const ~loc (string_of_int i)
| Some (Float f) -> add_nonstring_literal ~is_const ~loc f
| Some (BigInt i) -> add_nonstring_literal ~is_const ~loc i
| Some Null -> add_nonstring_literal ~is_const ~loc "null"
| Some Undefined -> add_nonstring_literal ~is_const ~loc "undefined"
| Some (Bool b) ->
add_nonstring_literal ~is_const ~loc (if b then "true" else "false")
| Some (Untagged _) -> ()
| None -> add_string_literal ~is_const ~loc literal.name
in

Ext_list.rev_iter consts (fun (loc, literal) ->
match literal.tag_type with
| Some (String s) -> add_string_literal ~loc s
| Some (Int i) -> add_nonstring_literal ~loc (string_of_int i)
| Some (Float f) -> add_nonstring_literal ~loc f
| Some (BigInt i) -> add_nonstring_literal ~loc i
| Some Null -> add_nonstring_literal ~loc "null"
| Some Undefined -> add_nonstring_literal ~loc "undefined"
| Some (Bool b) ->
add_nonstring_literal ~loc (if b then "true" else "false")
| Some (Untagged _) -> ()
| None -> add_string_literal ~loc literal.name);
check_literal ~is_const:true ~loc literal);
if is_untagged_def then
Ext_list.rev_iter blocks (fun (loc, block) ->
match block.block_type with
Expand All @@ -338,6 +350,9 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list)
| StringType -> incr string_types);
invariant loc block.tag.name
| None -> ())
else
Ext_list.rev_iter blocks (fun (loc, block) ->
check_literal ~is_const:false ~loc block.tag)

let get_cstr_loc_tag (cstr : Types.constructor_declaration) =
( cstr.cd_loc,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

We've found a bug for you!
/.../fixtures/multiple_tag_1.res:3:3-19

1 │ type ambiguous1 =
2 │ | @as("x") A(int)
3 │ | @as("x") B(int)
4 │

This untagged variant definition is invalid: Duplicate literal x.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

We've found a bug for you!
/.../fixtures/multiple_tag_2.res:3:3-17

1 │ type ambiguous2 =
2 │ | @as(3) A(int)
3 │ | @as(3) B(int)
4 │

This untagged variant definition is invalid: Duplicate literal 3.
3 changes: 3 additions & 0 deletions tests/build_tests/super_errors/fixtures/multiple_tag_1.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type ambiguous1 =
| @as("x") A(int)
| @as("x") B(int)
3 changes: 3 additions & 0 deletions tests/build_tests/super_errors/fixtures/multiple_tag_2.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type ambiguous2 =
| @as(3) A(int)
| @as(3) B(int)
27 changes: 27 additions & 0 deletions tests/tests/src/multiple_tags.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Generated by ReScript, PLEASE EDIT WITH CARE


let a1 = {
TAG: 3,
_0: 10
};

let b1 = {
TAG: "3",
_0: 10
};

let a2 = "x";

let b2 = {
TAG: "x",
_0: 10
};

export {
a1,
b1,
a2,
b2,
}
/* No side effect */
13 changes: 13 additions & 0 deletions tests/tests/src/multiple_tags.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type unambiguous1 =
| @as(3) A(int)
| @as("3") B(int)

let a1 = A(10)
let b1 = B(10)

type un_ambiguous2 =
| @as("x") A
| @as("x") B(int)

let a2 = A
let b2 = B(10)