2 * Copyright (c) 2015, 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
21 [@@deriving eq
, show
, enum
]
25 let phases_up_to_excl : phase
-> phase list
=
27 let range_incl_excl i j
=
32 aux (n
- 1) (n
:: acc
)
36 range_incl_excl min_phase
(phase_to_enum phase
)
37 |> List.map ~f
:(fun enum
-> Option.value_exn
(phase_of_enum enum
))
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
68 let compare x y
= rank x
- rank y
71 let pp pp_data
= make_pp pp_phase pp_data
73 let show pp_data x
= Format.asprintf
"%a" (pp pp_data
) x
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
->
93 Option.value (Relative_path.Map.find_opt
y k
) ~default
:PhaseMap.empty
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
:[]
104 let list_to_files_t = function
105 | [] -> Relative_path.Map.empty
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
120 | Some
(_, phase_map
) ->
122 PhaseMap.max_binding_opt phase_map
|> Option.value_map ~f
:snd ~default
:[]
124 (match List.rev
error_list with
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
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
169 Relative_path.Map.add acc ~key
:file ~data
:errs_without_fixmes)
171 let drop_fixmes_if (err
: t
) (drop
: bool) : t
=
173 drop_fixmed_errors_in_files err
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)
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 =
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
) =
213 let result = f1
() in
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
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
229 let (claim
, reasons
) =
230 let (prim_pos
, msg
) = claim
in
231 if String.equal msg
badpos_message || String.equal msg
badpos_message_2
234 | [] -> failwith
"in try_with_result"
235 | (_p
, claim
) :: reasons
-> ((prim_pos
, 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
) =
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
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
) =
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";
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 =
332 | Parsing
-> "Parsing"
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
346 let sort : error list
-> error list
=
348 let compare_reasons = List.compare (Message.compare Pos_or_decl.compare) in
354 reasons
= x_messages
;
362 reasons
= y_messages
;
366 (* The primary sort order is by file *)
368 Relative_path.compare
369 (fst x_claim
|> Pos.filename
)
370 (fst y_claim
|> Pos.filename
)
372 (* Then within each file, sort by phase *)
374 if comparison = 0 then
375 Int.compare (x_code
/ 1000) (y_code
/ 1000)
379 (* If the error codes are the same, sort by position *)
381 if comparison = 0 then
382 Pos.compare (fst x_claim
) (fst y_claim
)
386 (* If the primary positions are also the same, sort by message text *)
388 if comparison = 0 then
389 String.compare (snd x_claim
) (snd y_claim
)
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
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
422 Relative_path.Map.add
427 (get_current_file_t !file_t_map
)
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
445 (try Multifile.read_file_from_multifile path
with
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
=
459 (match String.Map.find grouped
key with
461 let grouped = String.Map.set
grouped ~
key ~data
:(members
@ [x
]) in
462 build_map xs
grouped keys
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 =
476 let total = List.length errors
+ dropped_count
in
477 let formatted_total =
487 match max_errors
with
488 | Some max_errors
when dropped_count
> 0 ->
490 " (only showing first %d, dropped %d).\n"
495 Some
(formatted_total ^
truncated)
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
505 match !current_context with
507 when Relative_path.equal path
Relative_path.default
508 && not
!allow_errors_in_default_path ->
510 "WARNING: adding an error in default path\n%s\n"
511 (Caml.Printexc.raw_backtrace_to_string
512 (Caml.Printexc.get_callstack
100))
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)
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
525 lazy_decl_error_logging msg error_map
User_error.to_absolute
to_string
527 if error
.User_error.is_fixmed
then
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 (*****************************************************************************)
543 (*****************************************************************************)
545 module type Error_category
= Error_category.S
547 (*****************************************************************************)
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 =
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
601 List.map
reasons ~f
:(fun (pos
, msg) ->
602 Pos.print_verbose_relative
(Pos_or_decl.unsafe_to_raw_pos pos
)
607 Exception.get_current_callstack_string
99 |> Exception.clean_stack
609 HackEventLogger.type_check_primary_position_bug ~
current_file ~
message ~
stack;
611 if Pos.equal current_span Pos.none
then
612 (Pos.make_from
current_file, badpos_message)
614 (current_span, badpos_message_2)
618 (* If primary position in error list isn't in current file, wrap with a sentinel error *)
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
637 wrap_error_in_different_file
640 (claim_as_reason claim)
643 let add_error_with_fixme_error error explanation
=
644 let User_error.{ code
; claim; reasons = _; quickfixes
; is_fixmed
= _ } =
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
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
675 "You cannot use `HH_FIXME` or `HH_IGNORE_ERROR` comments to suppress error %d, and this cannot be enabled by configuration"
678 add_error_with_fixme_error error explanation
682 is_not_raised_partial code
&& Relative_path.is_partial
(Pos.filename
pos)
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 *)
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
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
701 "You cannot use `HH_FIXME` or `HH_IGNORE_ERROR` comments to suppress error %d in declarations"
704 add_error_with_fixme_error error explanation
708 (not
(ISet.is_empty
!allowed_fixme_codes_partial))
709 && Relative_path.is_partial
(Pos.filename
pos)
711 is_allowed_code_partial
713 is_allowed_code_strict
715 if whitelist code
then
716 add_applied_fixme error
720 "You cannot use `HH_FIXME` or `HH_IGNORE_ERROR` comments to suppress error %d"
723 add_error_with_fixme_error error explanation
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
=
738 match Relative_path.Map.find_opt acc path
with
741 let new_phase_map = PhaseMap.remove phase_map phase
in
742 if PhaseMap.is_empty
new_phase_map then
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 *)
753 files_t_merge new_ old ~f
:(fun phase path new_ old
->
754 (if Relative_path.equal path
Relative_path.default
then
758 | Parsing
-> "Parsing"
763 Utils.assert_false_log_backtrace
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`."
771 | Some new_
-> Some
(List.rev new_
)
774 (* For files that were rechecked, but had no errors - remove them from maps *)
775 fold
res (fun path acc
->
777 match Relative_path.Map.find_opt new_ path
with
779 | Some phase_map
-> PhaseMap.mem phase_map
phase
784 remove path
phase acc
)
786 and from_error_list err
= list_to_files_t err
788 let count ?
(drop_fixmed
= true) err
=
790 (drop_fixmes_if err drop_fixmed
)
791 ~f
:(fun _ _ x acc
-> acc
+ List.length
x)
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
=
808 Eval_result.iter ~f
:add_error
809 @@ Eval_result.suppress_intersection ~
is_suppressed
810 @@ Reasons_callback.apply
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
;
822 "Exception while typechecking at position %s\n%s"
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)
834 "Invariant violation at position %s\n%s"
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
841 | Exception_occurred
{ pos; exn
} -> log_exception_occurred pos exn
842 | Invariant_violation
{ pos; telemetry
; desc
; _ } ->
843 log_invariant_violation ~desc
pos telemetry
846 let add_typing_error err
=
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 =
855 Relative_path.Set.fold
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
=
886 Relative_path.Map.map
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
->
897 User_error.{ code
; claim; reasons; quickfixes
= _; is_fixmed
= _ } =
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
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 ->
921 drop_fixmed_errors 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
940 files_t_fold err ~init ~f
:(fun source
_ errors acc
->
941 List.fold_right
errors ~init
:acc ~f
:(f source
))
943 Relative_path.Map.fold err ~init ~f
:(fun source phases acc
->
944 match PhaseMap.find_opt phases
phase with
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
->
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
964 Relative_path.Set.add acc source
)
966 (* Count errors which haven't been HH_FIXME'd. *)
967 let error_count : t
-> int =
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
) ->
978 Relative_path.Map.fold
981 ~f
:(fun _path
errors codes ->
982 PhaseMap.fold errors ~init
:codes ~f
:(fun _phase
errors codes ->
986 ~f
:(fun codes User_error.{ code; _ } ->
987 let codes = ISet.add code codes in
988 if ISet.cardinal
codes >= n
then
993 | Done
codes -> 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
1003 let as_telemetry : t
-> Telemetry.t
=
1006 |> Telemetry.int_ ~
key:"count" ~
value:(error_count errors)
1007 |> Telemetry.int_list
1008 ~
key:"first_5_distinct_error_codes"
1010 (first_n_distinct_error_codes
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 (*****************************************************************************)
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
= _ } =
1040 let origin = Render.strip_ns
origin in
1041 let class_ = Render.strip_ns
class_ in
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
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
1060 (Typing.err_code
Typing.ImplementsDynamic
)
1061 (pos, "Function " ^
function_name ^
" is not dynamically callable.")
1064 (** TODO: Remove use of `User_error.t` representation for nested error *)
1065 let method_is_not_dynamically_callable
1069 support_dynamic_type_attribute
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
1078 | Some e
-> User_error.to_list_ e
1081 let parent_class_reason =
1082 match parent_class_opt
with
1084 | Some
(parent_class_pos
, parent_class_name
) ->
1085 let parent_class_name =
1086 Markdown_lite.md_codify
(Render.strip_ns
parent_class_name)
1092 ^
" is defined in the super class "
1094 ^
" and is not overridden" );
1098 let attribute_reason =
1099 match support_dynamic_type_attribute
with
1103 ( Pos_or_decl.of_raw_pos
pos,
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." );
1115 (Typing.err_code
Typing.ImplementsDynamic
)
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
)
1142 ^
"] A global variable is passed to (or returned from) a function call.")
1144 (*****************************************************************************)
1146 (*****************************************************************************)
1148 let convert_errors_to_string ?
(include_filename
= false) (errors : error list
) :
1152 ~f
:(fun err acc_out
->
1153 let User_error.{ code = _; claim; reasons; quickfixes
= _; is_fixmed
= _ }
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
1166 (Pos.to_absolute
pos |> Pos.filename
)
1169 full_result :: acc_in
1172 (claim_as_reason claim :: reasons))
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 ->
1188 let has_no_errors (f
: unit -> 'a
) : bool =
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
) =
1200 let result = f1
() in
1202 (fun (result, _none
) error -> (result, Some
error))
1204 match error_opt
with
1210 (*****************************************************************************)
1212 (*****************************************************************************)
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;
1224 let try_when f ~if_error_and
:condition ~then_
:do_ =
1225 try_with_result f
(fun result error ->
1226 if condition
() then