You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
352 lines
13 KiB
352 lines
13 KiB
(*
|
|
* Copyright (c) 2009-2013, Monoidics ltd.
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*)
|
|
open! IStd
|
|
module L = Logging
|
|
module F = Format
|
|
|
|
let error_desc_to_plain_string error_desc =
|
|
let pp fmt = Localise.pp_error_desc fmt error_desc in
|
|
let s = F.asprintf "%t" pp in
|
|
let s = String.strip s in
|
|
let s =
|
|
(* end error description with a dot *)
|
|
if String.is_suffix ~suffix:"." s then s else s ^ "."
|
|
in
|
|
s
|
|
|
|
|
|
let error_desc_to_dotty_string error_desc = Localise.error_desc_get_dotty error_desc
|
|
|
|
let compute_key (bug_type : string) (proc_name : Procname.t) (filename : string) =
|
|
let base_filename = Filename.basename filename
|
|
and simple_procedure_name = Procname.get_method proc_name in
|
|
String.concat ~sep:"|" [base_filename; simple_procedure_name; bug_type]
|
|
|
|
|
|
let compute_hash ~(severity : string) ~(bug_type : string) ~(proc_name : Procname.t)
|
|
~(file : string) ~(qualifier : string) =
|
|
let base_filename = Filename.basename file in
|
|
let hashable_procedure_name = Procname.hashable_name proc_name in
|
|
let location_independent_qualifier =
|
|
(* Removing the line,column, and infer temporary variable (e.g., n$67) information from the
|
|
error message as well as the index of the annonymmous class to make the hash invariant
|
|
when moving the source code in the file *)
|
|
Str.global_replace (Str.regexp "\\(line \\|column \\|parameter \\|\\$\\)[0-9]+") "$_" qualifier
|
|
in
|
|
Utils.better_hash
|
|
(severity, bug_type, hashable_procedure_name, base_filename, location_independent_qualifier)
|
|
|> Caml.Digest.to_hex
|
|
|
|
|
|
let loc_trace_to_jsonbug_record trace_list ekind =
|
|
match ekind with
|
|
| Exceptions.Info ->
|
|
[]
|
|
| _ ->
|
|
let trace_item_to_record trace_item =
|
|
{ Jsonbug_j.level= trace_item.Errlog.lt_level
|
|
; filename= SourceFile.to_string trace_item.Errlog.lt_loc.Location.file
|
|
; line_number= trace_item.Errlog.lt_loc.Location.line
|
|
; column_number= trace_item.Errlog.lt_loc.Location.col
|
|
; description= trace_item.Errlog.lt_description }
|
|
in
|
|
let record_list = List.rev (List.rev_map ~f:trace_item_to_record trace_list) in
|
|
record_list
|
|
|
|
|
|
let should_report (issue_kind : Exceptions.severity) issue_type error_desc eclass =
|
|
if (not Config.filtering) || Exceptions.equal_err_class eclass Exceptions.Linters then true
|
|
else
|
|
let issue_kind_is_blacklisted =
|
|
match issue_kind with Info -> true | Advice | Error | Like | Warning -> false
|
|
in
|
|
if issue_kind_is_blacklisted then false
|
|
else
|
|
let issue_type_is_null_deref =
|
|
let null_deref_issue_types =
|
|
let open IssueType in
|
|
[ field_not_null_checked
|
|
; null_dereference
|
|
; parameter_not_null_checked
|
|
; premature_nil_termination
|
|
; empty_vector_access
|
|
; biabd_use_after_free ]
|
|
in
|
|
List.mem ~equal:IssueType.equal null_deref_issue_types issue_type
|
|
in
|
|
if issue_type_is_null_deref then Localise.error_desc_is_reportable_bucket error_desc else true
|
|
|
|
|
|
(* The reason an issue should be censored (that is, not reported). The empty
|
|
string (that is "no reason") means that the issue should be reported. *)
|
|
let censored_reason (issue_type : IssueType.t) source_file =
|
|
let filename = SourceFile.to_rel_path source_file in
|
|
let rejected_by ((issue_type_polarity, issue_type_re), (filename_polarity, filename_re), reason) =
|
|
let accepted =
|
|
(* matches issue_type_re implies matches filename_re *)
|
|
(not (Bool.equal issue_type_polarity (Str.string_match issue_type_re issue_type.unique_id 0)))
|
|
|| Bool.equal filename_polarity (Str.string_match filename_re filename 0)
|
|
in
|
|
Option.some_if (not accepted) reason
|
|
in
|
|
List.find_map Config.censor_report ~f:rejected_by
|
|
|
|
|
|
let potential_exception_message = "potential exception at line"
|
|
|
|
module type Printer = sig
|
|
type elt
|
|
|
|
val pp_open : F.formatter -> unit -> unit
|
|
|
|
val pp_close : F.formatter -> unit -> unit
|
|
|
|
val pp : F.formatter -> elt -> unit
|
|
end
|
|
|
|
module MakeJsonListPrinter (P : sig
|
|
type elt
|
|
|
|
val to_string : elt -> string option
|
|
end) : Printer with type elt = P.elt = struct
|
|
include P
|
|
|
|
let is_first_item = ref true
|
|
|
|
let pp_open fmt () =
|
|
is_first_item := true ;
|
|
F.fprintf fmt "[@?"
|
|
|
|
|
|
let pp_close fmt () = F.fprintf fmt "]@\n@?"
|
|
|
|
let pp fmt elt =
|
|
match to_string elt with
|
|
| Some s ->
|
|
if !is_first_item then is_first_item := false else F.pp_print_char fmt ',' ;
|
|
F.fprintf fmt "%s@?" s
|
|
| None ->
|
|
()
|
|
end
|
|
|
|
type json_issue_printer_typ =
|
|
{ error_filter: SourceFile.t -> IssueType.t -> bool
|
|
; proc_name: Procname.t
|
|
; proc_loc_opt: Location.t option
|
|
; err_key: Errlog.err_key
|
|
; err_data: Errlog.err_data }
|
|
|
|
let procedure_id_of_procname proc_name =
|
|
match Procname.get_language proc_name with
|
|
| Language.Java ->
|
|
Procname.to_unique_id proc_name
|
|
| _ ->
|
|
Procname.to_string proc_name
|
|
|
|
|
|
module JsonIssuePrinter = MakeJsonListPrinter (struct
|
|
type elt = json_issue_printer_typ
|
|
|
|
let to_string ({error_filter; proc_name; proc_loc_opt; err_key; err_data} : elt) =
|
|
let source_file, procedure_start_line =
|
|
match proc_loc_opt with
|
|
| Some proc_loc ->
|
|
(proc_loc.Location.file, proc_loc.Location.line)
|
|
| None ->
|
|
(err_data.loc.Location.file, 0)
|
|
in
|
|
if SourceFile.is_invalid source_file then
|
|
L.(die InternalError)
|
|
"Invalid source file for %a %a@.Trace: %a@." IssueType.pp err_key.err_name
|
|
Localise.pp_error_desc err_key.err_desc Errlog.pp_loc_trace err_data.loc_trace ;
|
|
let should_report_source_file =
|
|
(not (SourceFile.is_biabduction_model source_file))
|
|
|| Config.debug_mode || Config.debug_exceptions
|
|
in
|
|
if
|
|
error_filter source_file err_key.err_name
|
|
&& should_report_source_file
|
|
&& should_report err_key.severity err_key.err_name err_key.err_desc err_data.err_class
|
|
then
|
|
let severity = Exceptions.severity_string err_key.severity in
|
|
let bug_type = err_key.err_name.IssueType.unique_id in
|
|
let file =
|
|
SourceFile.to_string ~force_relative:Config.report_force_relative_path source_file
|
|
in
|
|
let json_ml_loc =
|
|
match err_data.loc_in_ml_source with
|
|
| Some (file, lnum, cnum, enum) when Config.reports_include_ml_loc ->
|
|
Some Jsonbug_j.{file; lnum; cnum; enum}
|
|
| _ ->
|
|
None
|
|
in
|
|
let qualifier =
|
|
let base_qualifier = error_desc_to_plain_string err_key.err_desc in
|
|
if IssueType.(equal resource_leak) err_key.err_name then
|
|
match Errlog.compute_local_exception_line err_data.loc_trace with
|
|
| None ->
|
|
base_qualifier
|
|
| Some line ->
|
|
let potential_exception_message =
|
|
Format.asprintf "%a: %s %d" MarkupFormatter.pp_bold "Note"
|
|
potential_exception_message line
|
|
in
|
|
Format.sprintf "%s@\n%s" base_qualifier potential_exception_message
|
|
else base_qualifier
|
|
in
|
|
let bug =
|
|
{ Jsonbug_j.bug_type
|
|
; qualifier
|
|
; severity
|
|
; line= err_data.loc.Location.line
|
|
; column= err_data.loc.Location.col
|
|
; procedure= procedure_id_of_procname proc_name
|
|
; procedure_start_line
|
|
; file
|
|
; bug_trace= loc_trace_to_jsonbug_record err_data.loc_trace err_key.severity
|
|
; node_key= Option.map ~f:Procdesc.NodeKey.to_string err_data.node_key
|
|
; key= compute_key bug_type proc_name file
|
|
; hash= compute_hash ~severity ~bug_type ~proc_name ~file ~qualifier
|
|
; dotty= error_desc_to_dotty_string err_key.err_desc
|
|
; infer_source_loc= json_ml_loc
|
|
; bug_type_hum= err_key.err_name.IssueType.hum
|
|
; linters_def_file= err_data.linters_def_file
|
|
; doc_url= err_data.doc_url
|
|
; traceview_id= None
|
|
; censored_reason= censored_reason err_key.err_name source_file
|
|
; access= err_data.access
|
|
; extras= err_data.extras }
|
|
in
|
|
Some (Jsonbug_j.string_of_jsonbug bug)
|
|
else None
|
|
end)
|
|
|
|
module IssuesJson = struct
|
|
include JsonIssuePrinter
|
|
|
|
(** Write bug report in JSON format *)
|
|
let pp_issues_of_error_log fmt error_filter _ proc_loc_opt proc_name err_log =
|
|
Errlog.iter
|
|
(fun err_key err_data -> pp fmt {error_filter; proc_name; proc_loc_opt; err_key; err_data})
|
|
err_log
|
|
end
|
|
|
|
type json_costs_printer_typ =
|
|
{loc: Location.t; proc_name: Procname.t; cost_opt: CostDomain.summary option}
|
|
|
|
module JsonCostsPrinter = MakeJsonListPrinter (struct
|
|
type elt = json_costs_printer_typ
|
|
|
|
let to_string {loc; proc_name; cost_opt} =
|
|
match cost_opt with
|
|
| Some {post; is_on_ui_thread} when not (Procname.is_java_access_method proc_name) ->
|
|
let hum cost =
|
|
let degree_with_term = CostDomain.BasicCost.get_degree_with_term cost in
|
|
{ Jsonbug_t.hum_polynomial= Format.asprintf "%a" CostDomain.BasicCost.pp_hum cost
|
|
; hum_degree=
|
|
Format.asprintf "%a"
|
|
(CostDomain.BasicCost.pp_degree ~only_bigO:false)
|
|
degree_with_term
|
|
; big_o=
|
|
Format.asprintf "%a" (CostDomain.BasicCost.pp_degree ~only_bigO:true) degree_with_term
|
|
}
|
|
in
|
|
let cost_info cost =
|
|
{ Jsonbug_t.polynomial_version= CostDomain.BasicCost.version
|
|
; polynomial= CostDomain.BasicCost.encode cost
|
|
; degree=
|
|
Option.map (CostDomain.BasicCost.degree cost) ~f:Polynomials.Degree.encode_to_int
|
|
; hum= hum cost }
|
|
in
|
|
let cost_item =
|
|
let file = SourceFile.to_rel_path loc.Location.file in
|
|
{ Jsonbug_t.hash= compute_hash ~severity:"" ~bug_type:"" ~proc_name ~file ~qualifier:""
|
|
; loc= {file; lnum= loc.Location.line; cnum= loc.Location.col; enum= -1}
|
|
; procedure_name= Procname.get_method proc_name
|
|
; procedure_id= procedure_id_of_procname proc_name
|
|
; is_on_ui_thread
|
|
; exec_cost= cost_info (CostDomain.get_cost_kind CostKind.OperationCost post) }
|
|
in
|
|
Some (Jsonbug_j.string_of_cost_item cost_item)
|
|
| _ ->
|
|
None
|
|
end)
|
|
|
|
let mk_error_filter filters proc_name file error_name =
|
|
(Config.write_html || not (IssueType.(equal skip_function) error_name))
|
|
&& filters.Inferconfig.path_filter file
|
|
&& filters.Inferconfig.error_filter error_name
|
|
&& filters.Inferconfig.proc_filter proc_name
|
|
|
|
|
|
let collect_issues summary issues_acc =
|
|
let err_log = Summary.get_err_log summary in
|
|
let proc_name = Summary.get_proc_name summary in
|
|
let proc_location = Summary.get_loc summary in
|
|
Errlog.fold
|
|
(fun err_key err_data acc -> {Issue.proc_name; proc_location; err_key; err_data} :: acc)
|
|
err_log issues_acc
|
|
|
|
|
|
let write_costs summary (outfile : Utils.outfile) =
|
|
JsonCostsPrinter.pp outfile.fmt
|
|
{ loc= Summary.get_loc summary
|
|
; proc_name= Summary.get_proc_name summary
|
|
; cost_opt= summary.Summary.payloads.Payloads.cost }
|
|
|
|
|
|
(** Process lint issues of a procedure *)
|
|
let write_lint_issues filters (issues_outf : Utils.outfile) linereader procname error_log =
|
|
let error_filter = mk_error_filter filters procname in
|
|
IssuesJson.pp_issues_of_error_log issues_outf.fmt error_filter linereader None procname error_log
|
|
|
|
|
|
(** Process a summary *)
|
|
let process_summary ~costs_outf summary issues_acc =
|
|
write_costs summary costs_outf ; collect_issues summary issues_acc
|
|
|
|
|
|
let process_all_summaries_and_issues ~issues_outf ~costs_outf =
|
|
let linereader = Printer.LineReader.create () in
|
|
let filters = Inferconfig.create_filters () in
|
|
let all_issues = ref [] in
|
|
SpecsFiles.iter_from_config ~f:(fun summary ->
|
|
all_issues := process_summary ~costs_outf summary !all_issues ) ;
|
|
all_issues := Issue.sort_filter_issues !all_issues ;
|
|
List.iter
|
|
~f:(fun {Issue.proc_name; proc_location; err_key; err_data} ->
|
|
let error_filter = mk_error_filter filters proc_name in
|
|
IssuesJson.pp issues_outf.Utils.fmt
|
|
{error_filter; proc_name; proc_loc_opt= Some proc_location; err_key; err_data} )
|
|
!all_issues ;
|
|
(* Issues that are generated and stored outside of summaries by linter and checkers *)
|
|
List.iter (ResultsDirEntryName.get_issues_directories ()) ~f:(fun dir_name ->
|
|
IssueLog.load dir_name |> IssueLog.iter ~f:(write_lint_issues filters issues_outf linereader)
|
|
) ;
|
|
()
|
|
|
|
|
|
let write_reports ~issues_json ~costs_json =
|
|
let mk_outfile fname =
|
|
match Utils.create_outfile fname with
|
|
| None ->
|
|
L.die InternalError "Could not create '%s'." fname
|
|
| Some outf ->
|
|
outf
|
|
in
|
|
let issues_outf = mk_outfile issues_json in
|
|
IssuesJson.pp_open issues_outf.fmt () ;
|
|
let costs_outf = mk_outfile costs_json in
|
|
JsonCostsPrinter.pp_open costs_outf.fmt () ;
|
|
process_all_summaries_and_issues ~issues_outf ~costs_outf ;
|
|
JsonCostsPrinter.pp_close costs_outf.fmt () ;
|
|
Utils.close_outf costs_outf ;
|
|
IssuesJson.pp_close issues_outf.fmt () ;
|
|
Utils.close_outf issues_outf ;
|
|
()
|