(*
 * Copyright (c) 2013-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

(** Eradicate NPEs. *)

module L = Logging
module F = Format
open Dataflow

(* ERADICATE CHECKER. TODOS:*)
(* 1) add support for constructors for anonymous inner classes (currently not checked) *)

(** Type for a module that provides a main callback function *)
module type CallBackT = sig
  val callback : TypeCheck.checks -> Callbacks.proc_callback_t
end

(* CallBackT *)

(** Extension to the type checker. *)
module type ExtensionT = sig
  val update_payloads : TypeState.t option -> Payloads.t -> Payloads.t
end

(** Create a module with the toplevel callback. *)
module MkCallback (Extension : ExtensionT) : CallBackT = struct
  let callback1 tenv find_canonical_duplicate calls_this checks idenv curr_pname curr_pdesc
      annotated_signature linereader proc_loc : bool * TypeState.t option =
    let mk s = Pvar.mk s curr_pname in
    let add_formal typestate (s, ia, typ) =
      let pvar = mk s in
      let ta =
        let origin = TypeOrigin.Formal s in
        TypeAnnotation.from_item_annotation ia origin
      in
      TypeState.add pvar (typ, ta, []) typestate
    in
    let get_initial_typestate () =
      let typestate_empty = TypeState.empty in
      List.fold ~f:add_formal ~init:typestate_empty annotated_signature.AnnotatedSignature.params
    in
    (* Check the nullable flag computed for the return value and report inconsistencies. *)
    let check_return find_canonical_duplicate exit_node final_typestate annotated_signature loc :
        unit =
      let _, ret_type = annotated_signature.AnnotatedSignature.ret in
      let ret_pvar = Procdesc.get_ret_var curr_pdesc in
      let ret_range = TypeState.lookup_pvar ret_pvar final_typestate in
      let typ_found_opt =
        match ret_range with Some (typ_found, _, _) -> Some typ_found | None -> None
      in
      let ret_implicitly_nullable =
        String.equal (PatternMatch.get_type_name ret_type) "java.lang.Void"
      in
      State.set_node exit_node ;
      if checks.TypeCheck.check_ret_type <> [] then
        List.iter
          ~f:(fun f -> f curr_pname curr_pdesc ret_type typ_found_opt loc)
          checks.TypeCheck.check_ret_type ;
      if checks.TypeCheck.eradicate then
        EradicateChecks.check_return_annotation tenv find_canonical_duplicate curr_pdesc ret_range
          annotated_signature ret_implicitly_nullable loc
    in
    let do_before_dataflow initial_typestate =
      if Config.eradicate_verbose then
        L.result "Initial Typestate@\n%a@." TypeState.pp initial_typestate
    in
    let do_after_dataflow find_canonical_duplicate final_typestate =
      let exit_node = Procdesc.get_exit_node curr_pdesc in
      check_return find_canonical_duplicate exit_node final_typestate annotated_signature proc_loc
    in
    let module DFTypeCheck = MakeDF (struct
      type t = TypeState.t

      let equal = TypeState.equal

      let join = TypeState.join

      let pp_name fmt = F.pp_print_string fmt "eradicate"

      let do_node tenv node typestate =
        NodePrinter.with_session ~pp_name node ~f:(fun () ->
            State.set_node node ;
            let typestates_succ, typestates_exn =
              TypeCheck.typecheck_node tenv calls_this checks idenv curr_pname curr_pdesc
                find_canonical_duplicate annotated_signature typestate node linereader
            in
            if Config.write_html then (
              let d_typestate ts = L.d_printfln "%a" TypeState.pp ts in
              L.d_strln "before:" ;
              d_typestate typestate ;
              L.d_strln "after:" ;
              List.iter ~f:d_typestate typestates_succ ) ;
            (typestates_succ, typestates_exn) )


      let proc_throws _ = DontKnow
    end) in
    let initial_typestate = get_initial_typestate () in
    do_before_dataflow initial_typestate ;
    let transitions = DFTypeCheck.run tenv curr_pdesc initial_typestate in
    match transitions (Procdesc.get_exit_node curr_pdesc) with
    | DFTypeCheck.Transition (final_typestate, _, _) ->
        do_after_dataflow find_canonical_duplicate final_typestate ;
        (!calls_this, Some final_typestate)
    | DFTypeCheck.Dead_state ->
        (!calls_this, None)


  let callback2 calls_this checks
      {Callbacks.proc_desc= curr_pdesc; summary; tenv; get_procs_in_file} annotated_signature
      linereader proc_loc : unit =
    let idenv = Idenv.create curr_pdesc in
    let curr_pname = Summary.get_proc_name summary in
    let find_duplicate_nodes = State.mk_find_duplicate_nodes curr_pdesc in
    let find_canonical_duplicate node =
      let duplicate_nodes = find_duplicate_nodes node in
      try Procdesc.NodeSet.min_elt duplicate_nodes with Caml.Not_found -> node
    in
    let typecheck_proc do_checks pname pdesc proc_details_opt =
      let ann_sig, loc, idenv_pn =
        match proc_details_opt with
        | Some (ann_sig, loc, idenv_pn) ->
            (ann_sig, loc, idenv_pn)
        | None ->
            let ann_sig =
              Models.get_modelled_annotated_signature (Procdesc.get_attributes pdesc)
            in
            let loc = Procdesc.get_loc pdesc in
            (ann_sig, loc, Idenv.create pdesc)
      in
      let checks', calls_this' =
        if do_checks then (checks, calls_this)
        else ({TypeCheck.eradicate= false; check_ret_type= []}, ref false)
      in
      callback1 tenv find_canonical_duplicate calls_this' checks' idenv_pn pname pdesc ann_sig
        linereader loc
    in
    let module Initializers = struct
      type init = Typ.Procname.t * Procdesc.t

      let equal_class_opt = [%compare.equal: string option]

      let final_typestates initializers_current_class =
        (* Get the private methods, from the same class, directly called by the initializers. *)
        let get_private_called (initializers : init list) : init list =
          let res = ref [] in
          let do_proc (init_pn, init_pd) =
            let filter callee_pn callee_attributes =
              let is_private =
                PredSymb.equal_access callee_attributes.ProcAttributes.access PredSymb.Private
              in
              let same_class =
                let get_class_opt pn =
                  match pn with
                  | Typ.Procname.Java pn_java ->
                      Some (Typ.Procname.Java.get_class_name pn_java)
                  | _ ->
                      None
                in
                equal_class_opt (get_class_opt init_pn) (get_class_opt callee_pn)
              in
              is_private && same_class
            in
            let private_called =
              PatternMatch.proc_calls (PatternMatch.lookup_attributes tenv) init_pd filter
            in
            let do_called (callee_pn, _) =
              match Ondemand.get_proc_desc callee_pn with
              | Some callee_pd ->
                  res := (callee_pn, callee_pd) :: !res
              | None ->
                  ()
            in
            List.iter ~f:do_called private_called
          in
          List.iter ~f:do_proc initializers ; !res
        in
        (* Get the initializers recursively called by computing a fixpoint.
           Start from the initializers of the current class and the current procedure. *)
        let initializers_recursive : init list =
          let initializers_base_case = initializers_current_class in
          let res = ref [] in
          let seen = ref Typ.Procname.Set.empty in
          let mark_seen (initializers : init list) : unit =
            List.iter ~f:(fun (pn, _) -> seen := Typ.Procname.Set.add pn !seen) initializers ;
            res := !res @ initializers
          in
          let rec fixpoint initializers_old =
            let initializers_new = get_private_called initializers_old in
            let initializers_new' =
              List.filter ~f:(fun (pn, _) -> not (Typ.Procname.Set.mem pn !seen)) initializers_new
            in
            mark_seen initializers_new' ;
            if initializers_new' <> [] then fixpoint initializers_new'
          in
          mark_seen initializers_base_case ; fixpoint initializers_base_case ; !res
        in
        (* Get the final typestates of all the initializers. *)
        let final_typestates = ref [] in
        let get_final_typestate (pname, pdesc) =
          match typecheck_proc false pname pdesc None with
          | _, Some final_typestate ->
              final_typestates := (pname, final_typestate) :: !final_typestates
          | _, None ->
              ()
        in
        List.iter ~f:get_final_typestate initializers_recursive ;
        List.rev !final_typestates


      let pname_and_pdescs_with f =
        let res = ref [] in
        let filter pname =
          match PatternMatch.lookup_attributes tenv pname with
          | Some proc_attributes ->
              f (pname, proc_attributes)
          | None ->
              false
        in
        let do_proc pname =
          if filter pname then
            match Ondemand.get_proc_desc pname with
            | Some pdesc ->
                res := (pname, pdesc) :: !res
            | None ->
                ()
        in
        List.iter ~f:do_proc (get_procs_in_file curr_pname) ;
        List.rev !res


      let get_class pn =
        match pn with
        | Typ.Procname.Java pn_java ->
            Some (Typ.Procname.Java.get_class_name pn_java)
        | _ ->
            None


      (** Typestates after the current procedure and all initializer procedures. *)
      let final_initializer_typestates_lazy =
        lazy
          (let is_initializer proc_attributes =
             PatternMatch.method_is_initializer tenv proc_attributes
             ||
             let ia, _ =
               (Models.get_modelled_annotated_signature proc_attributes).AnnotatedSignature.ret
             in
             Annotations.ia_is_initializer ia
           in
           let initializers_current_class =
             pname_and_pdescs_with (function pname, proc_attributes ->
                 is_initializer proc_attributes
                 && equal_class_opt (get_class pname) (get_class curr_pname) )
           in
           final_typestates ((curr_pname, curr_pdesc) :: initializers_current_class))


      (** Typestates after all constructors. *)
      let final_constructor_typestates_lazy =
        lazy
          (let constructors_current_class =
             pname_and_pdescs_with (fun (pname, _) ->
                 Typ.Procname.is_constructor pname
                 && equal_class_opt (get_class pname) (get_class curr_pname) )
           in
           final_typestates constructors_current_class)
    end
    (* Initializers *) in
    let do_final_typestate typestate_opt calls_this =
      let do_typestate typestate =
        let start_node = Procdesc.get_start_node curr_pdesc in
        if
          (not calls_this)
          (* if 'this(...)' is called, no need to check initialization *)
          && checks.TypeCheck.eradicate
        then
          EradicateChecks.check_constructor_initialization tenv find_canonical_duplicate curr_pname
            curr_pdesc start_node Initializers.final_initializer_typestates_lazy
            Initializers.final_constructor_typestates_lazy proc_loc ;
        if Config.eradicate_verbose then L.result "Final Typestate@\n%a@." TypeState.pp typestate
      in
      match typestate_opt with None -> () | Some typestate -> do_typestate typestate
    in
    TypeErr.reset () ;
    let calls_this, final_typestate_opt =
      typecheck_proc true curr_pname curr_pdesc (Some (annotated_signature, proc_loc, idenv))
    in
    do_final_typestate final_typestate_opt calls_this ;
    if checks.TypeCheck.eradicate then
      EradicateChecks.check_overridden_annotations find_canonical_duplicate tenv curr_pname
        curr_pdesc annotated_signature ;
    TypeErr.report_forall_checks_and_reset tenv (EradicateCheckers.report_error tenv) curr_pdesc ;
    ()


  (** Entry point for the eradicate-based checker infrastructure. *)
  let callback checks ({Callbacks.proc_desc; summary} as callback_args) : Summary.t =
    let proc_name = Procdesc.get_proc_name proc_desc in
    let calls_this = ref false in
    let filter_special_cases () =
      if
        ( match proc_name with
        | Typ.Procname.Java java_pname ->
            Typ.Procname.Java.is_access_method java_pname
            || Typ.Procname.Java.is_external java_pname
        | _ ->
            false )
        || (Procdesc.get_attributes proc_desc).ProcAttributes.is_bridge_method
      then None
      else
        let annotated_signature =
          Models.get_modelled_annotated_signature (Procdesc.get_attributes proc_desc)
        in
        Some annotated_signature
    in
    ( match filter_special_cases () with
    | None ->
        ()
    | Some annotated_signature ->
        let loc = Procdesc.get_loc proc_desc in
        let linereader = Printer.LineReader.create () in
        if Config.eradicate_verbose then
          L.result "%a@." (AnnotatedSignature.pp proc_name) annotated_signature ;
        callback2 calls_this checks callback_args annotated_signature linereader loc ) ;
    summary
end

(* MkCallback *)

module EmptyExtension : ExtensionT = struct
  let update_payloads typestate_opt (payloads : Payloads.t) =
    {payloads with typestate= typestate_opt}
end

module Main = struct
  module Callback = MkCallback (EmptyExtension)

  let callback = Callback.callback
end

(** Eradicate checker for Java @Nullable annotations. *)
let callback_eradicate =
  let checks = {TypeCheck.eradicate= true; check_ret_type= []} in
  Main.callback checks


(** Call the given check_return_type at the end of every procedure. *)
let callback_check_return_type check_return_type callback_args =
  let checks = {TypeCheck.eradicate= false; check_ret_type= [check_return_type]} in
  Main.callback checks callback_args
