diff --git a/infer/lib/python/inferlib/analyze.py b/infer/lib/python/inferlib/analyze.py index e90b6921b..8ddda175a 100644 --- a/infer/lib/python/inferlib/analyze.py +++ b/infer/lib/python/inferlib/analyze.py @@ -30,10 +30,6 @@ csv.field_size_limit(sys.maxsize) INFER_ANALYZE_BINARY = 'InferAnalyze' -CRASHCONTEXT_METHOD_FIELD = 'method' -CRASHCONTEXT_LOCATION_FIELD = 'location' -CRASHCONTEXT_CALLEES_FIELD = 'callees' - def get_infer_version(): try: @@ -492,54 +488,6 @@ class AnalyzerWrapper(object): return exit_status - def crashcontext_stitch_summaries(self): - """Take crashcontext per-method summaries and join them together to - produce the final crashcontext.json output file.""" - - crashcontext_dir = os.path.join(self.args.infer_out, 'crashcontext') - summaries_map_by_frame_id = {} - st_json = utils.load_json_from_path(self.args.stacktrace) - stacktrace = st_json['stack_trace'] - k = 1 # Where k is the number of levels of inlined calls. - for f in os.listdir(crashcontext_dir): - if f.endswith('.json'): - path = os.path.join(crashcontext_dir, f) - method_summary = utils.load_json_from_path(path) - method_signature = method_summary[CRASHCONTEXT_METHOD_FIELD] - method_name = method_signature.split('(')[0] - src_path = method_summary[CRASHCONTEXT_LOCATION_FIELD]['file'] - line = method_summary[CRASHCONTEXT_LOCATION_FIELD]['line'] - frame_id = "{0}({1}:{2})".format( - method_name, - os.path.basename(src_path), - line) - summaries_map_by_frame_id[frame_id] = method_summary - - def expand(summary, k): - if k == 0: - # Make sure leaf nodes have an empty 'callees' field. - leaf_nodes = summary[CRASHCONTEXT_CALLEES_FIELD] - for leaf_node in leaf_nodes: - if CRASHCONTEXT_CALLEES_FIELD not in leaf_node: - leaf_node[CRASHCONTEXT_CALLEES_FIELD] = [] - return summary - else: - NotImplementedError() # TODO - - json_frames = [] - for frame in stacktrace: - frame_id = frame.strip() - if not frame_id: - continue - assert frame_id.startswith('at ') - frame_id = frame_id[3:] - assert frame_id in summaries_map_by_frame_id - summary = summaries_map_by_frame_id[frame_id] - json_frames.append(expand(summary, k - 1)) - out_json = {} - out_json['stack'] = json_frames - out_file = os.path.join(crashcontext_dir, 'crashcontext.json') - utils.dump_json_to_path(out_json, out_file) def read_proc_stats(self): proc_stats_path = os.path.join( @@ -573,8 +521,6 @@ class AnalyzerWrapper(object): if self.args.analyzer not in [config.ANALYZER_COMPILE, config.ANALYZER_CAPTURE]: if self.analyze() == os.EX_OK: - if self.args.analyzer == config.ANALYZER_CRASHCONTEXT: - self.crashcontext_stitch_summaries() reporting_start_time = time.time() report_status = self.create_report() elapsed = utils.elapsed_time(reporting_start_time) diff --git a/infer/src/backend/crashcontext.ml b/infer/src/backend/crashcontext.ml new file mode 100644 index 000000000..dfa610fbe --- /dev/null +++ b/infer/src/backend/crashcontext.ml @@ -0,0 +1,74 @@ +(* + * Copyright (c) 2016 - present Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + *) + +open! Utils + +module F = Format +module L = Logging + +let frame_id_of_stackframe frame = + F.sprintf + "%s.%s(%s:%d)" + frame.Stacktrace.class_str + frame.Stacktrace.method_str + frame.Stacktrace.file_str + frame.Stacktrace.line_num + +let frame_id_of_summary stacktree = + let short_name = IList.hd + (Str.split (Str.regexp "(") stacktree.Stacktree_j.method_name) in + match stacktree.Stacktree_j.location with + | None -> + failwith "Attempted to take signature of a frame without location \ + information. This is undefined." + | Some loc -> + F.sprintf "%s(%s:%d)" short_name (Filename.basename loc.file) loc.line + +let stracktree_of_frame frame = + { Stacktree_j.method_name = F.sprintf + "%s.%s" + frame.Stacktrace.class_str + frame.Stacktrace.method_str; + location = Some { Stacktree_j.location_type = "call_site"; + file = frame.Stacktrace.file_str; + line = frame.Stacktrace.line_num }; + callees = []; + } + +(** k = 1 implementation, where k is the number of levels of calls inlined *) +let stitch_summaries stacktrace_file summary_files out_file = + let stacktrace = Stacktrace.of_json_file stacktrace_file in + let summaries = IList.map + (Ag_util.Json.from_file Stacktree_j.read_stacktree) + summary_files in + let summary_map = IList.fold_left + (fun acc stacktree -> + StringMap.add (frame_id_of_summary stacktree) stacktree acc) + StringMap.empty + summaries in + let expand_stack_frame frame = + (** TODO: Implement k > 1 case *) + let frame_id = frame_id_of_stackframe frame in + if StringMap.exists (fun key _ -> key = frame_id) summary_map then + StringMap.find frame_id summary_map + else + stracktree_of_frame frame in + let expanded_frames = IList.map expand_stack_frame stacktrace.frames in + let crashcontext = { Stacktree_j.stack = expanded_frames} in + Ag_util.Json.to_file Stacktree_j.write_crashcontext_t out_file crashcontext + +let collect_all_summaries root_out_dir stacktrace_file = + let out_dir = Filename.concat root_out_dir "crashcontext" in + DB.create_dir out_dir; + let out_file = Filename.concat out_dir "crashcontext.json" in + let path_regexp = Str.regexp ".*crashcontext/.*\\..*\\.json" in + let path_matcher path = Str.string_match path_regexp path 0 in + let method_summaries = + DB.paths_matching root_out_dir path_matcher in + stitch_summaries stacktrace_file method_summaries out_file; diff --git a/infer/src/backend/infer.ml b/infer/src/backend/infer.ml index 79e8ffed6..a12f03542 100644 --- a/infer/src/backend/infer.ml +++ b/infer/src/backend/infer.ml @@ -94,6 +94,37 @@ let () = ) in let pid = Unix.create_process args_py.(0) args_py Unix.stdin Unix.stdout Unix.stderr in let _, status = Unix.waitpid [] pid in + (** Collect crashcontext summaries *) + let analysis_is_crashcontext = match Config.analyzer with + | Some Crashcontext -> true + | _ -> false in + if analysis_is_crashcontext then + (** Check whether this is the top-level infer process *) + let top_level_infer = + (** if the '--buck' option was passed, then this is the top level process + iff the build command starts with 'buck' *) + if Config.buck then buck + (** otherwise, we assume javac as the build command and thus only + one process *) + else true in + if top_level_infer then + (** If we are the top-level process, then find the output directory and + collect all crashcontext summaries under it in a single + crashcontext.json file *) + let root_out_dir = if buck then begin + let project_root = match Config.project_root with + | Some root -> root + | None -> Filename.dirname Config.results_dir in + let buck_out = match Config.buck_out with + | Some dir -> dir + | None -> "buck-out" in + Filename.concat project_root buck_out + end + else Config.results_dir in + match Config.stacktrace with + | None -> failwith "Detected -a crashcontext without --stacktrace, \ + this should have been checked earlier." + | Some s -> Crashcontext.collect_all_summaries root_out_dir s; if status <> Unix.WEXITED 0 then ( prerr_endline ("Failed to execute: " ^ (String.concat " " (Array.to_list args_py))) ; exit 1 diff --git a/infer/src/checkers/BoundedCallTree.ml b/infer/src/checkers/BoundedCallTree.ml index ef431305e..ed107bee9 100644 --- a/infer/src/checkers/BoundedCallTree.ml +++ b/infer/src/checkers/BoundedCallTree.ml @@ -27,25 +27,22 @@ module TransferFunctions (CFG : ProcCfg.S) = struct module Domain = Domain type extras = Stacktrace.t - let json_of_summary caller astate loc loc_type = + let stacktree_of_summary caller astate loc location_type = let procs = Domain.elements astate in - let json = `Assoc [ - ("method", `String (Procname.to_unique_id caller)); - ("location", `Assoc [ - ("type", `String loc_type); - ("file", `String (DB.source_file_to_string loc.Location.file)); - ("line", `Int loc.Location.line); - ]); - ("callees", `List (IList.map - (fun pn -> `Assoc [ - ("method", `String (Procname.to_unique_id pn)) - ]) - procs)) - ] in - json + let method_name = Procname.to_unique_id caller in + let file = DB.source_file_to_string loc.Location.file in + let line = loc.Location.line in + let location = Some { Stacktree_j.location_type ; file ; line } in + let callees = IList.map + (fun pn -> + { Stacktree_j.method_name = Procname.to_unique_id pn; + location = None; + callees = [] } ) + procs in + { Stacktree_j.method_name; location; callees } let output_summary caller astate loc loc_type = - let json = json_of_summary caller astate loc loc_type in + let stacktree = stacktree_of_summary caller astate loc loc_type in let dir = Filename.concat Config.results_dir "crashcontext" in let suffix = F.sprintf "%s_%d" loc_type loc.Location.line in let fname = F.sprintf "%s.%s.json" @@ -53,14 +50,26 @@ module TransferFunctions (CFG : ProcCfg.S) = struct suffix in let fpath = Filename.concat dir fname in DB.create_dir dir; - Utils.write_json_to_file fpath json + Ag_util.Json.to_file Stacktree_j.write_stacktree fpath stacktree let exec_instr astate proc_data _ = function | Sil.Call (_, Const (Const.Cfun pn), _, loc, _) -> - (** TODO: Match class. *) let caller = Cfg.Procdesc.get_proc_name proc_data.ProcData.pdesc in let matches_proc frame = - frame.Stacktrace.method_str = (Procname.get_method caller) in + let matches_class pname = match pname with + | Procname.Java java_proc -> + string_equal + frame.Stacktrace.class_str + (Procname.java_get_class_name java_proc) + | Procname.ObjC_Cpp objc_cpp_prod -> + string_equal + frame.Stacktrace.class_str + (Procname.objc_cpp_get_class_name objc_cpp_prod) + | Procname.C _ -> true (** Needed for test code. *) + | Procname.Block _ -> + failwith "Proc type not supported by crashcontext: block" in + frame.Stacktrace.method_str = (Procname.get_method caller) && + matches_class caller in let proc_in_trace = IList.exists matches_proc proc_data.ProcData.extras.Stacktrace.frames in diff --git a/infer/tests/utils/CrashContextResults.java b/infer/tests/utils/CrashContextResults.java index 2480f6e8b..5aa33ff57 100644 --- a/infer/tests/utils/CrashContextResults.java +++ b/infer/tests/utils/CrashContextResults.java @@ -37,12 +37,12 @@ public class CrashContextResults { public boolean hasStackFrame(String methodSignature, int pos) { return methodSignature.equals( - json.path("stack").get(pos).path("method").asText()); + json.path("stack").get(pos).path("method_name").asText()); } public boolean hasStackFrame(String methodSignature) { for (JsonNode frame : json.path("stack")) { - if(methodSignature.equals(frame.path("method").asText())) { + if (methodSignature.equals(frame.path("method_name").asText())) { return true; } } @@ -52,10 +52,10 @@ public class CrashContextResults { private List findNodesForMethod(JsonNode node, String methodSignature, List accumulator) { - if(methodSignature.equals(node.path("method").asText())) { + if (methodSignature.equals(node.path("method_name").asText())) { accumulator.add(node); } - for(JsonNode callee : node.path("callees")) { + for (JsonNode callee : node.path("callees")) { findNodesForMethod(callee, methodSignature, accumulator); } return accumulator; @@ -78,8 +78,8 @@ public class CrashContextResults { } public boolean hasPath(String methodFrom, String methodTo) { - for(JsonNode from : findNodesForMethod(methodFrom)) { - if(!findNodesForMethod(from, methodTo, new ArrayList()).isEmpty()) { + for (JsonNode from : findNodesForMethod(methodFrom)) { + if (!findNodesForMethod(from, methodTo, new ArrayList()).isEmpty()) { return true; } }