disallow keywords as identifiers
[hiphop-php.git] / hphp / hack / src / parser / docblock_finder.ml
blob4b48a521aa13a023863e26da7b7d53a12961ebd6
1 (**
2 * Copyright (c) 2015, Facebook, Inc.
3 * All rights reserved.
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the "hack" directory of this source tree.
8 *)
10 open Core_kernel
11 module Syntax = Full_fidelity_positioned_syntax
12 module Trivia = Full_fidelity_positioned_trivia
13 module TriviaKind = Full_fidelity_trivia_kind
15 (**
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
34 type finder = {
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)
44 end
45 ) in
46 { comments }
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)
53 (idx : int)
54 (last_line : int)
55 (prev_line : int)
56 (acc : string list) : string list =
57 if idx < 0 then acc
58 else begin
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)
62 else acc
63 end
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.
87 Known failure cases:
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 =
95 let comment =
96 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
102 | []
103 | [_] -> Caml.String.trim
104 | _hd :: tail ->
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
109 let min =
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 ""
116 lines
117 |> List.map ~f:line_trimmer
118 |> String.concat ~sep:"\n"
120 let find_docblock (finder : finder)
121 (last_line : int)
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
129 | [] -> None
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 ^ "*/")
134 else
135 None
136 | None -> None
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))
147 else
148 Some ("/*" ^ str ^ "*/")
150 else
151 None
152 | None -> None
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
159 | [], _
160 | Trivia.{kind = TriviaKind.EndOfLine; _} :: _, 0 ->
161 begin match acc with
162 | [] -> None
163 | comments ->
164 let comment =
165 comments
166 |> List.map ~f:(Str.replace_first line_comment_prefix "")
167 |> String.concat ~sep:"\n"
168 |> Caml.String.trim
170 Some comment
172 | hd :: tail, _ ->
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)
180 | TriviaKind.FixMe
181 | TriviaKind.IgnoreError -> helper tail acc 1
182 | TriviaKind.DelimitedComment
183 | TriviaKind.Unsafe
184 | TriviaKind.UnsafeExpression
185 | TriviaKind.FallThrough
186 | TriviaKind.ExtraTokenError
187 | TriviaKind.AfterHaltCompiler ->
188 (* Short circuit immediately. *)
189 helper [] acc 0
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