(* * 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! IStd module MF = MarkupFormatter type linter = { condition : CTL.t; issue_desc : CIssue.issue_desc; def_file : string option; } let single_to_multi checker = fun ctx an -> let condition, issue_desc_opt = checker ctx an in (condition, Option.to_list issue_desc_opt) (* List of checkers on decls *that return 0 or 1 issue* *) let decl_single_checkers_list = [ComponentKit.component_with_unconventional_superclass_advice; ComponentKit.mutable_local_vars_advice; ComponentKit.component_factory_function_advice; ComponentKit.component_file_cyclomatic_complexity_info;] (* List of checkers on decls *) let decl_checkers_list = ComponentKit.component_with_multiple_factory_methods_advice:: (ComponentKit.component_file_line_count_info:: (List.map ~f:single_to_multi decl_single_checkers_list)) (* List of checkers on stmts *that return 0 or 1 issue* *) let stmt_single_checkers_list = [ComponentKit.component_file_cyclomatic_complexity_info; ComponentKit.component_initializer_with_side_effects_advice; GraphQL.DeprecatedAPIUsage.checker;] let stmt_checkers_list = List.map ~f:single_to_multi stmt_single_checkers_list (* List of checkers that will be filled after parsing them from input the linter def files *) let parsed_linters = ref [] let evaluate_place_holder ph an = match ph with | "%ivar_name%" -> MF.monospaced_to_string (CFrontend_checkers.ivar_name an) | "%decl_name%" -> MF.monospaced_to_string (CFrontend_checkers.decl_name an) | "%cxx_ref_captured_in_block%" -> MF.monospaced_to_string (CFrontend_checkers.cxx_ref_captured_in_block an) | "%decl_ref_or_selector_name%" -> MF.monospaced_to_string (CFrontend_checkers.decl_ref_or_selector_name an) | "%iphoneos_target_sdk_version%" -> MF.monospaced_to_string (CFrontend_checkers.iphoneos_target_sdk_version an) | "%available_ios_sdk%" -> MF.monospaced_to_string (CFrontend_checkers.available_ios_sdk an) | _ -> (Logging.err "ERROR: helper function %s is unknown. Stop.\n" ph; assert false) (* given a message this function searches for a place-holder identifier, eg %id%. Then it evaluates id and replaces %id% in message with the result of its evaluation. The function keeps on checking if other place-holders exist and repeats the process until there are no place-holder left. *) let rec expand_message_string message an = (* reg exp should match alphanumeric id with possibly somee _ *) let re = Str.regexp "%[a-zA-Z0-9_]+%" in try let _ = Str.search_forward re message 0 in let ms = Str.matched_string message in let res = evaluate_place_holder ms an in Logging.out "\nMatched string '%s'\n" ms; let re_ms = Str.regexp_string ms in let message' = Str.replace_first re_ms res message in Logging.out "Replacing %s in message: \n %s \n" ms message; Logging.out "Resulting message: \n %s \n" message'; expand_message_string message' an with Not_found -> message let remove_new_lines message = String.substr_replace_all ~pattern:"\n" ~with_:" " message let string_to_err_kind = function | "WARNING" -> Exceptions.Kwarning | "ERROR" -> Exceptions.Kerror | "INFO" -> Exceptions.Kinfo | "ADVICE" -> Exceptions.Kadvice | s -> (Logging.err "\n[ERROR] Severity %s does not exist. Stop.\n" s; assert false) let string_to_issue_mode m = match m with | "ON" -> CIssue.On | "OFF" -> CIssue.Off | s -> (Logging.err "\n[ERROR] Mode %s does not exist. Please specify ON/OFF\n" s; assert false) (** Convert a parsed checker in list of linters *) let create_parsed_linters linters_def_file checkers : linter list = let open CIssue in let open CTL in Logging.out "\n Converting checkers in (condition, issue) pairs\n"; let do_one_checker c = let dummy_issue = { name = c.name; description = ""; suggestion = None; loc = Location.dummy; severity = Exceptions.Kwarning; mode = CIssue.On; } in let issue_desc, condition = List.fold ~f:(fun (issue', cond') d -> match d with | CSet (av, phi) when ALVar.is_report_when_keyword av -> issue', phi | CDesc (av, msg) when ALVar.is_message_keyword av -> {issue' with description = msg}, cond' | CDesc (av, sugg) when ALVar.is_suggestion_keyword av -> {issue' with suggestion = Some sugg}, cond' | CDesc (av, sev) when ALVar.is_severity_keyword av -> {issue' with severity = string_to_err_kind sev}, cond' | CDesc (av, m) when ALVar.is_mode_keyword av -> {issue' with mode = string_to_issue_mode m }, cond' | _ -> issue', cond') ~init:(dummy_issue, CTL.False) c.definitions in if Config.debug_mode then ( Logging.out "\nMaking condition and issue desc for checker '%s'\n" c.name; Logging.out "\nCondition =\n %a\n" CTL.Debug.pp_formula condition; Logging.out "\nIssue_desc = %a\n" CIssue.pp_issue issue_desc); {condition; issue_desc; def_file = Some linters_def_file} in List.map ~f:do_one_checker checkers let check_def_well_expanded vars expanded_formula = let open CTL in let check_const c = match c with | ALVar.Const c when List.mem vars (ALVar.Var c) -> failwith ("[ERROR]: Const '" ^ c ^ "' is used as formal parameter of some LET definition.") | ALVar.Const _ -> () | ALVar.Var v | ALVar.FId (Formula_id v) -> failwith ("[ERROR]: Variable '" ^ v ^ "' could not be substituted and cannot be evaluated") in let rec check_expansion exp_f = match exp_f with | True | False -> () | Atomic (_, ps) -> List.iter ~f: check_const ps | Not f1 -> check_expansion f1 | And (f1, f2) -> check_expansion f1; check_expansion f2 | Or (f1, f2) -> check_expansion f1; check_expansion f2 | Implies (f1, f2) -> check_expansion f1; check_expansion f2 | InNode (node_type_list, f1) -> List.iter ~f:check_const node_type_list; check_expansion f1 | AU (f1, f2) -> check_expansion f1; check_expansion f2 | EU (_, f1, f2) -> check_expansion f1; check_expansion f2 | EF (_, f1) -> check_expansion f1 | AF f1 -> check_expansion f1 | AG f1 -> check_expansion f1 | EX (_, f1) -> check_expansion f1 | AX f1 -> check_expansion f1 | EH (cl, f1) -> List.iter ~f:check_const cl; check_expansion f1 | EG (_, f1) -> check_expansion f1 | ET (ntl, _, f1) -> List.iter ~f: check_const ntl; check_expansion f1 | ETX (ntl, _, f1) -> List.iter ~f: check_const ntl; check_expansion f1 in check_expansion expanded_formula let rec apply_substitution f sub = let sub_param p = try snd (List.find_exn sub ~f:(fun (a,_) -> ALVar.equal p a)) with Not_found -> p in let sub_list_param ps = List.map ps ~f:sub_param in let open CTL in match f with | True | False -> f | Atomic (name, ps) -> Atomic (name, sub_list_param ps) | Not f1 -> Not (apply_substitution f1 sub) | And (f1, f2) -> And (apply_substitution f1 sub, apply_substitution f2 sub) | Or (f1, f2) -> Or (apply_substitution f1 sub, apply_substitution f2 sub) | Implies (f1, f2) -> Implies (apply_substitution f1 sub, apply_substitution f2 sub) | InNode (node_type_list, f1) -> InNode (sub_list_param node_type_list, apply_substitution f1 sub) | AU (f1, f2) -> AU (apply_substitution f1 sub, apply_substitution f2 sub) | EU (trans, f1, f2) -> EU (trans, apply_substitution f1 sub, apply_substitution f2 sub) | EF (trans, f1) -> EF (trans, apply_substitution f1 sub) | AF f1 -> AF (apply_substitution f1 sub) | AG f1 -> AG (apply_substitution f1 sub) | EX (trans, f1) -> EX (trans, apply_substitution f1 sub) | AX f1 -> AX (apply_substitution f1 sub) | EH (cl, f1) -> EH (sub_list_param cl, apply_substitution f1 sub) | EG (trans, f1) -> EG (trans, apply_substitution f1 sub) | ET (ntl, sw, f1) -> ET (sub_list_param ntl, sw, apply_substitution f1 sub) | ETX (ntl, sw, f1) -> ETX (sub_list_param ntl, sw, apply_substitution f1 sub) (* expands use of let defined formula id in checkers with their definition *) let expand_checkers checkers = let fail_with_circular_macro_definition name error_msg = Logging.out "[ERROR]: Macro '%s' has a circular definition.\n Cycle:\n%s" name error_msg; failwith ("Cannot expand....\n") in let open CTL in let rec expand acc map error_msg = match acc with | True | False -> acc | Atomic (ALVar.Formula_id (name) as av, actual_param) -> (* it may be a macro *) (let error_msg' = error_msg ^ " -Expanding formula identifier '" ^ name ^"'\n" in (try match ALVar.FormulaIdMap.find av map with | (true, _, _) -> fail_with_circular_macro_definition name error_msg' | (false, fparams, f1) -> (* in this case it should be a defined macro *) (match List.zip fparams actual_param with | Some sub -> let f1_sub = apply_substitution f1 sub in let map' = ALVar.FormulaIdMap.add av (true, fparams, f1) map in expand f1_sub map' error_msg' | None -> failwith ("[ERROR]: Formula identifier '" ^ name ^ "' is not called with the right number of parameters")) with Not_found -> acc)) (* in this case it should be a predicate *) | Not f1 -> Not (expand f1 map error_msg) | And (f1, f2) -> And (expand f1 map error_msg, expand f2 map error_msg) | Or (f1, f2) -> Or (expand f1 map error_msg, expand f2 map error_msg) | Implies (f1, f2) -> Implies (expand f1 map error_msg, expand f2 map error_msg) | InNode (node_type_list, f1) -> InNode (node_type_list, expand f1 map error_msg) | AU (f1, f2) -> AU (expand f1 map error_msg, expand f2 map error_msg) | EU (trans, f1, f2) -> EU (trans, expand f1 map error_msg, expand f2 map error_msg) | EF (trans, f1) -> EF (trans, expand f1 map error_msg) | AF f1 -> AF (expand f1 map error_msg) | AG f1 -> AG (expand f1 map error_msg) | EX (trans, f1) -> EX (trans, expand f1 map error_msg) | AX f1 -> AX (expand f1 map error_msg) | EH (cl, f1) -> EH (cl, expand f1 map error_msg) | EG (trans, f1) -> EG (trans, expand f1 map error_msg) | ET (tl, sw, f1) -> ET (tl, sw, expand f1 map error_msg) | ETX (tl, sw, f1) -> ETX (tl, sw, expand f1 map error_msg) in let expand_one_checker checker = Logging.out " +Start expanding %s\n" checker.name; (* Map a formula id to a triple (visited, parameters, definition). Visited is used during the expansion phase to understand if the formula was already expanded and, if yes we have a cyclic definifion *) let map : (bool * ALVar.t list * CTL.t) ALVar.FormulaIdMap.t = ALVar.FormulaIdMap.empty in let map, vars = List.fold ~f:(fun (map', vars') definition -> match definition with | CLet (key, params, formula) -> ALVar.FormulaIdMap.add key (false, params, formula) map', List.append vars' params | _ -> map', vars') ~init:(map, []) checker.definitions in let exp_defs = List.fold ~f:(fun defs clause -> match clause with | CSet (report_when_const, phi) -> Logging.out " -Expanding report_when\n"; let exp_report_when = expand phi map "" in check_def_well_expanded vars exp_report_when; CSet (report_when_const, exp_report_when) :: defs | cl -> cl :: defs) ~init:[] checker.definitions in { checker with definitions = exp_defs} in let expanded_checkers = List.map ~f:expand_one_checker checkers in expanded_checkers let get_err_log translation_unit_context method_decl_opt = let procname = match method_decl_opt with | Some method_decl -> CProcname.from_decl translation_unit_context method_decl | None -> Typ.Procname.Linters_dummy_method in LintIssues.get_err_log procname (* Add a frontend warning with a description desc at location loc to the errlog of a proc desc *) let log_frontend_issue translation_unit_context method_decl_opt key issue_desc linters_def_file = let name = issue_desc.CIssue.name in let loc = issue_desc.CIssue.loc in let errlog = get_err_log translation_unit_context method_decl_opt in let err_desc = Errdesc.explain_frontend_warning issue_desc.CIssue.description issue_desc.CIssue.suggestion loc in let exn = Exceptions.Frontend_warning (name, err_desc, __POS__) in let trace = [ Errlog.make_trace_element 0 issue_desc.CIssue.loc "" [] ] in let err_kind = issue_desc.CIssue.severity in let method_name = CAst_utils.full_name_of_decl_opt method_decl_opt |> QualifiedCppName.to_qual_string in let key = Hashtbl.hash (key ^ method_name) in Reporting.log_issue_from_errlog err_kind errlog exn ~loc ~ltr:trace ~node_id:(0, key) ?linters_def_file let get_current_method context (an : Ctl_parser_types.ast_node) = match an with | Decl (FunctionDecl _ as d) | Decl (CXXMethodDecl _ as d) | Decl (CXXConstructorDecl _ as d) | Decl (CXXConversionDecl _ as d) | Decl (CXXDestructorDecl _ as d) | Decl (ObjCMethodDecl _ as d) | Decl (BlockDecl _ as d) -> Some d | _ -> context.CLintersContext.current_method let fill_issue_desc_info_and_log context an key issue_desc linters_def_file loc = let desc = remove_new_lines (expand_message_string issue_desc.CIssue.description an) in let issue_desc' = {issue_desc with CIssue.description = desc; CIssue.loc = loc } in log_frontend_issue context.CLintersContext.translation_unit_context (get_current_method context an) key issue_desc' linters_def_file (* Calls the set of hard coded checkers (if any) *) let invoke_set_of_hard_coded_checkers_an context (an : Ctl_parser_types.ast_node) = let checkers, key = match an with | Decl dec -> decl_checkers_list, CAst_utils.generate_key_decl dec | Stmt st -> stmt_checkers_list, CAst_utils.generate_key_stmt st in List.iter ~f:(fun checker -> let condition, issue_desc_list = checker context an in if CTL.eval_formula condition an context then List.iter ~f:(fun issue_desc -> if CIssue.should_run_check issue_desc.CIssue.mode then let loc = issue_desc.CIssue.loc in fill_issue_desc_info_and_log context an key issue_desc None loc ) issue_desc_list ) checkers (* Calls the set of checkers parsed from files (if any) *) let invoke_set_of_parsed_checkers_an parsed_linters context (an : Ctl_parser_types.ast_node) = let key = match an with | Decl dec -> CAst_utils.generate_key_decl dec | Stmt st -> CAst_utils.generate_key_stmt st in List.iter ~f:(fun (linter : linter) -> if CIssue.should_run_check linter.issue_desc.CIssue.mode && CTL.eval_formula linter.condition an context then let loc = CFrontend_checkers.location_from_an context an in fill_issue_desc_info_and_log context an key linter.issue_desc linter.def_file loc ) parsed_linters (* We decouple the hardcoded checkers from the parsed ones *) let invoke_set_of_checkers_on_node context an = (match an with | Ctl_parser_types.Decl (Clang_ast_t.TranslationUnitDecl _) -> (* Don't run parsed linters on TranslationUnitDecl node. Because depending on the formula it may give an error at line -1 *) () | _ -> invoke_set_of_parsed_checkers_an !parsed_linters context an); invoke_set_of_hard_coded_checkers_an context an