(*
 * Copyright (c) 2018-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 MF = MarkupFormatter

let debug fmt = L.(debug Analysis Verbose fmt)

let is_on_ui_thread pn =
  RacerDConfig.(match Models.get_thread pn with Models.MainThread -> true | _ -> false)


let is_nonblocking tenv proc_desc =
  let proc_attributes = Procdesc.get_attributes proc_desc in
  let is_method_suppressed =
    Annotations.pdesc_has_return_annot proc_desc Annotations.ia_is_nonblocking
  in
  let is_class_suppressed =
    PatternMatch.get_this_type proc_attributes
    |> Option.bind ~f:(PatternMatch.type_get_annotation tenv)
    |> Option.value_map ~default:false ~f:Annotations.ia_is_nonblocking
  in
  is_method_suppressed || is_class_suppressed


module Payload = SummaryPayload.Make (struct
  type t = StarvationDomain.summary

  let update_payloads post (payloads: Payloads.t) = {payloads with starvation= Some post}

  let of_payloads (payloads: Payloads.t) = payloads.starvation
end)

(* using an indentifier for a class object, create an access path representing that lock;
   this is for synchronizing on class objects only *)
let lock_of_class class_id =
  let ident = Ident.create_normal class_id 0 in
  let type_name = Typ.Name.Java.from_string "java.lang.Class" in
  let typ = Typ.mk (Typ.Tstruct type_name) in
  let typ' = Typ.mk (Typ.Tptr (typ, Typ.Pk_pointer)) in
  AccessPath.of_id ident typ'


let is_call_to_superclass tenv ~caller ~callee =
  match (caller, callee) with
  | Typ.Procname.Java caller_method, Typ.Procname.Java callee_method ->
      let caller_type = Typ.Procname.Java.get_class_type_name caller_method in
      let callee_type = Typ.Procname.Java.get_class_type_name callee_method in
      PatternMatch.is_subtype tenv caller_type callee_type
  | _ ->
      L.(die InternalError "Not supposed to run on non-Java code.")


module TransferFunctions (CFG : ProcCfg.S) = struct
  module CFG = CFG
  module Domain = StarvationDomain

  type extras = FormalMap.t

  let exec_instr (astate: Domain.astate) {ProcData.pdesc; tenv; extras} _ (instr: HilInstr.t) =
    let open RacerDConfig in
    let is_formal base = FormalMap.is_formal base extras in
    let get_path actuals =
      match actuals with
      | HilExp.AccessExpression access_exp :: _ -> (
        match AccessExpression.to_access_path access_exp with
        | (((Var.ProgramVar pvar, _) as base), _) as path
          when is_formal base || Pvar.is_global pvar ->
            Some (AccessPath.inner_class_normalize path)
        | _ ->
            (* ignore paths on local or logical variables *)
            None )
      | HilExp.Constant (Const.Cclass class_id) :: _ ->
          (* this is a synchronized/lock(CLASSNAME.class) construct *)
          Some (lock_of_class class_id)
      | _ ->
          None
    in
    let do_lock actuals loc astate =
      get_path actuals |> Option.value_map ~default:astate ~f:(Domain.acquire astate loc)
    in
    let do_unlock actuals astate =
      get_path actuals |> Option.value_map ~default:astate ~f:(Domain.release astate)
    in
    match instr with
    | Call (_, Direct callee, actuals, _, loc) -> (
      match Models.get_lock callee actuals with
      | Lock ->
          do_lock actuals loc astate
      | Unlock ->
          do_unlock actuals astate
      | LockedIfTrue ->
          astate
      | NoEffect when Models.should_skip_analysis tenv callee actuals ->
          astate
      | NoEffect when Models.is_synchronized_library_call tenv callee ->
          (* model a synchronized call without visible internal behaviour *)
          do_lock actuals loc astate |> do_unlock actuals
      | NoEffect when is_on_ui_thread callee ->
          let explanation = F.asprintf "it calls %a" (MF.wrap_monospaced Typ.Procname.pp) callee in
          Domain.set_on_ui_thread astate explanation
      | NoEffect ->
          let caller = Procdesc.get_proc_name pdesc in
          match Models.may_block tenv callee actuals with
          | Some sev ->
              Domain.blocking_call ~caller ~callee sev loc astate
          | None ->
              Payload.read pdesc callee
              |> Option.value_map ~default:astate ~f:(fun summary ->
                     (* if not calling a method in a superclass then set order to empty
                        to avoid blaming a caller in one class for deadlock/starvation
                        happening in the callee class *)
                     let summary =
                       if is_call_to_superclass tenv ~caller ~callee then summary
                       else {summary with Domain.order= Domain.OrderDomain.empty}
                     in
                     Domain.integrate_summary astate callee loc summary ) )
    | _ ->
        astate


  let pp_session_name _node fmt = F.pp_print_string fmt "starvation"
end

module Analyzer = LowerHil.MakeAbstractInterpreter (ProcCfg.Normal) (TransferFunctions)

let die_if_not_java proc_desc =
  let is_java =
    Procdesc.get_proc_name proc_desc |> Typ.Procname.get_language |> Language.(equal Java)
  in
  if not is_java then L.(die InternalError "Not supposed to run on non-Java code yet.")


let analyze_procedure {Callbacks.proc_desc; tenv; summary} =
  let open StarvationDomain in
  die_if_not_java proc_desc ;
  let pname = Procdesc.get_proc_name proc_desc in
  let formals = FormalMap.make proc_desc in
  let proc_data = ProcData.make proc_desc tenv formals in
  let initial =
    if not (Procdesc.is_java_synchronized proc_desc) then StarvationDomain.empty
    else
      let loc = Procdesc.get_loc proc_desc in
      let lock =
        match pname with
        | Typ.Procname.Java java_pname when Typ.Procname.Java.is_static java_pname ->
            (* this is crafted so as to match synchronized(CLASSNAME.class) constructs *)
            Typ.Procname.Java.get_class_type_name java_pname |> Typ.Name.name
            |> Ident.string_to_name |> lock_of_class |> Option.some
        | _ ->
            FormalMap.get_formal_base 0 formals |> Option.map ~f:(fun base -> (base, []))
      in
      Option.value_map lock ~default:StarvationDomain.empty
        ~f:(StarvationDomain.acquire StarvationDomain.empty loc)
  in
  let initial =
    RacerDConfig.Models.runs_on_ui_thread tenv proc_desc
    |> Option.value_map ~default:initial ~f:(StarvationDomain.set_on_ui_thread initial)
  in
  let filter_blocks =
    if is_nonblocking tenv proc_desc then fun ({events; order} as astate) ->
      { astate with
        events= EventDomain.filter (function {elem= MayBlock _} -> false | _ -> true) events
      ; order=
          OrderDomain.filter
            (function {elem= {eventually= {elem= MayBlock _}}} -> false | _ -> true)
            order }
    else Fn.id
  in
  Analyzer.compute_post proc_data ~initial |> Option.map ~f:filter_blocks
  |> Option.value_map ~default:summary ~f:(fun astate -> Payload.update_summary astate summary)


(** per-procedure report map, which takes care of deduplication *)
module ReportMap : sig
  type t

  val empty : t

  val add_deadlock : Tenv.t -> Procdesc.t -> Location.t -> Errlog.loc_trace -> string -> t -> t

  val add_starvation :
    Tenv.t -> StarvationDomain.Event.severity_t -> Procdesc.t -> Location.t -> Errlog.loc_trace
    -> string -> t -> t

  val log : t -> unit
end = struct
  type starvation_t = StarvationDomain.Event.severity_t

  type deadlock_t = int

  type 'weight_t report_t =
    {weight: 'weight_t; pname: Typ.Procname.t; ltr: Errlog.loc_trace; message: string}

  module LocMap = PrettyPrintable.MakePPMap (Location)

  type t = (deadlock_t report_t list * starvation_t report_t list) LocMap.t

  let empty : t = LocMap.empty

  let add_deadlock tenv pdesc loc ltr message (map: t) =
    let pname = Procdesc.get_proc_name pdesc in
    if Reporting.is_suppressed tenv pdesc IssueType.deadlock ~field_name:None then map
    else
      let rep = {weight= -List.length ltr; pname; ltr; message} in
      let deadlocks, starvations = try LocMap.find loc map with Caml.Not_found -> ([], []) in
      let new_reports = (rep :: deadlocks, starvations) in
      LocMap.add loc new_reports map


  let add_starvation tenv sev pdesc loc ltr message map =
    let pname = Procdesc.get_proc_name pdesc in
    if Reporting.is_suppressed tenv pdesc IssueType.starvation ~field_name:None then map
    else
      let rep = {weight= sev; pname; ltr; message} in
      let deadlocks, starvations = try LocMap.find loc map with Caml.Not_found -> ([], []) in
      let new_reports = (deadlocks, rep :: starvations) in
      LocMap.add loc new_reports map


  let log map =
    let log_report issuetype loc {pname; ltr; message} =
      let exn = Exceptions.Checkers (issuetype, Localise.verbatim_desc message) in
      Reporting.log_issue_external pname Exceptions.Error ~loc ~ltr exn
    in
    let mk_deduped_report num_of_reports ({message} as report) =
      { report with
        message=
          Printf.sprintf "%s %d more report(s) on the same line suppressed." message
            (num_of_reports - 1) }
    in
    let log_loc_reports issuetype compare loc = function
      | [] ->
          ()
      | [report] ->
          log_report issuetype loc report
      | reports ->
          List.max_elt ~compare:(fun {weight} {weight= weight'} -> compare weight weight') reports
          |> Option.iter ~f:(fun rep ->
                 mk_deduped_report (List.length reports) rep |> log_report issuetype loc )
    in
    let log_location loc (deadlocks, starvations) =
      log_loc_reports IssueType.deadlock Int.compare loc deadlocks ;
      log_loc_reports IssueType.starvation StarvationDomain.Event.compare_severity_t loc
        starvations
    in
    LocMap.iter log_location map
end

let should_report_deadlock_on_current_proc current_elem endpoint_elem =
  let open StarvationDomain in
  match (current_elem.Order.elem.first, current_elem.Order.elem.eventually.elem) with
  | _, MayBlock _ ->
      (* should never happen *)
      L.die InternalError "Deadlock cannot occur without two lock events: %a" Order.pp current_elem
  | ((Var.LogicalVar _, _), []), _ ->
      (* first elem is a class object (see [lock_of_class]), so always report because the
         reverse ordering on the events will not occur *)
      true
  | ((Var.LogicalVar _, _), _ :: _), _ | _, LockAcquire ((Var.LogicalVar _, _), _) ->
      (* first elem has an ident root, but has a non-empty access path, which means we are
         not filtering out local variables (see [exec_instr]), or,
         second elem has an ident root, which should not happen if we are filtering locals *)
      L.die InternalError "Deadlock cannot occur on these logical variables: %a @." Order.pp
        current_elem
  | ((_, typ1), _), LockAcquire ((_, typ2), _) ->
      (* use string comparison on types as a stable order to decide whether to report a deadlock *)
      let c = String.compare (Typ.to_string typ1) (Typ.to_string typ2) in
      c < 0
      || Int.equal 0 c
         && (* same class, so choose depending on location *)
            Location.compare current_elem.Order.elem.eventually.Event.loc
              endpoint_elem.Order.elem.eventually.Event.loc
            < 0


let should_report pdesc =
  match Procdesc.get_proc_name pdesc with
  | Typ.Procname.Java java_pname ->
      Procdesc.get_access pdesc <> PredSymb.Private
      && not (Typ.Procname.Java.is_autogen_method java_pname)
      && not (Typ.Procname.Java.is_class_initializer java_pname)
  | _ ->
      L.(die InternalError "Not supposed to run on non-Java code.")


let fold_reportable_summaries (tenv, current_pdesc) clazz ~init ~f =
  let methods =
    Tenv.lookup tenv clazz
    |> Option.value_map ~default:[] ~f:(fun tstruct -> tstruct.Typ.Struct.methods)
  in
  let f acc mthd =
    Ondemand.get_proc_desc mthd
    |> Option.value_map ~default:acc ~f:(fun other_pdesc ->
           if should_report other_pdesc then
             Payload.read current_pdesc mthd |> Option.map ~f:(fun payload -> (mthd, payload))
             |> Option.fold ~init:acc ~f
           else acc )
  in
  List.fold methods ~init ~f


(*  Note about how many times we report a deadlock: normally twice, at each trace starting point.
         Due to the fact we look for deadlocks in the summaries of the class at the root of a path,
         this will fail when (a) the lock is of class type (ie as used in static sync methods), because
         then the root is an identifier of type java.lang.Class and (b) when the lock belongs to an
         inner class but this is no longer obvious in the path, because of nested-class path normalisation.
         The net effect of the above issues is that we will only see these locks in conflicting pairs
         once, as opposed to twice with all other deadlock pairs. *)
let report_deadlocks env {StarvationDomain.order; ui} report_map' =
  let open StarvationDomain in
  let tenv, current_pdesc = env in
  let current_pname = Procdesc.get_proc_name current_pdesc in
  let pp_acquire fmt (lock, loc, pname) =
    F.fprintf fmt "%a (%a in %a)" Lock.pp lock Location.pp loc
      (MF.wrap_monospaced Typ.Procname.pp)
      pname
  in
  let pp_thread fmt
      ( pname
      , { Order.loc= loc1
        ; trace= trace1
        ; elem= {first= lock1; eventually= {elem= event; loc= loc2; trace= trace2}} } ) =
    match event with
    | LockAcquire lock2 ->
        let pname1 = List.last trace1 |> Option.value_map ~default:pname ~f:CallSite.pname in
        let pname2 = List.last trace2 |> Option.value_map ~default:pname1 ~f:CallSite.pname in
        F.fprintf fmt "first %a and then %a" pp_acquire (lock1, loc1, pname1) pp_acquire
          (lock2, loc2, pname2)
    | _ ->
        L.die InternalError "Trying to report a deadlock without two lock events."
  in
  let report_endpoint_elem current_elem endpoint_pname elem report_map =
    if
      not
        ( Order.may_deadlock current_elem elem
        && should_report_deadlock_on_current_proc current_elem elem )
    then report_map
    else
      let () = debug "Possible deadlock:@.%a@.%a@." Order.pp current_elem Order.pp elem in
      match (current_elem.Order.elem.eventually.elem, elem.Order.elem.eventually.elem) with
      | LockAcquire _, LockAcquire _ ->
          let error_message =
            Format.asprintf
              "Potential deadlock.@.Trace 1 (starts at %a) %a.@.Trace 2 (starts at %a), %a."
              (MF.wrap_monospaced Typ.Procname.pp)
              current_pname pp_thread (current_pname, current_elem)
              (MF.wrap_monospaced Typ.Procname.pp)
              endpoint_pname pp_thread (endpoint_pname, elem)
          in
          let first_trace = Order.make_trace ~header:"[Trace 1] " current_pname current_elem in
          let second_trace = Order.make_trace ~header:"[Trace 2] " endpoint_pname elem in
          let ltr = first_trace @ second_trace in
          let loc = Order.get_loc current_elem in
          ReportMap.add_deadlock tenv current_pdesc loc ltr error_message report_map
      | _, _ ->
          report_map
  in
  let report_on_current_elem elem report_map =
    match elem.Order.elem.eventually.elem with
    | MayBlock _ ->
        report_map
    | LockAcquire endpoint_lock ->
        Lock.owner_class endpoint_lock
        |> Option.value_map ~default:report_map ~f:(fun endpoint_class ->
               (* get the class of the root variable of the lock in the endpoint elem
                   and retrieve all the summaries of the methods of that class *)
               (* for each summary related to the endpoint, analyse and report on its pairs *)
               fold_reportable_summaries env endpoint_class ~init:report_map ~f:
                 (fun acc (endp_pname, endpoint_summary) ->
                   let endp_order = endpoint_summary.order in
                   let endp_ui = endpoint_summary.ui in
                   if UIThreadDomain.is_empty ui || UIThreadDomain.is_empty endp_ui then
                     OrderDomain.fold (report_endpoint_elem elem endp_pname) endp_order acc
                   else acc ) )
  in
  OrderDomain.fold report_on_current_elem order report_map'


let report_starvation env {StarvationDomain.events; ui} report_map' =
  let open StarvationDomain in
  let tenv, current_pdesc = env in
  let current_pname = Procdesc.get_proc_name current_pdesc in
  let report_remote_block ui_explain event current_lock endpoint_pname endpoint_elem report_map =
    let lock = endpoint_elem.Order.elem.first in
    match endpoint_elem.Order.elem.eventually.elem with
    | MayBlock (block_descr, sev) when Lock.equal current_lock lock ->
        let error_message =
          Format.asprintf
            "Method %a runs on UI thread (because %s) and %a, which may be held by another thread \
             which %s."
            (MF.wrap_monospaced Typ.Procname.pp)
            current_pname ui_explain Lock.pp lock block_descr
        in
        let first_trace = Event.make_trace ~header:"[Trace 1] " current_pname event in
        let second_trace = Order.make_trace ~header:"[Trace 2] " endpoint_pname endpoint_elem in
        let ltr = first_trace @ second_trace in
        let loc = Event.get_loc event in
        ReportMap.add_starvation tenv sev current_pdesc loc ltr error_message report_map
    | _ ->
        report_map
  in
  let report_on_current_elem ui_explain event report_map =
    match event.Event.elem with
    | MayBlock (_, sev) ->
        let error_message =
          Format.asprintf "Method %a runs on UI thread (because %s), and may block; %a."
            (MF.wrap_monospaced Typ.Procname.pp)
            current_pname ui_explain Event.pp event
        in
        let loc = Event.get_loc event in
        let ltr = Event.make_trace current_pname event in
        ReportMap.add_starvation tenv sev current_pdesc loc ltr error_message report_map
    | LockAcquire endpoint_lock ->
        Lock.owner_class endpoint_lock
        |> Option.value_map ~default:report_map ~f:(fun endpoint_class ->
               (* get the class of the root variable of the lock in the endpoint elem
                 and retrieve all the summaries of the methods of that class *)
               (* for each summary related to the endpoint, analyse and report on its pairs *)
               fold_reportable_summaries env endpoint_class ~init:report_map ~f:
                 (fun acc (endpoint_pname, {order; ui}) ->
                   (* skip methods known to run on ui thread, as they cannot run in parallel to us *)
                   if UIThreadDomain.is_empty ui then
                     OrderDomain.fold
                       (report_remote_block ui_explain event endpoint_lock endpoint_pname)
                       order acc
                   else acc ) )
  in
  match ui with
  | AbstractDomain.Types.Bottom ->
      report_map'
  | AbstractDomain.Types.NonBottom ui_explain ->
      EventDomain.fold (report_on_current_elem ui_explain) events report_map'


let reporting {Callbacks.procedures; source_file} =
  let report_procedure ((_, proc_desc) as env) =
    die_if_not_java proc_desc ;
    if should_report proc_desc then
      Payload.read proc_desc (Procdesc.get_proc_name proc_desc)
      |> Option.iter ~f:(fun summary ->
             report_deadlocks env summary ReportMap.empty |> report_starvation env summary
             |> ReportMap.log )
  in
  List.iter procedures ~f:report_procedure ;
  IssueLog.store Config.starvation_issues_dir_name source_file
