2 * Copyright (c) 2019, 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 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)
23 ~
(path
: Relative_path.t
) : unit =
24 let end_time = Unix.gettimeofday
() in
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
32 | symbols
when num_symbols <= max_num_symbols_to_show ->
33 symbols
|> strip_positions |> SSet.elements
|> String.concat ~sep
:", "
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
42 |> String.concat ~sep
:", " )
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>"
56 match (old_file_info
, new_file_info
) with
57 | (Some _
, Some _
) -> "updated"
58 | (Some _
, None
) -> "deleted"
59 | (None
, Some _
) -> "added"
61 (* May or may not indicate a bug in either the language client or the
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
80 "File changed (%.3fs) %s %s: old: %s vs. new: %s"
81 (end_time -. start_time
)
82 (Relative_path.to_absolute path
)
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
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
) =
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. *)
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
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)
122 let (funs
, classes
, record_defs
, typedefs
, consts
) =
125 (* File failed to parse or was not a Hack file. *)
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
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. *)
141 |> Facts.InvSMap.filter
(fun _k v
->
154 |> Facts.InvSMap.keys
155 |> to_ids FileInfo.Class
159 |> Facts.InvSMap.filter
(fun _k v
-> Facts.(v
.kind
= TKRecord
))
160 |> Facts.InvSMap.keys
161 |> to_ids FileInfo.RecordDef
165 |> Facts.InvSMap.filter
(fun _k v
->
168 | TKTypeAlias
-> true
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)
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
191 FileInfo.file_mode
= Some
fi_mode;
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. *)
213 match old_file_info
with
214 | None
-> naming_table
215 | Some old_file_info
->
216 (* Update reverse naming table *)
218 Naming_global.remove_decls
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
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
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 }
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
266 | Some
{ FileInfo.funs; classes; record_defs; typedefs; consts; _
} ->
267 funs |> strip_positions |> SSet.iter ~f
:(Decl_provider.invalidate_fun ctx
);
270 |> SSet.iter ~f
:(fun class_name
->
271 Decl_provider.invalidate_class ctx class_name
;
272 Shallow_classes_provider.invalidate_class ctx class_name
);
275 |> SSet.iter ~f
:(Decl_provider.invalidate_record_def ctx
);
278 |> SSet.iter ~f
:(Decl_provider.invalidate_typedef ctx
);
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
=
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
300 log "Ignored change to file %s, as it is not within our repo root" str_path;
303 let path = Relative_path.from_root
path in
304 if not
(FindUtils.path_filter
path) then
307 let start_time = Unix.gettimeofday
() in
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;
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