Reviewed By: mbouaziz Differential Revision: D15079846 fbshipit-source-id: c30a26f62master
parent
0ad15356c2
commit
4bdc6efd2e
@ -0,0 +1,173 @@
|
||||
(*
|
||||
* Copyright (c) 2019-present, Facebook, Inc.
|
||||
*
|
||||
* 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
|
||||
|
||||
module type NodeSig = sig
|
||||
type t = private {id: int; pname: Typ.Procname.t; successors: int list; mutable flag: bool}
|
||||
|
||||
val make : int -> Typ.Procname.t -> int list -> t
|
||||
|
||||
val set_flag : t -> unit
|
||||
|
||||
val unset_flag : t -> unit
|
||||
|
||||
val pp_dot : F.formatter -> t -> unit
|
||||
end
|
||||
|
||||
module Node : NodeSig = struct
|
||||
type t = {id: int; pname: Typ.Procname.t; successors: int list; mutable flag: bool}
|
||||
|
||||
let make id pname successors = {id; pname; successors; flag= false}
|
||||
|
||||
let set_flag n = n.flag <- true
|
||||
|
||||
let unset_flag n = n.flag <- false
|
||||
|
||||
let pp_dot fmt {id; pname; successors} =
|
||||
let pp_id fmt id = F.fprintf fmt "N%d" id in
|
||||
let pp_edge fmt src dst = F.fprintf fmt " %a -> %a ;@\n" pp_id src pp_id dst in
|
||||
F.fprintf fmt " %a [ label = %S ];@\n" pp_id id (F.asprintf "%a" Typ.Procname.pp pname) ;
|
||||
List.iter successors ~f:(pp_edge fmt id) ;
|
||||
F.pp_print_newline fmt ()
|
||||
end
|
||||
|
||||
module IdMap = Typ.Procname.Hash
|
||||
module NodeMap = Caml.Hashtbl.Make (Int)
|
||||
|
||||
(** [node_map] is a map from ids (unique ints) to nodes corresponding to defined procedures.
|
||||
[id_map] is a map from all encountered (not necessarily defined) procnames to their ids,
|
||||
and thus its image is a superset of the domain of [node_map], and usually a strict superset.
|
||||
[trim_id_map] makes the image equal to the domain of [node_map]. *)
|
||||
type t = {id_map: int IdMap.t; node_map: Node.t NodeMap.t}
|
||||
|
||||
let create initial_capacity =
|
||||
{id_map= IdMap.create initial_capacity; node_map= NodeMap.create initial_capacity}
|
||||
|
||||
|
||||
let id_of_procname {id_map} pname = IdMap.find_opt id_map pname
|
||||
|
||||
let node_of_id {node_map} id = NodeMap.find_opt node_map id
|
||||
|
||||
let mem {node_map} id = NodeMap.mem node_map id
|
||||
|
||||
(** [id_map] may contain undefined procedures, so use [node_map] for actual size *)
|
||||
let length {node_map} = NodeMap.length node_map
|
||||
|
||||
let node_of_procname g pname = id_of_procname g pname |> Option.bind ~f:(node_of_id g)
|
||||
|
||||
let remove (g : t) pname id = IdMap.remove g.id_map pname ; NodeMap.remove g.node_map id
|
||||
|
||||
let add ({id_map; node_map} as graph) pname successor_pnames =
|
||||
let get_or_set_id procname =
|
||||
match id_of_procname graph procname with
|
||||
| None ->
|
||||
let id = IdMap.length id_map in
|
||||
IdMap.replace id_map procname id ; id
|
||||
| Some id ->
|
||||
id
|
||||
in
|
||||
let id = get_or_set_id pname in
|
||||
let successors = List.map successor_pnames ~f:get_or_set_id in
|
||||
let node = Node.make id pname successors in
|
||||
NodeMap.replace node_map id node
|
||||
|
||||
|
||||
let remove_reachable g start_pname =
|
||||
let add_live_successors_and_remove_self init (n : Node.t) =
|
||||
remove g n.pname n.id ;
|
||||
List.fold n.successors ~init ~f:(fun init succ_id ->
|
||||
node_of_id g succ_id |> Option.fold ~init ~f:(fun acc s -> s :: acc) )
|
||||
in
|
||||
let rec remove_list frontier =
|
||||
if not (List.is_empty frontier) then
|
||||
remove_list (List.fold frontier ~init:[] ~f:add_live_successors_and_remove_self)
|
||||
in
|
||||
node_of_procname g start_pname |> Option.iter ~f:(fun start_node -> remove_list [start_node])
|
||||
|
||||
|
||||
let flag_reachable g start_pname =
|
||||
let process_node init (n : Node.t) =
|
||||
if n.flag then init
|
||||
else (
|
||||
Node.set_flag n ;
|
||||
List.fold n.successors ~init ~f:(fun acc id ->
|
||||
match node_of_id g id with Some n' when not n'.flag -> n' :: acc | _ -> acc ) )
|
||||
in
|
||||
let rec flag_list frontier =
|
||||
if not (List.is_empty frontier) then flag_list (List.fold frontier ~init:[] ~f:process_node)
|
||||
in
|
||||
node_of_procname g start_pname |> Option.iter ~f:(fun start_node -> flag_list [start_node])
|
||||
|
||||
|
||||
let remove_unflagged_and_unflag_all {id_map; node_map} =
|
||||
NodeMap.filter_map_inplace
|
||||
(fun _id (n : Node.t) ->
|
||||
if n.flag then ( Node.unset_flag n ; Some n ) else ( IdMap.remove id_map n.pname ; None ) )
|
||||
node_map
|
||||
|
||||
|
||||
(** remove pnames for all undefined procedures *)
|
||||
let trim_id_map (g : t) =
|
||||
IdMap.filter_map_inplace (fun _pname id -> Option.some_if (mem g id) id) g.id_map
|
||||
|
||||
|
||||
let pp_dot fmt {node_map} =
|
||||
F.fprintf fmt "@\ndigraph callgraph {@\n" ;
|
||||
NodeMap.iter (fun _id n -> Node.pp_dot fmt n) node_map ;
|
||||
F.fprintf fmt "}@."
|
||||
|
||||
|
||||
let to_dotty g =
|
||||
let outc = Filename.concat Config.results_dir "callgraph.dot" |> Out_channel.create in
|
||||
let fmt = F.formatter_of_out_channel outc in
|
||||
pp_dot fmt g ; Out_channel.close outc
|
||||
|
||||
|
||||
let build_from_captured_procs g =
|
||||
let hashcons_pname =
|
||||
let pname_tbl : Typ.Procname.t IdMap.t = IdMap.create 1001 in
|
||||
fun pname ->
|
||||
match IdMap.find_opt pname_tbl pname with
|
||||
| Some pname' ->
|
||||
pname'
|
||||
| None ->
|
||||
IdMap.add pname_tbl pname pname ; pname
|
||||
in
|
||||
let db = ResultsDatabase.get_database () in
|
||||
let stmt = Sqlite3.prepare db "SELECT proc_name, callees FROM procedures" in
|
||||
SqliteUtils.result_fold_rows db ~log:"creating call graph" stmt ~init:() ~f:(fun () stmt ->
|
||||
let proc_name = Sqlite3.column stmt 0 |> Typ.Procname.SQLite.deserialize |> hashcons_pname in
|
||||
let callees =
|
||||
Sqlite3.column stmt 1 |> Typ.Procname.SQLiteList.deserialize |> List.map ~f:hashcons_pname
|
||||
in
|
||||
add g proc_name callees )
|
||||
|
||||
|
||||
let build_from_sources g sources =
|
||||
let time0 = Mtime_clock.counter () in
|
||||
L.progress "Building call graph...@\n%!" ;
|
||||
build_from_captured_procs g ;
|
||||
let captured_length = length g in
|
||||
List.iter sources ~f:(fun sf ->
|
||||
SourceFiles.proc_names_of_source sf |> List.iter ~f:(flag_reachable g) ) ;
|
||||
remove_unflagged_and_unflag_all g ;
|
||||
trim_id_map g ;
|
||||
if Config.debug_level_analysis > 0 then to_dotty g ;
|
||||
L.progress "Building call graph took %a@\n" Mtime.Span.pp (Mtime_clock.count time0) ;
|
||||
L.progress
|
||||
"Constructed call graph from %d total procs, %d reachable defined procs, and takes %d bytes@."
|
||||
captured_length (length g)
|
||||
(Obj.(reachable_words (repr g)) * (Sys.word_size / 8))
|
||||
|
||||
|
||||
let get_unflagged_leaves g =
|
||||
NodeMap.fold
|
||||
(fun _id (n : Node.t) acc ->
|
||||
if n.flag || List.exists n.successors ~f:(mem g) then acc else n :: acc )
|
||||
g.node_map []
|
@ -0,0 +1,41 @@
|
||||
(*
|
||||
* Copyright (c) 2019-present, Facebook, Inc.
|
||||
*
|
||||
* 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 type NodeSig = sig
|
||||
type t = private {id: int; pname: Typ.Procname.t; successors: int list; mutable flag: bool}
|
||||
|
||||
val make : int -> Typ.Procname.t -> int list -> t
|
||||
|
||||
val set_flag : t -> unit
|
||||
|
||||
val unset_flag : t -> unit
|
||||
|
||||
val pp_dot : F.formatter -> t -> unit
|
||||
end
|
||||
|
||||
module Node : NodeSig
|
||||
|
||||
type t
|
||||
|
||||
val create : int -> t
|
||||
(** [create n] makes an empty graph with initial capacity [n] which grows as required *)
|
||||
|
||||
val build_from_sources : t -> SourceFile.t list -> unit
|
||||
(** build restriction of call graph to procedures reachable from provided sources *)
|
||||
|
||||
val mem : t -> int -> bool
|
||||
(** is an int [id] the index of a node in the graph? *)
|
||||
|
||||
val flag_reachable : t -> Typ.Procname.t -> unit
|
||||
(** flag all nodes reachable from the node of the given procname, if it exists *)
|
||||
|
||||
val remove_reachable : t -> Typ.Procname.t -> unit
|
||||
(** remove all nodes reachable from procname *)
|
||||
|
||||
val get_unflagged_leaves : t -> Node.t list
|
@ -0,0 +1,100 @@
|
||||
(*
|
||||
* Copyright (c) 2019-present, Facebook, Inc.
|
||||
*
|
||||
* 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
|
||||
|
||||
type target = Procname of Typ.Procname.t | File of SourceFile.t
|
||||
|
||||
type 'a task_generator = 'a Tasks.task_generator
|
||||
|
||||
let chain (gen1 : 'a task_generator) (gen2 : 'a task_generator) : 'a task_generator =
|
||||
let n_tasks = gen1.n_tasks + gen2.n_tasks in
|
||||
let gen1_returned_empty = ref false in
|
||||
let gen1_is_empty () =
|
||||
gen1_returned_empty := !gen1_returned_empty || gen1.is_empty () ;
|
||||
!gen1_returned_empty
|
||||
in
|
||||
let is_empty () = gen1_is_empty () && gen2.is_empty () in
|
||||
let next x = if gen1_is_empty () then gen2.next x else gen1.next x in
|
||||
{n_tasks; is_empty; next}
|
||||
|
||||
|
||||
let count_procedures () =
|
||||
let db = ResultsDatabase.get_database () in
|
||||
let stmt = Sqlite3.prepare db "SELECT COUNT(rowid) FROM procedures" in
|
||||
let count =
|
||||
match SqliteUtils.result_single_column_option db ~log:"counting procedures" stmt with
|
||||
| Some (Sqlite3.Data.INT i64) ->
|
||||
Int64.to_int i64 |> Option.value ~default:Int.max_value
|
||||
| _ ->
|
||||
L.die InternalError "Got no result trying to count procedures"
|
||||
in
|
||||
L.debug Analysis Quiet "Found %d procedures in procedures table.@." count ;
|
||||
count
|
||||
|
||||
|
||||
(** choose some reasonable minimum capacity that also is a prime number *)
|
||||
let initial_call_graph_capacity = 1009
|
||||
|
||||
let bottom_up sources : target task_generator =
|
||||
(* this will potentially grossly overapproximate the tasks *)
|
||||
let n_tasks = count_procedures () in
|
||||
let g = CallGraph.create initial_call_graph_capacity in
|
||||
let initialized = ref false in
|
||||
let pending : CallGraph.Node.t list ref = ref [] in
|
||||
let scheduled = ref Typ.Procname.Set.empty in
|
||||
let is_empty () =
|
||||
!initialized && List.is_empty !pending && Typ.Procname.Set.is_empty !scheduled
|
||||
in
|
||||
let rec next_aux () =
|
||||
match !pending with
|
||||
| [] ->
|
||||
pending := CallGraph.get_unflagged_leaves g ;
|
||||
if List.is_empty !pending then None else next_aux ()
|
||||
| n :: ns when n.flag || not (CallGraph.mem g n.id) ->
|
||||
pending := ns ;
|
||||
next_aux ()
|
||||
| n :: ns ->
|
||||
pending := ns ;
|
||||
scheduled := Typ.Procname.Set.add n.pname !scheduled ;
|
||||
CallGraph.flag_reachable g n.pname ;
|
||||
Some (Procname n.pname)
|
||||
in
|
||||
let next target_opt =
|
||||
(* do construction here, to avoid having the call graph into forked workers *)
|
||||
if not !initialized then (
|
||||
CallGraph.build_from_sources g sources ;
|
||||
initialized := true ) ;
|
||||
( match target_opt with
|
||||
| None ->
|
||||
()
|
||||
| Some (File _) ->
|
||||
assert false
|
||||
| Some (Procname pname) ->
|
||||
scheduled := Typ.Procname.Set.remove pname !scheduled ;
|
||||
CallGraph.remove_reachable g pname ) ;
|
||||
next_aux ()
|
||||
in
|
||||
{n_tasks; is_empty; next}
|
||||
|
||||
|
||||
let of_sources sources =
|
||||
let gen =
|
||||
List.rev_map sources ~f:(fun sf -> File sf)
|
||||
|> List.permute ~random_state:(Random.State.make (Array.create ~len:1 0))
|
||||
|> Tasks.gen_of_list
|
||||
in
|
||||
let next x =
|
||||
let res = gen.next x in
|
||||
match res with None -> None | Some (Procname _) -> assert false | Some (File _) as v -> v
|
||||
in
|
||||
{gen with next}
|
||||
|
||||
|
||||
let schedule sources =
|
||||
if Config.call_graph_schedule then chain (bottom_up sources) (of_sources sources)
|
||||
else of_sources sources
|
@ -0,0 +1,11 @@
|
||||
(*
|
||||
* Copyright (c) 2019-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*)
|
||||
open! IStd
|
||||
|
||||
type target = Procname of Typ.Procname.t | File of SourceFile.t
|
||||
|
||||
val schedule : SourceFile.t list -> target Tasks.task_generator
|
@ -1,3 +1,2 @@
|
||||
build_systems/codetoanalyze/objc_missing_fld/A.m, badOnlyOneNDA, 5, NULL_DEREFERENCE, no_bucket, ERROR, [start of procedure badOnlyOneNDA(),start of procedure predA(),start of procedure implOnlyFn:,return from a call to A::implOnlyFn:,Executing synthesized getter delegate,Condition is true,return from a call to predA,Taking false branch]
|
||||
build_systems/codetoanalyze/objc_missing_fld/B.m, badOnlyOneNDB, 3, NULL_DEREFERENCE, no_bucket, ERROR, [start of procedure badOnlyOneNDB(),Taking true branch]
|
||||
build_systems/codetoanalyze/objc_missing_fld/B.m, badOnlyOneNDB, 5, NULL_DEREFERENCE, no_bucket, ERROR, [start of procedure badOnlyOneNDB(),Taking false branch]
|
||||
build_systems/codetoanalyze/objc_missing_fld/B.m, badOnlyOneNDB, 5, NULL_DEREFERENCE, no_bucket, ERROR, [start of procedure badOnlyOneNDB(),start of procedure predB(),start of procedure implOnlyFn:,return from a call to A::implOnlyFn:,Executing synthesized getter delegate,Condition is true,return from a call to predB,Taking false branch]
|
||||
|
Loading…
Reference in new issue