package yamlx

  1. Overview
  2. Docs

Source file Comment_attacher.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
(** Comment attachment pass.

    After the Composer has built a node tree (with all comment fields empty),
    this module takes the raw comment list collected by the Scanner and attaches
    each comment to the most appropriate node. *)

(*
    Best-effort semantics
    ~~~~~~~~~~~~~~~~~~~~~
    Comment attachment is a heuristic process and cannot be perfect in all
    cases.  The rules applied are:

    - Head comments: standalone comment lines (lines that contain nothing but a
      comment) that appear immediately before a node are attached as
      [head_comments] of that node.

    - Line comments: a comment on the same line as a node's start position is
      attached as [line_comment].  For mapping pairs where key and value start
      on the same line, the line comment is attached to the value (the last node
      on that line), not the key.

    - Foot comments: standalone comment lines that appear after the last child
      of a collection, before the next sibling, are attached as [foot_comments]
      of the collection.

    Comments inside flow collections are not recorded by the Scanner.  Comments
    that cannot be attributed to any node are silently discarded.  The behavior
    is subject to change as the heuristics are refined.
*)

open Types

(* ------------------------------------------------------------------ *)
(* Comment cursor                                                        *)
(* ------------------------------------------------------------------ *)

type cursor = (int * int * bool * string) list ref
(** A mutable cursor through the sorted comment list. Each entry is
    [(line, col, is_line_comment, text)]. *)

let make_cursor comments =
  ref (List.sort (fun (a, _, _, _) (b, _, _, _) -> compare a b) comments)

(** Take elements from the front of [xs] as long as they satisfy [p]. Returns
    the matching prefix (in order) and the remaining suffix. *)
let rec take_while p acc xs =
  match xs with
  | [] -> (List.rev acc, [])
  | x :: tail -> if p x then take_while p (x :: acc) tail else (List.rev acc, xs)

(** Consume and return the texts of non-line-comment entries at lines strictly
    before [line]. When [~min_col] is given (default 0), only entries whose
    column is >= [min_col] are taken; entries with a smaller column are left in
    the cursor for an outer scope to claim as head comments. *)
let take_head_before ?(min_col = 0) line cur =
  let taken, rest =
    take_while
      (fun (l, col, is_line, _) -> (not is_line) && l < line && col >= min_col)
      [] !cur
  in
  cur := rest;
  List_ext.map (fun (_, _, _, t) -> t) taken

(** Consume and return the text of a line-comment entry exactly at [line], if
    present. *)
let take_line_comment line (cur : cursor) =
  match !cur with
  | (l, _, true, t) :: rest when l = line ->
      cur := rest;
      Some t
  | _ -> None

(* ------------------------------------------------------------------ *)
(* Node helpers                                                          *)
(* ------------------------------------------------------------------ *)

let node_line = function
  | Scalar_node r -> r.loc.start_pos.line
  | Sequence_node r -> r.loc.start_pos.line
  | Mapping_node r -> r.loc.start_pos.line
  | Alias_node r -> r.loc.start_pos.line

(** Return the [head_comments] field of a node. *)
let get_heads = function
  | Scalar_node r -> r.head_comments
  | Sequence_node r -> r.head_comments
  | Mapping_node r -> r.head_comments
  | Alias_node r -> r.head_comments

(** Return the [line_comment] field of a node. *)
let get_lc = function
  | Scalar_node r -> r.line_comment
  | Sequence_node r -> r.line_comment
  | Mapping_node r -> r.line_comment
  | Alias_node r -> r.line_comment

(** Set [head_comments] on a node. *)
let set_heads heads = function
  | Scalar_node r -> Scalar_node { r with head_comments = heads }
  | Sequence_node r -> Sequence_node { r with head_comments = heads }
  | Mapping_node r -> Mapping_node { r with head_comments = heads }
  | Alias_node r -> Alias_node { r with head_comments = heads }

(** Set [line_comment] on a node. *)
let set_lc lc = function
  | Scalar_node r -> Scalar_node { r with line_comment = lc }
  | Sequence_node r -> Sequence_node { r with line_comment = lc }
  | Mapping_node r -> Mapping_node { r with line_comment = lc }
  | Alias_node r -> Alias_node { r with line_comment = lc }

(** Set [foot_comments] on a node (scalars, aliases, and collections). *)
let set_feet feet = function
  | Scalar_node r -> Scalar_node { r with foot_comments = feet }
  | Sequence_node r -> Sequence_node { r with foot_comments = feet }
  | Mapping_node r -> Mapping_node { r with foot_comments = feet }
  | Alias_node r -> Alias_node { r with foot_comments = feet }

(* ------------------------------------------------------------------ *)
(* Attachment traversal                                                  *)
(* ------------------------------------------------------------------ *)

(** Attach comments to a single node. [next_line] is the source line where the
    next sibling begins (or [max_int] for the last sibling), used to bound
    foot-comment collection. *)
let rec attach_node cur ~next_line node =
  let nl = node_line node in

  (* Head comments: standalone lines before this node *)
  let heads = take_head_before nl cur in
  let node = set_heads (get_heads node @ heads) node in

  (* Descend into children and then collect foot comments *)
  match node with
  | Scalar_node _
  | Alias_node _ ->
      let lc = take_line_comment nl cur in
      let node = set_lc lc node in
      let feet = take_head_before next_line ~min_col:0 cur in
      set_feet feet node
  | Sequence_node r ->
      (* For a flow sequence or an empty sequence, a same-line comment belongs to
       the sequence node itself.  For a non-empty block sequence the comment on
       its start line belongs to the first item, so we skip take_line_comment
       here and let attach_siblings pick it up. *)
      let lc =
        if r.flow || r.items = [] then take_line_comment nl cur else None
      in
      let items = attach_siblings cur r.items ~parent_next_line:next_line in
      let feet =
        take_head_before next_line ~min_col:r.loc.start_pos.column cur
      in
      Sequence_node
        {
          r with
          head_comments = get_heads node;
          line_comment = lc;
          items;
          foot_comments = feet;
        }
  | Mapping_node r ->
      (* Same reasoning as Sequence_node above. *)
      let lc =
        if r.flow || r.pairs = [] then take_line_comment nl cur else None
      in
      let pairs = attach_pairs cur r.pairs ~parent_next_line:next_line in
      let feet =
        take_head_before next_line ~min_col:r.loc.start_pos.column cur
      in
      Mapping_node
        {
          r with
          head_comments = get_heads node;
          line_comment = lc;
          pairs;
          foot_comments = feet;
        }

(** Attach comments to a list of sibling nodes. [parent_next_line] bounds the
    foot-comment zone of the last sibling. *)
and attach_siblings cur nodes ~parent_next_line:_ =
  let arr = Array.of_list nodes in
  let n = Array.length arr in
  for i = 0 to n - 1 do
    (* For the last sibling, use a tight bound (one line past the item itself)
       rather than parent_next_line.  This prevents the last scalar/alias from
       consuming trailing comments that belong to the parent collection as foot
       comments — those are picked up by the parent's own foot-collection step. *)
    let next =
      if i + 1 < n then node_line arr.(i + 1) else node_line arr.(i) + 1
    in
    arr.(i) <- attach_node cur ~next_line:next arr.(i)
  done;
  Array.to_list arr

(** Attach comments to a list of mapping pairs. [parent_next_line] bounds the
    foot-comment zone of the last pair. *)
and attach_pairs cur pairs ~parent_next_line =
  let arr = Array.of_list pairs in
  let n = Array.length arr in
  for i = 0 to n - 1 do
    let k, v = arr.(i) in
    let next_pair_line =
      if i + 1 < n then node_line (fst arr.(i + 1)) else parent_next_line
    in
    let key_line = node_line k in
    let value_line = node_line v in

    (* Attach to the key with a tight next_line so comments between the key
       and value are NOT consumed as foot comments of the key — they should
       become head comments of the value instead. *)
    let k' = attach_node cur ~next_line:(key_line + 1) k in
    let k', transferred_lc =
      if key_line = value_line then
        (* Strip the line comment we just put on the key and defer it *)
        (set_lc None k', get_lc k')
      else (k', None)
    in

    let v' = attach_node cur ~next_line:next_pair_line v in
    (* Apply the transferred line comment to the value if it has none *)
    let v' =
      match transferred_lc with
      | Some _ when get_lc v' = None -> set_lc transferred_lc v'
      | _ -> v'
    in

    arr.(i) <- (k', v')
  done;
  Array.to_list arr

(* ------------------------------------------------------------------ *)
(* Entry point                                                           *)
(* ------------------------------------------------------------------ *)

(** Attach [raw_comments] to [docs] and return the annotated node list.
    [raw_comments] is the [(line, col, is_line_comment, text)] list returned by
    {!Scanner.drain_comments}. [doc_start_lines] is the source line of each
    document's [Document_start] event (the [---] line for explicit documents, or
    the first-content line for implicit ones); it is used as the upper bound for
    foot-comment collection on the preceding document, preventing standalone
    comments that precede a [---] from being mis-attached as head comments of
    the next document's root. Comments after the last document are discarded. *)
let attach ~doc_start_lines (docs : node list)
    (raw_comments : (int * int * bool * string) list) : node list =
  if raw_comments = [] then docs
  else begin
    let cur = make_cursor raw_comments in
    let arr = Array.of_list docs in
    let starts = Array.of_list doc_start_lines in
    let n = Array.length arr in
    for i = 0 to n - 1 do
      (* Use the next document's --- line as boundary so that comments between
         the end of document i and the --- of document i+1 become foot comments
         of document i rather than head comments of document i+1. *)
      let next =
        if i + 1 < n && i + 1 < Array.length starts then starts.(i + 1)
        else max_int
      in
      arr.(i) <- attach_node cur ~next_line:next arr.(i)
    done;
    Array.to_list arr
  end