(* * Copyright (c) 2015-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 module CLOpt = CommandLineOption module L = Logging type path_filter = SourceFile.t -> bool type error_filter = IssueType.t -> bool type proc_filter = Typ.Procname.t -> bool type filters = {path_filter: path_filter; error_filter: error_filter; proc_filter: proc_filter} let default_path_filter : path_filter = function _ -> true let default_error_filter : error_filter = function _ -> true let default_proc_filter : proc_filter = function _ -> true let do_not_filter : filters = { path_filter= default_path_filter ; error_filter= default_error_filter ; proc_filter= default_proc_filter } type filter_config = { whitelist: string list ; blacklist: string list ; blacklist_files_containing: string list ; suppress_errors: string list } let is_matching patterns source_file = let path = SourceFile.to_rel_path source_file in List.exists ~f:(fun pattern -> try Int.equal (Str.search_forward pattern path 0) 0 with Caml.Not_found -> false ) patterns (** Check if a proc name is matching the name given as string. *) let match_method language proc_name method_name = (not (BuiltinDecl.is_declared proc_name)) && Language.equal (Typ.Procname.get_language proc_name) language && String.equal (Typ.Procname.get_method proc_name) method_name (* Module to create matcher based on strings present in the source file *) module FileContainsStringMatcher = struct type matcher = SourceFile.t -> bool let default_matcher : matcher = fun _ -> false let file_contains regexp file_in = let rec loop () = try Str.search_forward regexp (In_channel.input_line_exn file_in) 0 >= 0 with | Caml.Not_found -> loop () | End_of_file -> false in loop () let create_matcher s_patterns = if List.is_empty s_patterns then default_matcher else let source_map = ref SourceFile.Map.empty in let regexp = Str.regexp (String.concat ~sep:"\\|" s_patterns) in fun source_file -> try SourceFile.Map.find source_file !source_map with Caml.Not_found -> ( try let file_in = In_channel.create (SourceFile.to_abs_path source_file) in let pattern_found = file_contains regexp file_in in In_channel.close file_in ; source_map := SourceFile.Map.add source_file pattern_found !source_map ; pattern_found with Sys_error _ -> false ) end type method_pattern = {class_name: string; method_name: string option} type pattern = | Method_pattern of Language.t * method_pattern | Source_contains of Language.t * string (* Module to create matcher based on source file names or class names and method names *) module FileOrProcMatcher = struct type matcher = SourceFile.t -> Typ.Procname.t -> bool let default_matcher : matcher = fun _ _ -> false let create_method_matcher m_patterns = if List.is_empty m_patterns then default_matcher else let pattern_map = List.fold ~f:(fun map pattern -> let previous = try String.Map.find_exn map pattern.class_name with Not_found_s _ | Caml.Not_found -> [] in String.Map.set ~key:pattern.class_name ~data:(pattern :: previous) map ) ~init:String.Map.empty m_patterns in let do_java pname_java = let class_name = Typ.Procname.Java.get_class_name pname_java and method_name = Typ.Procname.Java.get_method pname_java in try let class_patterns = String.Map.find_exn pattern_map class_name in List.exists ~f:(fun p -> match p.method_name with None -> true | Some m -> String.equal m method_name ) class_patterns with Not_found_s _ | Caml.Not_found -> false in fun _ proc_name -> match proc_name with Typ.Procname.Java pname_java -> do_java pname_java | _ -> false let create_file_matcher patterns = let s_patterns, m_patterns = let collect (s_patterns, m_patterns) = function | Source_contains (_, s) -> (s :: s_patterns, m_patterns) | Method_pattern (_, mp) -> (s_patterns, mp :: m_patterns) in List.fold ~f:collect ~init:([], []) patterns in let s_matcher = let matcher = FileContainsStringMatcher.create_matcher s_patterns in fun source_file _ -> matcher source_file and m_matcher = create_method_matcher m_patterns in fun source_file proc_name -> m_matcher source_file proc_name || s_matcher source_file proc_name let load_matcher = create_file_matcher let _pp_pattern fmt pattern = let pp_key_value pp_value fmt (key, value) = Format.fprintf fmt " %s: %a,@\n" key (Pp.option pp_value) value in let pp_method_pattern fmt mp = Format.fprintf fmt "%a%a" (pp_key_value Format.pp_print_string) ("class", Some mp.class_name) (pp_key_value Format.pp_print_string) ("method", mp.method_name) and pp_source_contains fmt sc = Format.fprintf fmt " pattern: %s@\n" sc in match pattern with | Method_pattern (language, mp) -> Format.fprintf fmt "Method pattern (%s) {@\n%a}@\n" (Language.to_string language) pp_method_pattern mp | Source_contains (language, sc) -> Format.fprintf fmt "Source contains (%s) {@\n%a}@\n" (Language.to_string language) pp_source_contains sc end (* of module FileOrProcMatcher *) (* Module to create patterns that will match all overriding methods in the pattern *) module OverridesMatcher = struct let load_matcher patterns is_subtype proc_name = let is_matching = function | Method_pattern (language, mp) -> is_subtype mp.class_name && Option.value_map ~f:(match_method language proc_name) ~default:false mp.method_name | _ -> L.(die UserError) "Expecting method pattern" in List.exists ~f:is_matching patterns end let patterns_of_json_with_key (json_key, json) = let default_method_pattern = {class_name= ""; method_name= None} in let default_source_contains = "" in let language_of_string s = match Language.of_string s with | Some Language.Java -> Ok Language.Java | _ -> Error ("JSON key " ^ json_key ^ " not supported for language " ^ s) in let rec detect_language = function | [] -> Error ("No language found for " ^ json_key) | ("language", `String s) :: _ -> language_of_string s | _ :: tl -> detect_language tl in (* Detect the kind of pattern, method pattern or pattern based on the content of the source file. Detecting the kind of patterns in a first step makes it easier to parse the parts of the pattern in a second step *) let detect_pattern assoc = match detect_language assoc with | Ok language -> let is_method_pattern key = List.exists ~f:(String.equal key) ["class"; "method"] and is_source_contains key = List.exists ~f:(String.equal key) ["source_contains"] in let rec loop = function | [] -> Error ("Unknown pattern for " ^ json_key) | (key, _) :: _ when is_method_pattern key -> Ok (Method_pattern (language, default_method_pattern)) | (key, _) :: _ when is_source_contains key -> Ok (Source_contains (language, default_source_contains)) | _ :: tl -> loop tl in loop assoc | Error _ as error -> error in (* Translate a JSON entry into a matching pattern *) let create_pattern (assoc : (string * Yojson.Basic.json) list) = let create_method_pattern assoc = let loop mp = function | key, `String s when String.equal key "class" -> {mp with class_name= s} | key, `String s when String.equal key "method" -> {mp with method_name= Some s} | key, _ when String.equal key "language" -> mp | _ -> L.(die UserError) "Failed to parse %s" (Yojson.Basic.to_string (`Assoc assoc)) in List.fold ~f:loop ~init:default_method_pattern assoc and create_string_contains assoc = let loop sc = function | key, `String pattern when String.equal key "source_contains" -> pattern | key, _ when String.equal key "language" -> sc | _ -> L.(die UserError) "Failed to parse %s" (Yojson.Basic.to_string (`Assoc assoc)) in List.fold ~f:loop ~init:default_source_contains assoc in match detect_pattern assoc with | Ok (Method_pattern (language, _)) -> Ok (Method_pattern (language, create_method_pattern assoc)) | Ok (Source_contains (language, _)) -> Ok (Source_contains (language, create_string_contains assoc)) | Error _ as error -> error in let warn_user msg = CLOpt.warnf "WARNING: error parsing option %s@\n%s@." json_key msg in (* Translate all the JSON entries into matching patterns *) let rec translate accu = function | `Assoc l -> ( match create_pattern l with | Ok pattern -> pattern :: accu | Error msg -> warn_user msg ; accu ) | `List l -> List.fold ~f:translate ~init:accu l | json -> warn_user (Printf.sprintf "expected list or assoc json type, but got value %s" (Yojson.Basic.to_string json)) ; accu in translate [] json let modeled_expensive_matcher = OverridesMatcher.load_matcher (patterns_of_json_with_key Config.patterns_modeled_expensive) let never_return_null_matcher = FileOrProcMatcher.load_matcher (patterns_of_json_with_key Config.patterns_never_returning_null) let skip_translation_matcher = FileOrProcMatcher.load_matcher (patterns_of_json_with_key Config.patterns_skip_translation) let skip_implementation_matcher = FileOrProcMatcher.load_matcher (patterns_of_json_with_key Config.patterns_skip_implementation) let load_filters () = { whitelist= Config.analysis_path_regex_whitelist ; blacklist= Config.analysis_path_regex_blacklist ; blacklist_files_containing= Config.analysis_blacklist_files_containing ; suppress_errors= Config.analysis_suppress_errors } let filters_from_inferconfig inferconfig : filters = let path_filter = let whitelist_filter : path_filter = if List.is_empty inferconfig.whitelist then default_path_filter else is_matching (List.map ~f:Str.regexp inferconfig.whitelist) in let blacklist_filter : path_filter = is_matching (List.map ~f:Str.regexp inferconfig.blacklist) in let blacklist_files_containing_filter : path_filter = FileContainsStringMatcher.create_matcher inferconfig.blacklist_files_containing in function | source_file -> whitelist_filter source_file && (not (blacklist_filter source_file)) && not (blacklist_files_containing_filter source_file) in let error_filter = function | error_name -> let error_str = error_name.IssueType.unique_id in not (List.exists ~f:(String.equal error_str) inferconfig.suppress_errors) in {path_filter; error_filter; proc_filter= default_proc_filter} (* Create filters based on configuration options *) let create_filters () = if not Config.filter_paths then do_not_filter else filters_from_inferconfig (load_filters ()) (** This function loads and list the path that are being filtered by the analyzer. The results are of the form: path/to/file.java -> true/false meaning that analysis results will be reported on path/to/file.java or not *) let test () = let filters = create_filters () in let matches path = filters.path_filter path in Sys.getcwd () |> Utils.directory_iter (fun path -> if DB.is_source_file path then let source_file = SourceFile.from_abs_path path in let matching = matches source_file in L.result "%s -> %b@." (SourceFile.to_rel_path source_file) matching )