2 * Copyright (c) 2015, Facebook, Inc.
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the "hack" directory of this source tree.
11 module Syntax
= Full_fidelity_positioned_syntax
12 module Trivia
= Full_fidelity_positioned_trivia
13 module TriviaKind
= Full_fidelity_trivia_kind
16 * This is a simple data structure that allows querying for the docblock
17 * given a line in the source code. Rough description:
19 * 1. Find the last comment preceding the line of the definition.
20 * We also make sure this doesn't overlap with the preceding definition.
21 * If the last comment is more than 1 line away, it is ignored.
23 * 2. If the last comment is a block-style comment (/* */) just return it.
25 * 3. Otherwise (if it is a line style comment //) attempt to merge it with
26 * preceding line comments, if they exist.
27 * NOTE: We also enforce that line comments must be on the definition's
28 * immediately preceding line.
31 (* line, string, is_line_comment *)
32 type comment
= int * string * bool
35 comments
: comment array
;
38 let make_docblock_finder (comments
: (Pos.t
* Prim_defs.comment
) list
) : finder
=
39 (* The Hack parser produces comments in reverse but sorted order. *)
40 let comments = Array.of_list
(
41 List.rev_map
comments ~f
:begin fun (pos
, cmt
) ->
42 let str = Prim_defs.string_of_comment cmt
in
43 (Pos.end_line pos
, str, Prim_defs.is_line_comment cmt
)
48 (* Binary search for the index of the last comment before a given line. *)
50 (* Merge all consecutive line comments preceding prev_line.
51 * Stop when we reach last_line. *)
52 let rec merge_line_comments (finder
: finder
)
56 (acc
: string list
) : string list
=
59 let (line
, str, is_line_comment
) = Array.get finder
.comments idx
in
60 if is_line_comment
&& line
> last_line
&& line
= prev_line
- 1 then
61 merge_line_comments finder
(idx
- 1) last_line line
(("//" ^
str) :: acc
)
65 let find_last_comment_index finder line
=
66 Utils.infimum finder
.comments line
67 (fun (comment_line
, _
, _
) line
-> comment_line
- line
)
69 let open_multiline = Str.regexp
"^/\\*\\(\\*?\\) *"
70 let close_multiline = Str.regexp
" *\\*/$"
72 (** Tidies up a delimited comment.
74 1. Strip the leading `/*` and trailing `*/`.
75 2. Remove leading whitespace equal to the least amount of whitespace
76 before any comment lines after the first (since the first line is on
77 the same line as the opening `/*`, it will almost always have only a
78 single leading space).
80 We remove leading whitespace equal to the least amount rather than
81 removing all leading whitespace in order to preserve manual indentation
82 of text in the doc block.
84 3. Remove leading `*` characters to properly handle box-style multiline
85 doc blocks. Without this they would be formatted as Markdown lists.
88 1. A doc block which is legitimately just a list of items will need at
89 least one non-list item which is under-indented compared to the list in
90 order to not strip all of the whitespace from the list items. The
91 easiest way to do this is to write a little one line summary before the
92 list and place it on its own line instead of directly after the `/*`.
94 let tidy_delimited_comment comment
=
97 |> Str.replace_first
open_multiline ""
98 |> Str.replace_first
close_multiline ""
100 let lines = String_utils.split_into_lines
comment in
101 let line_trimmer = match lines with
103 | [_
] -> Caml.String.trim
105 let get_whitespace_count x
=
106 String_utils.index_not_opt x
" "
108 let counts = List.filter_map ~f
:get_whitespace_count tail
in
110 List.min_elt
counts ~compare
:(fun a b
-> a
- b
)
111 |> Option.value ~default
:0
113 let removal = Str.regexp
(Printf.sprintf
"^%s\\(\\* ?\\)?" (String.make
min ' '
)) in
114 Str.replace_first
removal ""
117 |> List.map ~f
:line_trimmer
118 |> String.concat ~sep
:"\n"
120 let find_docblock (finder
: finder
)
122 (line
: int) : string option =
123 match find_last_comment_index finder line
with
124 | Some comment_index
->
125 let (comment_line
, str, is_line_comment
) =
126 Array.get finder
.comments comment_index
in
127 if is_line_comment
then begin
128 match merge_line_comments finder comment_index last_line line
[] with
130 | lines -> Some
(Caml.String.trim
(String.concat ~sep
:"" lines))
132 else if comment_line
> last_line
&& comment_line
>= line
- 2 then
133 Some
("/*" ^
str ^
"*/")
138 (* Find the last comment on `line` if it exists. *)
139 let find_inline_comment (finder
: finder
) (line
: int) : string option =
140 match find_last_comment_index finder
(line
+ 1) with
141 | Some last_comment_index
->
142 let (comment_line
, str, is_line_comment
) =
143 Array.get finder
.comments last_comment_index
in
144 if comment_line
= line
then begin
145 if is_line_comment
then
146 Some
(Caml.String.trim
("//" ^
str))
148 Some
("/*" ^
str ^
"*/")
154 let line_comment_prefix = Str.regexp
"^// ?"
156 let get_docblock node
=
157 let rec helper trivia_list acc eols_until_exit
=
158 match trivia_list
, eols_until_exit
with
160 | Trivia.{kind
= TriviaKind.EndOfLine
; _
} :: _
, 0 ->
166 |> List.map ~f
:(Str.replace_first
line_comment_prefix "")
167 |> String.concat ~sep
:"\n"
173 begin match Trivia.kind hd
with
174 | TriviaKind.DelimitedComment
when List.is_empty acc
->
175 Some
(tidy_delimited_comment (Trivia.text hd
))
176 | TriviaKind.SingleLineComment
->
177 helper tail
(Trivia.text hd
:: acc
) 1
178 | TriviaKind.WhiteSpace
-> helper tail acc eols_until_exit
179 | TriviaKind.EndOfLine
-> helper tail acc
(eols_until_exit
- 1)
181 | TriviaKind.IgnoreError
-> helper tail acc
1
182 | TriviaKind.DelimitedComment
184 | TriviaKind.UnsafeExpression
185 | TriviaKind.FallThrough
186 | TriviaKind.ExtraTokenError
187 | TriviaKind.AfterHaltCompiler
->
188 (* Short circuit immediately. *)
192 let trivia_list = Syntax.leading_trivia node
in
194 (* Set the starting EOL count to 2 instead of 1 because it's valid to have up
195 to one blank line between the end of a docblock and the start of its
196 associated element. *)
197 helper (List.rev
trivia_list) [] 2