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

open CFrontend_utils
(* To create a new checker you should: *)
(* 1. Define a checker function, say my_checker, in this module. *)
(* my_checker should define: *)
(*   -a) a condition that determine if the checker fires *)
(*   -b) a issue_desc that describes the warning (see warning_desc definition) *)
(* 2. Add your checker to the CFrontend_checkers interface *)
(* 3. Decide in which element of the AST my_checker should be evaluated. *)
(*    - If it is a statement then you need to invoke my_checker from *)
(*    run_frontend_checkers_on_stmt in CFrontend_error module.*)
(*    - If it is a declaration invoke it from run_frontend_checkers_on_decl *)

(* Helper functions *)
let location_from_stmt stmt =
  let info, _ = Clang_ast_proj.get_stmt_tuple stmt in
  CLocation.get_sil_location_from_range info.Clang_ast_t.si_source_range true

let location_from_dinfo info =
  CLocation.get_sil_location_from_range info.Clang_ast_t.di_source_range true

let location_from_decl dec =
  let info = Clang_ast_proj.get_decl_tuple dec in
  CLocation.get_sil_location_from_range info.Clang_ast_t.di_source_range true

let decl_name dec =
  match Clang_ast_proj.get_named_decl_tuple dec with
  | Some (_, n) -> n.Clang_ast_t.ni_name
  | None -> ""

let ivar_name stmt =
  let open Clang_ast_t in
  match stmt with
  | ObjCIvarRefExpr (_, _, _, rei) ->
      let dr_ref = rei.ovrei_decl_ref in
      let ivar_pointer = dr_ref.dr_decl_pointer in
      (match Ast_utils.get_decl ivar_pointer with
       | Some (ObjCIvarDecl (_, named_decl_info, _, _, _)) ->
           named_decl_info.Clang_ast_t.ni_name
       | _ -> "")
  | _ -> ""


(* (is_CallExpr /\ not call_function_named) ||
    is_CXXTemporaryObjectExpr || is_CXXMemberCallExpr
   || is_CXXOperatorCallExpr || is_ObjCMessageExpr *)
let ctl_makes_an_expensive_call () =
  let open CTL in
  let white_list_functions = ["CGPointMake"] in
  Or (Or (Or (Or (And (Atomic ("is_statement_kind", ["CallExpr"]),
                       Not(Atomic("call_function_named", white_list_functions))),
                  Atomic ("is_statement_kind", ["CXXTemporaryObjectExpr"])),
              Atomic ("is_statement_kind", ["CXXMemberCallExpr"])),
          Atomic ("is_statement_kind", ["CXXOperatorCallExpr"])),
      Atomic ("is_statement_kind", ["ObjCMessageExpr"]))


(*
   ET([ObjCMethodDecl][->Body] (EF call_addObserver
                                  Or EF call_addObserverForName)
   =>
   ET([ObjCImplementationDecl,ObjCProtocolDecl][->]
         ET([ObjCMethodDecl][->Body] EF remove_observer) Or
         EH([ObjCImplementationDecl, ObjCProtocolDecl] EF remove_observer)
*)
let ctl_ns_notification decl =
  let open CTL in
  let exists_method_calling_addObserver =
    EF (Atomic ("call_method", ["addObserver:selector:name:object:"])) in
  let exists_method_calling_addObserverForName =
    EF (Atomic ("call_method", ["addObserverForName:object:queue:usingBlock:"])) in
  let add_observer = Or (exists_method_calling_addObserver,
                         exists_method_calling_addObserverForName) in
  let eventually_addObserver = ET(["ObjCMethodDecl"], Some Body, add_observer) in
  let exists_method_calling_removeObserver =
    EF(Atomic ("call_method",["removeObserver:"])) in
  let exists_method_calling_removeObserverName =
    EF(Atomic ("call_method",["removeObserver:name:object:"])) in
  let remove_observer = Or(exists_method_calling_removeObserver,
                           exists_method_calling_removeObserverName) in
  let remove_observer_in_block = ET(["BlockDecl"], Some Body, remove_observer) in
  let remove_observer' = Or(remove_observer, remove_observer_in_block) in
  let remove_observer_in_method = ET(["ObjCMethodDecl"], Some Body, remove_observer') in
  let eventually_removeObserver =
    ET(["ObjCImplementationDecl"; "ObjCProtocolDecl"], None,
       Or(remove_observer_in_method ,
          EH(["ObjCImplementationDecl"; "ObjCProtocolDecl"], remove_observer_in_method))) in
  let condition = Not (Implies (eventually_addObserver, eventually_removeObserver)) in
  let issue_desc = {
    CIssue.issue = CIssue.Registered_observer_being_deallocated;
    CIssue.description =
      Localise.registered_observer_being_deallocated_str CFrontend_config.self;
    CIssue.suggestion =
      Some "Consider removing the object from the notification center before its deallocation.";
    CIssue.loc = location_from_decl decl;
  } in
  condition, issue_desc

(* BAD_POINTER_COMPARISON: Fires whenever a NSNumber is dangerously coerced to
    a boolean in a comparison *)
let ctl_bad_pointer_comparison_warning stmt =
  let open CTL in
  let is_binop = Atomic ("is_stmt", ["BinaryOperator"]) in
  let is_binop_eq = Atomic ("is_binop_with_kind", ["EQ"]) in
  let is_binop_ne = Atomic ("is_binop_with_kind", ["NE"]) in
  let is_binop_neq = Or (is_binop_eq, is_binop_ne) in
  let is_unop_lnot = Atomic ("is_unop_with_kind", ["LNot"]) in
  let is_implicit_cast_expr = Atomic ("is_stmt", ["ImplicitCastExpr"]) in
  let is_expr_with_cleanups = Atomic ("is_stmt", ["ExprWithCleanups"]) in
  let is_nsnumber = Atomic ("isa", ["NSNumber"]) in
  (*
  NOT is_binop_neq AND
  (is_expr_with_cleanups OR is_implicit_cast_expr OR is_binop OR is_unop_lnot)
  UNTIL is_nsnumber
  *)
  let p = Or (is_expr_with_cleanups, Or (is_implicit_cast_expr, Or (is_binop, is_unop_lnot))) in
  let p' = And (Not is_binop_neq, p) in
  let condition = EU (p', is_nsnumber) in
  let issue_desc =
    { CIssue.
      issue = CIssue.Bad_pointer_comparison;
      description = "Implicitly checking whether NSNumber pointer is nil";
      suggestion =
        Some ("Did you mean to compare against the unboxed value instead? " ^
              "Please either explicitly compare the NSNumber instance to nil, " ^
              "or use one of the NSNumber accessors before the comparison.");
      loc = location_from_stmt stmt
    } in
  condition, issue_desc

(* name_contains_delegate AND not name_contains_queue AND is_strong_property *)
let ctl_strong_delegate dec =
  let open CTL in
  let name_contains_delegate =
    Atomic ("property_name_contains_word", ["delegate"]) in
  let name_does_not_contains_queue =
    Not(Atomic ("property_name_contains_word", ["queue"])) in
  let is_strong_property =
    Atomic("is_strong_property", []) in
  let condition = And (name_contains_delegate,
                       And (name_does_not_contains_queue,
                            is_strong_property)) in
  let issue_desc = {
    CIssue.issue = CIssue.Strong_delegate_warning;
    CIssue.description = Printf.sprintf
        "Property or ivar %s declared strong" (decl_name dec);
    CIssue.suggestion = Some "In general delegates should be declared weak or assign";
    CIssue.loc = location_from_decl dec
  } in
  condition, issue_desc

(* (is_ObjC || is_Objc++) /\ is_global_var /\ not is_const_var  /\
   ET([VarDecl][->InitExpr] EF ctl_makes_an_expensive_call)
*)
let ctl_global_var_init_with_calls_warning decl =
  let open CTL in
  let ctl_is_global_var =
    And (And (Atomic ("is_objc_extension", []), Atomic ("is_global_var", [])),
         Not (Atomic ("is_const_var", []))) in
  let ctl_is_initialized_with_expensive_call  =
    ET(["VarDecl"], Some InitExpr, EF (ctl_makes_an_expensive_call ())) in
  let condition = And (ctl_is_global_var, ctl_is_initialized_with_expensive_call) in
  let issue_desc = {
    CIssue.issue = CIssue.Global_variable_initialized_with_function_or_method_call;
    CIssue.description = Printf.sprintf
        "Global variable %s is initialized using a function or method call"
        (decl_name decl);
    CIssue.suggestion = Some
        "If the function/method call is expensive, it can affect the starting time of the app.";
    CIssue.loc = location_from_decl decl
  } in
  condition, issue_desc

(* is_assign_property AND is_property_pointer_type *)
let ctl_assign_pointer_warning decl =
  let open CTL in
  let condition =
    And (Atomic("is_assign_property", []), Atomic("is_property_pointer_type", [])) in
  let issue_desc =
    { CIssue.issue = CIssue.Assign_pointer_warning;
      CIssue.description =
        Printf.sprintf
          "Property `%s` is a pointer type marked with the `assign` attribute"
          (decl_name decl);
      CIssue.suggestion = Some "Use a different attribute like `strong` or `weak`.";
      CIssue.loc = location_from_decl decl
    } in
  condition, issue_desc

(*
  not context_in_synchronized_block /\ not is_method_property_accessor_of_ivar
  /\ not is_objc_constructor /\ not is_objc_dealloc
*)
let ctl_direct_atomic_property_access_warning stmt =
  let open CTL in
  let condition =
    And (And (And (And (Not (Atomic ("context_in_synchronized_block", [])),
                        Atomic("is_ivar_atomic", [])),
                   Not (Atomic ("is_method_property_accessor_of_ivar", []))),
              Not (Atomic ("is_objc_constructor", []))),
         Not (Atomic ("is_objc_dealloc", []))) in
  let issue_desc = {
    CIssue.issue = CIssue.Direct_atomic_property_access;
    CIssue.description = Printf.sprintf
        "Direct access to ivar %s of an atomic property" (ivar_name stmt);
    CIssue.suggestion =
      Some "Accessing an ivar of an atomic property makes the property nonatomic";
    CIssue.loc = location_from_stmt stmt
  } in
  condition, issue_desc

let ctl_captured_cxx_ref_in_objc_block_warning stmt  =
  (* Fire if the list of captured references is not empty *)
  let condition = CTL.Atomic ("captures_cxx_references", []) in
  let issue_desc = {
    CIssue.issue = CIssue.Cxx_reference_captured_in_objc_block;
    CIssue.description = Printf.sprintf
        "C++ Reference variable(s) %s captured by Objective-C block"
        (Predicates.var_descs_name stmt);
    CIssue.suggestion = Some ("C++ References are unmanaged and may be invalid " ^
                              "by the time the block executes.");
    CIssue.loc = location_from_stmt stmt
  } in
  condition, issue_desc

(* === Warnings on properties === *)

(* Assing Pointer Warning: a property with a pointer type should not be declared `assign` *)
let assign_pointer_warning lcxt decl =
  let open CTL in
  let condition, issue_desc = ctl_assign_pointer_warning decl in
  if CTL.eval_formula condition (Decl decl) lcxt then
    Some issue_desc
  else None

(* Strong Delegate Warning: a property with name delegate should not be declared strong *)
let strong_delegate_warning lcxt decl =
  let condition, issue_desc = ctl_strong_delegate decl in
  if CTL.eval_formula condition (Decl decl) lcxt then
    Some issue_desc
  else None

(* GLOBAL_VARIABLE_INITIALIZED_WITH_FUNCTION_OR_METHOD_CALL warning: *)
(* a global variable initialization should not *)
(* contain calls to functions or methods as these can be expensive an delay the starting time *)
(* of an app *)
let global_var_init_with_calls_warning lcxt decl =
  let condition, issue_desc = ctl_global_var_init_with_calls_warning decl in
  if  CTL.eval_formula condition (CTL.Decl decl) lcxt then
    Some issue_desc
  else None

(* Direct Atomic Property access:
   a property declared atomic should not be accessed directly via its ivar *)
let direct_atomic_property_access_warning context stmt =
  let condition, issue_desc = ctl_direct_atomic_property_access_warning stmt in
  if  CTL.eval_formula condition (CTL.Stmt stmt) context then
    Some issue_desc
  else None

(* CXX_REFERENCE_CAPTURED_IN_OBJC_BLOCK: C++ references
   should not be captured in blocks.  *)
let captured_cxx_ref_in_objc_block_warning lcxt stmt  =
  let condition, issue_desc = ctl_captured_cxx_ref_in_objc_block_warning stmt  in
  if CTL.eval_formula condition (CTL.Stmt stmt) lcxt  then
    Some issue_desc
  else None

let checker_NSNotificationCenter lcxt dec =
  let condition, issue_desc = ctl_ns_notification dec in
  if CTL.eval_formula  condition (CTL.Decl dec) lcxt then
    Some issue_desc
  else None

let bad_pointer_comparison_warning lcxt stmt =
  let condition, issue_desc = ctl_bad_pointer_comparison_warning stmt in
  if CTL.eval_formula condition (CTL.Stmt stmt) lcxt then
    Some issue_desc
  else None
