small refactorings
[hiphop-php.git] / hphp / hack / src / errors / errors.ml
blob667aba39e59415de8814a2a19804856f9a6cd933
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 Hh_prelude
11 open Reordered_argument_collections
13 type error_code = int
15 type phase =
16 | Init
17 | Parsing
18 | Naming
19 | Decl
20 | Typing
21 [@@deriving eq, show, enum]
23 let _ = max_phase
25 let phases_up_to_excl : phase -> phase list =
26 fun phase ->
27 let range_incl_excl i j =
28 let rec aux n acc =
29 if n < i then
30 acc
31 else
32 aux (n - 1) (n :: acc)
34 aux (j - 1) []
36 range_incl_excl min_phase (phase_to_enum phase)
37 |> List.map ~f:(fun enum -> Option.value_exn (phase_of_enum enum))
39 type format =
40 | Context
41 | Raw
42 | Highlighted
44 let claim_as_reason : Pos.t Message.t -> Pos_or_decl.t Message.t =
45 (fun (p, m) -> (Pos_or_decl.of_raw_pos p, m))
47 (* The file and phase of analysis being currently performed *)
48 let current_context : (Relative_path.t * phase) ref =
49 ref (Relative_path.default, Typing)
51 let current_span : Pos.t ref = ref Pos.none
53 let ignore_pos_outside_current_span : bool ref = ref false
55 let allow_errors_in_default_path = ref true
57 module PhaseMap = struct
58 include Reordered_argument_map (WrappedMap.Make (struct
59 type t = phase
61 let rank = function
62 | Init -> 0
63 | Parsing -> 1
64 | Naming -> 2
65 | Decl -> 3
66 | Typing -> 4
68 let compare x y = rank x - rank y
69 end))
71 let pp pp_data = make_pp pp_phase pp_data
73 let show pp_data x = Format.asprintf "%a" (pp pp_data) x
74 end
76 (** Results of single file analysis. *)
77 type 'a file_t = 'a list PhaseMap.t [@@deriving eq, show]
79 (** Results of multi-file analysis. *)
80 type 'a files_t = 'a file_t Relative_path.Map.t [@@deriving eq, show]
82 let files_t_fold v ~f ~init =
83 Relative_path.Map.fold v ~init ~f:(fun path v acc ->
84 PhaseMap.fold v ~init:acc ~f:(fun phase v acc -> f path phase v acc))
86 let files_t_map v ~f = Relative_path.Map.map v ~f:(fun v -> PhaseMap.map v ~f)
88 let files_t_merge ~f x y =
89 (* Using fold instead of merge to make the runtime proportional to the size
90 * of first argument (like List.rev_append ) *)
91 Relative_path.Map.fold x ~init:y ~f:(fun k x acc ->
92 let y =
93 Option.value (Relative_path.Map.find_opt y k) ~default:PhaseMap.empty
95 Relative_path.Map.add
96 acc
97 ~key:k
98 ~data:(PhaseMap.merge x y ~f:(fun phase x y -> f phase k x y)))
100 let files_t_to_list x =
101 files_t_fold x ~f:(fun _ _ x acc -> List.rev_append x acc) ~init:[]
102 |> List.rev
104 let list_to_files_t = function
105 | [] -> Relative_path.Map.empty
106 | x ->
107 (* Values constructed here should not be used with incremental mode.
108 * See assert in incremental_update. *)
109 Relative_path.Map.singleton
110 Relative_path.default
111 (PhaseMap.singleton Typing x)
113 (* Get most recently-ish added error. *)
114 let get_last error_map =
115 (* If this map has more than one element, we pick an arbitrary file. Because
116 * of that, we might not end up with the most recent error and generate a
117 * less-specific error message. This should be rare. *)
118 match Relative_path.Map.max_binding_opt error_map with
119 | None -> None
120 | Some (_, phase_map) ->
121 let error_list =
122 PhaseMap.max_binding_opt phase_map |> Option.value_map ~f:snd ~default:[]
124 (match List.rev error_list with
125 | [] -> None
126 | e :: _ -> Some e)
128 type finalized_error = (Pos.absolute, Pos.absolute) User_error.t
129 [@@deriving eq, ord, show]
131 type error = (Pos.t, Pos_or_decl.t) User_error.t [@@deriving eq, ord, show]
133 type per_file_errors = error file_t
135 type t = error files_t [@@deriving eq, show]
137 module Error = struct
138 type t = error [@@deriving ord]
141 module ErrorSet = Caml.Set.Make (Error)
143 module FinalizedError = struct
144 type t = finalized_error [@@deriving ord]
147 module FinalizedErrorSet = Caml.Set.Make (FinalizedError)
149 let drop_fixmed_errors (errs : ('a, 'b) User_error.t list) :
150 ('a, 'b) User_error.t list =
151 List.filter errs ~f:(fun e -> not e.User_error.is_fixmed)
153 let drop_fixmed_errors_in_file (ef : ('a, 'b) User_error.t file_t) :
154 ('a, 'b) User_error.t file_t =
155 PhaseMap.fold ef ~init:PhaseMap.empty ~f:(fun phase errs acc ->
156 match drop_fixmed_errors errs with
157 | [] -> acc
158 | errs -> PhaseMap.add acc ~key:phase ~data:errs)
160 let drop_fixmed_errors_in_files efs =
161 Relative_path.Map.fold
163 ~init:Relative_path.Map.empty
164 ~f:(fun file errs acc ->
165 let errs_without_fixmes = drop_fixmed_errors_in_file errs in
166 if PhaseMap.is_empty errs_without_fixmes then
168 else
169 Relative_path.Map.add acc ~key:file ~data:errs_without_fixmes)
171 let drop_fixmes_if (err : t) (drop : bool) : t =
172 if drop then
173 drop_fixmed_errors_in_files err
174 else
177 (** Get all errors emitted. Drops errors with valid HH_FIXME comments
178 unless ~drop_fixmed:false is set. *)
179 let get_error_list ?(drop_fixmed = true) err =
180 files_t_to_list (drop_fixmes_if err drop_fixmed)
182 let (error_map : error files_t ref) = ref Relative_path.Map.empty
184 let accumulate_errors = ref false
186 (* Some filename when declaring *)
187 let in_lazy_decl = ref None
189 let (is_hh_fixme : (Pos.t -> error_code -> bool) ref) = ref (fun _ _ -> false)
191 let badpos_message =
192 Printf.sprintf
193 "Incomplete position information! Your type error is in this file, but we could only find related positions in another file. %s"
194 Error_message_sentinel.please_file_a_bug_message
196 let badpos_message_2 =
197 Printf.sprintf
198 "Incomplete position information! We couldn't find the exact line of your type error in this definition. %s"
199 Error_message_sentinel.please_file_a_bug_message
201 let try_with_result (f1 : unit -> 'res) (f2 : 'res -> error -> 'res) : 'res =
202 let error_map_copy = !error_map in
203 let accumulate_errors_copy = !accumulate_errors in
204 let is_hh_fixme_copy = !is_hh_fixme in
205 (is_hh_fixme := (fun _ _ -> false));
206 error_map := Relative_path.Map.empty;
207 accumulate_errors := true;
208 let (result, errors) =
209 Utils.try_finally
211 begin
212 fun () ->
213 let result = f1 () in
214 (result, !error_map)
216 ~finally:
217 begin
218 fun () ->
219 error_map := error_map_copy;
220 accumulate_errors := accumulate_errors_copy;
221 is_hh_fixme := is_hh_fixme_copy
224 match get_last errors with
225 | None -> result
226 | Some User_error.{ code; claim; reasons; quickfixes; is_fixmed = _ } ->
227 (* Remove bad position sentinel if present: we might be about to add a new primary
228 * error position. *)
229 let (claim, reasons) =
230 let (prim_pos, msg) = claim in
231 if String.equal msg badpos_message || String.equal msg badpos_message_2
232 then
233 match reasons with
234 | [] -> failwith "in try_with_result"
235 | (_p, claim) :: reasons -> ((prim_pos, claim), reasons)
236 else
237 (claim, reasons)
239 f2 result @@ User_error.make code claim reasons ~quickfixes
241 let try_with_result_pure ~fail f g =
242 let error_map_copy = !error_map in
243 let accumulate_errors_copy = !accumulate_errors in
244 let is_hh_fixme_copy = !is_hh_fixme in
245 (is_hh_fixme := (fun _ _ -> false));
246 error_map := Relative_path.Map.empty;
247 accumulate_errors := true;
248 let (result, errors) =
249 Utils.try_finally
251 begin
252 fun () ->
253 let result = f () in
254 (result, !error_map)
256 ~finally:
257 begin
258 fun () ->
259 error_map := error_map_copy;
260 accumulate_errors := accumulate_errors_copy;
261 is_hh_fixme := is_hh_fixme_copy
264 match get_last errors with
265 | None when not @@ fail result -> result
266 | _ -> g result
268 (* Reset errors before running [f] so that we can return the errors
269 * caused by f. These errors are not added in the global list of errors. *)
270 let do_ ?(apply_fixmes = true) ?(drop_fixmed = true) f =
271 let error_map_copy = !error_map in
272 let accumulate_errors_copy = !accumulate_errors in
273 error_map := Relative_path.Map.empty;
274 accumulate_errors := true;
275 let is_hh_fixme_copy = !is_hh_fixme in
276 (if not apply_fixmes then is_hh_fixme := (fun _ _ -> false));
277 let (result, out_errors) =
278 Utils.try_finally
280 begin
281 fun () ->
282 let result = f () in
283 (result, !error_map)
285 ~finally:
286 begin
287 fun () ->
288 error_map := error_map_copy;
289 accumulate_errors := accumulate_errors_copy;
290 is_hh_fixme := is_hh_fixme_copy
293 let out_errors = files_t_map ~f:List.rev out_errors in
294 (drop_fixmes_if out_errors drop_fixmed, result)
296 let run_in_context path phase f =
297 let context_copy = !current_context in
298 current_context := (path, phase);
299 Utils.try_finally ~f ~finally:(fun () -> current_context := context_copy)
301 let run_with_span span f =
302 let old_span = !current_span in
303 current_span := span;
304 Utils.try_finally ~f ~finally:(fun () -> current_span := old_span)
306 (* Log important data if lazy_decl triggers a crash *)
307 let lazy_decl_error_logging error error_map to_absolute to_string =
308 let error_list = files_t_to_list !error_map in
309 (* Print the current error list, which should be empty *)
310 Printf.eprintf "%s" "Error list(should be empty):\n";
311 List.iter error_list ~f:(fun err ->
312 let msg = err |> to_absolute |> to_string in
313 Printf.eprintf "%s\n" msg);
314 Printf.eprintf "%s" "Offending error:\n";
315 Printf.eprintf "%s" error;
317 (* Print out a larger stack trace *)
318 Printf.eprintf "%s" "Callstack:\n";
319 Printf.eprintf
320 "%s"
321 (Caml.Printexc.raw_backtrace_to_string (Caml.Printexc.get_callstack 500));
323 (* Exit with special error code so we can see the log after *)
324 Exit.exit Exit_status.Lazy_decl_bug
326 (*****************************************************************************)
327 (* Error code printing. *)
328 (*****************************************************************************)
329 let phase_to_string (phase : phase) : string =
330 match phase with
331 | Init -> "Init"
332 | Parsing -> "Parsing"
333 | Naming -> "Naming"
334 | Decl -> "Decl"
335 | Typing -> "Typing"
337 let phase_of_string (value : string) : phase option =
338 match Caml.String.lowercase_ascii value with
339 | "init" -> Some Init
340 | "parsing" -> Some Parsing
341 | "naming" -> Some Naming
342 | "decl" -> Some Decl
343 | "typing" -> Some Typing
344 | _ -> None
346 let sort : error list -> error list =
347 fun err ->
348 let compare_reasons = List.compare (Message.compare Pos_or_decl.compare) in
349 let compare
350 User_error.
352 code = x_code;
353 claim = x_claim;
354 reasons = x_messages;
355 quickfixes = _;
356 is_fixmed = _;
358 User_error.
360 code = y_code;
361 claim = y_claim;
362 reasons = y_messages;
363 quickfixes = _;
364 is_fixmed = _;
366 (* The primary sort order is by file *)
367 let comparison =
368 Relative_path.compare
369 (fst x_claim |> Pos.filename)
370 (fst y_claim |> Pos.filename)
372 (* Then within each file, sort by phase *)
373 let comparison =
374 if comparison = 0 then
375 Int.compare (x_code / 1000) (y_code / 1000)
376 else
377 comparison
379 (* If the error codes are the same, sort by position *)
380 let comparison =
381 if comparison = 0 then
382 Pos.compare (fst x_claim) (fst y_claim)
383 else
384 comparison
386 (* If the primary positions are also the same, sort by message text *)
387 let comparison =
388 if comparison = 0 then
389 String.compare (snd x_claim) (snd y_claim)
390 else
391 comparison
393 (* Finally, if the message text is also the same, then continue comparing
394 the reason messages (which indicate the reason why Hack believes
395 there is an error reported in the claim message) *)
396 if comparison = 0 then
397 compare_reasons x_messages y_messages
398 else
399 comparison
401 let equal x y = compare x y = 0 in
402 List.sort ~compare:(fun x y -> compare x y) err
403 |> List.remove_consecutive_duplicates ~equal:(fun x y -> equal x y)
405 let get_sorted_error_list ?(drop_fixmed = true) err =
406 sort (files_t_to_list (drop_fixmes_if err drop_fixmed))
408 (* Getters and setter for passed-in map, based on current context *)
409 let get_current_file_t file_t_map =
410 let current_file = fst !current_context in
411 Relative_path.Map.find_opt file_t_map current_file
412 |> Option.value ~default:PhaseMap.empty
414 let get_current_list file_t_map =
415 let current_phase = snd !current_context in
416 get_current_file_t file_t_map |> fun x ->
417 PhaseMap.find_opt x current_phase |> Option.value ~default:[]
419 let set_current_list file_t_map new_list =
420 let (current_file, current_phase) = !current_context in
421 file_t_map :=
422 Relative_path.Map.add
423 !file_t_map
424 ~key:current_file
425 ~data:
426 (PhaseMap.add
427 (get_current_file_t !file_t_map)
428 ~key:current_phase
429 ~data:new_list)
431 let do_with_context ?(drop_fixmed = true) path phase f =
432 run_in_context path phase (fun () -> do_ ~drop_fixmed f)
434 (** Turn on lazy decl mode for the duration of the closure.
435 This runs without returning the original state,
436 since we collect it later in do_with_lazy_decls_ *)
437 let run_in_decl_mode filename f =
438 let old_in_lazy_decl = !in_lazy_decl in
439 in_lazy_decl := Some filename;
440 Utils.try_finally ~f ~finally:(fun () -> in_lazy_decl := old_in_lazy_decl)
442 let read_lines path =
443 try In_channel.read_lines path with
444 | Sys_error _ ->
445 (try Multifile.read_file_from_multifile path with
446 | Sys_error _ -> [])
448 let num_digits x = int_of_float (Float.log10 (float_of_int x)) + 1
450 (* Sort [xs] such that all the values that return the same
451 value for (f x) are consecutive. For any two elements
452 x1 and x2 in xs where (f x1) = (f x2), if x1 occurs before x2 in
453 xs, then x1 also occurs before x2 in the returned list. *)
454 let combining_sort (xs : 'a list) ~(f : 'a -> string) : _ list =
455 let rec build_map xs grouped keys =
456 match xs with
457 | x :: xs ->
458 let key = f x in
459 (match String.Map.find grouped key with
460 | Some members ->
461 let grouped = String.Map.set grouped ~key ~data:(members @ [x]) in
462 build_map xs grouped keys
463 | None ->
464 let grouped = String.Map.set grouped ~key ~data:[x] in
465 build_map xs grouped (key :: keys))
466 | [] -> (grouped, keys)
468 let (grouped, keys) = build_map xs String.Map.empty [] in
469 List.concat_map (List.rev keys) ~f:(fun fn -> String.Map.find_exn grouped fn)
471 (* E.g. "10 errors found." *)
472 let format_summary format errors dropped_count max_errors : string option =
473 match format with
474 | Context
475 | Highlighted ->
476 let total = List.length errors + dropped_count in
477 let formatted_total =
478 Printf.sprintf
479 "%d error%s found"
480 total
481 (if total = 1 then
483 else
484 "s")
486 let truncated =
487 match max_errors with
488 | Some max_errors when dropped_count > 0 ->
489 Printf.sprintf
490 " (only showing first %d, dropped %d).\n"
491 max_errors
492 dropped_count
493 | _ -> ".\n"
495 Some (formatted_total ^ truncated)
496 | Raw -> None
498 let report_pos_from_reason = ref false
500 let to_string error = User_error.to_string !report_pos_from_reason error
502 let add_error_impl error =
503 if !accumulate_errors then
504 let () =
505 match !current_context with
506 | (path, _)
507 when Relative_path.equal path Relative_path.default
508 && not !allow_errors_in_default_path ->
509 Hh_logger.log
510 "WARNING: adding an error in default path\n%s\n"
511 (Caml.Printexc.raw_backtrace_to_string
512 (Caml.Printexc.get_callstack 100))
513 | _ -> ()
515 (* Cheap test to avoid duplicating most recent error *)
516 let error_list = get_current_list !error_map in
517 match error_list with
518 | old_error :: _ when equal_error error old_error -> ()
519 | _ -> set_current_list error_map (error :: error_list)
520 else
521 (* We have an error, but haven't handled it in any way *)
522 let msg = error |> User_error.to_absolute |> to_string in
523 match !in_lazy_decl with
524 | Some _ ->
525 lazy_decl_error_logging msg error_map User_error.to_absolute to_string
526 | None ->
527 if error.User_error.is_fixmed then
529 else
530 Utils.assert_false_log_backtrace (Some msg)
532 (* Whether we've found at least one error *)
533 let currently_has_errors () = not (List.is_empty (get_current_list !error_map))
535 module Parsing = Error_codes.Parsing
536 module Naming = Error_codes.Naming
537 module NastCheck = Error_codes.NastCheck
538 module Typing = Error_codes.Typing
539 module GlobalWriteCheck = Error_codes.GlobalWriteCheck
541 (*****************************************************************************)
542 (* Types *)
543 (*****************************************************************************)
545 module type Error_category = Error_category.S
547 (*****************************************************************************)
548 (* HH_FIXMEs hook *)
549 (*****************************************************************************)
551 let error_codes_treated_strictly = ref (ISet.of_list [])
553 let is_strict_code code = ISet.mem code !error_codes_treated_strictly
555 (* The 'phps FixmeAllHackErrors' tool must be kept in sync with this list *)
556 let hard_banned_codes =
557 ISet.of_list
559 Typing.err_code Typing.InvalidIsAsExpressionHint;
560 Typing.err_code Typing.InvalidEnforceableTypeArgument;
561 Typing.err_code Typing.RequireArgsReify;
562 Typing.err_code Typing.InvalidReifiedArgument;
563 Typing.err_code Typing.GenericsNotAllowed;
564 Typing.err_code Typing.InvalidNewableTypeArgument;
565 Typing.err_code Typing.InvalidNewableTypeParamConstraints;
566 Typing.err_code Typing.NewWithoutNewable;
567 Typing.err_code Typing.NewClassReified;
568 Typing.err_code Typing.MemoizeReified;
569 Typing.err_code Typing.ClassGetReified;
572 let allowed_fixme_codes_strict = ref ISet.empty
574 let allowed_fixme_codes_partial = ref ISet.empty
576 let codes_not_raised_partial = ref ISet.empty
578 let set_allow_errors_in_default_path x = allow_errors_in_default_path := x
580 let is_allowed_code_strict code = ISet.mem code !allowed_fixme_codes_strict
582 let is_allowed_code_partial code = ISet.mem code !allowed_fixme_codes_partial
584 let is_not_raised_partial code = ISet.mem code !codes_not_raised_partial
586 let (get_hh_fixme_pos : (Pos.t -> error_code -> Pos.t option) ref) =
587 ref (fun _ _ -> None)
589 let (is_hh_fixme_disallowed : (Pos.t -> error_code -> bool) ref) =
590 ref (fun _ _ -> false)
592 (*****************************************************************************)
593 (* Errors accumulator. *)
594 (*****************************************************************************)
596 (* We still want to error somewhere to avoid silencing any kind of errors, which
597 * could cause errors to creep into the codebase. *)
598 let wrap_error_in_different_file ~current_file ~current_span claim reasons =
599 let reasons = claim :: reasons in
600 let message =
601 List.map reasons ~f:(fun (pos, msg) ->
602 Pos.print_verbose_relative (Pos_or_decl.unsafe_to_raw_pos pos)
603 ^ ": "
604 ^ msg)
606 let stack =
607 Exception.get_current_callstack_string 99 |> Exception.clean_stack
609 HackEventLogger.type_check_primary_position_bug ~current_file ~message ~stack;
610 let claim =
611 if Pos.equal current_span Pos.none then
612 (Pos.make_from current_file, badpos_message)
613 else
614 (current_span, badpos_message_2)
616 (claim, reasons)
618 (* If primary position in error list isn't in current file, wrap with a sentinel error *)
619 let check_pos_msg :
620 Pos.t Message.t * Pos_or_decl.t Message.t list ->
621 Pos.t Message.t * Pos_or_decl.t Message.t list =
622 fun (claim, reasons) ->
623 let pos = fst claim in
624 let current_file = fst !current_context in
625 let current_span = !current_span in
626 (* If error is reported inside the current span, or no span has been set but the error
627 * is reported in the current file, then accept the error *)
629 !ignore_pos_outside_current_span
630 || Pos.contains current_span pos
631 || Pos.equal current_span Pos.none
632 && Relative_path.equal (Pos.filename pos) current_file
633 || Relative_path.equal current_file Relative_path.default
634 then
635 (claim, reasons)
636 else
637 wrap_error_in_different_file
638 ~current_file
639 ~current_span
640 (claim_as_reason claim)
641 reasons
643 let add_error_with_fixme_error error explanation =
644 let User_error.{ code; claim; reasons = _; quickfixes; is_fixmed = _ } =
645 error
647 let (pos, _) = claim in
648 let pos = Option.value (!get_hh_fixme_pos pos code) ~default:pos in
649 add_error_impl error;
650 add_error_impl @@ User_error.make code (pos, explanation) [] ~quickfixes
652 let add_applied_fixme error =
653 let error = { error with User_error.is_fixmed = true } in
654 add_error_impl error
656 let rec add code pos msg = add_list code (pos, msg) []
658 and fixme_present (pos : Pos.t) code =
659 !is_hh_fixme pos code || !is_hh_fixme_disallowed pos code
661 and add_list code ?quickfixes (claim : _ Message.t) reasons =
662 add_error @@ User_error.make code claim reasons ?quickfixes
664 and add_error (error : error) =
665 let User_error.{ code; claim; reasons; quickfixes; is_fixmed = _ } = error in
666 let (claim, reasons) = check_pos_msg (claim, reasons) in
667 let error = User_error.make code claim reasons ~quickfixes in
669 let pos = fst claim in
671 if ISet.mem code hard_banned_codes then
672 if fixme_present pos code then
673 let explanation =
674 Printf.sprintf
675 "You cannot use `HH_FIXME` or `HH_IGNORE_ERROR` comments to suppress error %d, and this cannot be enabled by configuration"
676 code
678 add_error_with_fixme_error error explanation
679 else
680 add_error_impl error
681 else if
682 is_not_raised_partial code && Relative_path.is_partial (Pos.filename pos)
683 then
685 else if not (fixme_present pos code) then
686 (* Fixmes and banned decl fixmes are separated by the parser because Errors can't recover
687 * the position information after the fact. This is the default case, where an HH_FIXME
688 * comment is not present. Therefore, the remaining cases are variations on behavior when
689 * a fixme is present *)
690 add_error_impl error
691 else if Relative_path.(is_hhi (prefix (Pos.filename pos))) then
692 add_applied_fixme error
693 else if !report_pos_from_reason && Pos.get_from_reason pos then
694 let explanation =
695 "You cannot use `HH_FIXME` or `HH_IGNORE_ERROR` comments to suppress an error whose position was derived from reason information"
697 add_error_with_fixme_error error explanation
698 else if !is_hh_fixme_disallowed pos code then
699 let explanation =
700 Printf.sprintf
701 "You cannot use `HH_FIXME` or `HH_IGNORE_ERROR` comments to suppress error %d in declarations"
702 code
704 add_error_with_fixme_error error explanation
705 else
706 let whitelist =
708 (not (ISet.is_empty !allowed_fixme_codes_partial))
709 && Relative_path.is_partial (Pos.filename pos)
710 then
711 is_allowed_code_partial
712 else
713 is_allowed_code_strict
715 if whitelist code then
716 add_applied_fixme error
717 else
718 let explanation =
719 Printf.sprintf
720 "You cannot use `HH_FIXME` or `HH_IGNORE_ERROR` comments to suppress error %d"
721 code
723 add_error_with_fixme_error error explanation
725 and merge err' err =
726 let append _ _ x y =
727 let x = Option.value x ~default:[] in
728 let y = Option.value y ~default:[] in
729 Some (List.rev_append x y)
731 files_t_merge ~f:append err' err
733 and incremental_update old new_ fold phase =
734 (* Helper to remove acc[path][phase]. If acc[path] becomes empty afterwards,
735 * remove it too (i.e do not store empty maps or lists ever). *)
736 let remove path phase acc =
737 let new_phase_map =
738 match Relative_path.Map.find_opt acc path with
739 | None -> None
740 | Some phase_map ->
741 let new_phase_map = PhaseMap.remove phase_map phase in
742 if PhaseMap.is_empty new_phase_map then
743 None
744 else
745 Some new_phase_map
747 match new_phase_map with
748 | None -> Relative_path.Map.remove acc path
749 | Some x -> Relative_path.Map.add acc ~key:path ~data:x
751 (* Replace old errors with new *)
752 let res =
753 files_t_merge new_ old ~f:(fun phase path new_ old ->
754 (if Relative_path.equal path Relative_path.default then
755 let phase =
756 match phase with
757 | Init -> "Init"
758 | Parsing -> "Parsing"
759 | Naming -> "Naming"
760 | Decl -> "Decl"
761 | Typing -> "Typing"
763 Utils.assert_false_log_backtrace
764 (Some
765 ("Default (untracked) error sources should not get into incremental "
766 ^ "mode. There might be a missing call to `Errors.do_with_context`/"
767 ^ "`run_in_context` somwhere or incorrectly used `Errors.from_error_list`."
768 ^ "Phase: "
769 ^ phase)));
770 match new_ with
771 | Some new_ -> Some (List.rev new_)
772 | None -> old)
774 (* For files that were rechecked, but had no errors - remove them from maps *)
775 fold res (fun path acc ->
776 let has_errors =
777 match Relative_path.Map.find_opt new_ path with
778 | None -> false
779 | Some phase_map -> PhaseMap.mem phase_map phase
781 if has_errors then
783 else
784 remove path phase acc)
786 and from_error_list err = list_to_files_t err
788 let count ?(drop_fixmed = true) err =
789 files_t_fold
790 (drop_fixmes_if err drop_fixmed)
791 ~f:(fun _ _ x acc -> acc + List.length x)
792 ~init:0
794 let is_empty ?(drop_fixmed = true) err =
795 Relative_path.Map.is_empty (drop_fixmes_if err drop_fixmed)
797 let add_parsing_error err = add_error @@ Parsing_error.to_user_error err
799 let add_naming_error err = add_error @@ Naming_error.to_user_error err
801 let add_nast_check_error err = add_error @@ Nast_check_error.to_user_error err
803 let is_suppressed User_error.{ claim; code; _ } =
804 fixme_present Message.(get_message_pos claim) code
806 let apply_error_from_reasons_callback ?code ?claim ?reasons ?quickfixes err =
807 Typing_error.(
808 Eval_result.iter ~f:add_error
809 @@ Eval_result.suppress_intersection ~is_suppressed
810 @@ Reasons_callback.apply
811 ?code
812 ?claim
813 ?reasons
814 ?quickfixes
816 ~current_span:!current_span)
818 let log_exception_occurred pos e =
819 let pos_str = pos |> Pos.to_absolute |> Pos.string in
820 HackEventLogger.type_check_exn_bug ~path:(Pos.filename pos) ~pos:pos_str ~e;
821 Hh_logger.error
822 "Exception while typechecking at position %s\n%s"
823 pos_str
824 (Exception.to_string e)
826 let log_invariant_violation ~desc pos telemetry =
827 let pos_str = pos |> Pos.to_absolute |> Pos.string in
828 HackEventLogger.invariant_violation_bug
829 ~path:(Pos.filename pos)
830 ~pos:pos_str
831 ~desc
832 telemetry;
833 Hh_logger.error
834 "Invariant violation at position %s\n%s"
835 pos_str
836 Telemetry.(telemetry |> string_ ~key:"desc" ~value:desc |> to_string)
838 let log_primary_typing_error prim_err =
839 let open Typing_error.Primary in
840 match prim_err with
841 | Exception_occurred { pos; exn } -> log_exception_occurred pos exn
842 | Invariant_violation { pos; telemetry; desc; _ } ->
843 log_invariant_violation ~desc pos telemetry
844 | _ -> ()
846 let add_typing_error err =
847 Typing_error.(
848 iter ~on_prim:log_primary_typing_error ~on_snd:(fun _ -> ()) err;
849 Eval_result.iter ~f:add_error
850 @@ Eval_result.suppress_intersection ~is_suppressed
851 @@ to_user_error err ~current_span:!current_span)
853 let incremental_update ~old ~new_ ~rechecked phase =
854 let fold init g =
855 Relative_path.Set.fold
857 begin
858 fun path acc ->
859 g path acc
861 ~init
862 rechecked
864 incremental_update old new_ fold phase
866 and empty = Relative_path.Map.empty
868 let from_file_error_list : ?phase:phase -> (Relative_path.t * error) list -> t =
869 fun ?(phase = Typing) errors ->
870 List.fold errors ~init:Relative_path.Map.empty ~f:(fun errors (file, error) ->
871 let errors_for_file =
872 Relative_path.Map.find_opt errors file
873 |> Option.value ~default:PhaseMap.empty
875 let errors_for_phase =
876 PhaseMap.find_opt errors_for_file phase |> Option.value ~default:[]
878 let errors_for_phase = error :: errors_for_phase in
879 let errors_for_file =
880 PhaseMap.add errors_for_file ~key:phase ~data:errors_for_phase
882 Relative_path.Map.add errors ~key:file ~data:errors_for_file)
884 let as_map : t -> error list Relative_path.Map.t =
885 fun errors ->
886 Relative_path.Map.map
887 errors
888 ~f:(PhaseMap.fold ~init:[] ~f:(fun _ -> List.append))
890 let merge_into_current errors = error_map := merge errors !error_map
892 (* Until we return a list of errors from typing, we have to apply
893 'client errors' to a callback for using in subtyping *)
894 let apply_callback_to_errors : t -> Typing_error.Reasons_callback.t -> unit =
895 fun errors on_error ->
896 let on_error
897 User_error.{ code; claim; reasons; quickfixes = _; is_fixmed = _ } =
898 Typing_error.(
899 let code = Option.value_exn (Error_code.of_enum code) in
900 Eval_result.iter ~f:add_error
901 @@ Eval_result.suppress_intersection ~is_suppressed
902 @@ Reasons_callback.apply
903 on_error
904 ~code
905 ~claim:(lazy claim)
906 ~reasons:(lazy reasons)
907 ~current_span:!current_span)
909 Relative_path.Map.iter errors ~f:(fun _ ->
910 PhaseMap.iter ~f:(fun _ -> List.iter ~f:on_error))
912 (*****************************************************************************)
913 (* Accessors. (All methods delegated to the parameterized module.) *)
914 (*****************************************************************************)
916 let per_file_error_count : ?drop_fixmed:bool -> per_file_errors -> int =
917 fun ?(drop_fixmed = true) ->
918 PhaseMap.fold ~init:0 ~f:(fun _phase errors count ->
919 let errors =
920 if drop_fixmed then
921 drop_fixmed_errors errors
922 else
923 errors
925 List.length errors + count)
927 let get_file_errors :
928 ?drop_fixmed:bool -> t -> Relative_path.t -> per_file_errors =
929 fun ?(drop_fixmed = true) errors file ->
930 Relative_path.Map.find_opt (drop_fixmes_if errors drop_fixmed) file
931 |> Option.value ~default:PhaseMap.empty
933 let iter_error_list ?(drop_fixmed = true) f err =
934 List.iter ~f (get_sorted_error_list ~drop_fixmed err)
936 let fold_errors ?(drop_fixmed = true) ?phase err ~init ~f =
937 let err = drop_fixmes_if err drop_fixmed in
938 match phase with
939 | None ->
940 files_t_fold err ~init ~f:(fun source _ errors acc ->
941 List.fold_right errors ~init:acc ~f:(f source))
942 | Some phase ->
943 Relative_path.Map.fold err ~init ~f:(fun source phases acc ->
944 match PhaseMap.find_opt phases phase with
945 | None -> acc
946 | Some errors -> List.fold_right errors ~init:acc ~f:(f source))
948 let fold_errors_in ?(drop_fixmed = true) ?phase err ~file ~init ~f =
949 let err = drop_fixmes_if err drop_fixmed in
950 Relative_path.Map.find_opt err file
951 |> Option.value ~default:PhaseMap.empty
952 |> PhaseMap.fold ~init ~f:(fun p errors acc ->
953 match phase with
954 | Some x when not (equal_phase x p) -> acc
955 | _ -> List.fold_right errors ~init:acc ~f)
957 (* Get paths that have errors which haven't been HH_FIXME'd. *)
958 let get_failed_files (err : t) phase =
959 let err = drop_fixmed_errors_in_files err in
960 files_t_fold err ~init:Relative_path.Set.empty ~f:(fun source p _ acc ->
961 if not (equal_phase phase p) then
963 else
964 Relative_path.Set.add acc source)
966 (* Count errors which haven't been HH_FIXME'd. *)
967 let error_count : t -> int =
968 fun errors ->
969 Relative_path.Map.fold errors ~init:0 ~f:(fun _path errors count ->
970 count + per_file_error_count ~drop_fixmed:true errors)
972 exception Done of ISet.t
974 let first_n_distinct_error_codes : n:int -> t -> error_code list =
975 fun ~n (errors : _ files_t) ->
976 let codes =
978 Relative_path.Map.fold
979 errors
980 ~init:ISet.empty
981 ~f:(fun _path errors codes ->
982 PhaseMap.fold errors ~init:codes ~f:(fun _phase errors codes ->
983 List.fold
984 errors
985 ~init:codes
986 ~f:(fun codes User_error.{ code; _ } ->
987 let codes = ISet.add code codes in
988 if ISet.cardinal codes >= n then
989 raise (Done codes)
990 else
991 codes)))
992 with
993 | Done codes -> codes
995 ISet.elements codes
997 (* Get the error code of the first error which hasn't been HH_FIXME'd. *)
998 let choose_code_opt (errors : t) : int option =
999 drop_fixmed_errors_in_files errors
1000 |> first_n_distinct_error_codes ~n:1
1001 |> List.hd
1003 let as_telemetry : t -> Telemetry.t =
1004 fun errors ->
1005 Telemetry.create ()
1006 |> Telemetry.int_ ~key:"count" ~value:(error_count errors)
1007 |> Telemetry.int_list
1008 ~key:"first_5_distinct_error_codes"
1009 ~value:
1010 (first_n_distinct_error_codes
1011 ~n:5
1012 (drop_fixmed_errors_in_files errors))
1014 (*****************************************************************************)
1015 (* Error code printing. *)
1016 (*****************************************************************************)
1018 let internal_error pos msg = add 0 pos ("Internal error: " ^ msg)
1020 let unimplemented_feature pos msg = add 0 pos ("Feature not implemented: " ^ msg)
1022 let experimental_feature pos msg =
1023 add 0 pos ("Cannot use experimental feature: " ^ msg)
1025 (*****************************************************************************)
1026 (* Typing errors *)
1027 (*****************************************************************************)
1029 (*****************************************************************************)
1030 (* Typing decl errors *)
1031 (*****************************************************************************)
1033 (** TODO: Remove use of `User_error.t` representation for nested error &
1034 callback application *)
1035 let ambiguous_inheritance
1036 pos class_ origin error (on_error : Typing_error.Reasons_callback.t) =
1037 let User_error.{ code; claim; reasons; quickfixes = _; is_fixmed = _ } =
1038 error
1040 let origin = Render.strip_ns origin in
1041 let class_ = Render.strip_ns class_ in
1042 let message =
1043 "This declaration was inherited from an object of type "
1044 ^ Markdown_lite.md_codify origin
1045 ^ ". Redeclare this member in "
1046 ^ Markdown_lite.md_codify class_
1047 ^ " with a compatible signature."
1049 let code = Option.value_exn (Error_codes.Typing.of_enum code) in
1050 apply_error_from_reasons_callback
1051 on_error
1052 ~code
1053 ~reasons:(lazy (claim_as_reason claim :: reasons @ [(pos, message)]))
1055 (** TODO: Remove use of `User_error.t` representation for nested error *)
1056 let function_is_not_dynamically_callable pos function_name error =
1057 let function_name = Markdown_lite.md_codify (Render.strip_ns function_name) in
1058 let nested_error_reason = User_error.to_list_ error in
1059 add_list
1060 (Typing.err_code Typing.ImplementsDynamic)
1061 (pos, "Function " ^ function_name ^ " is not dynamically callable.")
1062 nested_error_reason
1064 (** TODO: Remove use of `User_error.t` representation for nested error *)
1065 let method_is_not_dynamically_callable
1067 method_name
1068 class_name
1069 support_dynamic_type_attribute
1070 parent_class_opt
1071 error_opt =
1072 let method_name = Markdown_lite.md_codify (Render.strip_ns method_name) in
1073 let class_name = Markdown_lite.md_codify (Render.strip_ns class_name) in
1075 let nested_error_reason =
1076 match error_opt with
1077 | None -> []
1078 | Some e -> User_error.to_list_ e
1081 let parent_class_reason =
1082 match parent_class_opt with
1083 | None -> []
1084 | Some (parent_class_pos, parent_class_name) ->
1085 let parent_class_name =
1086 Markdown_lite.md_codify (Render.strip_ns parent_class_name)
1089 ( parent_class_pos,
1090 "Method "
1091 ^ method_name
1092 ^ " is defined in the super class "
1093 ^ parent_class_name
1094 ^ " and is not overridden" );
1098 let attribute_reason =
1099 match support_dynamic_type_attribute with
1100 | true -> []
1101 | false ->
1103 ( Pos_or_decl.of_raw_pos pos,
1104 "A parameter of "
1105 ^ method_name
1106 ^ " is not enforceable. "
1107 ^ "Try adding the <<"
1108 ^ Naming_special_names.UserAttributes.uaSupportDynamicType
1109 ^ ">> attribute to "
1110 ^ "the method to check if its code is safe for dynamic calling." );
1114 add_list
1115 (Typing.err_code Typing.ImplementsDynamic)
1116 ( pos,
1117 "Method "
1118 ^ method_name
1119 ^ " in class "
1120 ^ class_name
1121 ^ " is not dynamically callable." )
1122 (parent_class_reason @ attribute_reason @ nested_error_reason)
1124 let static_var_direct_write_error pos fun_name =
1126 (GlobalWriteCheck.err_code GlobalWriteCheck.StaticVariableDirectWrite)
1128 ("[" ^ fun_name ^ "] A static variable is directly written.")
1130 let global_var_write_error pos fun_name =
1132 (GlobalWriteCheck.err_code GlobalWriteCheck.GlobalVariableWrite)
1134 ("[" ^ fun_name ^ "] A global variable is written.")
1136 let global_var_in_fun_call_error pos fun_name =
1138 (GlobalWriteCheck.err_code GlobalWriteCheck.GlobalVariableInFunctionCall)
1140 ("["
1141 ^ fun_name
1142 ^ "] A global variable is passed to (or returned from) a function call.")
1144 (*****************************************************************************)
1145 (* Printing *)
1146 (*****************************************************************************)
1148 let convert_errors_to_string ?(include_filename = false) (errors : error list) :
1149 string list =
1150 List.fold_right
1151 ~init:[]
1152 ~f:(fun err acc_out ->
1153 let User_error.{ code = _; claim; reasons; quickfixes = _; is_fixmed = _ }
1157 List.fold_right
1158 ~init:acc_out
1159 ~f:(fun (pos, msg) acc_in ->
1160 let pos = Pos_or_decl.unsafe_to_raw_pos pos in
1161 let result = Format.asprintf "%a %s" Pos.pp pos msg in
1162 if include_filename then
1163 let full_result =
1164 Printf.sprintf
1165 "%s %s"
1166 (Pos.to_absolute pos |> Pos.filename)
1167 result
1169 full_result :: acc_in
1170 else
1171 result :: acc_in)
1172 (claim_as_reason claim :: reasons))
1173 errors
1175 (*****************************************************************************)
1176 (* Try if errors. *)
1177 (*****************************************************************************)
1179 let try_ f1 f2 = try_with_result f1 (fun _ err -> f2 err)
1181 let try_pred ~fail f g = try_with_result_pure ~fail f (fun _ -> g ())
1183 let try_with_error f1 f2 =
1184 try_ f1 (fun error ->
1185 add_error error;
1186 f2 ())
1188 let has_no_errors (f : unit -> 'a) : bool =
1189 try_
1190 (fun () ->
1191 let _ = f () in
1192 true)
1193 (fun _ -> false)
1195 (* Runs f2 on the result only if f1 returns has no errors. *)
1196 let try_if_no_errors f1 f2 =
1197 let (result, error_opt) =
1198 try_with_result
1199 (fun () ->
1200 let result = f1 () in
1201 (result, None))
1202 (fun (result, _none) error -> (result, Some error))
1204 match error_opt with
1205 | Some err ->
1206 add_error err;
1207 result
1208 | None -> f2 result
1210 (*****************************************************************************)
1211 (* Do. *)
1212 (*****************************************************************************)
1214 let ignore_ f =
1215 let allow_errors_in_default_path_copy = !allow_errors_in_default_path in
1216 let ignore_pos_outside_current_span_copy = !ignore_pos_outside_current_span in
1217 set_allow_errors_in_default_path true;
1218 ignore_pos_outside_current_span := true;
1219 let (_, result) = do_ f in
1220 set_allow_errors_in_default_path allow_errors_in_default_path_copy;
1221 ignore_pos_outside_current_span := ignore_pos_outside_current_span_copy;
1222 result
1224 let try_when f ~if_error_and:condition ~then_:do_ =
1225 try_with_result f (fun result error ->
1226 if condition () then
1227 do_ error
1228 else
1229 add_error error;
1230 result)