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

(** Module for Pattern matching. *)

module L = Logging
module F = Format

let type_is_object typ =
  match typ.Typ.desc with
  | Tptr ({desc= Tstruct name}, _)
   -> Typ.Name.equal name Typ.Name.Java.java_lang_object
  | _
   -> false

let java_proc_name_with_class_method pn_java class_with_path method_name =
  try
    String.equal (Typ.Procname.java_get_class_name pn_java) class_with_path
    && String.equal (Typ.Procname.java_get_method pn_java) method_name
  with _ -> false

(** Holds iff the predicate holds on a supertype of the named type, including the type itself *)
let rec supertype_exists tenv pred name =
  match Tenv.lookup tenv name with
  | Some ({supers} as struct_typ)
   -> pred name struct_typ || List.exists ~f:(fun name -> supertype_exists tenv pred name) supers
  | None
   -> false

let rec supertype_find_map_opt tenv f name =
  match f name with
  | None -> (
    match Tenv.lookup tenv name with
    | Some {supers}
     -> List.find_map ~f:(supertype_find_map_opt tenv f) supers
    | None
     -> None )
  | result
   -> result

let is_immediate_subtype tenv this_type_name super_type_name =
  match Tenv.lookup tenv this_type_name with
  | Some {supers}
   -> List.exists ~f:(Typ.Name.equal super_type_name) supers
  | None
   -> false

(** return true if [typ0] <: [typ1] *)
let is_subtype tenv name0 name1 =
  Typ.Name.equal name0 name1
  || supertype_exists tenv (fun name _ -> Typ.Name.equal name name1) name0

let is_subtype_of_str tenv cn1 classname_str =
  let typename = Typ.Name.Java.from_string classname_str in
  is_subtype tenv cn1 typename

(** The type the method is invoked on *)
let get_this_type proc_attributes =
  match proc_attributes.ProcAttributes.formals with (_, t) :: _ -> Some t | _ -> None

let type_get_direct_supertypes tenv (typ: Typ.t) =
  match typ.desc with
  | Tptr ({desc= Tstruct name}, _) | Tstruct name -> (
    match Tenv.lookup tenv name with Some {supers} -> supers | None -> [] )
  | _
   -> []

let type_get_class_name {Typ.desc} =
  match desc with Typ.Tptr (typ, _) -> Typ.name typ | _ -> None

let type_get_annotation tenv (typ: Typ.t) : Annot.Item.t option =
  match typ.desc with
  | Tptr ({desc= Tstruct name}, _) | Tstruct name -> (
    match Tenv.lookup tenv name with Some {annots} -> Some annots | None -> None )
  | _
   -> None

let rec get_type_name {Typ.desc} =
  match desc with
  | Typ.Tstruct name
   -> Typ.Name.name name
  | Typ.Tptr (t, _)
   -> get_type_name t
  | _
   -> "_"

let get_field_type_name tenv (typ: Typ.t) (fieldname: Typ.Fieldname.t) : string option =
  match typ.desc with
  | Tstruct name | Tptr ({desc= Tstruct name}, _) -> (
    match Tenv.lookup tenv name with
    | Some {fields} -> (
      match List.find ~f:(function fn, _, _ -> Typ.Fieldname.equal fn fieldname) fields with
      | Some (_, ft, _)
       -> Some (get_type_name ft)
      | None
       -> None )
    | None
     -> None )
  | _
   -> None

let java_get_const_type_name (const: Const.t) : string =
  match const with
  | Const.Cstr _
   -> "java.lang.String"
  | Const.Cint _
   -> "java.lang.Integer"
  | Const.Cfloat _
   -> "java.lang.Double"
  | _
   -> "_"

let get_vararg_type_names tenv (call_node: Procdesc.Node.t) (ivar: Pvar.t) : string list =
  (* Is this the node creating ivar? *)
  let rec initializes_array instrs =
    match instrs with
    | (Sil.Call (Some (t1, _), Exp.Const Const.Cfun pn, _, _, _))
      :: (Sil.Store (Exp.Lvar iv, _, Exp.Var t2, _)) :: is
     -> Pvar.equal ivar iv && Ident.equal t1 t2
        && Typ.Procname.equal pn (Typ.Procname.from_string_c_fun "__new_array")
        || initializes_array is
    | _ :: is
     -> initializes_array is
    | _
     -> false
  in
  (* Get the type name added to ivar or None *)
  let added_type_name node =
    let rec nvar_type_name nvar instrs =
      match instrs with
      | (Sil.Load (nv, Exp.Lfield (_, id, t), _, _)) :: _ when Ident.equal nv nvar
       -> get_field_type_name tenv t id
      | (Sil.Load (nv, _, t, _)) :: _ when Ident.equal nv nvar
       -> Some (get_type_name t)
      | _ :: is
       -> nvar_type_name nvar is
      | _
       -> None
    in
    let rec added_nvar array_nvar instrs =
      match instrs with
      | (Sil.Store (Exp.Lindex (Exp.Var iv, _), _, Exp.Var nvar, _)) :: _
        when Ident.equal iv array_nvar
       -> nvar_type_name nvar (Procdesc.Node.get_instrs node)
      | (Sil.Store (Exp.Lindex (Exp.Var iv, _), _, Exp.Const c, _)) :: _
        when Ident.equal iv array_nvar
       -> Some (java_get_const_type_name c)
      | _ :: is
       -> added_nvar array_nvar is
      | _
       -> None
    in
    let rec array_nvar instrs =
      match instrs with
      | (Sil.Load (nv, Exp.Lvar iv, _, _)) :: _ when Pvar.equal iv ivar
       -> added_nvar nv instrs
      | _ :: is
       -> array_nvar is
      | _
       -> None
    in
    array_nvar (Procdesc.Node.get_instrs node)
  in
  (* Walk nodes backward until definition of ivar, adding type names *)
  let rec type_names node =
    if initializes_array (Procdesc.Node.get_instrs node) then []
    else
      match Procdesc.Node.get_preds node with
      | [n] -> (
        match added_type_name node with Some name -> name :: type_names n | None -> type_names n )
      | _
       -> raise Not_found
  in
  List.rev (type_names call_node)

let has_formal_proc_argument_type_names proc_desc argument_type_names =
  let formals = Procdesc.get_formals proc_desc in
  let equal_formal_arg (_, typ) arg_type_name = String.equal (get_type_name typ) arg_type_name in
  Int.equal (List.length formals) (List.length argument_type_names)
  && List.for_all2_exn ~f:equal_formal_arg formals argument_type_names

let has_formal_method_argument_type_names cfg pname_java argument_type_names =
  has_formal_proc_argument_type_names cfg
    (Typ.Procname.java_get_class_name pname_java :: argument_type_names)

let is_getter pname_java =
  Str.string_match (Str.regexp "get*") (Typ.Procname.java_get_method pname_java) 0

let is_setter pname_java =
  Str.string_match (Str.regexp "set*") (Typ.Procname.java_get_method pname_java) 0

(** Returns the signature of a field access (class name, field name, field type name) *)
let get_java_field_access_signature = function
  | Sil.Load (_, Exp.Lfield (_, fn, ft), bt, _)
   -> Some (get_type_name bt, Typ.Fieldname.java_get_field fn, get_type_name ft)
  | _
   -> None

(** Returns the formal signature (class name, method name,
    argument type names and return type name) *)
let get_java_method_call_formal_signature = function
  | Sil.Call (_, Exp.Const Const.Cfun pn, (_, tt) :: args, _, _) -> (
    match pn with
    | Typ.Procname.Java pn_java
     -> let arg_names = List.map ~f:(function _, t -> get_type_name t) args in
        let rt_name = Typ.Procname.java_get_return_type pn_java in
        let m_name = Typ.Procname.java_get_method pn_java in
        Some (get_type_name tt, m_name, arg_names, rt_name)
    | _
     -> None )
  | _
   -> None

let type_is_class typ =
  match typ.Typ.desc with
  | Tptr ({desc= Tstruct _}, _)
   -> true
  | Tptr ({desc= Tarray _}, _)
   -> true
  | Tstruct _
   -> true
  | _
   -> false

let initializer_classes =
  List.map ~f:Typ.Name.Java.from_string
    [ "android.app.Activity"
    ; "android.app.Application"
    ; "android.app.Fragment"
    ; "android.app.Service"
    ; "android.support.v4.app.Fragment"
    ; "junit.framework.TestCase" ]

let initializer_methods = ["onActivityCreated"; "onAttach"; "onCreate"; "onCreateView"; "setUp"]

(** Check if the type has in its supertypes from the initializer_classes list. *)
let type_has_initializer (tenv: Tenv.t) (t: Typ.t) : bool =
  let is_initializer_class typename _ =
    List.mem ~equal:Typ.Name.equal initializer_classes typename
  in
  match t.desc with
  | Typ.Tstruct name | Tptr ({desc= Tstruct name}, _)
   -> supertype_exists tenv is_initializer_class name
  | _
   -> false

(** Check if the method is one of the known initializer methods. *)
let method_is_initializer (tenv: Tenv.t) (proc_attributes: ProcAttributes.t) : bool =
  match get_this_type proc_attributes with
  | Some this_type
   -> if type_has_initializer tenv this_type then
        match proc_attributes.ProcAttributes.proc_name with
        | Typ.Procname.Java pname_java
         -> let mname = Typ.Procname.java_get_method pname_java in
            List.exists ~f:(String.equal mname) initializer_methods
        | _
         -> false
      else false
  | None
   -> false

(** Get the vararg values by looking for array assignments to the pvar. *)
let java_get_vararg_values node pvar idenv =
  let values = ref [] in
  let do_instr = function
    | Sil.Store (Exp.Lindex (array_exp, _), _, content_exp, _)
      when Exp.equal (Exp.Lvar pvar) (Idenv.expand_expr idenv array_exp)
     -> (* Each vararg argument is an assigment to a pvar denoting an array of objects. *)
        values := content_exp :: !values
    | _
     -> ()
  in
  let do_node n = List.iter ~f:do_instr (Procdesc.Node.get_instrs n) in
  let () =
    match Errdesc.find_program_variable_assignment node pvar with
    | Some (node', _)
     -> Procdesc.iter_slope_range do_node node' node
    | None
     -> ()
  in
  !values

let proc_calls resolve_attributes pdesc filter : (Typ.Procname.t * ProcAttributes.t) list =
  let res = ref [] in
  let do_instruction _ instr =
    match instr with
    | Sil.Call (_, Exp.Const Const.Cfun callee_pn, _, _, _) -> (
      match resolve_attributes callee_pn with
      | Some callee_attributes
       -> if filter callee_pn callee_attributes then res := (callee_pn, callee_attributes) :: !res
      | None
       -> () )
    | _
     -> ()
  in
  let do_node node =
    let instrs = Procdesc.Node.get_instrs node in
    List.iter ~f:(do_instruction node) instrs
  in
  let nodes = Procdesc.get_nodes pdesc in
  List.iter ~f:do_node nodes ;
  List.rev !res

let override_exists f tenv proc_name =
  let rec super_type_exists tenv super_class_name =
    let super_proc_name = Typ.Procname.replace_class proc_name super_class_name in
    match Tenv.lookup tenv super_class_name with
    | Some {methods; supers}
     -> let is_override pname =
          Typ.Procname.equal pname super_proc_name && not (Typ.Procname.is_constructor pname)
        in
        List.exists ~f:(fun pname -> is_override pname && f pname) methods
        || List.exists ~f:(super_type_exists tenv) supers
    | _
     -> false
  in
  f proc_name
  ||
  match proc_name with
  | Typ.Procname.Java proc_name_java
   -> let type_name =
        Typ.Name.Java.from_string (Typ.Procname.java_get_class_name proc_name_java)
      in
      List.exists ~f:(super_type_exists tenv)
        (type_get_direct_supertypes tenv (Typ.mk (Tstruct type_name)))
  | _
   -> false

(* Only java supported at the moment *)

let override_iter f tenv proc_name =
  ignore (override_exists (fun pname -> f pname ; false) tenv proc_name)

(** return the set of instance fields that are assigned to a null literal in [procdesc] *)
let get_fields_nullified procdesc =
  (* walk through the instructions and look for instance fields that are assigned to null *)
  let collect_nullified_flds (nullified_flds, this_ids) _ = function
    | Sil.Store (Exp.Lfield (Exp.Var lhs, fld, _), _, rhs, _)
      when Exp.is_null_literal rhs && Ident.IdentSet.mem lhs this_ids
     -> (Typ.Fieldname.Set.add fld nullified_flds, this_ids)
    | Sil.Load (id, rhs, _, _) when Exp.is_this rhs
     -> (nullified_flds, Ident.IdentSet.add id this_ids)
    | _
     -> (nullified_flds, this_ids)
  in
  let nullified_flds, _ =
    Procdesc.fold_instrs collect_nullified_flds (Typ.Fieldname.Set.empty, Ident.IdentSet.empty)
      procdesc
  in
  nullified_flds

(** Checks if the exception is an unchecked exception *)
let is_runtime_exception tenv typename =
  is_subtype_of_str tenv typename "java.lang.RuntimeException"

(** Checks if the class name is a Java exception *)
let is_exception tenv typename = is_subtype_of_str tenv typename "java.lang.Exception"

(** Checks if the class name is a Java exception *)
let is_throwable tenv typename = is_subtype_of_str tenv typename "java.lang.Throwable"

(** tests whether any class attributes (e.g., @ThreadSafe) pass check of first argument,
    including for supertypes*)
let check_class_attributes check tenv = function
  | Typ.Procname.Java java_pname
   -> let check_class_annots _ {Typ.Struct.annots} = check annots in
      supertype_exists tenv check_class_annots (Typ.Procname.java_get_class_type_name java_pname)
  | _
   -> false

(** tests whether any class attributes (e.g., @ThreadSafe) pass check of first argument,
    for the current class only*)
let check_current_class_attributes check tenv = function
  | Typ.Procname.Java java_pname -> (
    match Tenv.lookup tenv (Typ.Procname.java_get_class_type_name java_pname) with
    | Some struct_typ
     -> check struct_typ.annots
    | _
     -> false )
  | _
   -> false

(** find superclasss with attributes (e.g., @ThreadSafe), including current class*)
let rec find_superclasses_with_attributes check tenv tname =
  match Tenv.lookup tenv tname with
  | Some struct_typ
   -> let result_from_supers =
        List.concat (List.map ~f:(find_superclasses_with_attributes check tenv) struct_typ.supers)
      in
      if check struct_typ.annots then tname :: result_from_supers else result_from_supers
  | _
   -> []
