Add new function that uses FFP directly for single docblock finding.
[hiphop-php.git] / hphp / hack / src / parser / docblock_finder.ml
blob20126ef87c216b606519de6cdea548ed43233a19
1 (**
2 * Copyright (c) 2015, Facebook, Inc.
3 * All rights reserved.
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the "hack" directory of this source tree. An additional grant
7 * of patent rights can be found in the PATENTS file in the same directory.
9 *)
11 open Hh_core
12 module Syntax = Full_fidelity_positioned_syntax
13 module Trivia = Full_fidelity_positioned_trivia
14 module TriviaKind = Full_fidelity_trivia_kind
16 (**
17 * This is a simple data structure that allows querying for the docblock
18 * given a line in the source code. Rough description:
20 * 1. Find the last comment preceding the line of the definition.
21 * We also make sure this doesn't overlap with the preceding definition.
22 * If the last comment is more than 1 line away, it is ignored.
24 * 2. If the last comment is a block-style comment (/* */) just return it.
26 * 3. Otherwise (if it is a line style comment //) attempt to merge it with
27 * preceding line comments, if they exist.
28 * NOTE: We also enforce that line comments must be on the definition's
29 * immediately preceding line.
32 (* line, string, is_line_comment *)
33 type comment = int * string * bool
35 type finder = {
36 comments: comment array;
39 let make_docblock_finder (comments: (Pos.t * Prim_defs.comment) list) : finder =
40 (* The Hack parser produces comments in reverse but sorted order. *)
41 let comments = Array.of_list (
42 List.rev_map comments ~f:begin fun (pos, cmt) ->
43 let str = Prim_defs.string_of_comment cmt in
44 (File_pos.line (Pos.pos_end pos), str, Prim_defs.is_line_comment cmt)
45 end
46 ) in
47 { comments }
49 (* Binary search for the index of the last comment before a given line. *)
51 (* Merge all consecutive line comments preceding prev_line.
52 * Stop when we reach last_line. *)
53 let rec merge_line_comments (finder : finder)
54 (idx : int)
55 (last_line : int)
56 (prev_line : int)
57 (acc : string list) : string list =
58 if idx < 0 then acc
59 else begin
60 let (line, str, is_line_comment) = Array.get finder.comments idx in
61 if is_line_comment && line > last_line && line = prev_line - 1 then
62 merge_line_comments finder (idx - 1) last_line line (("//" ^ str) :: acc)
63 else acc
64 end
66 let find_last_comment_index finder line =
67 Utils.infimum finder.comments line
68 (fun (comment_line, _, _) line -> comment_line - line)
70 let open_multiline = Str.regexp "^/\\*\\(\\*?\\) *"
71 let close_multiline = Str.regexp " *\\*/$"
73 (** Tidies up a delimited comment.
75 1. Strip the leading `/*` and trailing `*/`.
76 2. Remove leading whitespace equal to the least amount of whitespace
77 before any comment lines after the first (since the first line is on
78 the same line as the opening `/*`, it will almost always have only a
79 single leading space).
81 We remove leading whitespace equal to the least amount rather than
82 removing all leading whitespace in order to preserve manual indentation
83 of text in the doc block.
85 3. Remove leading `*` characters to properly handle box-style multiline
86 doc blocks. Without this they would be formatted as Markdown lists.
88 Known failure cases:
89 1. A doc block which is legitimately just a list of items will need at
90 least one non-list item which is under-indented compared to the list in
91 order to not strip all of the whitespace from the list items. The
92 easiest way to do this is to write a little one line summary before the
93 list and place it on its own line instead of directly after the `/*`.
95 let tidy_delimited_comment comment =
96 let comment =
97 comment
98 |> Str.replace_first open_multiline ""
99 |> Str.replace_first close_multiline ""
101 let lines = String_utils.split_into_lines comment in
102 let line_trimmer = match lines with
103 | []
104 | [_] -> String.trim
105 | _hd :: tail ->
106 let get_whitespace_count x =
107 String_utils.index_not_opt x " "
109 let counts = List.filter_map ~f:get_whitespace_count tail in
110 let min =
111 List.min_elt counts ~cmp:(fun a b -> a - b)
112 |> Option.value ~default:0
114 let removal = Str.regexp (Printf.sprintf "^%s\\(\\* ?\\)?" (String.make min ' ')) in
115 Str.replace_first removal ""
117 lines
118 |> List.map ~f:line_trimmer
119 |> String.concat "\n"
121 let find_docblock (finder : finder)
122 (last_line : int)
123 (line : int) : string option =
124 match find_last_comment_index finder line with
125 | Some comment_index ->
126 let (comment_line, str, is_line_comment) =
127 Array.get finder.comments comment_index in
128 if is_line_comment then begin
129 match merge_line_comments finder comment_index last_line line [] with
130 | [] -> None
131 | lines -> Some (String.trim (String.concat "" lines))
133 else if comment_line > last_line && comment_line >= line - 2 then
134 Some ("/*" ^ str ^ "*/")
135 else
136 None
137 | None -> None
139 (* Find the last comment on `line` if it exists. *)
140 let find_inline_comment (finder : finder) (line : int) : string option =
141 match find_last_comment_index finder (line + 1) with
142 | Some last_comment_index ->
143 let (comment_line, str, is_line_comment) =
144 Array.get finder.comments last_comment_index in
145 if comment_line = line then begin
146 if is_line_comment then
147 Some (String.trim ("//" ^ str))
148 else
149 Some ("/*" ^ str ^ "*/")
151 else
152 None
153 | None -> None
155 let line_comment_prefix = Str.regexp "^// ?"
157 let get_docblock node =
158 let rec helper trivia_list acc eols_until_exit =
159 match trivia_list, eols_until_exit with
160 | [], _
161 | Trivia.{kind = TriviaKind.EndOfLine; _} :: _, 0 ->
162 begin match acc with
163 | [] -> None
164 | comments ->
165 let comment =
166 comments
167 |> List.map ~f:(Str.replace_first line_comment_prefix "")
168 |> String.concat "\n"
169 |> String.trim
171 Some comment
173 | hd :: tail, _ ->
174 begin match Trivia.kind hd with
175 | TriviaKind.DelimitedComment when List.is_empty acc ->
176 Some (tidy_delimited_comment (Trivia.text hd))
177 | TriviaKind.SingleLineComment ->
178 helper tail (Trivia.text hd :: acc) 1
179 | TriviaKind.WhiteSpace -> helper tail acc eols_until_exit
180 | TriviaKind.EndOfLine -> helper tail acc (eols_until_exit - 1)
181 | TriviaKind.FixMe
182 | TriviaKind.IgnoreError -> helper tail acc 1
183 | TriviaKind.DelimitedComment
184 | TriviaKind.Unsafe
185 | TriviaKind.UnsafeExpression
186 | TriviaKind.FallThrough
187 | TriviaKind.ExtraTokenError
188 | TriviaKind.AfterHaltCompiler ->
189 (* Short circuit immediately. *)
190 helper [] acc 0
193 let trivia_list = Syntax.leading_trivia node in
195 (* Set the starting EOL count to 2 instead of 1 because it's valid to have up
196 to one blank line between the end of a docblock and the start of its
197 associated element. *)
198 helper (List.rev trivia_list) [] 2