Skip to content

Commit fc7226b

Browse files
authored
Better error when trying to use a keyword as a record field (#7784)
* better error when trying to use a keyword as a record field * error recovery * changelog * cleanup * format * cleanup
1 parent 93db5f0 commit fc7226b

14 files changed

+229
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#### :nail_care: Polish
3434

3535
- Allow skipping the leading pipe in variant definition with a leading constructor with an attribute. https://github.com/rescript-lang/rescript/pull/7782
36+
- Better error message (and recovery) when using a keyword as a record field name. https://github.com/rescript-lang/rescript/pull/7784
3637

3738
#### :house: Internal
3839

compiler/syntax/src/res_core.ml

Lines changed: 128 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,22 @@ module ErrorMessages = struct
155155
let multiple_inline_record_definitions_at_same_path =
156156
"Only one inline record definition is allowed per record field. This \
157157
defines more than one inline record."
158+
159+
let keyword_field_in_expr keyword_txt =
160+
"Cannot use keyword `" ^ keyword_txt
161+
^ "` as a record field name. Suggestion: rename it (e.g. `" ^ keyword_txt
162+
^ "_`)"
163+
164+
let keyword_field_in_pattern keyword_txt =
165+
"Cannot use keyword `" ^ keyword_txt
166+
^ "` here. Keywords are not allowed as record field names."
167+
168+
let keyword_field_in_type keyword_txt =
169+
"Cannot use keyword `" ^ keyword_txt
170+
^ "` as a record field name. Suggestion: rename it (e.g. `" ^ keyword_txt
171+
^ "_`)\n If you need the field to be \"" ^ keyword_txt
172+
^ "\" at runtime, annotate the field: `@as(\"" ^ keyword_txt ^ "\") "
173+
^ keyword_txt ^ "_ : ...`"
158174
end
159175

160176
module InExternal = struct
@@ -403,6 +419,30 @@ let build_longident words =
403419
| [] -> assert false
404420
| hd :: tl -> List.fold_left (fun p s -> Longident.Ldot (p, s)) (Lident hd) tl
405421

422+
let emit_keyword_field_error (p : Parser.t) ~mk_message =
423+
let keyword_txt = Token.to_string p.token in
424+
let keyword_start = p.Parser.start_pos in
425+
let keyword_end = p.Parser.end_pos in
426+
Parser.err ~start_pos:keyword_start ~end_pos:keyword_end p
427+
(Diagnostics.message (mk_message keyword_txt))
428+
429+
(* Recovers a keyword used as field name if it's probable that it's a full
430+
field name (not punning etc), by checking if there's a colon after it. *)
431+
let recover_keyword_field_name_if_probably_field p ~mk_message :
432+
(string * Location.t) option =
433+
if
434+
Token.is_keyword p.Parser.token
435+
&& Parser.lookahead p (fun st ->
436+
Parser.next st;
437+
st.Parser.token = Colon)
438+
then (
439+
emit_keyword_field_error p ~mk_message;
440+
let loc = mk_loc p.Parser.start_pos p.Parser.end_pos in
441+
let recovered_field_name = Token.to_string p.token ^ "_" in
442+
Parser.next p;
443+
Some (recovered_field_name, loc))
444+
else None
445+
406446
let make_infix_operator (p : Parser.t) token start_pos end_pos =
407447
let stringified_token =
408448
if token = Token.Equal then (
@@ -1382,7 +1422,25 @@ and parse_record_pattern_row p =
13821422
| Underscore ->
13831423
Parser.next p;
13841424
Some (false, PatUnderscore)
1385-
| _ -> None
1425+
| _ ->
1426+
if Token.is_keyword p.token then (
1427+
match
1428+
recover_keyword_field_name_if_probably_field p
1429+
~mk_message:ErrorMessages.keyword_field_in_pattern
1430+
with
1431+
| Some (recovered_field_name, loc) ->
1432+
Parser.expect Colon p;
1433+
let optional = parse_optional_label p in
1434+
let pat = parse_pattern p in
1435+
let field =
1436+
Location.mkloc (Longident.Lident recovered_field_name) loc
1437+
in
1438+
Some (false, PatField {lid = field; x = pat; opt = optional})
1439+
| None ->
1440+
emit_keyword_field_error p
1441+
~mk_message:ErrorMessages.keyword_field_in_pattern;
1442+
None)
1443+
else None
13861444

13871445
and parse_record_pattern ~attrs p =
13881446
let start_pos = p.start_pos in
@@ -2928,6 +2986,26 @@ and parse_braced_or_record_expr p =
29282986
let start_pos = p.Parser.start_pos in
29292987
Parser.expect Lbrace p;
29302988
match p.Parser.token with
2989+
| token when Token.is_keyword token -> (
2990+
match
2991+
recover_keyword_field_name_if_probably_field p
2992+
~mk_message:ErrorMessages.keyword_field_in_expr
2993+
with
2994+
| Some (recovered_field_name, loc) ->
2995+
Parser.expect Colon p;
2996+
let optional = parse_optional_label p in
2997+
let field_expr = parse_expr p in
2998+
let field = Location.mkloc (Longident.Lident recovered_field_name) loc in
2999+
let first_row = {Parsetree.lid = field; x = field_expr; opt = optional} in
3000+
let expr = parse_record_expr ~start_pos [first_row] p in
3001+
Parser.expect Rbrace p;
3002+
expr
3003+
| None ->
3004+
let expr = parse_expr_block p in
3005+
Parser.expect Rbrace p;
3006+
let loc = mk_loc start_pos p.prev_end_pos in
3007+
let braces = make_braces_attr loc in
3008+
{expr with pexp_attributes = braces :: expr.pexp_attributes})
29313009
| Rbrace ->
29323010
Parser.next p;
29333011
let loc = mk_loc start_pos p.prev_end_pos in
@@ -3244,7 +3322,25 @@ and parse_record_expr_row p :
32443322
in
32453323
Some {lid = field; x = value; opt = true}
32463324
| _ -> None)
3247-
| _ -> None
3325+
| _ ->
3326+
if Token.is_keyword p.token then (
3327+
match
3328+
recover_keyword_field_name_if_probably_field p
3329+
~mk_message:ErrorMessages.keyword_field_in_expr
3330+
with
3331+
| Some (recovered_field_name, loc) ->
3332+
Parser.expect Colon p;
3333+
let optional = parse_optional_label p in
3334+
let field_expr = parse_expr p in
3335+
let field =
3336+
Location.mkloc (Longident.Lident recovered_field_name) loc
3337+
in
3338+
Some {lid = field; x = field_expr; opt = optional}
3339+
| None ->
3340+
emit_keyword_field_error p
3341+
~mk_message:ErrorMessages.keyword_field_in_expr;
3342+
None)
3343+
else None
32483344

32493345
and parse_dict_expr_row p =
32503346
match p.Parser.token with
@@ -4742,17 +4838,36 @@ and parse_field_declaration_region ?current_type_name_path ?inline_types_context
47424838
let loc = mk_loc start_pos typ.ptyp_loc.loc_end in
47434839
Some (Ast_helper.Type.field ~attrs ~loc ~mut ~optional name typ)
47444840
| _ ->
4745-
if attrs <> [] then
4746-
Parser.err ~start_pos p
4747-
(Diagnostics.message
4748-
"Attributes and doc comments can only be used at the beginning of a \
4749-
field declaration");
4750-
if mut = Mutable then
4751-
Parser.err ~start_pos p
4752-
(Diagnostics.message
4753-
"The `mutable` qualifier can only be used at the beginning of a \
4754-
field declaration");
4755-
None
4841+
if Token.is_keyword p.token then (
4842+
match
4843+
recover_keyword_field_name_if_probably_field p
4844+
~mk_message:ErrorMessages.keyword_field_in_type
4845+
with
4846+
| Some (recovered_field_name, name_loc) ->
4847+
let optional = parse_optional_label p in
4848+
Parser.expect Colon p;
4849+
let typ =
4850+
parse_poly_type_expr ?current_type_name_path ?inline_types_context p
4851+
in
4852+
let loc = mk_loc start_pos typ.ptyp_loc.loc_end in
4853+
let name = Location.mkloc recovered_field_name name_loc in
4854+
Some (Ast_helper.Type.field ~attrs ~loc ~mut ~optional name typ)
4855+
| None ->
4856+
emit_keyword_field_error p
4857+
~mk_message:ErrorMessages.keyword_field_in_type;
4858+
None)
4859+
else (
4860+
if attrs <> [] then
4861+
Parser.err ~start_pos p
4862+
(Diagnostics.message
4863+
"Attributes and doc comments can only be used at the beginning of \
4864+
a field declaration");
4865+
if mut = Mutable then
4866+
Parser.err ~start_pos p
4867+
(Diagnostics.message
4868+
"The `mutable` qualifier can only be used at the beginning of a \
4869+
field declaration");
4870+
None)
47564871

47574872
(* record-decl ::=
47584873
* | { field-decl }
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
Syntax error!
3+
syntax_tests/data/parsing/errors/structure/recordFieldKeywordInExpr.res:1:10-13
4+
5+
1 │ let r = {type: 1}
6+
2 │
7+
8+
Cannot use keyword `type` as a record field name. Suggestion: rename it (e.g. `type_`)
9+
10+
let r = { type_ = 1 }
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
Syntax error!
3+
syntax_tests/data/parsing/errors/structure/recordFieldKeywordInExpr2.res:1:16-19
4+
5+
1 │ let r = {a: 1, type: 2}
6+
2 │
7+
8+
Cannot use keyword `type` as a record field name. Suggestion: rename it (e.g. `type_`)
9+
10+
let r = { a = 1; type_ = 2 }
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
Syntax error!
3+
syntax_tests/data/parsing/errors/structure/recordFieldKeywordInPattern.res:1:6-9
4+
5+
1 │ let {type} = r
6+
2 │
7+
3 │
8+
9+
Cannot use keyword `type` here. Keywords are not allowed as record field names.
10+
11+
12+
Syntax error!
13+
syntax_tests/data/parsing/errors/structure/recordFieldKeywordInPattern.res:1:10
14+
15+
1 │ let {type} = r
16+
2 │
17+
3 │
18+
19+
I'm not sure what to parse here when looking at "}".
20+
21+
let { } = [%rescript.exprhole ]
22+
;;r
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
Syntax error!
3+
syntax_tests/data/parsing/errors/structure/recordFieldKeywordInPattern2.res:1:12-15
4+
5+
1 │ let {a: _, type: x} = r
6+
2 │
7+
8+
Cannot use keyword `type` here. Keywords are not allowed as record field names.
9+
10+
let { a = _; type_ = x } = r
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
Syntax error!
3+
syntax_tests/data/parsing/errors/structure/recordFieldKeywordInType.res:3:3-6
4+
5+
1 │ type r = {
6+
2 │ id: string,
7+
3 │ type: int,
8+
4 │ }
9+
5 │
10+
11+
Cannot use keyword `type` as a record field name. Suggestion: rename it (e.g. `type_`)
12+
If you need the field to be "type" at runtime, annotate the field: `@as("type") type_ : ...`
13+
14+
type nonrec r = {
15+
id: string ;
16+
type_: int }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
Syntax error!
3+
syntax_tests/data/parsing/errors/structure/recordFieldKeywordInType2.res:3:3-6
4+
5+
1 │ type r = {
6+
2 │ id: string,
7+
3 │ type: int,
8+
4 │ x: bool,
9+
5 │ }
10+
11+
Cannot use keyword `type` as a record field name. Suggestion: rename it (e.g. `type_`)
12+
If you need the field to be "type" at runtime, annotate the field: `@as("type") type_ : ...`
13+
14+
type nonrec r = {
15+
id: string ;
16+
type_: int ;
17+
x: bool }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let r = {type: 1}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let r = {a: 1, type: 2}

0 commit comments

Comments
 (0)