Skip to content

Commit 83f5eea

Browse files
cknittclaude
andauthored
Keep attributes in front of a record or open type representation (#8919)
* Keep attributes in front of a record or open type representation Why: The parser accepts attributes between `=` and a record declaration or `..` (since #7787) but threw them away, so a PPX such as sury never saw `@s.strict`. They now go on the type declaration, after its leading attributes, and the formatter prints them in front of `type`. The attributed path also passes the inline-record context on, so nested record types inside such a record parse. AST modified (attribute before a record representation): Example: @Schema type t = @s.strict {email: string} Before (parse): type_declaration "t" attribute "schema" [] ptype_params = [] ptype_cstrs = [] ptype_kind = Ptype_record … After (parse): type_declaration "t" attribute "schema" [] attribute "s.strict" [] ptype_params = [] ptype_cstrs = [] ptype_kind = Ptype_record … Diagnostic removed (nested record after an attribute): Example: type u = @attr {x: {y: int}} Before (diagnostic): An inline record type declaration is only allowed in a variant constructor's declaration or nested inside of a record type declaration Format modified (attribute before a record representation): Example: @Schema type t = @s.strict {email: string} type u = @attr {x: {y: int}} Before (rescript): @Schema type t = {email: string} // type u: syntax error After (rescript): @Schema @s.strict type t = {email: string} @attr type u = {x: {y: int}} Review: Why the declaration and not an error? The issue asks for the attribute to reach the PPX, and the declaration is the only parsetree node a record or open type kind has. An attribute in front of a constructor still attaches to that constructor. Fixes #7814 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel <ck@cca.io> * Keep a doc comment in front of a record or open type representation Why: Only a doc comment after an attribute reached the declaration; one directly after `=` was parsed as the start of a constructor list and failed on `{` or `..`. It now goes through the same lookahead as an attribute: in front of `{` or `..` it attaches to the type declaration, in front of a constructor it still attaches to that constructor. AST modified (doc comment before a record representation): Example: type t = /** doc */ {x: int} Before (diagnostic): At this point, I'm looking for an uppercased name like `Array` or `Math` After (parse): type_declaration "t" attribute "res.doc" [ structure_item Pstr_eval expression Pexp_constant PConst_string (source=" doc ", semantic=" doc ") ] ptype_params = [] ptype_cstrs = [] ptype_kind = Ptype_record … Format modified (doc comment before a record or open representation): Example: type t = /** doc */ {x: int} type u = /** doc */ .. After (rescript): /** doc */ type t = {x: int} /** doc */ type u = .. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel <ck@cca.io> * Share the type representation arm for a doc comment after `=` The doc comment arm differed from the `Bar | DotDot` arm only in passing the inline-record context, which the other two never reach, so one arm serves all three. Behaviour is unchanged; the existing parser and printer fixtures cover it, so no test is added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel <ck@cca.io> --------- Signed-off-by: Christoph Knittel <ck@cca.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 1a49d91 commit 83f5eea

7 files changed

Lines changed: 170 additions & 63 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Keep attributes and doc comments written in front of a record or `..` type representation, attaching them to the type declaration instead of dropping them.

‎compiler/syntax/src/res_core.ml‎

Lines changed: 71 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5417,14 +5417,14 @@ and parse_type_representation ?current_type_name_path ?inline_types_context p =
54175417
if Parser.optional p Token.Private then Asttypes.Private
54185418
else Asttypes.Public
54195419
in
5420-
let kind =
5420+
let kind, attrs =
54215421
match Parser.peek p with
5422-
| Bar | Uident _ | DocComment _ ->
5423-
Parsetree.Ptype_variant (parse_type_constructor_declarations p)
5424-
| At -> (
5425-
(* Attributes can prefix either a variant (constructor list), a record, or an
5426-
open/extensible variant marker (`..`). Peek past attributes and any doc
5427-
comments to decide which kind it is. *)
5422+
| Bar | Uident _ ->
5423+
(Parsetree.Ptype_variant (parse_type_constructor_declarations p), [])
5424+
| At | DocComment _ -> (
5425+
(* Attributes and doc comments can prefix either a variant (constructor
5426+
list), a record, or an open/extensible variant marker (`..`). Peek past
5427+
them to decide which kind it is. *)
54285428
let after_attrs =
54295429
Parser.lookahead p (fun state ->
54305430
ignore (parse_attributes state);
@@ -5433,37 +5433,39 @@ and parse_type_representation ?current_type_name_path ?inline_types_context p =
54335433
in
54345434
match after_attrs with
54355435
| Lbrace ->
5436-
(* consume the attributes and any doc comments before the record *)
5437-
ignore (parse_attributes p);
5438-
skip_doc_comments p;
5439-
Parsetree.Ptype_record
5440-
(parse_record_declaration ?current_type_name_path
5441-
?inline_types_context p)
5436+
(* Attributes (and doc comments) before a record have no node of their
5437+
own: they are returned so the caller attaches them to the type
5438+
declaration. *)
5439+
let attrs = parse_attributes p in
5440+
( Parsetree.Ptype_record
5441+
(parse_record_declaration ?current_type_name_path
5442+
?inline_types_context p),
5443+
attrs )
54425444
| DotDot ->
5443-
(* attributes before an open variant marker; consume attrs/docs then handle `..` *)
5444-
ignore (parse_attributes p);
5445-
skip_doc_comments p;
5445+
(* Same for attributes before an open variant marker `..`. *)
5446+
let attrs = parse_attributes p in
54465447
Parser.next p;
54475448
(* consume DotDot *)
5448-
Ptype_open
5449+
(Ptype_open, attrs)
54495450
| _ ->
54505451
(* fall back to variant constructor declarations; leave attributes for the
54515452
constructor parsing so they attach to the first constructor. *)
5452-
Parsetree.Ptype_variant (parse_type_constructor_declarations p))
5453+
(Parsetree.Ptype_variant (parse_type_constructor_declarations p), []))
54535454
| Lbrace ->
5454-
Parsetree.Ptype_record
5455-
(parse_record_declaration ?current_type_name_path ?inline_types_context
5456-
p)
5455+
( Parsetree.Ptype_record
5456+
(parse_record_declaration ?current_type_name_path
5457+
?inline_types_context p),
5458+
[] )
54575459
| DotDot ->
54585460
Parser.next p;
5459-
Ptype_open
5461+
(Ptype_open, [])
54605462
| token ->
54615463
Parser.err p (Diagnostics.unexpected token p.breadcrumbs);
54625464
(* TODO: I have no idea if this is even remotely a good idea *)
5463-
Parsetree.Ptype_variant []
5465+
(Parsetree.Ptype_variant [], [])
54645466
in
54655467
Parser.eat_breadcrumb p;
5466-
(private_flag, kind)
5468+
(private_flag, kind, attrs)
54675469

54685470
(* type-param ::=
54695471
* | variance 'lident
@@ -5614,8 +5616,8 @@ and parse_type_equation_or_constr_decl p =
56145616
match Parser.peek p with
56155617
| Equal ->
56165618
Parser.next p;
5617-
let priv, kind = parse_type_representation p in
5618-
(Some typ, priv, kind)
5619+
let priv, kind, attrs = parse_type_representation p in
5620+
(Some typ, priv, kind, attrs)
56195621
| EqualGreater ->
56205622
Parser.next p;
56215623
let return_type = parse_typ_expr ~alias:false p in
@@ -5626,8 +5628,8 @@ and parse_type_equation_or_constr_decl p =
56265628
return_type
56275629
in
56285630
let typ = parse_type_alias p arrow_type in
5629-
(Some typ, Asttypes.Public, Parsetree.Ptype_abstract)
5630-
| _ -> (Some typ, Asttypes.Public, Parsetree.Ptype_abstract))
5631+
(Some typ, Asttypes.Public, Parsetree.Ptype_abstract, [])
5632+
| _ -> (Some typ, Asttypes.Public, Parsetree.Ptype_abstract, []))
56315633
| _ ->
56325634
let uident_end_pos = Parser.position p in
56335635
let args, res = parse_constr_decl_args p in
@@ -5641,12 +5643,12 @@ and parse_type_equation_or_constr_decl p =
56415643
in
56425644
( None,
56435645
Asttypes.Public,
5644-
Parsetree.Ptype_variant (parse_type_constructor_declarations p ?first)
5645-
))
5646+
Parsetree.Ptype_variant (parse_type_constructor_declarations p ?first),
5647+
[] ))
56465648
| t ->
56475649
Parser.err p (Diagnostics.uident t);
56485650
(* TODO: is this a good idea? *)
5649-
(None, Asttypes.Public, Parsetree.Ptype_abstract)
5651+
(None, Asttypes.Public, Parsetree.Ptype_abstract, [])
56505652

56515653
and parse_spread_tail_classified ?current_type_name_path ?inline_types_context
56525654
~start_pos ~spread_typ ~grammar p =
@@ -5856,18 +5858,18 @@ and parse_private_eq_or_repr p =
58565858
match Parser.peek p with
58575859
| Lbrace ->
58585860
let manifest, _, kind = parse_record_or_object_decl p in
5859-
(manifest, Asttypes.Private, kind)
5861+
(manifest, Asttypes.Private, kind, [])
58605862
| Uident _ ->
5861-
let manifest, _, kind = parse_type_equation_or_constr_decl p in
5862-
(manifest, Asttypes.Private, kind)
5863+
let manifest, _, kind, attrs = parse_type_equation_or_constr_decl p in
5864+
(manifest, Asttypes.Private, kind, attrs)
58635865
| Bar | DotDot ->
5864-
let _, kind = parse_type_representation p in
5865-
(None, Asttypes.Private, kind)
5866+
let _, kind, attrs = parse_type_representation p in
5867+
(None, Asttypes.Private, kind, attrs)
58665868
| t when Grammar.is_typ_expr_start t ->
5867-
(Some (parse_typ_expr p), Asttypes.Private, Parsetree.Ptype_abstract)
5869+
(Some (parse_typ_expr p), Asttypes.Private, Parsetree.Ptype_abstract, [])
58685870
| _ ->
5869-
let _, kind = parse_type_representation p in
5870-
(None, Asttypes.Private, kind)
5871+
let _, kind, attrs = parse_type_representation p in
5872+
(None, Asttypes.Private, kind, attrs)
58715873

58725874
(*
58735875
polymorphic-variant-type ::=
@@ -6092,12 +6094,18 @@ and parse_type_equation_and_representation ?current_type_name_path
60926094
match Parser.peek p with
60936095
| Uident _ -> parse_type_equation_or_constr_decl p
60946096
| Lbrace ->
6095-
parse_record_or_object_decl ?current_type_name_path ?inline_types_context
6096-
p
6097+
let manifest, priv, kind =
6098+
parse_record_or_object_decl ?current_type_name_path
6099+
?inline_types_context p
6100+
in
6101+
(manifest, priv, kind, [])
60976102
| Private -> parse_private_eq_or_repr p
60986103
| Bar | DotDot | DocComment _ ->
6099-
let priv, kind = parse_type_representation p in
6100-
(None, priv, kind)
6104+
let priv, kind, attrs =
6105+
parse_type_representation ?current_type_name_path ?inline_types_context
6106+
p
6107+
in
6108+
(None, priv, kind, attrs)
61016109
| At -> (
61026110
(* Attributes can start a representation (variant/record/open variant) or a manifest.
61036111
Look ahead past attributes (and doc comments). If a representation-like token follows,
@@ -6135,31 +6143,34 @@ and parse_type_equation_and_representation ?current_type_name_path
61356143
| _ -> false)
61366144
in
61376145
if is_representation_after_attrs then
6138-
let priv, kind = parse_type_representation p in
6139-
(None, priv, kind)
6146+
let priv, kind, attrs =
6147+
parse_type_representation ?current_type_name_path
6148+
?inline_types_context p
6149+
in
6150+
(None, priv, kind, attrs)
61406151
else
61416152
let manifest = Some (parse_typ_expr p) in
61426153
match Parser.peek p with
61436154
| Equal ->
61446155
Parser.next p;
6145-
let priv, kind =
6156+
let priv, kind, attrs =
61466157
parse_type_representation ?current_type_name_path
61476158
?inline_types_context p
61486159
in
6149-
(manifest, priv, kind)
6150-
| _ -> (manifest, Public, Parsetree.Ptype_abstract))
6160+
(manifest, priv, kind, attrs)
6161+
| _ -> (manifest, Public, Parsetree.Ptype_abstract, []))
61516162
| _ -> (
61526163
let manifest = Some (parse_typ_expr p) in
61536164
match Parser.peek p with
61546165
| Equal ->
61556166
Parser.next p;
6156-
let priv, kind =
6167+
let priv, kind, attrs =
61576168
parse_type_representation ?current_type_name_path
61586169
?inline_types_context p
61596170
in
6160-
(manifest, priv, kind)
6161-
| _ -> (manifest, Public, Parsetree.Ptype_abstract)))
6162-
| _ -> (None, Public, Parsetree.Ptype_abstract)
6171+
(manifest, priv, kind, attrs)
6172+
| _ -> (manifest, Public, Parsetree.Ptype_abstract, [])))
6173+
| _ -> (None, Public, Parsetree.Ptype_abstract, [])
61636174

61646175
(* type-definition ::= type [rec] typedef { and typedef }
61656176
* typedef ::= typeconstr-name [type-params] type-information
@@ -6176,11 +6187,13 @@ and parse_type_def ~attrs ~start_pos p =
61766187
parse_type_params ~parent:constr_name p
61776188
in
61786189
let type_def =
6179-
let manifest, priv, kind = parse_type_equation_and_representation p in
6190+
let manifest, priv, kind, repr_attrs =
6191+
parse_type_equation_and_representation p
6192+
in
61806193
let cstrs = parse_type_constraints p in
61816194
let loc = mk_loc start_pos (Parser.position p) in
6182-
Ast_helper.Type.mk ~loc ~attrs ~priv ~kind ~params ~cstrs ?manifest
6183-
type_constr_name
6195+
Ast_helper.Type.mk ~loc ~attrs:(attrs @ repr_attrs) ~priv ~kind ~params
6196+
~cstrs ?manifest type_constr_name
61846197
in
61856198
Parser.eat_breadcrumb p;
61866199
type_def
@@ -6224,13 +6237,14 @@ and parse_type_extension ~params ~attrs ~name p =
62246237
and parse_type_definitions ~current_type_name_path ~inline_types_context ~attrs
62256238
~name ~params ~start_pos p =
62266239
let type_def =
6227-
let manifest, priv, kind =
6240+
let manifest, priv, kind, repr_attrs =
62286241
parse_type_equation_and_representation ~current_type_name_path
62296242
~inline_types_context p
62306243
in
62316244
let cstrs = parse_type_constraints p in
62326245
let loc = mk_loc start_pos (Parser.position p) in
6233-
Ast_helper.Type.mk ~loc ~attrs ~priv ~kind ~params ~cstrs ?manifest
6246+
Ast_helper.Type.mk ~loc ~attrs:(attrs @ repr_attrs) ~priv ~kind ~params
6247+
~cstrs ?manifest
62346248
{name with txt = lident_of_path name.Location.txt}
62356249
in
62366250
let rec loop p defs =

‎tests/syntax_tests/data/parsing/grammar/typedefinition/expected/recordDeclaration.res.txt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@ type nonrec domProps =
5555
list: string [@optional ];
5656
loop: bool [@optional ]}
5757
type nonrec t = {
58-
x: int }
58+
x: int }[@@attr ]
5959
type nonrec t = {
60-
x: int }
60+
x: int }[@@attr ][@@res.doc {js|doc before record|js}]

‎tests/syntax_tests/data/parsing/grammar/typedefinition/expected/typeRepresentation.res.txt‎

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,40 @@ type nonrec t = private {
4343
x: int ;
4444
y: int }
4545
type nonrec t = {
46-
x: int }
46+
x: int }[@@attr ]
4747
type nonrec t = {
48-
x: int }
49-
type nonrec t = ..
50-
type nonrec t = ..
48+
x: int }[@@attr ][@@res.doc {js|doc before record|js}]
49+
type nonrec t = ..[@@attr ]
50+
type nonrec t = ..[@@attr ][@@res.doc {js|doc before open|js}]
51+
type t.x = {
52+
y: int }
53+
and t = {
54+
x: t.x }[@@attr ]
55+
type nonrec t = M.t = {
56+
x: int }[@@attr ]
57+
type nonrec 'a t = {
58+
x: 'a }[@@attr ]
59+
type nonrec t = int
60+
and u = {
61+
x: int }[@@attr ]
62+
type nonrec t = {
63+
x: int }[@@res.doc {js|doc before record|js}]
64+
type nonrec t = ..[@@res.doc {js|doc before open|js}]
65+
type nonrec t = {
66+
x: int }[@@res.doc {js|doc|js}][@@attr ]
67+
type nonrec t = ..[@@attr ][@@res.doc {js|doc|js}][@@attr2 ]
68+
type t.x = {
69+
y: int }
70+
and t = {
71+
x: t.x }[@@res.doc {js|doc|js}]
72+
type nonrec t = private {
73+
x: int }[@@res.doc {js|doc|js}]
74+
type nonrec t =
75+
| A [@res.doc {js|doc|js}]
76+
| B
77+
type nonrec t =
78+
| A [@res.doc {js|doc|js}]
79+
| B [@res.doc {js|doc2|js}]
80+
type nonrec t =
81+
| A [@res.doc {js|doc|js}][@attr ]
82+
| B

‎tests/syntax_tests/data/parsing/grammar/typedefinition/typeRepresentation.res‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,16 @@ type t = @attr {x: int}
3232
type t = @attr /**doc before record*/ {x: int}
3333
type t = @attr ..
3434
type t = @attr /**doc before open*/ ..
35+
type t = @attr {x: {y: int}}
36+
type t = M.t = @attr {x: int}
37+
type t<'a> = @attr {x: 'a}
38+
type t = int and u = @attr {x: int}
39+
type t = /**doc before record*/ {x: int}
40+
type t = /**doc before open*/ ..
41+
type t = /**doc*/ @attr {x: int}
42+
type t = @attr /**doc*/ @attr2 ..
43+
type t = /**doc*/ {x: {y: int}}
44+
type t = private /**doc*/ {x: int}
45+
type t = /**doc*/ A | B
46+
type t = /**doc*/ | A | /**doc2*/ B
47+
type t = /**doc*/ @attr A | B

‎tests/syntax_tests/data/printer/typeDef/attributes.res‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,20 @@ type t = int
99
@attrWithSuperLongIdentifeirName @attrWithSuperLongIdentifeirName @attrWithSuperLongIdentifeirName@attrWithSuperLongIdentifeirName type t = string
1010
@attrWithSuperLongIdentifeirName @attrWithSuperLongIdentifeirName @attrWithSuperLongIdentifeirName@attrWithSuperLongIdentifeirName
1111
type t = string
12+
13+
// attributes in front of a record or `..` move to the declaration
14+
@schema
15+
type t = @s.strict {
16+
email: string,
17+
name: string,
18+
}
19+
type t = @attr /** doc */ {x: int}
20+
type t = @attr ..
21+
type t = /** doc */ {x: int}
22+
type t = /** doc */ ..
23+
type t = /** doc */ @attr {x: int}
24+
type t = @attr /** doc */ @attr2 ..
25+
@outer
26+
type t = /* c1 */ /** doc */ /* c2 */ {x: int} // trail
27+
type t = /** doc */ A | B
28+
type t = | /** doc */ A | /** doc2 */ B

‎tests/syntax_tests/data/printer/typeDef/expected/attributes.res.txt‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,33 @@ type t = string
1616
@attrWithSuperLongIdentifeirName
1717
@attrWithSuperLongIdentifeirName
1818
type t = string
19+
20+
// attributes in front of a record or `..` move to the declaration
21+
@schema @s.strict
22+
type t = {
23+
email: string,
24+
name: string,
25+
}
26+
/** doc */
27+
@attr type t = {x: int}
28+
@attr type t = ..
29+
/** doc */
30+
type t = {x: int}
31+
/** doc */
32+
type t = ..
33+
/** doc */
34+
@attr type t = {x: int}
35+
/** doc */
36+
@attr @attr2 type t = ..
37+
/** doc */
38+
@outer
39+
type t = {/* c1 */ /* c2 */ x: int} // trail
40+
type t =
41+
/** doc */
42+
| A
43+
| B
44+
type t =
45+
/** doc */
46+
| A
47+
/** doc2 */
48+
| B

0 commit comments

Comments
 (0)