Unified symbol-to-docblock server command
[hiphop-php.git] / hphp / hack / src / server / serverRefactor.ml
blob8fc3ed273e8105b69da61994417f643b75f1c740
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 open ServerEnv
12 open ServerRefactorTypes
14 let maybe_add_dollar s =
15 if s.[0] <> '$' then "$" ^ s
16 else s
18 let get_fixme_patches codes (env: env) =
19 let fixmelist = Errors.get_applied_fixmes env.errorl in
20 let poslist = Fixme_provider.get_unused_fixmes
21 ~codes
22 ~applied_fixmes:fixmelist
23 ~fold:Naming_table.fold
24 ~files_info:env.naming_table in
25 List.map ~f:(fun pos -> Remove (Pos.to_absolute pos)) poslist
27 let get_lambda_parameter_rewrite_patches env files =
28 List.concat_map files (fun file ->
29 ServerRewriteLambdaParameters.get_patches env.tcopt (Relative_path.from_root file))
31 let find_def_filename current_filename definition =
32 let open SymbolDefinition in
33 if Pos.filename definition.pos = ServerIdeUtils.path then
34 (* When the definition is in an IDE buffer with local changes, the filename
35 in the definition will be empty. *)
36 current_filename
37 else Pos.filename definition.pos
40 We construct the text for the deprecated wrapper here.
42 Example of deprecated wrapper & its relation to the newly named function:
44 <<__Deprecated("Deprecated: Use `newlyNamedFunction` instead")>>
45 public function oldFunctionName(int $x, SomeClass $y, ...$nums): string {
46 return $this->newlyNamedFunction($x, $y, ...$nums);
49 /**
50 * Some docblock
53 public function newlyNamedFunction(int $x, SomeClass $y, ...$nums): string {
54 // some function body
58 let construct_deprecated_wrapper_stub
59 ~(func_decl_text: string)
60 ~(params_text_list: string list)
61 ~(col_start: int)
62 ~(returns_void: bool)
63 ~(is_async: bool)
64 ~(func_ref: deprecated_wrapper_function_ref)
65 (new_name: string)
66 : string =
67 (* Since the starting column position points to the beginning of the function
68 declaration header, we can use it to figure out the indentation level
69 of the function, and insert whitespace accordingly *)
70 let base_indentation = String.make col_start ' ' in
71 let deprecated_header =
72 base_indentation ^ "<<__Deprecated(\"Use `" ^ new_name ^ "` instead\")>>"
74 let func_decl = base_indentation ^ func_decl_text in
75 (* The immediate body of a function is indented by 2 extra spaces *)
76 let func_body_indentation = String.make 2 ' ' in
77 let return_indentation = base_indentation ^ func_body_indentation in
78 let parameter_input = String.concat ~sep:", " params_text_list in
79 let maybe_return =
80 if returns_void
81 then ""
82 else "return "
84 let maybe_await =
85 if is_async
86 then "await "
87 else ""
89 let maybe_this_or_self = match func_ref with
90 | DeprecatedStaticMethodRef -> "self::"
91 | DeprecatedNonStaticMethodRef -> "$this->"
92 | DeprecatedFunctionRef -> ""
94 let return_statement =
95 return_indentation ^
96 maybe_return ^ maybe_await ^ maybe_this_or_self ^
97 new_name ^ "(" ^ parameter_input ^ ");"
99 "\n" ^ deprecated_header ^
100 "\n" ^ func_decl ^ " {" ^
101 "\n" ^ return_statement ^
102 "\n" ^ base_indentation ^ "}" ^
103 "\n"
105 let get_pos_before_docblock_from_cst_node filename node =
106 let open Full_fidelity_positioned_syntax in
107 let source_text = source_text node in
108 let start_offset = leading_start_offset node in
109 SourceText.relative_pos filename source_text start_offset start_offset
111 (* This function will capture a variadic parameter and give it a name if it is
112 * anonymous. Example:
114 * public static function newName(int $x, ...): string {
116 * would become:
118 * public static function newName(int $x, mixed ...$args): string {
121 let fixup_anonymous_variadic (func_decl: Full_fidelity_positioned_syntax.t)
122 (has_anonymous_variadic: bool): string =
124 let open Full_fidelity_positioned_syntax in
125 if has_anonymous_variadic then
126 let r = Str.regexp "\\.\\.\\." in
127 Str.global_replace r "mixed ...$args" (text func_decl)
128 else
129 text func_decl
131 (* Contains just enough information to properly wrap a function *)
132 type wrapper_call_signature_info = {
133 params_text_list: string list;
134 returns_void: bool;
135 is_async: bool;
136 is_static: bool;
137 has_anonymous_variadic: bool;
140 (* Identify key information about a function so we can produce a deprecated wrapper *)
141 let get_call_signature_for_wrap (func_decl: Full_fidelity_positioned_syntax.t)
142 : wrapper_call_signature_info =
143 let open Full_fidelity_positioned_syntax in
144 match syntax func_decl with
145 | FunctionDeclarationHeader {
146 function_parameter_list = params;
147 function_type = ret_type;
148 function_modifiers = modifiers; _
149 } ->
150 let params_text_list = match syntax params with
151 | SyntaxList params ->
152 let params_text_list = List.map params ~f:begin fun param ->
153 let param = match syntax param with
154 | ListItem { list_item; _ } -> list_item
155 | _ -> failwith "Expected ListItem"
157 match syntax param with
158 (* NOTE:
159 `ParameterDeclaration` includes regular params like "$x" and
160 _named_ variadic parameters like "...$nums". For the latter case,
161 calling `text parameter_name` will return the entire "...$nums"
162 string, including the ellipsis.
164 `VariadicParameter` addresses the unnamed variadic parameter
165 "...". In this case, we provide as a parameter a function call
166 that outputs only the variadic params (and dropping the
167 non-variadic ones).
169 | ParameterDeclaration { parameter_name = name; _ } -> text name
170 | VariadicParameter _ -> "...$args"
171 | _ -> failwith "Expected some parameter type"
172 end in
173 params_text_list
174 | Missing -> []
175 | _ -> []
177 let has_anonymous_variadic = match syntax params with
178 | SyntaxList params ->
179 List.exists params ~f:begin fun param ->
180 let param = match syntax param with
181 | ListItem { list_item; _ } -> list_item
182 | _ -> failwith "Expected ListItem"
184 match syntax param with
185 | VariadicParameter _ -> true
186 | _ -> false
188 | Missing -> false
189 | _ -> false
191 let returns_void = match syntax ret_type with
192 | GenericTypeSpecifier {
193 generic_class_type = generic_type;
194 generic_argument_list = { syntax =
195 TypeArguments {
196 type_arguments_types = { syntax =
197 SyntaxList [{ syntax =
198 ListItem {
199 list_item = { syntax =
200 SimpleTypeSpecifier {
201 simple_type_specifier = type_spec
202 }; _
203 }; _
204 }; _
205 }]; _
206 }; _
207 }; _
208 }; _
209 } -> (text generic_type) = "Awaitable" && (text type_spec) = "void"
210 | SimpleTypeSpecifier { simple_type_specifier = type_spec } ->
211 (text type_spec) = "void"
212 | _ -> false
214 let is_async, is_static = match syntax modifiers with
215 | SyntaxList modifiers ->
216 let is_async =
217 List.exists modifiers ~f:(fun modifier -> (text modifier) = "async")
219 let is_static =
220 List.exists modifiers ~f:(fun modifier -> (text modifier) = "static")
222 is_async, is_static
223 | _ -> false, false
226 params_text_list = params_text_list;
227 returns_void = returns_void;
228 is_async = is_async;
229 is_static = is_static;
230 has_anonymous_variadic = has_anonymous_variadic;
232 | _ -> {
233 params_text_list = [];
234 returns_void = false;
235 is_async = false;
236 is_static = false;
237 has_anonymous_variadic = false;
240 (* Produce a "deprecated" version of the old function so that calls to it can be rerouted *)
241 let get_deprecated_wrapper_patch ~filename ~definition new_name =
242 let open SymbolDefinition in
243 let open Full_fidelity_positioned_syntax in
244 let open Option.Monad_infix in
245 filename >>= fun filename ->
246 definition >>= fun definition ->
247 let definition = SymbolDefinition.to_relative definition in
248 (* We need the number of spaces that the function declaration is offsetted so that we can
249 format our wrapper properly with the correct indent (i.e. we need 0-indexed columns).
251 However, even though column offsets are already indexed accordingly when
252 stored in positions, `destruct_range` adds 1 in order to
253 return an [inclusive, exclusive) span.
255 Thus, we subtract 1.
257 let _, col_start_plus1, _, _ = Pos.destruct_range definition.span in
258 let col_start = col_start_plus1 - 1 in
259 let filename_server_type = ServerCommandTypes.FileName filename in
260 let cst_node =
261 ServerSymbolDefinition.get_definition_cst_node filename_server_type definition in
262 cst_node >>= fun cst_node ->
263 begin match syntax cst_node with
264 | MethodishDeclaration { methodish_function_decl_header = func_decl; _ } ->
265 let call_signature = get_call_signature_for_wrap func_decl in
266 let func_decl_text =
267 fixup_anonymous_variadic func_decl call_signature.has_anonymous_variadic
269 let func_ref =
270 if call_signature.is_static
271 then DeprecatedStaticMethodRef
272 else DeprecatedNonStaticMethodRef
274 Some (
275 func_decl_text,
276 call_signature.params_text_list,
277 call_signature.returns_void,
278 call_signature.is_async,
279 func_ref
281 | FunctionDeclaration { function_declaration_header = func_decl; _ } ->
282 let call_signature = get_call_signature_for_wrap func_decl in
283 let func_decl_text =
284 fixup_anonymous_variadic func_decl call_signature.has_anonymous_variadic
286 let func_ref = DeprecatedFunctionRef in
287 Some (
288 func_decl_text,
289 call_signature.params_text_list,
290 call_signature.returns_void,
291 call_signature.is_async,
292 func_ref
294 | _ -> None
295 end >>| fun (func_decl_text, params_text_list, returns_void, is_async, func_ref) ->
296 let deprecated_wrapper_stub =
297 construct_deprecated_wrapper_stub
298 ~func_decl_text
299 ~params_text_list
300 ~col_start
301 ~returns_void
302 ~is_async
303 ~func_ref
304 new_name
306 let filename =
307 find_def_filename (Relative_path.create_detect_prefix filename) definition
309 let deprecated_wrapper_pos = get_pos_before_docblock_from_cst_node filename cst_node in
310 let patch = {
311 pos = Pos.to_absolute deprecated_wrapper_pos;
312 text = deprecated_wrapper_stub;
313 } in
314 Insert patch
316 let go action genv env =
317 let module Types = ServerCommandTypes.Find_refs in
318 let find_refs_action, new_name = match action with
319 | ClassRename (old_name, new_name) ->
320 Types.Class old_name, new_name
321 | ClassConstRename (class_name, old_name, new_name) ->
322 Types.Member (class_name, Types.Class_const old_name),
323 new_name
324 | MethodRename { class_name; old_name; new_name; _ } ->
325 Types.Member (class_name, Types.Method old_name),
326 new_name
327 | FunctionRename { old_name; new_name; _ } ->
328 Types.Function old_name, new_name
329 | LocalVarRename { filename; file_content; line; char; new_name } ->
330 Types.LocalVar { filename; file_content; line; char }, new_name in
331 let include_defs = true in
332 ServerFindRefs.go find_refs_action include_defs genv env |>
333 ServerCommandTypes.Done_or_retry.map_env ~f:begin fun refs ->
334 let changes = List.fold_left refs ~f:begin fun acc x ->
335 let replacement = {
336 pos = Pos.to_absolute (snd x);
337 text = new_name;
338 } in
339 let patch = Replace replacement in
340 patch :: acc
341 end ~init:[] in
342 let deprecated_wrapper_patch = match action with
343 | FunctionRename { filename; definition; _ }
344 | MethodRename { filename; definition; _ } ->
345 get_deprecated_wrapper_patch ~filename ~definition new_name
346 | ClassRename _
347 | ClassConstRename _
348 | LocalVarRename _ -> None
350 Option.value_map deprecated_wrapper_patch ~default:changes
351 ~f:begin fun patch -> patch :: changes end
354 let go_ide (filename, line, char) new_name genv env =
355 let open SymbolDefinition in
356 let file_content = ServerFileSync.get_file_content (ServerCommandTypes.FileName filename) in
357 let definitions = ServerIdentifyFunction.go_absolute file_content line char env.tcopt in
358 match definitions with
359 | (_, Some definition) :: [] -> begin
360 let {full_name; kind; _} = definition in
361 let pieces = Str.split (Str.regexp "::") full_name in
362 match kind, pieces with
363 | Function, [function_name] ->
364 let command =
365 ServerRefactorTypes.FunctionRename {
366 filename = Some filename;
367 definition = Some definition;
368 old_name = function_name;
369 new_name;
370 } in
371 Ok (go command genv env)
372 | Enum, [enum_name] ->
373 let command =
374 ServerRefactorTypes.ClassRename (enum_name, new_name) in
375 Ok (go command genv env)
376 | Class, [class_name] ->
377 let command =
378 ServerRefactorTypes.ClassRename (class_name, new_name) in
379 Ok (go command genv env)
380 | Const, [class_name; const_name] ->
381 let command =
382 ServerRefactorTypes.ClassConstRename (class_name, const_name, new_name) in
383 Ok (go command genv env)
384 | Method, [class_name; method_name] ->
385 let command =
386 ServerRefactorTypes.MethodRename {
387 filename = Some filename;
388 definition = Some definition;
389 class_name;
390 old_name = method_name;
391 new_name;
392 } in
393 Ok (go command genv env)
394 | LocalVar, _ ->
395 let command =
396 ServerRefactorTypes.LocalVarRename {
397 filename = Relative_path.create_detect_prefix filename;
398 file_content;
399 line;
400 char;
401 new_name = maybe_add_dollar new_name;
402 } in
403 Ok (go command genv env)
404 | _, _ -> Error "Tried to rename a non-renameable symbol"
406 (* We have 0 or >1 definitions so correct behavior is unknown *)
407 | _ -> Error "Tried to rename a non-renameable symbol"