From 79ac089461e975114d6ff6194feb61009dfd2c3c Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 18 Sep 2025 16:30:46 +0200 Subject: [PATCH 1/2] specialized errors for when colon and equals are mixed up in records, function arguments, and more --- compiler/syntax/src/res_core.ml | 148 +++++++++++++++++- .../labelledArgumentMissingEqual.res.txt | 22 +++ .../labelledArgumentMissingEqual.res | 2 + .../recordFieldWrongAssignment.res.txt | 10 ++ .../pattern/recordFieldWrongAssignment.res | 1 + .../recordFieldWrongAssignment.res.txt | 10 ++ .../structure/recordFieldWrongAssignment.res | 1 + .../recordFieldWrongAssignment.res.txt | 11 ++ .../typeDef/recordFieldWrongAssignment.res | 1 + 9 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 tests/syntax_tests/data/parsing/errors/expressions/expected/labelledArgumentMissingEqual.res.txt create mode 100644 tests/syntax_tests/data/parsing/errors/expressions/labelledArgumentMissingEqual.res create mode 100644 tests/syntax_tests/data/parsing/errors/pattern/expected/recordFieldWrongAssignment.res.txt create mode 100644 tests/syntax_tests/data/parsing/errors/pattern/recordFieldWrongAssignment.res create mode 100644 tests/syntax_tests/data/parsing/errors/structure/expected/recordFieldWrongAssignment.res.txt create mode 100644 tests/syntax_tests/data/parsing/errors/structure/recordFieldWrongAssignment.res create mode 100644 tests/syntax_tests/data/parsing/errors/typeDef/expected/recordFieldWrongAssignment.res.txt create mode 100644 tests/syntax_tests/data/parsing/errors/typeDef/recordFieldWrongAssignment.res diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index 08889c8ce3..999743ac21 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -99,6 +99,24 @@ module ErrorMessages = struct let dict_expr_spread = "Dict literals do not support spread (`...`) yet." + let record_field_missing_colon = + "Records use `:` when assigning fields. Example: `{field: value}`" + + let record_pattern_field_missing_colon = + "Record patterns use `:` when matching fields. Example: `{field: value}`" + + let record_type_field_missing_colon = + "Record fields in type declarations use `:`. Example: `{field: string}`" + + let dict_field_missing_colon = + "Dict entries use `:` to separate keys from values. Example: `{\"k\": v}`" + + let labelled_argument_missing_equal = + "Use `=` to pass a labelled argument. Example: `~label=value`" + + let optional_labelled_argument_missing_equal = + "Optional labelled arguments use `=?`. Example: `~label=?value`" + let variant_ident = "A polymorphic variant (e.g. #id) must start with an alphabetical letter \ or be a number (e.g. #742)" @@ -1414,6 +1432,13 @@ and parse_record_pattern_row_field ~attrs p = let optional = parse_optional_label p in let pat = parse_pattern p in (pat, optional) + | Equal -> + Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p + (Diagnostics.message ErrorMessages.record_pattern_field_missing_colon); + Parser.next p; + let optional = parse_optional_label p in + let pat = parse_pattern p in + (pat, optional) | _ -> ( Ast_helper.Pat.var ~loc:label.loc ~attrs (Location.mkloc (Longident.last label.txt) label.loc), @@ -3062,6 +3087,19 @@ and parse_braced_or_record_expr p = in Parser.expect Rbrace p; expr + | Equal -> + Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p + (Diagnostics.message ErrorMessages.record_field_missing_colon); + Parser.next p; + let field_expr = parse_expr p in + Parser.optional p Comma |> ignore; + let expr = + parse_record_expr_with_string_keys ~start_pos + {Parsetree.lid = field; x = field_expr; opt = false} + p + in + Parser.expect Rbrace p; + expr | _ -> ( let tag = if p.mode = ParseForTypeChecker then Some "js" else None in let constant = @@ -3155,6 +3193,28 @@ and parse_braced_or_record_expr p = in Parser.expect Rbrace p; expr) + | Equal -> ( + Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p + (Diagnostics.message ErrorMessages.record_field_missing_colon); + Parser.next p; + let optional = parse_optional_label p in + let field_expr = parse_expr p in + match p.Parser.token with + | Rbrace -> + Parser.next p; + let loc = mk_loc start_pos p.prev_end_pos in + Ast_helper.Exp.record ~loc + [{lid = path_ident; x = field_expr; opt = optional}] + None + | _ -> + Parser.expect Comma p; + let expr = + parse_record_expr ~start_pos + [{lid = path_ident; x = field_expr; opt = optional}] + p + in + Parser.expect Rbrace p; + expr) (* error case *) | Lident _ -> if p.prev_end_pos.pos_lnum < p.start_pos.pos_lnum then ( @@ -3297,6 +3357,12 @@ and parse_record_expr_row_with_string_key p : Parser.next p; let field_expr = parse_expr p in Some {lid = field; x = field_expr; opt = false} + | Equal -> + Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p + (Diagnostics.message ErrorMessages.record_field_missing_colon); + Parser.next p; + let field_expr = parse_expr p in + Some {lid = field; x = field_expr; opt = false} | _ -> Some { @@ -3326,6 +3392,13 @@ and parse_record_expr_row p : let optional = parse_optional_label p in let field_expr = parse_expr p in Some {lid = field; x = field_expr; opt = optional} + | Equal -> + Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p + (Diagnostics.message ErrorMessages.record_field_missing_colon); + Parser.next p; + let optional = parse_optional_label p in + let field_expr = parse_expr p in + Some {lid = field; x = field_expr; opt = optional} | _ -> let value = Ast_helper.Exp.ident ~loc:field.loc ~attrs field in let value = @@ -3385,6 +3458,12 @@ and parse_dict_expr_row p = Parser.next p; let fieldExpr = parse_expr p in Some (field, fieldExpr) + | Equal -> + Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p + (Diagnostics.message ErrorMessages.dict_field_missing_colon); + Parser.next p; + let fieldExpr = parse_expr p in + Some (field, fieldExpr) | _ -> Some (field, Ast_helper.Exp.ident ~loc:field.loc field)) | _ -> None @@ -3889,12 +3968,42 @@ and parse_argument2 p : argument option = in Some {label; expr} | Colon -> + let colon_start = p.start_pos in Parser.next p; - let typ = parse_typ_expr p in - let loc = mk_loc start_pos p.prev_end_pos in - let expr = Ast_helper.Exp.constraint_ ~loc ident_expr typ in - Some - {label = Asttypes.Labelled {txt = ident; loc = named_arg_loc}; expr} + let colon_end = p.prev_end_pos in + if Grammar.is_typ_expr_start p.Parser.token then + let typ = parse_typ_expr p in + let loc = mk_loc start_pos p.prev_end_pos in + let expr = Ast_helper.Exp.constraint_ ~loc ident_expr typ in + Some + {label = Asttypes.Labelled {txt = ident; loc = named_arg_loc}; expr} + else + let label, expr = + match p.Parser.token with + | Question -> + Parser.err ~start_pos:colon_start ~end_pos:colon_end p + (Diagnostics.message + ErrorMessages.optional_labelled_argument_missing_equal); + Parser.next p; + let expr = parse_constrained_or_coerced_expr p in + (Asttypes.Optional {txt = ident; loc = named_arg_loc}, expr) + | _ -> + Parser.err ~start_pos:colon_start ~end_pos:colon_end p + (Diagnostics.message + ErrorMessages.labelled_argument_missing_equal); + let expr = + match p.Parser.token with + | Underscore + when not (is_es6_arrow_expression ~in_ternary:false p) -> + let loc = mk_loc p.start_pos p.end_pos in + Parser.next p; + Ast_helper.Exp.ident ~loc + (Location.mkloc (Longident.Lident "_") loc) + | _ -> parse_constrained_or_coerced_expr p + in + (Asttypes.Labelled {txt = ident; loc = named_arg_loc}, expr) + in + Some {label; expr} | _ -> Some { @@ -4791,7 +4900,13 @@ and parse_string_field_declaration p = let name_end_pos = p.end_pos in Parser.next p; let field_name = Location.mkloc name (mk_loc name_start_pos name_end_pos) in - Parser.expect ~grammar:Grammar.TypeExpression Colon p; + (match p.Parser.token with + | Colon -> Parser.next p + | Equal -> + Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p + (Diagnostics.message ErrorMessages.record_type_field_missing_colon); + Parser.next p + | _ -> Parser.expect ~grammar:Grammar.TypeExpression Colon p); let typ = parse_poly_type_expr p in Some (Parsetree.Otag (field_name, attrs, typ)) | DotDotDot -> @@ -4804,7 +4919,13 @@ and parse_string_field_declaration p = (Diagnostics.message (ErrorMessages.object_quoted_field_name name)); Parser.next p; let field_name = Location.mkloc name name_loc in - Parser.expect ~grammar:Grammar.TypeExpression Colon p; + (match p.Parser.token with + | Colon -> Parser.next p + | Equal -> + Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p + (Diagnostics.message ErrorMessages.record_type_field_missing_colon); + Parser.next p + | _ -> Parser.expect ~grammar:Grammar.TypeExpression Colon p); let typ = parse_poly_type_expr p in Some (Parsetree.Otag (field_name, attrs, typ)) | _token -> None @@ -4833,6 +4954,14 @@ and parse_field_declaration ?current_type_name_path ?inline_types_context p = extend_current_type_name_path current_type_name_path name.txt in parse_poly_type_expr ?current_type_name_path ?inline_types_context p + | Equal -> + Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p + (Diagnostics.message ErrorMessages.record_type_field_missing_colon); + Parser.next p; + let current_type_name_path = + extend_current_type_name_path current_type_name_path name.txt + in + parse_poly_type_expr ?current_type_name_path ?inline_types_context p | _ -> Ast_helper.Typ.constr ~loc:name.loc {name with txt = Lident name.txt} [] in @@ -4874,6 +5003,11 @@ and parse_field_declaration_region ?current_type_name_path ?inline_types_context | Colon -> Parser.next p; parse_poly_type_expr ?current_type_name_path ?inline_types_context p + | Equal -> + Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p + (Diagnostics.message ErrorMessages.record_type_field_missing_colon); + Parser.next p; + parse_poly_type_expr ?current_type_name_path ?inline_types_context p | _ -> Ast_helper.Typ.constr ~loc:name.loc ~attrs {name with txt = Lident name.txt} diff --git a/tests/syntax_tests/data/parsing/errors/expressions/expected/labelledArgumentMissingEqual.res.txt b/tests/syntax_tests/data/parsing/errors/expressions/expected/labelledArgumentMissingEqual.res.txt new file mode 100644 index 0000000000..7d83b0172f --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/expressions/expected/labelledArgumentMissingEqual.res.txt @@ -0,0 +1,22 @@ + + Syntax error! + syntax_tests/data/parsing/errors/expressions/labelledArgumentMissingEqual.res:1:16 + + 1 │ let _ = fn(~foo:1) + 2 │ let _ = fn(~bar:?value) + 3 │ + + Use `=` to pass a labelled argument. Example: `~label=value` + + + Syntax error! + syntax_tests/data/parsing/errors/expressions/labelledArgumentMissingEqual.res:2:16 + + 1 │ let _ = fn(~foo:1) + 2 │ let _ = fn(~bar:?value) + 3 │ + + Optional labelled arguments use `=?`. Example: `~label=?value` + +let _ = fn ~foo:1 +let _ = fn ?bar:value \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/errors/expressions/labelledArgumentMissingEqual.res b/tests/syntax_tests/data/parsing/errors/expressions/labelledArgumentMissingEqual.res new file mode 100644 index 0000000000..d5c75824b5 --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/expressions/labelledArgumentMissingEqual.res @@ -0,0 +1,2 @@ +let _ = fn(~foo:1) +let _ = fn(~bar:?value) diff --git a/tests/syntax_tests/data/parsing/errors/pattern/expected/recordFieldWrongAssignment.res.txt b/tests/syntax_tests/data/parsing/errors/pattern/expected/recordFieldWrongAssignment.res.txt new file mode 100644 index 0000000000..408736252f --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/pattern/expected/recordFieldWrongAssignment.res.txt @@ -0,0 +1,10 @@ + + Syntax error! + syntax_tests/data/parsing/errors/pattern/recordFieldWrongAssignment.res:1:9 + + 1 │ let {foo=bar} = record + 2 │ + + Record patterns use `:` when matching fields. Example: `{field: value}` + +let { foo = bar } = record \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/errors/pattern/recordFieldWrongAssignment.res b/tests/syntax_tests/data/parsing/errors/pattern/recordFieldWrongAssignment.res new file mode 100644 index 0000000000..fd1dbc0a7b --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/pattern/recordFieldWrongAssignment.res @@ -0,0 +1 @@ +let {foo=bar} = record diff --git a/tests/syntax_tests/data/parsing/errors/structure/expected/recordFieldWrongAssignment.res.txt b/tests/syntax_tests/data/parsing/errors/structure/expected/recordFieldWrongAssignment.res.txt new file mode 100644 index 0000000000..528caef166 --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/structure/expected/recordFieldWrongAssignment.res.txt @@ -0,0 +1,10 @@ + + Syntax error! + syntax_tests/data/parsing/errors/structure/recordFieldWrongAssignment.res:1:13 + + 1 │ let r = {foo=1} + 2 │ + + Records use `:` when assigning fields. Example: `{field: value}` + +let r = { foo = 1 } \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/errors/structure/recordFieldWrongAssignment.res b/tests/syntax_tests/data/parsing/errors/structure/recordFieldWrongAssignment.res new file mode 100644 index 0000000000..575cc0f8a8 --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/structure/recordFieldWrongAssignment.res @@ -0,0 +1 @@ +let r = {foo=1} diff --git a/tests/syntax_tests/data/parsing/errors/typeDef/expected/recordFieldWrongAssignment.res.txt b/tests/syntax_tests/data/parsing/errors/typeDef/expected/recordFieldWrongAssignment.res.txt new file mode 100644 index 0000000000..504f3509da --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/typeDef/expected/recordFieldWrongAssignment.res.txt @@ -0,0 +1,11 @@ + + Syntax error! + syntax_tests/data/parsing/errors/typeDef/recordFieldWrongAssignment.res:1:14 + + 1 │ type t = {foo=string} + 2 │ + + Record fields in type declarations use `:`. Example: `{field: string}` + +type nonrec t = { + foo: string } \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/errors/typeDef/recordFieldWrongAssignment.res b/tests/syntax_tests/data/parsing/errors/typeDef/recordFieldWrongAssignment.res new file mode 100644 index 0000000000..8771d6ac58 --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/typeDef/recordFieldWrongAssignment.res @@ -0,0 +1 @@ +type t = {foo=string} From d9295b6a16a8903b378e51d283256be0608bd232 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 18 Sep 2025 16:50:47 +0200 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41acaf240a..fa2bfa774c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - Add (dev-)dependencies to build schema. https://github.com/rescript-lang/rescript/pull/7892 - Dedicated error for dict literal spreads. https://github.com/rescript-lang/rescript/pull/7901 +- Dedicated error message for when mixing up `:` and `=` in various positions. https://github.com/rescript-lang/rescript/pull/7900 #### :house: Internal