From cc4fcd6837b33357714f2ef5dc319730bba46449 Mon Sep 17 00:00:00 2001 From: jrm Date: Sat, 12 Mar 2016 07:41:07 -0800 Subject: [PATCH] Add support for modeling @Expensive methods using .inferconfig Summary:public Use the configuration file .inferconfig to model the library method that are considered expensive Reviewed By: cristianoc Differential Revision: D3045288 fb-gh-sync-id: e58d85c shipit-source-id: e58d85c --- infer/lib/python/inferlib/analyze.py | 2 + infer/src/backend/config.ml | 7 + infer/src/backend/inferconfig.ml | 247 +++++++++++-------- infer/src/backend/inferconfig.mli | 9 +- infer/src/backend/inferprint.ml | 2 +- infer/src/backend/utils.ml | 12 + infer/src/checkers/performanceCritical.ml | 45 ++-- infer/src/harness/androidFramework.mli | 2 + infer/src/java/jAnnotation.ml | 38 +-- infer/src/java/jMain.ml | 2 +- infer/tests/codetoanalyze/java/.inferconfig | 12 + infer/tests/codetoanalyze/java/checkers/BUCK | 4 +- 12 files changed, 221 insertions(+), 161 deletions(-) diff --git a/infer/lib/python/inferlib/analyze.py b/infer/lib/python/inferlib/analyze.py index 37d45b918..c790bc74d 100644 --- a/infer/lib/python/inferlib/analyze.py +++ b/infer/lib/python/inferlib/analyze.py @@ -268,6 +268,8 @@ class AnalyzerWrapper(object): # to be reported infer_options += ['-allow_specs_cleanup'] + infer_options += ['-inferconfig_home', os.getcwd()] + if self.args.analyzer == config.ANALYZER_ERADICATE: infer_options += ['-eradicate'] elif self.args.analyzer == config.ANALYZER_CHECKERS: diff --git a/infer/src/backend/config.ml b/infer/src/backend/config.ml index 0696c4bf2..7d7bf44e9 100644 --- a/infer/src/backend/config.ml +++ b/infer/src/backend/config.ml @@ -46,6 +46,13 @@ let proc_stats_filename = "proc_stats.json" let global_tenv_filename = "global.tenv" +(** Name of the infer configuration file *) +let inferconfig_file = ".inferconfig" + +let inferconfig_home : string option ref = ref None + +let suppress_warnings_annotations : string option ref = ref None + (** List of paths to the directories containing specs for library functions. *) let specs_library = ref [] diff --git a/infer/src/backend/inferconfig.ml b/infer/src/backend/inferconfig.ml index bbef92071..6aba8ed27 100644 --- a/infer/src/backend/inferconfig.ml +++ b/infer/src/backend/inferconfig.ml @@ -7,12 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. *) -(** Name of the infer configuration file *) -let inferconfig_file = ".inferconfig" - -let inferconfig_home = ref None - -let suppress_warnings_annotations = ref None +module L = Logging (** Look up a key in a json file containing a list of strings *) let lookup_string_list key json = @@ -60,58 +55,8 @@ let is_matching patterns = with Not_found -> false) patterns -module FileContainsStringMatcher = struct - type matcher = DB.source_file -> bool - - let default_matcher : matcher = fun _ -> false - - let file_contains regexp file_in = - let rec loop () = - try - (Str.search_forward regexp (input_line file_in) 0) >= 0 - with - | Not_found -> loop () - | End_of_file -> false in - loop () - - let create_matcher s_patterns = - if s_patterns = [] then - default_matcher - else - let source_map = ref DB.SourceFileMap.empty in - let regexp = - Str.regexp (join_strings "\\|" s_patterns) in - fun source_file -> - try - DB.SourceFileMap.find source_file !source_map - with Not_found -> - try - let file_in = open_in (DB.source_file_to_string source_file) in - let pattern_found = file_contains regexp file_in in - close_in file_in; - source_map := DB.SourceFileMap.add source_file pattern_found !source_map; - pattern_found - with Sys_error _ -> false -end - -module type MATCHABLE_JSON = sig - val json_key : string -end - -module type Matcher = sig - type matcher = DB.source_file -> Procname.t -> bool - val load_matcher : string -> matcher -end - -module FileOrProcMatcher = functor (M : MATCHABLE_JSON) -> -struct - - type matcher = DB.source_file -> Procname.t -> bool - - let default_matcher : matcher = - fun _ _ -> false - type method_pattern = { +type method_pattern = { class_name : string; method_name : string option; parameters : (string list) option @@ -129,27 +74,30 @@ struct | Method_pattern of Config.language * method_pattern | Source_contains of Config.language * string - let language_of_string = function - | "Java" -> Config.Java - | l -> failwith ("Inferconfig JSON key " ^ M.json_key ^ " not supported for language " ^ l) - - let detect_language assoc = - let rec loop = function - | [] -> - failwith - ("No language found for " ^ M.json_key ^ " in " ^ inferconfig_file) - | (key, `String s) :: _ when key = "language" -> - language_of_string s +let language_of_string json_key = function + | "Java" -> Config.Java + | l -> failwith ("Inferconfig JSON key " ^ json_key ^ " not supported for language " ^ l) + +let detect_language json_key assoc = + let rec loop = function + | [] -> + failwith + ("No language found for " ^ json_key ^ " in " ^ Config.inferconfig_file) + | (key, `String s) :: _ when key = "language" -> + language_of_string json_key s | _:: tl -> loop tl in loop assoc - let detect_pattern assoc = - let language = detect_language assoc in - let is_method_pattern key = IList.exists (string_equal key) ["class"; "method"] - and is_source_contains key = IList.exists (string_equal key) ["source_contains"] in - let rec loop = function - | [] -> - failwith ("Unknown pattern for " ^ M.json_key ^ " in " ^ inferconfig_file) +(* 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 json_key assoc = + let language = detect_language json_key assoc in + let is_method_pattern key = IList.exists (string_equal key) ["class"; "method"] + and is_source_contains key = IList.exists (string_equal key) ["source_contains"] in + let rec loop = function + | [] -> + failwith ("Unknown pattern for " ^ json_key ^ " in " ^ Config.inferconfig_file) | (key, _) :: _ when is_method_pattern key -> Method_pattern (language, default_method_pattern) | (key, _) :: _ when is_source_contains key -> @@ -157,7 +105,8 @@ struct | _:: tl -> loop tl in loop assoc - let create_pattern (assoc : (string * Yojson.Basic.json) list) = +(* Translate a JSON entry into a matching pattern *) +let create_pattern json_key (assoc : (string * Yojson.Basic.json) list) = let collect_params l = let collect accu = function | `String s -> s:: accu @@ -176,22 +125,96 @@ struct IList.fold_left loop default_method_pattern assoc and create_string_contains assoc = let loop sc = function - | (key, `String pattern) when key = "source_contains" -> pattern - | (key, _) when key = "language" -> sc - | _ -> failwith ("Fails to parse " ^ Yojson.Basic.to_string (`Assoc assoc)) in - IList.fold_left loop default_source_contains assoc in - match detect_pattern assoc with + | (key, `String pattern) when key = "source_contains" -> pattern + | (key, _) when key = "language" -> sc + | _ -> failwith ("Fails to parse " ^ Yojson.Basic.to_string (`Assoc assoc)) in + IList.fold_left loop default_source_contains assoc in + match detect_pattern json_key assoc with | Method_pattern (language, _) -> Method_pattern (language, create_method_pattern assoc) | Source_contains (language, _) -> Source_contains (language, create_string_contains assoc) - let rec translate accu (json : Yojson.Basic.json) : pattern list = - match json with - | `Assoc l -> (create_pattern l):: accu - | `List l -> IList.fold_left translate accu l +(* Translate all the JSON entries into matching patterns *) +let rec translate json_key accu (json : Yojson.Basic.json) : pattern list = + match json with + | `Assoc l -> (create_pattern json_key l):: accu + | `List l -> IList.fold_left (translate json_key) accu l | _ -> assert false +(* Creates a list of matching patterns for the given inferconfig file *) +let load_patterns json_key inferconfig = + let found = + Yojson.Basic.Util.filter_member + json_key + [Yojson.Basic.from_file inferconfig] in + IList.fold_left (translate json_key) [] found + + +(* Check if a proc name is matching the name given as string *) +let match_method language proc_name method_name = + not (SymExec.function_is_builtin proc_name) && + match language with + | Config.Java -> + Procname.java_get_method proc_name = method_name + | Config.C_CPP -> + Procname.c_get_method proc_name = method_name + + +(* Module to create matcher based on strings present in the source file *) +module FileContainsStringMatcher = struct + type matcher = DB.source_file -> bool + + let default_matcher : matcher = fun _ -> false + + let file_contains regexp file_in = + let rec loop () = + try + (Str.search_forward regexp (input_line file_in) 0) >= 0 + with + | Not_found -> loop () + | End_of_file -> false in + loop () + + let create_matcher s_patterns = + if s_patterns = [] then + default_matcher + else + let source_map = ref DB.SourceFileMap.empty in + let regexp = + Str.regexp (join_strings "\\|" s_patterns) in + fun source_file -> + try + DB.SourceFileMap.find source_file !source_map + with Not_found -> + try + let file_in = open_in (DB.source_file_to_string source_file) in + let pattern_found = file_contains regexp file_in in + close_in file_in; + source_map := DB.SourceFileMap.add source_file pattern_found !source_map; + pattern_found + with Sys_error _ -> false +end + +module type MATCHABLE_JSON = sig + val json_key : string +end + +module type Matcher = sig + type matcher = DB.source_file -> Procname.t -> bool + val load_matcher : string -> matcher +end + +(* Module to create matcher based on source file names or class names and method names *) +module FileOrProcMatcher = functor (M : MATCHABLE_JSON) -> +struct + + type matcher = DB.source_file -> Procname.t -> bool + + let default_matcher : matcher = + fun _ _ -> false + + let create_method_matcher m_patterns = if m_patterns = [] then default_matcher @@ -234,20 +257,12 @@ struct let load_matcher inferconfig = if Sys.file_exists inferconfig then - try - let patterns = - let found = - Yojson.Basic.Util.filter_member - M.json_key - [Yojson.Basic.from_file inferconfig] in - IList.fold_left translate [] found in - create_file_matcher patterns - with Sys_error _ -> - default_matcher - else default_matcher + create_file_matcher (load_patterns M.json_key inferconfig) + else + default_matcher -(* - let pp_pattern fmt pattern = + + let _pp_pattern fmt pattern = let pp_string fmt s = Format.fprintf fmt "%s" s in let pp_option pp_value fmt = function @@ -272,9 +287,30 @@ struct | Source_contains (language, sc) -> Format.fprintf fmt "Source contains (%s) {\n%a}\n" (Config.string_of_language language) pp_source_contains sc -*) + end (* of module FileOrProcMatcher *) +(* Module to create patterns that will match all overriding methods in the pattern *) +module OverridesMatcher = functor (M : MATCHABLE_JSON) -> +struct + + type matcher = (string -> bool) -> Procname.t -> bool + + let default_matcher _ _ = false + + let load_matcher inferconfig = + if Sys.file_exists inferconfig then + fun is_subtype proc_name -> + let is_matching = function + | Method_pattern (language, mp) -> + is_subtype mp.class_name + && Option.map_default (match_method language proc_name) false mp.method_name + | _ -> failwith "Expecting method pattern" in + IList.exists is_matching (load_patterns M.json_key inferconfig) + else + default_matcher + +end module NeverReturnNull = FileOrProcMatcher(struct let json_key = "never_returning_null" @@ -288,10 +324,15 @@ module SkipTranslationMatcher = FileOrProcMatcher(struct let json_key = "skip_translation" end) +module ModeledExpensiveMatcher = OverridesMatcher(struct + let json_key = "modeled_expensive" + end) + -let inferconfig () = match !inferconfig_home with - | Some dir -> Filename.concat dir inferconfig_file - | None -> inferconfig_file +let inferconfig () = + match !Config.inferconfig_home with + | Some dir -> Filename.concat dir Config.inferconfig_file + | None -> Config.inferconfig_file let load_filters analyzer = let inferconfig_file = inferconfig () in @@ -364,7 +405,7 @@ let test () = let matching_s = join_strings ", " (IList.map string_of_analyzer matching) in - Logging.stderr "%s -> {%s}@." + L.stderr "%s -> {%s}@." (DB.source_file_to_rel_path source_file) matching_s) (Sys.getcwd ()) diff --git a/infer/src/backend/inferconfig.mli b/infer/src/backend/inferconfig.mli index cdd787336..f5a1ad3b9 100644 --- a/infer/src/backend/inferconfig.mli +++ b/infer/src/backend/inferconfig.mli @@ -7,10 +7,6 @@ * of patent rights can be found in the PATENTS file in the same directory. *) -val inferconfig_home : string option ref - -val suppress_warnings_annotations : string option ref - (** get the path to the .inferconfig file *) val inferconfig : unit -> string @@ -47,5 +43,10 @@ module SkipTranslationMatcher : Matcher module SuppressWarningsMatcher : Matcher +module ModeledExpensiveMatcher : sig + type matcher = (string -> bool) -> Procname.t -> bool + val load_matcher : string -> matcher +end + (** Load the config file and list the files to report on *) val test: unit -> unit diff --git a/infer/src/backend/inferprint.ml b/infer/src/backend/inferprint.ml index aa3c3d194..9c933f6de 100644 --- a/infer/src/backend/inferprint.ml +++ b/infer/src/backend/inferprint.ml @@ -176,7 +176,7 @@ let arg_desc = "setup the analyzer for the path filtering" ; "-inferconfig_home", - Arg.String (fun s -> Inferconfig.inferconfig_home := Some s), + Arg.String (fun s -> Config.inferconfig_home := Some s), Some "dir", "Path to the .inferconfig file" ; diff --git a/infer/src/backend/utils.ml b/infer/src/backend/utils.ml index a36b88aeb..fd9600b4b 100644 --- a/infer/src/backend/utils.ml +++ b/infer/src/backend/utils.ml @@ -667,35 +667,47 @@ let base_arg_desc = Arg.String (fun s -> Config.results_dir := s), Some "dir", "set the project results directory (default dir=" ^ Config.default_results_dir ^ ")"; + "-coverage", Arg.Unit (fun () -> Config.worklist_mode:= 2), None, "analysis mode to maximize coverage (can take longer)"; + "-lib", Arg.String (fun s -> Config.specs_library := filename_to_absolute s :: !Config.specs_library), Some "dir", "add dir to the list of directories to be searched for spec files"; + "-specs-dir-list-file", Arg.String (fun s -> Config.specs_library := (read_specs_dir_list_file s) @ !Config.specs_library), Some "file", "add the newline-separated directories listed in to the list of directories to \ be searched for spec files"; + "-models", Arg.String (fun s -> Config.add_models (filename_to_absolute s)), Some "zip file", "add a zip file containing the models"; + "-ziplib", Arg.String (fun s -> Config.add_zip_library (filename_to_absolute s)), Some "zip file", "add a zip file containing library spec files"; + "-project_root", Arg.String (fun s -> Config.project_root := Some (filename_to_absolute s)), Some "dir", "root directory of the project"; + "-infer_cache", Arg.String (fun s -> Config.JarCache.infer_cache := Some (filename_to_absolute s)), Some "dir", "Select a directory to contain the infer cache"; + + "-inferconfig_home", + Arg.String (fun s -> Config.inferconfig_home := Some s), + Some "dir", + "Path to the .inferconfig file"; ] let reserved_arg_desc = diff --git a/infer/src/checkers/performanceCritical.ml b/infer/src/checkers/performanceCritical.ml index 5c562a4fe..01d6140d5 100644 --- a/infer/src/checkers/performanceCritical.ml +++ b/infer/src/checkers/performanceCritical.ml @@ -29,6 +29,17 @@ let expensive_overrides_unexpensive = "CHECKERS_EXPENSIVE_OVERRIDES_UNANNOTATED" +let is_modeled_expensive = + let matcher = + lazy (let config_file = Inferconfig.inferconfig () in + Inferconfig.ModeledExpensiveMatcher.load_matcher config_file) in + fun tenv proc_name -> + not (SymExec.function_is_builtin proc_name) && + let classname = + Typename.Java.from_string (Procname.java_get_class proc_name) in + (Lazy.force matcher) (AndroidFramework.is_subclass tenv classname) proc_name + + let check_attributes check attributes = let annotated_signature = Annotations.get_annotated_signature attributes in let ret_annotation, _ = annotated_signature.Annotations.ret in @@ -75,24 +86,6 @@ let method_overrides_no_allocation tenv pname = method_overrides method_is_no_allcation tenv pname -let is_modeled_expensive tenv pname = - if SymExec.function_is_builtin pname then false - else if Procname.java_get_method pname <> "findViewById" then false - else - let package = - match Procname.java_get_package pname with - | None -> "" - | Some p -> p in - let classname = - Mangled.from_package_class package (Procname.java_get_simple_class pname) in - match Sil.tenv_lookup tenv (Typename.TN_csu (Csu.Class Csu.Java, classname)) with - | None -> false - | Some struct_typ -> - let typ = Sil.Tstruct struct_typ in - AndroidFramework.is_view typ tenv - || AndroidFramework.is_activity typ tenv - - let method_is_expensive tenv pname = is_modeled_expensive tenv pname || check_method Annotations.ia_is_expensive pname @@ -239,9 +232,9 @@ let report_allocation_stack pname loc trace stack_str constructor_pname call_loc Reporting.log_error pname ~loc: (Some loc) ~ltr: (Some final_trace) exn -let report_call_stack end_of_stack lookup_next_calls report tenv pname pdesc loc calls = +let report_call_stack end_of_stack lookup_next_calls report pname pdesc loc calls = let rec loop visited_pnames (trace, stack_str) (callee_pname, callee_loc) = - if end_of_stack tenv callee_pname then + if end_of_stack callee_pname then report pname loc trace stack_str callee_pname callee_loc else let next_calls = lookup_next_calls callee_pname in @@ -261,14 +254,14 @@ let report_call_stack end_of_stack lookup_next_calls report tenv pname pdesc loc let report_expensive_calls tenv pname pdesc loc calls = report_call_stack - method_is_expensive lookup_expensive_calls report_expensive_call_stack - tenv pname pdesc loc calls + (method_is_expensive tenv) lookup_expensive_calls + report_expensive_call_stack pname pdesc loc calls -let report_allocations tenv pname pdesc loc calls = +let report_allocations pname pdesc loc calls = report_call_stack - (fun _ p -> Procname.is_constructor p) lookup_allocations report_allocation_stack - tenv pname pdesc loc calls + Procname.is_constructor lookup_allocations + report_allocation_stack pname pdesc loc calls let check_one_procedure tenv pname pdesc = @@ -311,7 +304,7 @@ let check_one_procedure tenv pname pdesc = if performance_critical then report_expensive_calls tenv pname pdesc loc call_summary.Specs.expensive_calls; if no_allocation then - report_allocations tenv pname pdesc loc call_summary.Specs.allocations + report_allocations pname pdesc loc call_summary.Specs.allocations let callback_performance_checker diff --git a/infer/src/harness/androidFramework.mli b/infer/src/harness/androidFramework.mli index 8bf78cce0..be5f9ab32 100644 --- a/infer/src/harness/androidFramework.mli +++ b/infer/src/harness/androidFramework.mli @@ -52,6 +52,8 @@ val is_android_lib_class : Typename.t -> bool (** Path to the android.jar file containing real code, not just the method stubs as in the SDK *) val non_stub_android_jar : unit -> string +val is_subclass : Sil.tenv -> Typename.t -> string -> bool + (** [is_exception tenv class_name] checks if class_name is of type java.lang.Exception *) val is_exception : Sil.tenv -> Typename.t -> bool diff --git a/infer/src/java/jAnnotation.ml b/infer/src/java/jAnnotation.ml index e17edc3fe..4b32888e4 100644 --- a/infer/src/java/jAnnotation.ml +++ b/infer/src/java/jAnnotation.ml @@ -11,32 +11,20 @@ open Javalib_pack -let suppress_warnings_lookup = ref None - - -let load_suppress_warnings_lookup () = - let default_matcher = fun _ -> false in - let matcher = - match !Inferconfig.suppress_warnings_annotations with - | Some f -> - (try - let m = Inferconfig.SuppressWarningsMatcher.load_matcher f in - (m DB.source_file_empty) - with Yojson.Json_error _ -> - default_matcher) - | None -> failwith "Local config expected!" in - suppress_warnings_lookup := Some matcher - - -let is_suppress_warnings_annotated proc_name = +let is_suppress_warnings_annotated = let matcher = - let () = - match !suppress_warnings_lookup with - | None -> - load_suppress_warnings_lookup () - | Some _ -> () in - Option.get !suppress_warnings_lookup in - matcher proc_name + lazy + (let default_matcher = fun _ -> false in + match !Config.suppress_warnings_annotations with + | Some f -> + (try + let m = Inferconfig.SuppressWarningsMatcher.load_matcher f in + (m DB.source_file_empty) + with Yojson.Json_error _ -> + default_matcher) + | None -> failwith "Local config expected!") in + fun proc_name -> + (Lazy.force matcher) proc_name let suppress_warnings = diff --git a/infer/src/java/jMain.ml b/infer/src/java/jMain.ml index f710a41ed..24257a08d 100644 --- a/infer/src/java/jMain.ml +++ b/infer/src/java/jMain.ml @@ -63,7 +63,7 @@ let arg_desc = "Set the path to the javac verbose output" ; "-suppress_warnings_out", - Arg.String (fun s -> Inferconfig.suppress_warnings_annotations := Some s), + Arg.String (fun s -> Config.suppress_warnings_annotations := Some s), Some "Path", "Path to list of collected @SuppressWarnings annotations" ; diff --git a/infer/tests/codetoanalyze/java/.inferconfig b/infer/tests/codetoanalyze/java/.inferconfig index 04d35658b..3fbcbfd4d 100644 --- a/infer/tests/codetoanalyze/java/.inferconfig +++ b/infer/tests/codetoanalyze/java/.inferconfig @@ -18,5 +18,17 @@ "language": "Java", "source_contains": "_SHOULD_BE_SKIPPED_" } + ], + "modeled_expensive": [ + { + "language": "Java", + "class": "android.app.Activity", + "method": "findViewById" + }, + { + "language": "Java", + "class": "android.view.View", + "method": "findViewById" + } ] } diff --git a/infer/tests/codetoanalyze/java/checkers/BUCK b/infer/tests/codetoanalyze/java/checkers/BUCK index dc85ce926..b95f781e9 100644 --- a/infer/tests/codetoanalyze/java/checkers/BUCK +++ b/infer/tests/codetoanalyze/java/checkers/BUCK @@ -16,6 +16,8 @@ java_library( ) out = 'out' +inferconfig_file = '$(location //infer/tests/codetoanalyze/java:inferconfig)' +copy_inferconfig = ' '.join(['cp', inferconfig_file, '$SRCDIR']) clean_cmd = ' '.join(['rm', '-rf', out]) classpath = ':'.join([('$(classpath ' + path + ')') for path in dependencies]) infer_cmd = ' '.join([ @@ -30,7 +32,7 @@ infer_cmd = ' '.join([ '$SRCS', ]) copy_cmd = ' '.join(['cp', out + '/report.csv', '$OUT']) -command = ' && '.join([clean_cmd, infer_cmd, copy_cmd]) +command = ' && '.join([clean_cmd, copy_inferconfig, infer_cmd, copy_cmd]) genrule( name = 'analyze',