3/10 update_symbol_index takes sienv
[hiphop-php.git] / hphp / hack / src / client / ide_service / clientIdeIncremental.ml
blob2c6b86be7bb8729072f1080a1d13208c43c8b7da
1 (*
2 * Copyright (c) 2019, 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 Reordered_argument_collections
13 let log s = Hh_logger.log ("[ide-incremental] " ^^ s)
15 let strip_positions symbols =
16 List.fold symbols ~init:SSet.empty ~f:(fun acc (_, x) -> SSet.add acc x)
18 (* Print old and new symbols in a file after a change *)
19 let log_file_info_change
20 ~(old_file_info : FileInfo.t option)
21 ~(new_file_info : FileInfo.t option)
22 ~(start_time : float)
23 ~(path : Relative_path.t) : unit =
24 let end_time = Unix.gettimeofday () in
25 FileInfo.(
26 let list_symbols_in_file_info file_info =
27 let symbol_list_to_string symbols =
28 let num_symbols = List.length symbols in
29 let max_num_symbols_to_show = 5 in
30 match symbols with
31 | [] -> "<none>"
32 | symbols when num_symbols <= max_num_symbols_to_show ->
33 symbols |> strip_positions |> SSet.elements |> String.concat ~sep:", "
34 | symbols ->
35 let num_remaining_symbols = num_symbols - max_num_symbols_to_show in
36 let symbols = List.take symbols max_num_symbols_to_show in
37 Printf.sprintf
38 "%s (+%d more...)"
39 ( symbols
40 |> strip_positions
41 |> SSet.elements
42 |> String.concat ~sep:", " )
43 num_remaining_symbols
45 match file_info with
46 | Some file_info ->
47 Printf.sprintf
48 "funs: %s, classes: %s, typedefs: %s, consts: %s"
49 (symbol_list_to_string file_info.funs)
50 (symbol_list_to_string file_info.classes)
51 (symbol_list_to_string file_info.typedefs)
52 (symbol_list_to_string file_info.consts)
53 | None -> "<file absent>"
55 let verb =
56 match (old_file_info, new_file_info) with
57 | (Some _, Some _) -> "updated"
58 | (Some _, None) -> "deleted"
59 | (None, Some _) -> "added"
60 | (None, None) ->
61 (* May or may not indicate a bug in either the language client or the
62 language server.
64 - Could happen if the language client sends spurious notifications.
65 - Could happen if the editor writes files in a certain way, such as if
66 they delete the file before moving a new one into place.
67 - Could happen if the language server was not able to read the file,
68 despite it existing on disk (e.g. due to permissions). In this case,
69 we would fail to generate its [FileInfo.t] and assume that it was
70 deleted. This is correct from a certain point of view.
71 - Could happen due to a benign race condition where we process
72 file-change notifications more slowly than they happen. If a file is
73 quickly created, then deleted before we process the create event,
74 we'll think it was deleted twice. This is the correct way to handle
75 the race condition.
77 "spuriously updated"
79 log
80 "File changed (%.3fs) %s %s: old: %s vs. new: %s"
81 (end_time -. start_time)
82 (Relative_path.to_absolute path)
83 verb
84 (list_symbols_in_file_info old_file_info)
85 (list_symbols_in_file_info new_file_info))
88 * This fetches the new names out of the modified file
89 * Result: (old * new)
91 let compute_fileinfo_for_path (env : ServerEnv.env) (path : Relative_path.t) :
92 (FileInfo.t option * Facts.facts option) Lwt.t =
93 (* Fetch file contents *)
94 let%lwt contents = Lwt_utils.read_all (Relative_path.to_absolute path) in
95 let contents = Result.ok contents in
96 let (new_file_info, facts) =
97 match contents with
98 | None -> (None, None)
99 (* The file couldn't be read from disk. Assume it's been deleted or is
100 otherwise inaccessible. Our caller will delete the entries from the
101 naming and reverse naming table; there's nothing for us to do here. *)
102 | Some contents ->
103 (* We don't want our symbols to be mangled for export. Mangling would
104 * convert :xhp:myclass to __xhp_myclass, which would fail name lookup *)
105 Facts_parser.mangle_xhp_mode := false;
106 let popt = env.ServerEnv.popt in
107 let facts =
108 Facts_parser.from_text
109 ~php5_compat_mode:false
110 ~hhvm_compat_mode:true
111 ~disable_nontoplevel_declarations:false
112 ~disable_legacy_soft_typehints:false
113 ~allow_new_attribute_syntax:false
114 ~disable_legacy_attribute_syntax:false
115 ~enable_xhp_class_modifier:
116 (ParserOptions.enable_xhp_class_modifier popt)
117 ~disable_xhp_element_mangling:
118 (ParserOptions.disable_xhp_element_mangling popt)
119 ~filename:path
120 ~text:contents
122 let (funs, classes, record_defs, typedefs, consts) =
123 match facts with
124 | None ->
125 (* File failed to parse or was not a Hack file. *)
126 ([], [], [], [], [])
127 | Some facts ->
128 let to_ids name_type names =
129 List.map names ~f:(fun name ->
130 let fixed_name = Utils.add_ns name in
131 let pos = FileInfo.File (name_type, path) in
132 (pos, fixed_name))
134 let funs = facts.Facts.functions |> to_ids FileInfo.Fun in
135 (* Classes and typedefs are both stored under `types`. There's also a
136 `typeAliases` field which only stores typedefs that we could use if we
137 wanted, but we write out the pattern-matches here for
138 exhaustivity-checking. *)
139 let classes =
140 facts.Facts.types
141 |> Facts.InvSMap.filter (fun _k v ->
142 Facts.(
143 match v.kind with
144 | TKClass
145 | TKInterface
146 | TKEnum
147 | TKTrait
148 | TKUnknown
149 | TKMixed ->
150 true
151 | TKTypeAlias
152 | TKRecord ->
153 false))
154 |> Facts.InvSMap.keys
155 |> to_ids FileInfo.Class
157 let record_defs =
158 facts.Facts.types
159 |> Facts.InvSMap.filter (fun _k v -> Facts.(v.kind = TKRecord))
160 |> Facts.InvSMap.keys
161 |> to_ids FileInfo.RecordDef
163 let typedefs =
164 facts.Facts.types
165 |> Facts.InvSMap.filter (fun _k v ->
166 Facts.(
167 match v.kind with
168 | TKTypeAlias -> true
169 | TKClass
170 | TKInterface
171 | TKEnum
172 | TKTrait
173 | TKUnknown
174 | TKMixed
175 | TKRecord ->
176 false))
177 |> Facts.InvSMap.keys
178 |> to_ids FileInfo.Typedef
180 let consts = facts.Facts.constants |> to_ids FileInfo.Const in
181 (funs, classes, record_defs, typedefs, consts)
183 let fi_mode =
184 Full_fidelity_parser.parse_mode
185 (Full_fidelity_source_text.make path contents)
186 |> Option.value (* TODO: is this a reasonable default? *)
187 ~default:FileInfo.Mstrict
189 ( Some
191 FileInfo.file_mode = Some fi_mode;
192 funs;
193 classes;
194 record_defs;
195 typedefs;
196 consts;
197 hash = None;
198 comments = None;
200 facts )
202 Lwt.return (new_file_info, facts)
204 let update_naming_table
205 ~(env : ServerEnv.env)
206 ~(ctx : Provider_context.t)
207 ~(path : Relative_path.t)
208 ~(old_file_info : FileInfo.t option)
209 ~(new_file_info : FileInfo.t option) : ServerEnv.env =
210 let naming_table = env.ServerEnv.naming_table in
211 (* Remove the old entries from the forward and reverse naming tables. *)
212 let naming_table =
213 match old_file_info with
214 | None -> naming_table
215 | Some old_file_info ->
216 (* Update reverse naming table *)
217 FileInfo.(
218 Naming_global.remove_decls
219 ~ctx
220 ~funs:(strip_positions old_file_info.funs)
221 ~classes:(strip_positions old_file_info.classes)
222 ~record_defs:(strip_positions old_file_info.record_defs)
223 ~typedefs:(strip_positions old_file_info.typedefs)
224 ~consts:(strip_positions old_file_info.consts);
226 (* Update and return the forward naming table *)
227 Naming_table.remove naming_table path)
229 (* Update forward naming table and reverse naming table with the new
230 declarations. *)
231 let naming_table =
232 match new_file_info with
233 | None -> naming_table
234 | Some new_file_info ->
235 (* Update reverse naming table.
236 TODO: this doesn't handle name collisions in erroneous programs.
237 NOTE: We don't use [Naming_global.ndecl_file_fast] here because it
238 attempts to look up the symbol by doing a file parse, but the file may not
239 exist on disk anymore. We also don't need to do the file parse in this
240 case anyways, since we just did one and know for a fact where the symbol
241 is. *)
242 let open FileInfo in
243 List.iter new_file_info.funs ~f:(fun (pos, fun_name) ->
244 Naming_provider.add_fun ctx fun_name pos);
245 List.iter new_file_info.classes ~f:(fun (pos, class_name) ->
246 Naming_provider.add_class ctx class_name pos);
247 List.iter new_file_info.record_defs ~f:(fun (pos, record_def_name) ->
248 Naming_provider.add_record_def ctx record_def_name pos);
249 List.iter new_file_info.typedefs ~f:(fun (pos, typedef_name) ->
250 Naming_provider.add_typedef ctx typedef_name pos);
251 List.iter new_file_info.consts ~f:(fun (pos, const_name) ->
252 Naming_provider.add_const ctx const_name pos);
254 (* Update and return the forward naming table *)
255 Naming_table.update naming_table path new_file_info
257 { env with ServerEnv.naming_table }
259 let invalidate_decls
260 ~(ctx : Provider_context.t) ~(old_file_info : FileInfo.t option) : unit =
261 (* TODO(ljw): this isn't right... It's correct for us to invalidate shallow
262 decls found in this file. But notionally, for correctness, we should also
263 invalidate all decls and all linearizations. *)
264 match old_file_info with
265 | None -> ()
266 | Some { FileInfo.funs; classes; record_defs; typedefs; consts; _ } ->
267 funs |> strip_positions |> SSet.iter ~f:(Decl_provider.invalidate_fun ctx);
268 classes
269 |> strip_positions
270 |> SSet.iter ~f:(fun class_name ->
271 Decl_provider.invalidate_class ctx class_name;
272 Shallow_classes_provider.invalidate_class ctx class_name);
273 record_defs
274 |> strip_positions
275 |> SSet.iter ~f:(Decl_provider.invalidate_record_def ctx);
276 typedefs
277 |> strip_positions
278 |> SSet.iter ~f:(Decl_provider.invalidate_typedef ctx);
279 consts
280 |> strip_positions
281 |> SSet.iter ~f:(Decl_provider.invalidate_gconst ctx);
284 let update_symbol_index
285 ~(sienv : SearchUtils.si_env)
286 ~(path : Relative_path.t)
287 ~(facts : Facts.facts option) : SearchUtils.si_env =
288 match facts with
289 | None ->
290 let paths = Relative_path.Set.singleton path in
291 SymbolIndex.remove_files ~sienv ~paths
292 | Some facts -> SymbolIndex.update_from_facts ~sienv ~path ~facts
294 let process_changed_file
295 ~(env : ServerEnv.env) ~(ctx : Provider_context.t) ~(path : Path.t) :
296 ServerEnv.env Lwt.t =
297 let str_path = Path.to_string path in
298 match Relative_path.strip_root_if_possible str_path with
299 | None ->
300 log "Ignored change to file %s, as it is not within our repo root" str_path;
301 Lwt.return env
302 | Some path ->
303 let path = Relative_path.from_root path in
304 if not (FindUtils.path_filter path) then
305 Lwt.return env
306 else
307 let start_time = Unix.gettimeofday () in
308 let old_file_info =
309 Naming_table.get_file_info env.ServerEnv.naming_table path
311 let%lwt (new_file_info, facts) = compute_fileinfo_for_path env path in
312 log_file_info_change ~old_file_info ~new_file_info ~start_time ~path;
313 invalidate_decls ~ctx ~old_file_info;
314 let env =
315 update_naming_table ~env ~ctx ~path ~old_file_info ~new_file_info
317 let local_symbol_table =
318 update_symbol_index ~sienv:env.ServerEnv.local_symbol_table ~path ~facts
320 let env = { env with ServerEnv.local_symbol_table } in
321 Lwt.return env