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.
206 lines
7.6 KiB
206 lines
7.6 KiB
(*
|
|
* 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 F = Format
|
|
module L = Logging
|
|
|
|
[@@@warning "+9"]
|
|
|
|
let is_past_limit limit =
|
|
match limit with None -> fun _ -> false | Some limit -> fun n -> n >= limit
|
|
|
|
|
|
let has_trace {Jsonbug_t.bug_trace; _} = not (List.is_empty bug_trace)
|
|
|
|
let with_file_fmt file ~f =
|
|
Utils.with_file_out file ~f:(fun outc ->
|
|
let fmt = F.formatter_of_out_channel outc in
|
|
f fmt ;
|
|
F.pp_print_flush fmt () )
|
|
|
|
|
|
let pp_trace_item ~show_source_context fmt
|
|
Jsonbug_t.{level; filename; line_number; column_number; description} =
|
|
let pp_col_number fmt c = if c >= 0 then F.fprintf fmt ":%d" c in
|
|
F.fprintf fmt "%s:%d%a: %s@\n" filename line_number pp_col_number column_number description ;
|
|
if show_source_context then
|
|
TextReport.pp_source_context ~indent:(2 * level) fmt
|
|
{Jsonbug_t.file= filename; lnum= line_number; cnum= column_number; enum= -1}
|
|
|
|
|
|
let pp_issue_with_trace ~show_source_context ~max_nested_level fmt issue_with_n =
|
|
F.fprintf fmt "%a@\n" TextReport.pp_jsonbug_with_number issue_with_n ;
|
|
let issue : Jsonbug_t.jsonbug = snd issue_with_n in
|
|
if List.is_empty issue.bug_trace then F.fprintf fmt "@\nEmpty trace@\n%!"
|
|
else
|
|
List.iter issue.bug_trace ~f:(fun trace_item ->
|
|
(* subtract 1 to get inclusive limits on the nesting level *)
|
|
if not (is_past_limit max_nested_level (trace_item.Jsonbug_t.level - 1)) then
|
|
F.fprintf fmt "@\n%a" (pp_trace_item ~show_source_context) trace_item )
|
|
|
|
|
|
let user_select_issue ~selector_limit report =
|
|
List.fold_until report
|
|
~finish:(fun _ -> ())
|
|
~init:0
|
|
~f:(fun n issue ->
|
|
if is_past_limit selector_limit n then Stop ()
|
|
else (
|
|
L.result "%a@\n" TextReport.pp_jsonbug_with_number (n, issue) ;
|
|
Continue (n + 1) ) ) ;
|
|
let rec ask_until_valid_input max_report =
|
|
L.result "@\nSelect report to display (0-%d) (default: 0): %!" max_report ;
|
|
let input = In_channel.input_line_exn In_channel.stdin in
|
|
if String.is_empty input then 0
|
|
else
|
|
match int_of_string_opt input with
|
|
| Some n when n >= 0 && n <= max_report ->
|
|
n
|
|
| Some n ->
|
|
L.progress "Error: %d is not between 0 and %d, try again.@\n%!" n max_report ;
|
|
ask_until_valid_input max_report
|
|
| None ->
|
|
L.progress "Error: please input a number, not '%s'.@\n%!" input ;
|
|
ask_until_valid_input max_report
|
|
in
|
|
let n = ask_until_valid_input (List.length report - 1) in
|
|
(n, List.nth_exn report n)
|
|
|
|
|
|
let read_report report_json = Atdgen_runtime.Util.Json.from_file Jsonbug_j.read_report report_json
|
|
|
|
let explore ~selector_limit ~report_txt:_ ~report_json ~show_source_context ~selected
|
|
~max_nested_level =
|
|
let report = read_report report_json in
|
|
let display_issue issue =
|
|
L.result "@\n%a" (pp_issue_with_trace ~show_source_context ~max_nested_level) issue
|
|
in
|
|
match (selected, report) with
|
|
| Some `All, _ ->
|
|
List.iteri report ~f:(fun n issue -> display_issue (n, issue))
|
|
| Some (`Select n), _ -> (
|
|
(* an issue number has been pre-selected, use that *)
|
|
match List.nth report n with
|
|
| None ->
|
|
L.die UserError "Cannot select issues #%d: only %d issues in '%s'" n (List.length report)
|
|
report_json
|
|
| Some issue ->
|
|
display_issue (n, issue) )
|
|
| None, [] ->
|
|
(* empty report, can't print anything *)
|
|
L.progress "No issues found in '%s', exiting.@\n" report_json
|
|
| None, [issue] ->
|
|
(* single-issue report: no need to prompt the user to select which issue to display *)
|
|
L.progress "Auto-selecting the only issue in '%s'@\n%!" report_json ;
|
|
display_issue (0, issue)
|
|
| None, _ :: _ :: _ ->
|
|
(* user prompt *)
|
|
display_issue (user_select_issue ~selector_limit report)
|
|
|
|
|
|
module GitHub = struct
|
|
type t = {project: string; commit: string; root: string}
|
|
|
|
let pp fmt {project; commit; root} =
|
|
F.fprintf fmt "project '%s' at root '%s', commit '%s'" project root commit
|
|
|
|
|
|
let discover () =
|
|
let git_remote, result =
|
|
Utils.with_process_in "git config --get remote.origin.url" In_channel.input_all
|
|
in
|
|
let git_remote = String.strip git_remote in
|
|
let pattern = "github.com/" in
|
|
match (result, String.substr_index git_remote ~pattern) with
|
|
| Error _, _ | Ok (), None ->
|
|
None
|
|
| Ok (), Some i -> (
|
|
let project = String.drop_prefix git_remote (i + String.length pattern) in
|
|
(* some remotes end in .git, but the http urls don't have these *)
|
|
let project =
|
|
String.chop_suffix project ~suffix:".git"
|
|
|> Option.bind ~f:(String.chop_suffix ~suffix:"/")
|
|
|> Option.value ~default:project
|
|
in
|
|
let commit, result = Utils.with_process_in "git rev-parse HEAD" In_channel.input_all in
|
|
Result.bind result ~f:(fun () ->
|
|
let commit = String.strip commit in
|
|
let root, result =
|
|
Utils.with_process_in "git rev-parse --show-toplevel" In_channel.input_all
|
|
in
|
|
Result.map result ~f:(fun () ->
|
|
let root = String.strip root in
|
|
{project; commit; root} ) )
|
|
|> function
|
|
| Ok repo_state ->
|
|
L.progress "Found GitHub %a@\n" pp repo_state ;
|
|
Some repo_state
|
|
| Error _ ->
|
|
None )
|
|
|
|
|
|
let get_file_line_url {project; commit; root} (file, line) =
|
|
let file_path =
|
|
String.chop_prefix ~prefix:root (Config.project_root ^/ file) |> Option.value ~default:file
|
|
in
|
|
Printf.sprintf "https://github.com/%s/blob/%s/%s#L%d" project commit file_path line
|
|
end
|
|
|
|
let trace_path_of_bug_number ~traces_dir i = traces_dir ^/ Printf.sprintf "bug_%d.txt" i
|
|
|
|
let pp_html_index ~traces_dir fmt report =
|
|
let github_project = GitHub.discover () in
|
|
let pp_issue_entry fmt ((_, (issue : Jsonbug_t.jsonbug)) as issue_i) =
|
|
let pp_trace_uri fmt (i, (issue : Jsonbug_t.jsonbug)) =
|
|
if has_trace issue then
|
|
F.fprintf fmt "<a href=\"%s\">trace</a>" (trace_path_of_bug_number ~traces_dir i)
|
|
else F.pp_print_string fmt "no trace"
|
|
in
|
|
let pp_github_source github_project fmt file_line =
|
|
match github_project with
|
|
| None ->
|
|
()
|
|
| Some project ->
|
|
F.fprintf fmt " (<a href=\"%s\">source</a>)" (GitHub.get_file_line_url project file_line)
|
|
in
|
|
F.fprintf fmt "<li>%a (%a)%a</li>" TextReport.pp_jsonbug issue pp_trace_uri issue_i
|
|
(pp_github_source github_project) (issue.file, issue.line)
|
|
in
|
|
let pp_issues_list fmt report =
|
|
F.fprintf fmt "<ol start=\"0\">@\n" ;
|
|
List.iteri report ~f:(fun i issue -> pp_issue_entry fmt (i, issue)) ;
|
|
F.fprintf fmt "@\n</ol>"
|
|
in
|
|
F.fprintf fmt
|
|
{|<html>
|
|
<head>
|
|
<title>Infer found %d issues</title>
|
|
</head>
|
|
<body>
|
|
<h2>List of issues found</h2>
|
|
%a
|
|
</body>
|
|
</html>|}
|
|
(List.length report) pp_issues_list report
|
|
|
|
|
|
let gen_html_report ~show_source_context ~max_nested_level ~report_json ~report_html_dir =
|
|
(* delete previous report if present *)
|
|
Utils.rmtree report_html_dir ;
|
|
Utils.create_dir report_html_dir ;
|
|
let traces_dir = "traces" in
|
|
Utils.create_dir (report_html_dir ^/ traces_dir) ;
|
|
let report = read_report report_json in
|
|
List.iteri report ~f:(fun i issue ->
|
|
if has_trace issue then
|
|
let file = report_html_dir ^/ trace_path_of_bug_number ~traces_dir i in
|
|
with_file_fmt file ~f:(fun fmt ->
|
|
pp_issue_with_trace ~show_source_context ~max_nested_level fmt (i, issue) ) ) ;
|
|
let report_html = report_html_dir ^/ "index.html" in
|
|
with_file_fmt report_html ~f:(fun fmt -> pp_html_index ~traces_dir fmt report) ;
|
|
L.result "Saved HTML report in '%s'@." report_html
|