You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

520 lines
19 KiB

(*
* 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 F = Format
module L = Logging
module MF = MarkupFormatter
type linter =
{ condition: CTL.t
; issue_desc: CIssue.issue_desc
; def_file: string option
; whitelist_paths: ALVar.t list
; blacklist_paths: ALVar.t list }
(* If in linter developer mode and if current linter was passed, filter it out *)
let filter_parsed_linters_developer parsed_linters =
if List.length parsed_linters > 1 && Config.linters_developer_mode then
match Config.linter with
| None ->
L.(die UserError)
"In linters developer mode you should debug only one linter at a time. This is \
important for debugging the rule. Pass the flag --linter <name> to specify the linter \
you want to debug."
| Some lint ->
List.filter ~f:(fun (rule: linter) -> String.equal rule.issue_desc.id lint) parsed_linters
else parsed_linters
let filter_parsed_linters_by_path parsed_linters source_file =
let filter_parsed_linter_by_path linter =
let should_lint paths =
List.exists
~f:(fun path -> ALVar.compare_str_with_alexp (SourceFile.to_rel_path source_file) path)
paths
in
let whitelist_ok =
List.is_empty linter.whitelist_paths || should_lint linter.whitelist_paths
in
let blacklist_ok =
List.is_empty linter.blacklist_paths || not (should_lint linter.blacklist_paths)
in
whitelist_ok && blacklist_ok
in
List.filter ~f:filter_parsed_linter_by_path parsed_linters
let filter_parsed_linters parsed_linters source_file =
let linters = filter_parsed_linters_developer parsed_linters in
if Config.debug_mode || not Config.filtering then linters
(* do not filter by path if in debug or no filtering mode *)
else filter_parsed_linters_by_path linters source_file
let pp_linters fmt linters =
let pp_linter fmt {issue_desc= {id}} = F.fprintf fmt "%s@\n" id in
List.iter ~f:(pp_linter fmt) linters
(* 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 *)
type macros_map = (bool * ALVar.t list * CTL.t) ALVar.FormulaIdMap.t
(* Map a path name to a list of paths. *)
type paths_map = ALVar.t list ALVar.VarMap.t
let single_to_multi checker ctx an =
let issue_desc_opt = checker ctx an in
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 ]
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 context ph an =
match ph with
| "%ivar_name%" ->
MF.monospaced_to_string (CFrontend_checkers.ivar_name an)
| "%decl_name%" ->
MF.monospaced_to_string (Ctl_parser_types.ast_node_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)
| "%receiver_method_call%" ->
MF.monospaced_to_string (CFrontend_checkers.receiver_method_call an)
| "%iphoneos_target_sdk_version%" ->
MF.monospaced_to_string (CFrontend_checkers.iphoneos_target_sdk_version context an)
| "%available_ios_sdk%" ->
MF.monospaced_to_string (CFrontend_checkers.available_ios_sdk an)
| "%class_available_ios_sdk%" ->
MF.monospaced_to_string (CFrontend_checkers.class_available_ios_sdk an)
| "%type%" ->
MF.monospaced_to_string (Ctl_parser_types.ast_node_type an)
| "%class_name%" ->
CFrontend_checkers.class_name an
| "%child_type%" ->
MF.monospaced_to_string (Ctl_parser_types.stmt_node_child_type an)
| "%name%" ->
MF.monospaced_to_string (Ctl_parser_types.ast_node_name an)
| _ ->
L.die InternalError "helper function %s is unknown" ph
(* 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 context 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 context ms an in
L.(debug Linters Medium) "@\nMatched string '%s'@\n" ms ;
let re_ms = Str.regexp_string ms in
let message' = Str.replace_first re_ms res message in
L.(debug Linters Medium) "Replacing %s in message: @\n %s @\n" ms message ;
L.(debug Linters Medium) "Resulting message: @\n %s @\n" message' ;
expand_message_string context message' an
with Not_found -> message
let remove_new_lines_and_whitespace message =
let words = List.map ~f:String.strip (String.split ~on:'\n' message) in
String.concat words ~sep:" "
let string_to_err_kind = function
| "WARNING" ->
Exceptions.Kwarning
| "ERROR" ->
Exceptions.Kerror
| "INFO" ->
Exceptions.Kinfo
| "ADVICE" ->
Exceptions.Kadvice
| "LIKE" ->
Exceptions.Klike
| s ->
L.die InternalError "Severity %s does not exist" s
let string_to_issue_mode m =
match m with
| "ON" ->
CIssue.On
| "OFF" ->
CIssue.Off
| s ->
L.die InternalError "Mode %s does not exist. Please specify ON/OFF" s
let post_process_linter_definition (linter: linter) =
match
List.find Config.linters_doc_url ~f:(fun (linter_doc_url: Config.linter_doc_url) ->
String.equal linter.issue_desc.id linter_doc_url.linter )
with
| Some linter_doc_url ->
let issue_desc = {linter.issue_desc with doc_url= Some linter_doc_url.doc_url} in
{linter with issue_desc}
| None ->
linter
(** 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
L.(debug Linters Medium) "@\nConverting checkers in (condition, issue) pairs@\n" ;
let do_one_checker checker : linter =
let dummy_issue =
{ id= checker.id
; name= None
; description= ""
; suggestion= None
; loc= Location.dummy
; severity= Exceptions.Kwarning
; doc_url= None
; mode= CIssue.On }
in
let issue_desc, condition, whitelist_paths, blacklist_paths =
let process_linter_definitions (issue, cond, wl_paths, bl_paths) description =
match description with
| CSet (av, phi) when ALVar.is_report_when_keyword av ->
(issue, phi, wl_paths, bl_paths)
| CDesc (av, msg) when ALVar.is_message_keyword av ->
({issue with description= msg}, cond, wl_paths, bl_paths)
| CDesc (av, sugg) when ALVar.is_suggestion_keyword av ->
({issue with suggestion= Some sugg}, cond, wl_paths, bl_paths)
| CDesc (av, sev) when ALVar.is_severity_keyword av ->
({issue with severity= string_to_err_kind sev}, cond, wl_paths, bl_paths)
| CDesc (av, m) when ALVar.is_mode_keyword av ->
({issue with mode= string_to_issue_mode m}, cond, wl_paths, bl_paths)
| CDesc (av, doc) when ALVar.is_doc_url_keyword av ->
({issue with doc_url= Some doc}, cond, wl_paths, bl_paths)
| CDesc (av, name) when ALVar.is_name_keyword av ->
({issue with name= Some name}, cond, wl_paths, bl_paths)
| CPath (`WhitelistPath, paths) ->
(issue, cond, paths, bl_paths)
| CPath (`BlacklistPath, paths) ->
(issue, cond, wl_paths, paths)
| _ ->
(issue, cond, wl_paths, bl_paths)
in
List.fold ~f:process_linter_definitions ~init:(dummy_issue, CTL.False, [], [])
checker.definitions
in
L.(debug Linters Medium) "@\nMaking condition and issue desc for checker '%s'@\n" checker.id ;
L.(debug Linters Medium) "@\nCondition =@\n %a@\n" CTL.Debug.pp_formula condition ;
L.(debug Linters Medium) "@\nIssue_desc = %a@\n" CIssue.pp_issue issue_desc ;
let linter =
{condition; issue_desc; def_file= Some linters_def_file; whitelist_paths; blacklist_paths}
in
post_process_linter_definition linter
in
List.map ~f:do_one_checker checkers
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 (trans, f1, f2) ->
AU (trans, 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 (trans, f1) ->
AF (trans, apply_substitution f1 sub)
| AG (trans, f1) ->
AG (trans, apply_substitution f1 sub)
| EX (trans, f1) ->
EX (trans, apply_substitution f1 sub)
| AX (trans, f1) ->
AX (trans, 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)
let expand_formula phi map_ error_msg_ =
let fail_with_circular_macro_definition name error_msg =
L.(die ExternalError) "Macro '%s' has a circular definition.@\n Cycle:@\n%s" name error_msg
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 ->
L.(die ExternalError)
"Formula identifier '%s' is not called with the right number of parameters" name
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 (trans, f1, f2) ->
AU (trans, 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 (trans, f1) ->
AF (trans, expand f1 map error_msg)
| AG (trans, f1) ->
AG (trans, expand f1 map error_msg)
| EX (trans, f1) ->
EX (trans, expand f1 map error_msg)
| AX (trans, f1) ->
AX (trans, 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)
in
expand phi map_ error_msg_
let rec expand_path paths path_map =
match paths with
| [] ->
[]
| (ALVar.Var path_var) :: rest -> (
try
let paths = ALVar.VarMap.find path_var path_map in
List.append paths (expand_path rest path_map)
with Not_found -> L.(die ExternalError) "Path variable %s not found. " path_var )
| path :: rest ->
path :: expand_path rest path_map
let build_macros_map_ macros init_map =
let macros_map =
List.fold
~f:(fun map' data ->
match data with
| CTL.CLet (key, params, formula) ->
if ALVar.FormulaIdMap.mem key map' then
L.(die ExternalError)
"Macro '%s' has more than one definition." (ALVar.formula_id_to_string key)
else ALVar.FormulaIdMap.add key (false, params, formula) map'
| _ ->
map' )
~init:init_map macros
in
macros_map
let build_macros_map macros =
let init_map : macros_map = ALVar.FormulaIdMap.empty in
build_macros_map_ macros init_map
let build_paths_map paths =
let build_paths_map_aux paths init_map =
let paths_map =
List.fold
~f:(fun map' data ->
match data with path_name, paths ->
if ALVar.VarMap.mem path_name map' then
L.(die ExternalError) "Path '%s' has more than one definition." path_name
else ALVar.VarMap.add path_name paths map' )
~init:init_map paths
in
paths_map
in
build_paths_map_aux paths ALVar.VarMap.empty
(* expands use of let defined formula id in checkers with their definition *)
let expand_checkers macro_map path_map checkers =
let open CTL in
let expand_one_checker c =
L.(debug Linters Medium) " +Start expanding %s@\n" c.id ;
let map = build_macros_map_ c.definitions macro_map in
let exp_defs =
List.fold
~f:(fun defs clause ->
match clause with
| CSet (report_when_const, phi) ->
L.(debug Linters Medium) " -Expanding report_when@\n" ;
CSet (report_when_const, expand_formula phi map "") :: defs
| CPath (black_or_white_list, paths) ->
L.(debug Linters Medium) " -Expanding path@\n" ;
CPath (black_or_white_list, expand_path paths path_map) :: defs
| cl ->
cl :: defs )
~init:[] c.definitions
in
{c with definitions= exp_defs}
in
List.map ~f:expand_one_checker checkers
(** 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 (node: Ctl_parser_types.ast_node)
(issue_desc: CIssue.issue_desc) linters_def_file =
let procname =
match method_decl_opt with
| Some method_decl ->
CProcname.from_decl_for_linters translation_unit_context method_decl
| None ->
Typ.Procname.Linters_dummy_method
in
let errlog = LintIssues.get_err_log procname in
let err_desc =
Errdesc.explain_frontend_warning issue_desc.description issue_desc.suggestion issue_desc.loc
in
let exn = Exceptions.Frontend_warning ((issue_desc.id, issue_desc.name), err_desc, __POS__) in
let trace = [Errlog.make_trace_element 0 issue_desc.loc "" []] in
let err_kind = issue_desc.severity in
let key_str =
match node with
| Decl dec ->
CAst_utils.generate_key_decl dec
| Stmt st ->
CAst_utils.generate_key_stmt st
in
let key = Utils.better_hash key_str in
Reporting.log_issue_from_errlog procname err_kind errlog exn ~loc:issue_desc.loc ~ltr:trace
~node_id:(0, key) ?linters_def_file ?doc_url:issue_desc.doc_url
let fill_issue_desc_info_and_log context an (issue_desc: CIssue.issue_desc) linters_def_file loc =
let process_message message =
remove_new_lines_and_whitespace (expand_message_string context message an)
in
let description = process_message issue_desc.description in
let suggestion = Option.map ~f:process_message issue_desc.suggestion in
let issue_desc' = {issue_desc with description; loc; suggestion} in
try
log_frontend_issue context.CLintersContext.translation_unit_context
context.CLintersContext.current_method an issue_desc' linters_def_file
with CFrontend_config.IncorrectAssumption e ->
let trans_unit_ctx = context.CLintersContext.translation_unit_context in
ClangLogging.log_caught_exception trans_unit_ctx "IncorrectAssumption" e.position
e.source_range e.ast_node
(* 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 = match an with Decl _ -> decl_checkers_list | Stmt _ -> stmt_checkers_list in
List.iter
~f:(fun checker ->
let issue_desc_list = checker context an in
List.iter
~f:(fun issue_desc ->
if CIssue.should_run_check issue_desc.CIssue.mode then
fill_issue_desc_info_and_log context an issue_desc None issue_desc.CIssue.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) =
List.iter
~f:(fun (linter: linter) ->
if CIssue.should_run_check linter.issue_desc.CIssue.mode then
match CTL.eval_formula linter.condition an context with
| None ->
()
| Some witness ->
let loc = CFrontend_checkers.location_from_an context witness in
fill_issue_desc_info_and_log context witness 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 *)
()
| _ ->
if not CFrontend_config.tableaux_evaluation then
invoke_set_of_parsed_checkers_an !parsed_linters context an ) ;
if Config.default_linters then invoke_set_of_hard_coded_checkers_an context an