From f8a65e698c962b2c5c5f8f96c2c04bc8a22010a4 Mon Sep 17 00:00:00 2001 From: Martino Luca Date: Tue, 7 Mar 2017 03:33:54 -0800 Subject: [PATCH] Add differential and filtering functionalities to Infer, to compare two analyses Summary: Given two analysis results, it's now possible to compare them with the following command: infer --diff --report-current reportA.json --report-previous reportB.json --file-renamings file_renamings.json this command will then generate 3 files in `infer-out/differential/{introduced, fixed, preexisting}.json`, whose meaning is the following: - `introduced.json` has all issues in `current` that are not in `previous` - `fixed.json` has all issues in `previous` that are not in `current` - `preexisting.json` has all issues that are in both `current` and `previous` The json files generated can then be used to categorise results coming from incremental analyses of a codebase. Reviewed By: jvillard Differential Revision: D4482517 fbshipit-source-id: 1f7df3e --- .gitignore | 4 + Makefile | 4 + infer/src/IR/Procname.re | 2 +- infer/src/backend/Differential.ml | 44 ++ infer/src/backend/Differential.mli | 18 + infer/src/backend/DifferentialFilters.ml | 219 ++++++++ infer/src/backend/DifferentialFilters.mli | 42 ++ infer/src/backend/InferPrint.re | 2 + infer/src/backend/infer.ml | 27 +- infer/src/base/CommandLineOption.ml | 35 +- infer/src/base/CommandLineOption.mli | 7 +- infer/src/base/Config.ml | 33 ++ infer/src/base/Config.mli | 12 +- infer/src/base/DB.ml | 2 +- infer/src/base/Logging.ml | 1 + infer/src/base/SourceFile.ml | 27 +- infer/src/base/SourceFile.mli | 15 + infer/src/base/Utils.ml | 11 - infer/src/base/Utils.mli | 5 - infer/src/unit/DifferentialFiltersTests.ml | 523 ++++++++++++++++++ infer/src/unit/DifferentialTests.ml | 61 ++ infer/src/unit/DifferentialTestsUtils.ml | 62 +++ infer/src/unit/inferunit.ml | 2 + .../.inferconfig | 5 + .../Makefile | 26 + .../fixed.exp | 0 .../introduced.exp | 2 + .../preexisting.exp | 0 .../src/DiffExampleTwo.java.current | 19 + .../src/com/example/DiffExample.java.current | 35 ++ .../src/com/example/DiffExample.java.previous | 24 + .../Makefile | 21 + .../fixed.exp | 0 .../introduced.exp | 1 + .../preexisting.exp | 0 .../src/DiffExample.java.current | 39 ++ .../src/DiffExample.java.previous | 27 + .../src/SimpleInterfaceExample.java | 13 + .../src/SimpleNestedInterface.java | 13 + .../Makefile | 24 + .../fixed.exp | 0 .../introduced.exp | 2 + .../preexisting.exp | 0 .../src/DiffExample.java.current | 33 ++ .../src/DiffExample.java.previous | 22 + .../src/DiffExampleTwo.java.current | 19 + .../Makefile | 24 + .../file_renamings.json | 6 + .../fixed.exp | 0 .../introduced.exp | 0 .../preexisting.exp | 0 .../src/DiffExample.java.previous | 33 ++ .../src/DiffExampleRenamed.java.current | 33 ++ infer/tests/differential.make | 65 +++ 54 files changed, 1612 insertions(+), 32 deletions(-) create mode 100644 infer/src/backend/Differential.ml create mode 100644 infer/src/backend/Differential.mli create mode 100644 infer/src/backend/DifferentialFilters.ml create mode 100644 infer/src/backend/DifferentialFilters.mli create mode 100644 infer/src/unit/DifferentialFiltersTests.ml create mode 100644 infer/src/unit/DifferentialTests.ml create mode 100644 infer/src/unit/DifferentialTestsUtils.ml create mode 100644 infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/.inferconfig create mode 100644 infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/Makefile create mode 100644 infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/fixed.exp create mode 100644 infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/introduced.exp create mode 100644 infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/preexisting.exp create mode 100644 infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/src/DiffExampleTwo.java.current create mode 100644 infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/src/com/example/DiffExample.java.current create mode 100644 infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/src/com/example/DiffExample.java.previous create mode 100644 infer/tests/build_systems/differential_skip_anonymous_class_renamings/Makefile create mode 100644 infer/tests/build_systems/differential_skip_anonymous_class_renamings/fixed.exp create mode 100644 infer/tests/build_systems/differential_skip_anonymous_class_renamings/introduced.exp create mode 100644 infer/tests/build_systems/differential_skip_anonymous_class_renamings/preexisting.exp create mode 100644 infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/DiffExample.java.current create mode 100644 infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/DiffExample.java.previous create mode 100644 infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/SimpleInterfaceExample.java create mode 100644 infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/SimpleNestedInterface.java create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/Makefile create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/fixed.exp create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/introduced.exp create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/preexisting.exp create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/src/DiffExample.java.current create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/src/DiffExample.java.previous create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/src/DiffExampleTwo.java.current create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/Makefile create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/file_renamings.json create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/fixed.exp create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/introduced.exp create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/preexisting.exp create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/src/DiffExample.java.previous create mode 100644 infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/src/DiffExampleRenamed.java.current create mode 100644 infer/tests/differential.make diff --git a/.gitignore b/.gitignore index 87eb18cd4..ad221dbaf 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,10 @@ duplicates.txt /infer/tests/build_systems/clang_compilation_db_relpath/compile_commands.json /infer/tests/build_systems/codetoanalyze/xcodebuild/simple_app/app_built /infer/tests/build_systems/codetoanalyze/xcodebuild/simple_app/build/ +infer/tests/build_systems/differential_*/**/*.class +infer/tests/build_systems/differential_*/**/Diff*.java +infer/tests/build_systems/differential_*/infer-current +infer/tests/build_systems/differential_*/infer-previous # generated by oUnit /oUnit-all.cache diff --git a/Makefile b/Makefile index dd2ec62ad..9b0e7dfa5 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,10 @@ BUILD_SYSTEMS_TESTS += \ clang_translation \ clang_unknown_ext \ delete_results_dir \ + differential_resolve_infer_eradicate_conflict \ + differential_skip_anonymous_class_renamings \ + differential_skip_duplicated_types_on_filenames \ + differential_skip_duplicated_types_on_filenames_with_renamings \ fail_on_issue \ j1 \ linters \ diff --git a/infer/src/IR/Procname.re b/infer/src/IR/Procname.re index 03c669a5e..821fbff4e 100644 --- a/infer/src/IR/Procname.re +++ b/infer/src/IR/Procname.re @@ -545,7 +545,7 @@ let to_simplified_string withclass::withclass=false p => /** Convert a proc name to a filename */ let to_filename proc_name => - Escape.escape_filename @@ Utils.string_append_crc_cutoff @@ to_unique_id proc_name; + Escape.escape_filename @@ SourceFile.append_crc_cutoff @@ to_unique_id proc_name; /** Pretty print a proc name */ diff --git a/infer/src/backend/Differential.ml b/infer/src/backend/Differential.ml new file mode 100644 index 000000000..c91fe1728 --- /dev/null +++ b/infer/src/backend/Differential.ml @@ -0,0 +1,44 @@ +(* + * Copyright (c) 2017 - 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 + +type t = { + introduced: Jsonbug_t.report; + fixed: Jsonbug_t.report; + preexisting: Jsonbug_t.report; +} + +(** Set operations should keep duplicated issues with identical hashes *) +let of_reports ~(current_report: Jsonbug_t.report) ~(previous_report: Jsonbug_t.report) : t = + let to_map report = + List.fold_left + ~f:(fun map issue -> Map.add_multi map ~key:issue.Jsonbug_t.hash ~data:issue) + ~init:Int.Map.empty + report in + let fold_aux ~key:_ ~data (left, both, right) = + match data with + | `Left left' -> (List.rev_append left' left, both, right) + | `Both (both', _) -> (left, List.rev_append both' both, right) + | `Right right' -> (left, both, List.rev_append right' right) in + let introduced, preexisting, fixed = + Map.fold2 (to_map current_report) (to_map previous_report) ~f:fold_aux ~init:([],[],[]) in + {introduced; fixed; preexisting} + + +let to_files {introduced; fixed; preexisting} destdir = + Out_channel.write_all + (destdir ^/ "introduced.json") + ~data:(Jsonbug_j.string_of_report introduced); + Out_channel.write_all + (destdir ^/ "fixed.json") + ~data:(Jsonbug_j.string_of_report fixed); + Out_channel.write_all + (destdir ^/ "preexisting.json") + ~data:(Jsonbug_j.string_of_report preexisting) diff --git a/infer/src/backend/Differential.mli b/infer/src/backend/Differential.mli new file mode 100644 index 000000000..383673f77 --- /dev/null +++ b/infer/src/backend/Differential.mli @@ -0,0 +1,18 @@ +(* + * Copyright (c) 2017 - 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. + *) + +type t = { + introduced : Jsonbug_t.report; + fixed : Jsonbug_t.report; + preexisting : Jsonbug_t.report; +} + +val of_reports : current_report:Jsonbug_t.report -> previous_report:Jsonbug_t.report -> t + +val to_files : t -> string -> unit diff --git a/infer/src/backend/DifferentialFilters.ml b/infer/src/backend/DifferentialFilters.ml new file mode 100644 index 000000000..dc793c369 --- /dev/null +++ b/infer/src/backend/DifferentialFilters.ml @@ -0,0 +1,219 @@ +(* + * Copyright (c) 2017 - 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 FileRenamings = struct + type renaming = { + current: string; + previous: string; + } [@@deriving compare] + + type t = renaming list [@@deriving compare] + + let equal = [%compare.equal : t] + + let empty = [] + + let from_renamings rl : t = rl + + (* A json renaming assoc list looks like: + [{"current": "aaa.java", "previous": "BBB.java"}, ...] *) + let from_json input : t = + let j = Yojson.Basic.from_string input in + let renaming_of_assoc assoc : renaming = + match assoc with + | `Assoc [("current", `String current); ("previous", `String previous)] -> {current; previous} + | _ -> failwithf "Expected JSON object of the following form: '%s', but instead got: '%s'" + "{\"current\": \"aaa.java\", \"previous\": \"BBB.java\"}" + (Yojson.Basic.to_string assoc) in + match j with + | `List json_renamings -> List.map ~f:renaming_of_assoc json_renamings + | _ -> failwithf "Expected JSON list but got '%s'" input + + let from_json_file file : t = from_json (In_channel.read_all file) + + let find_previous (t: t) current = + let r = List.find ~f:(fun r -> String.equal current r.current) t in + Option.map ~f:(fun r -> r.previous) r + + let pp fmt t = + let pp_tuple fmt {current; previous} = + Format.fprintf fmt "{\"current\": \"%s\", \"previous\": \"%s\"}" current previous in + Format.fprintf fmt "[%a]" (Pp.comma_seq pp_tuple) t + + module VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY = struct + let from_renamings = from_renamings + let equal = equal + let pp = pp + end +end + +(* Remove duplicates between two lists whenever pred is true for such element *) +let relative_complements ~cmp ?(pred=(fun _ -> true)) l1 l2 = + let rec aux last_dup ((out_l1, out_l2) as out) in_l1 in_l2 = + let is_last_seen_dup v = match last_dup with + | Some ld -> Int.equal (cmp ld v) 0 + | None -> false in + match in_l1, in_l2 with + | i::is, f::fs when Int.equal (cmp i f) 0 -> (* i = f *) + if pred i then aux (Some i) (out_l1, out_l2) is fs + else aux None (i::out_l1, f::out_l2) is fs + | i::is, f::_ when cmp i f < 0 -> (* i < f *) + let out_l1' = if is_last_seen_dup i then out_l1 else i::out_l1 in + aux last_dup (out_l1', out_l2) is in_l2 + | _::_, f::fs -> (* i > f *) + let out_l2' = if is_last_seen_dup f then out_l2 else f::out_l2 in + aux last_dup (out_l1, out_l2') in_l1 fs + | i::is, [] when is_last_seen_dup i -> aux last_dup out is in_l2 + | [], f::fs when is_last_seen_dup f -> aux last_dup out in_l1 fs + | _, _ -> List.rev_append in_l1 out_l1, List.rev_append in_l2 out_l2 in + let l1_sorted = List.sort ~cmp l1 in + let l2_sorted = List.sort ~cmp l2 in + aux None ([], []) l1_sorted l2_sorted + +type issue_file_with_renaming = Jsonbug_t.jsonbug * (string option) + +let skip_duplicated_types_on_filenames + renamings + (diff: Differential.t) : Differential.t = + let compare_issue_file_with_renaming (issue1, previous_file1) (issue2, previous_file2) = + let f1, f2 = + Option.value previous_file1 ~default:issue1.Jsonbug_t.file, + Option.value previous_file2 ~default:issue2.Jsonbug_t.file in + String.compare f1 f2 in + let cmp ((issue1, _) as issue_with_previous_file1) ((issue2, _) as issue_with_previous_file2) = + [%compare : string * issue_file_with_renaming] + (issue1.Jsonbug_t.bug_type, issue_with_previous_file1) + (issue2.Jsonbug_t.bug_type, issue_with_previous_file2) in + let introduced, fixed = + (* All comparisons will be made against filenames *before* renamings. + This way, all introduced and fixed issues can be sorted independently + over the same domain. *) + let introduced_normalized = + List.map diff.introduced + ~f:(fun i -> i, FileRenamings.find_previous renamings i.Jsonbug_t.file) in + let fixed_normalized = List.map diff.fixed ~f:(fun f -> f, None) in + let introduced_normalized', fixed_normalized' = + relative_complements ~cmp introduced_normalized fixed_normalized in + List.map ~f:fst introduced_normalized', List.map ~f:fst fixed_normalized' in + {introduced; fixed; preexisting = diff.preexisting} + +let java_anon_class_pattern = Str.regexp "\\$[0-9]+" + +type procedure_id = string + +let compare_procedure_id pid1 pid2 = + (* THIS COMPARISON FUNCTION IS INTENDED FOR JAVA ONLY *) + let normalize_procedure_id pid = + let anon_token = "$ANON" in + Str.global_replace java_anon_class_pattern anon_token pid in + let pid1_norm = normalize_procedure_id pid1 in + let pid2_norm = normalize_procedure_id pid2 in + (* NOTE: The CRC may swallow some extra chars if the anon class has more + * digits (e.g. ...$9.abcde():int.A1B2 and ...$10.abcde():in.C1FF), and this + * makes the 2 strings different. + * Cut the length to the min_length to match the 2 strings *) + let pid1_norm_trimmed, pid2_norm_trimmed = + let min_length = min (String.length pid1_norm) (String.length pid2_norm) in + String.sub pid1_norm ~pos:0 ~len:min_length, + String.sub pid2_norm ~pos:0 ~len:min_length in + String.compare pid1_norm_trimmed pid2_norm_trimmed + +let value_of_qualifier_tag qts tag = + match List.find ~f:(fun elem -> String.equal elem.Jsonbug_t.tag tag) qts with + | Some qt -> Some qt.Jsonbug_t.value + | None -> None + +type file_extension = string [@@deriving compare] + +type weak_hash = string * string * string * int * (string option) [@@deriving compare] + +let skip_anonymous_class_renamings (diff: Differential.t) : Differential.t = + (* + * THIS HEURISTIC IS INTENDED FOR JAVA ONLY. + * Two issues are similar (for the purpose of anonymous class renamings detection) + * when all of the following apply: + * 1) they are Java files + * 2) their weak hashes match + * 3) their anonymous procedure ids match + *) + let string_of_procedure_id issue = SourceFile.strip_crc issue.Jsonbug_t.procedure_id in + let extension fname = snd (Filename.split_extension fname) in + let cmp (i1:Jsonbug_t.jsonbug) (i2:Jsonbug_t.jsonbug) = + [%compare : + (file_extension option) * weak_hash * procedure_id] + (extension i1.file, + (i1.kind, i1.bug_type, i1.file, i1.key, + value_of_qualifier_tag i1.qualifier_tags "call_procedure"), + string_of_procedure_id i1) + (extension i2.file, + (i2.kind, i2.bug_type, i2.file, i2.key, + value_of_qualifier_tag i2.qualifier_tags "call_procedure"), + string_of_procedure_id i2) in + let pred (issue: Jsonbug_t.jsonbug) = + let is_java_file () = + match extension issue.file with + | Some ext -> String.equal (String.lowercase ext) "java" + | None -> false in + let has_anonymous_class_token () = + try + ignore (Str.search_forward java_anon_class_pattern issue.procedure_id 0); + true + with Not_found -> false in + is_java_file () && has_anonymous_class_token () in + let introduced, fixed = relative_complements ~cmp ~pred diff.introduced diff.fixed in + {introduced; fixed; preexisting = diff.preexisting} + +(* Filter out null dereferences reported by infer if file has eradicate + enabled, to avoid double reporting. *) +let resolve_infer_eradicate_conflict + (analyzer: Config.analyzer) + (filters_of_analyzer: Config.analyzer -> Inferconfig.filters) + (diff: Differential.t) : Differential.t = + let should_discard_issue (issue: Jsonbug_t.jsonbug) = + let file_is_whitelisted () = + let source_file = SourceFile.UNSAFE.from_string issue.file in + let filters = filters_of_analyzer Config.Eradicate in + filters.path_filter source_file in + Config.equal_analyzer analyzer Config.Infer && + String.equal issue.bug_type (Localise.to_string Localise.null_dereference) && + file_is_whitelisted () in + let filter issues = List.filter ~f:(Fn.non should_discard_issue) issues in + { + introduced = filter diff.introduced; + fixed = filter diff.fixed; + preexisting = filter diff.preexisting; + } + +let do_filter + (diff: Differential.t) + (renamings: FileRenamings.t) + ~(skip_duplicated_types: bool): Differential.t = + if Config.filtering then ( + diff + |> (if Config.equal_analyzer Config.analyzer Config.Infer then + skip_anonymous_class_renamings + else Fn.id) + |> (if skip_duplicated_types then + skip_duplicated_types_on_filenames renamings + else Fn.id) + |> (if Config.resolve_infer_eradicate_conflict then + resolve_infer_eradicate_conflict Config.analyzer Inferconfig.create_filters + else Fn.id)) + else diff + +module VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY = struct + let relative_complements = relative_complements + let skip_duplicated_types_on_filenames = skip_duplicated_types_on_filenames + let java_anon_class_pattern = java_anon_class_pattern + let value_of_qualifier_tag = value_of_qualifier_tag + let skip_anonymous_class_renamings = skip_anonymous_class_renamings + let resolve_infer_eradicate_conflict = resolve_infer_eradicate_conflict +end diff --git a/infer/src/backend/DifferentialFilters.mli b/infer/src/backend/DifferentialFilters.mli new file mode 100644 index 000000000..4e279add7 --- /dev/null +++ b/infer/src/backend/DifferentialFilters.mli @@ -0,0 +1,42 @@ +(* + * Copyright (c) 2017 - 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. + *) + +module FileRenamings : +sig + type renaming = { + current: string; + previous: string; + } + type t + val empty : t + val from_json : string -> t + val from_json_file : string -> t + val find_previous : t -> string -> string option + + module VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY : sig + val from_renamings : renaming list -> t + val equal : t -> t -> bool + val pp : Format.formatter -> t -> unit + end +end + +val do_filter : Differential.t -> FileRenamings.t -> skip_duplicated_types:bool -> Differential.t + +module VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY : sig + val relative_complements : + cmp:('a -> 'a -> int) -> ?pred:('a -> bool) -> 'a list -> 'a list -> 'a list * 'a list + val skip_duplicated_types_on_filenames : FileRenamings.t -> Differential.t -> Differential.t + val java_anon_class_pattern : Str.regexp + val value_of_qualifier_tag : Jsonbug_t.tag_value_record list -> string -> string option + val skip_anonymous_class_renamings : Differential.t -> Differential.t + val resolve_infer_eradicate_conflict : + Config.analyzer -> + (Config.analyzer -> Inferconfig.filters) -> + Differential.t -> Differential.t +end diff --git a/infer/src/backend/InferPrint.re b/infer/src/backend/InferPrint.re index fbeb853d2..c801f0a16 100644 --- a/infer/src/backend/InferPrint.re +++ b/infer/src/backend/InferPrint.re @@ -548,6 +548,8 @@ let pp_custom_of_report fmt report fields => { | `Issue_field_hash => Format.fprintf fmt "%s%d" (comma_separator index) issue.hash | `Issue_field_line_offset => Format.fprintf fmt "%s%d" (comma_separator index) (issue.line - issue.procedure_start_line) + | `Issue_field_procedure_id_without_crc => + Format.fprintf fmt "%s%s" (comma_separator index) (SourceFile.strip_crc issue.procedure_id) }; List.iteri f::pp_field fields; Format.fprintf fmt "@." diff --git a/infer/src/backend/infer.ml b/infer/src/backend/infer.ml index 64d797f96..11656d65d 100644 --- a/infer/src/backend/infer.ml +++ b/infer/src/backend/infer.ml @@ -434,7 +434,7 @@ let get_driver_mode () = | None -> driver_mode_of_build_cmd (List.rev Config.rest) -let () = +let infer_mode () = let driver_mode = get_driver_mode () in if not (equal_driver_mode driver_mode Analyze || Config.(buck || continue_capture || maven || reactive_mode)) then @@ -466,3 +466,28 @@ let () = ); if Config.buck_cache_mode then clean_results_dir () + +let differential_mode () = + let arg_or_fail arg_name arg_opt = + match arg_opt with + | Some arg -> arg + | None -> failwithf "Expected '%s' argument not found" arg_name in + let load_report filename : Jsonbug_t.report = + Jsonbug_j.report_of_string (In_channel.read_all filename) in + let current_report = load_report (arg_or_fail "report-current" Config.report_current) in + let previous_report = load_report (arg_or_fail "report-previous" Config.report_previous) in + let file_renamings = match Config.file_renamings with + | Some f -> DifferentialFilters.FileRenamings.from_json_file f + | None -> DifferentialFilters.FileRenamings.empty in + let diff = DifferentialFilters.do_filter + (Differential.of_reports ~current_report ~previous_report) + file_renamings + ~skip_duplicated_types:Config.skip_duplicated_types in + let out_path = Config.results_dir ^/ "differential" in + Unix.mkdir_p out_path; + Differential.to_files diff out_path + +let () = + match Config.final_parse_action with + | Differential -> differential_mode () + | _ -> infer_mode () diff --git a/infer/src/base/CommandLineOption.ml b/infer/src/base/CommandLineOption.ml index 913db0112..9d485b3e8 100644 --- a/infer/src/base/CommandLineOption.ml +++ b/infer/src/base/CommandLineOption.ml @@ -63,7 +63,8 @@ let equal_section = [%compare.equal : section ] let all_sections = [ Analysis; BufferOverrun; Checkers; Clang; Crashcontext; Driver; Java; Print; Quandary ] -type 'a parse = Infer of 'a | Javac | NoParse [@@deriving compare] +(* NOTE: All variants must be also added to `all_parse_tags` below *) +type 'a parse = Differential | Infer of 'a | Javac | NoParse [@@deriving compare] type parse_mode = section list parse [@@deriving compare] @@ -74,14 +75,20 @@ let equal_parse_action = [%compare.equal : parse_action ] type parse_tag = unit parse [@@deriving compare] let equal_parse_tag = [%compare.equal : parse_tag ] -let all_parse_tags = [ Infer (); Javac; NoParse ] +let all_parse_tags = [ Differential; Infer (); Javac; NoParse ] -let to_parse_tag = function | Infer _ -> Infer () | Javac -> Javac | NoParse -> NoParse +(* NOTE: All variants must be also added to `all_parse_tags` below *) +let to_parse_tag tag = + match tag with + | Differential -> Differential + | Infer _ -> Infer () + | Javac -> Javac + | NoParse -> NoParse let accept_unknown_args = function | Infer Print | Javac | NoParse -> true - | Infer (Analysis | BufferOverrun | Checkers | Clang | Crashcontext | Driver | Java | Quandary) -> - false + | Infer (Analysis | BufferOverrun | Checkers | Clang | Crashcontext | Driver | Java | Quandary) + | Differential -> false type desc = { long: string; short: string; meta: string; doc: string; spec: spec; @@ -240,7 +247,7 @@ let add parse_mode desc = let full_desc_list = List.Assoc.find_exn parse_tag_desc_lists tag in full_desc_list := desc :: !full_desc_list ; match parse_mode with - | Javac | NoParse -> () + | Differential | Javac | NoParse -> () | Infer sections -> List.iter infer_section_desc_lists ~f:(fun (section, desc_list) -> let desc = if List.mem ~equal:equal_section sections section then @@ -252,7 +259,7 @@ let add parse_mode desc = let deprecate_desc parse_mode ~long ~short ~deprecated desc = let warn () = match parse_mode with | Javac | NoParse -> () - | Infer _ -> + | Differential | Infer _ -> warnf "WARNING: '-%s' is deprecated. Use '--%s'%s instead.@." deprecated long (if short = "" then "" else Printf.sprintf " or '-%s'" short) in let warn_then_f f x = warn (); f x in @@ -287,7 +294,7 @@ let mk ?(deprecated=[]) ?(parse_mode=Infer []) (* add desc for short option only for parsing, without documentation *) let parse_mode_no_sections = match parse_mode with | Infer _ -> Infer [] - | Javac | NoParse -> parse_mode in + | Differential | Javac | NoParse -> parse_mode in if short <> "" then add parse_mode_no_sections {desc with long = ""; meta = ""; doc = ""} ; (* add desc for deprecated options only for parsing, without documentation *) @@ -617,7 +624,7 @@ let set_curr_speclist_for_parse_action ~incomplete ~usage parse_action = match parse_action with | Infer section -> List.Assoc.find_exn ~equal:equal_section infer_section_desc_lists section - | Javac | NoParse -> + | Differential | Javac | NoParse -> to_parse_tag parse_action |> List.Assoc.find_exn ~equal:equal_parse_tag parse_tag_desc_lists in let (exe_speclist, widths) = normalize !exe_descs in @@ -676,6 +683,16 @@ let mk_rest_actions ?(parse_mode=Infer []) doc ~usage decode_action = add parse_mode {long = "--"; short = ""; meta = ""; doc; spec; decode_json = fun _ -> []} ; rest +let mk_switch_parse_action + parse_action ~usage ?(deprecated=[]) ~long ?short ?parse_mode ?(meta="") doc = + let switch () = + select_parse_action ~incomplete:false ~usage parse_action |> ignore in + ignore( + mk ~deprecated ~long ?short ~default:() ?parse_mode ~meta doc + ~default_to_string:(fun () -> "") + ~decode_json:(string_json_decoder ~long) + ~mk_setter:(fun _ _ -> switch ()) + ~mk_spec:(fun _ -> Unit switch)) let decode_inferconfig_to_argv path = let json = match Utils.read_optional_json_file path with diff --git a/infer/src/base/CommandLineOption.mli b/infer/src/base/CommandLineOption.mli index 75bda9635..250722718 100644 --- a/infer/src/base/CommandLineOption.mli +++ b/infer/src/base/CommandLineOption.mli @@ -18,7 +18,7 @@ type section = val all_sections : section list -type 'a parse = Infer of 'a | Javac | NoParse +type 'a parse = Differential | Infer of 'a | Javac | NoParse type parse_mode = section list parse [@@deriving compare] @@ -140,6 +140,11 @@ val mk_rest_actions : usage:string -> (string -> parse_action) -> string list ref + +(** when the option is found on the command line, the current parse action is discarded and the + following arguments are parsed using [parse_action] *) +val mk_switch_parse_action : parse_action -> usage:string -> unit t + (** environment variable use to pass arguments from parent to child processes *) val args_env_var : string diff --git a/infer/src/base/Config.ml b/infer/src/base/Config.ml index 38bc1c6af..8a2b26c08 100644 --- a/infer/src/base/Config.ml +++ b/infer/src/base/Config.ml @@ -92,6 +92,7 @@ let issues_fields_symbols = [ ("key", `Issue_field_key); ("hash", `Issue_field_hash); ("line_offset", `Issue_field_line_offset); + ("procedure_id_without_crc", `Issue_field_procedure_id_without_crc); ] type os_type = Unix | Win32 | Cygwin @@ -432,6 +433,10 @@ let inferconfig_path = let anon_args = CLOpt.mk_anon () +and () = + CLOpt.mk_switch_parse_action CLOpt.Differential ~usage:"infer --diff [options]" + ~long:"diff" "[experimental] compute differential report" + and abs_struct = CLOpt.mk_int ~deprecated:["absstruct"] ~long:"abs-struct" ~default:1 ~meta:"int" "Specify abstraction level for fields of structs:\n\ @@ -893,6 +898,11 @@ and fcp_syntax_only = CLOpt.mk_bool ~long:"fcp-syntax-only" "Skip creation of object files" +and file_renamings = + CLOpt.mk_path_opt + ~long:"file-renamings" ~parse_mode:CLOpt.Differential + "JSON with a list of file renamings to use while computing differential reports" + and filter_paths = CLOpt.mk_bool ~long:"filter-paths" ~default:true "Filters specified in .inferconfig" @@ -1139,6 +1149,10 @@ and report = CLOpt.mk_path_opt ~deprecated:["report"] ~long:"report" ~meta:"file" "Write a report of the analysis results to a file" +and report_current = + CLOpt.mk_path_opt ~long:"report-current" ~parse_mode:CLOpt.Differential + "report of the latest revision" + and report_custom_error = CLOpt.mk_bool ~long:"report-custom-error" "" @@ -1151,6 +1165,15 @@ and report_hook = passed --issues-csv, --issues-json, --issues-txt, --issues-xml, --project-root, and \ --results-dir." +and report_previous = + CLOpt.mk_path_opt ~long:"report-previous" ~parse_mode:CLOpt.Differential + "report of the base revision to use for comparison" + +and resolve_infer_eradicate_conflict = + CLOpt.mk_bool ~long:"resolve-infer-eradicate-conflict" + ~default:false ~parse_mode:CLOpt.Differential + "Filter out Null Dereferences reported by Infer if Eradicate is enabled" + and rest = CLOpt.mk_rest_actions ~parse_mode:CLOpt.(Infer [Driver]) @@ -1189,6 +1212,10 @@ and skip_analysis_in_path = ~meta:"path prefix OCaml regex" "Ignore files whose path matches the given prefix (can be specified multiple times)" +and skip_duplicated_types = + CLOpt.mk_bool ~long:"skip-duplicated-types" ~default:true ~parse_mode:CLOpt.Differential + "Skip fixed-then-introduced duplicated types while computing differential reports" + and skip_translation_headers = CLOpt.mk_string_list ~deprecated:["skip_translation_headers"] ~long:"skip-translation-headers" ~parse_mode:CLOpt.(Infer [Clang]) @@ -1544,8 +1571,10 @@ and fail_on_bug = !fail_on_bug and failures_allowed = !failures_allowed and fcp_apple_clang = !fcp_apple_clang and fcp_syntax_only = !fcp_syntax_only +and file_renamings = !file_renamings and filter_paths = !filter_paths and filtering = !filtering +and final_parse_action = parse_action and flavors = !flavors and from_json_report = !from_json_report and frontend_debug = !frontend_debug @@ -1598,10 +1627,13 @@ and quiet = !quiet and reactive_mode = !reactive and reactive_capture = !reactive_capture and report = !report +and report_current = !report_current and report_custom_error = !report_custom_error and report_hook = !report_hook +and report_previous = !report_previous and report_runtime_exceptions = !tracing and reports_include_ml_loc = !reports_include_ml_loc +and resolve_infer_eradicate_conflict = !resolve_infer_eradicate_conflict and results_dir = !results_dir and save_analysis_results = !save_results and seconds_per_iteration = !seconds_per_iteration @@ -1609,6 +1641,7 @@ and show_buckets = !print_buckets and show_progress_bar = !progress_bar and siof_safe_methods = !siof_safe_methods and skip_analysis_in_path = !skip_analysis_in_path +and skip_duplicated_types = !skip_duplicated_types and skip_translation_headers = !skip_translation_headers and sources = !sources and sourcepath = !sourcepath diff --git a/infer/src/base/Config.mli b/infer/src/base/Config.mli index 832b5dc79..32adcca68 100644 --- a/infer/src/base/Config.mli +++ b/infer/src/base/Config.mli @@ -56,7 +56,8 @@ val issues_fields_symbols : | `Issue_field_bug_trace | `Issue_field_key | `Issue_field_hash - | `Issue_field_line_offset]) list + | `Issue_field_line_offset + | `Issue_field_procedure_id_without_crc]) list type os_type = Unix | Win32 | Cygwin @@ -224,8 +225,10 @@ val fail_on_bug : bool val failures_allowed : bool val fcp_apple_clang : string option val fcp_syntax_only : bool +val file_renamings : string option val filter_paths : bool val filtering : bool +val final_parse_action : CommandLineOption.parse_action val flavors : bool val from_json_report : string option val frontend_debug : bool @@ -251,7 +254,8 @@ val issues_fields : [`Issue_field_bug_class | `Issue_field_bug_trace | `Issue_field_key | `Issue_field_hash - | `Issue_field_line_offset] list + | `Issue_field_line_offset + | `Issue_field_procedure_id_without_crc] list val iterations : int val java_jar_compiler : string option val javac_classes_out : string @@ -291,9 +295,12 @@ val quiet : bool val reactive_mode : bool val reactive_capture : bool val report : string option +val report_current : string option val report_hook : string option +val report_previous : string option val report_runtime_exceptions : bool val reports_include_ml_loc : bool +val resolve_infer_eradicate_conflict : bool val results_dir : string val save_analysis_results : string option val seconds_per_iteration : float option @@ -301,6 +308,7 @@ val show_buckets : bool val show_progress_bar : bool val siof_safe_methods : string list val skip_analysis_in_path : string list +val skip_duplicated_types : bool val skip_translation_headers : string list val spec_abs_level : int val specs_library : string list diff --git a/infer/src/base/DB.ml b/infer/src/base/DB.ml index 822e1cda4..5c533a280 100644 --- a/infer/src/base/DB.ml +++ b/infer/src/base/DB.ml @@ -27,7 +27,7 @@ let source_dir_to_string source_dir = source_dir (** get the path to an internal file with the given extention (.cfg, .cg, .tenv) *) let source_dir_get_internal_file source_dir extension = let source_dir_name = - Utils.string_append_crc_cutoff (Filename.chop_extension (Filename.basename source_dir)) in + SourceFile.append_crc_cutoff (Filename.chop_extension (Filename.basename source_dir)) in let fname = source_dir_name ^ extension in Filename.concat source_dir fname diff --git a/infer/src/base/Logging.ml b/infer/src/base/Logging.ml index 7e2fe8ffb..82b31f8f7 100644 --- a/infer/src/base/Logging.ml +++ b/infer/src/base/Logging.ml @@ -31,6 +31,7 @@ let dup_formatter fmt1 fmt2 = (** Name of dir for logging the output in the specific executable *) let log_dir_of_action (action : CLOpt.parse_action) = match action with | Infer (Analysis | BufferOverrun | Checkers | Crashcontext | Quandary) -> "analyze" + | Differential -> "differential" | Infer Driver -> "driver" | Infer Clang | Infer Java diff --git a/infer/src/base/SourceFile.ml b/infer/src/base/SourceFile.ml index 2e4bca314..46d683935 100644 --- a/infer/src/base/SourceFile.ml +++ b/infer/src/base/SourceFile.ml @@ -77,6 +77,23 @@ let to_rel_path fname = | RelativeProjectRoot path -> path | _ -> to_abs_path fname +let cutoff_length = 100 +let crc_token = '.' + +let append_crc_cutoff ?(key="") name = + let name_up_to_cutoff = + if String.length name <= cutoff_length + then name + else String.sub name ~pos:0 ~len:cutoff_length in + let crc_str = + let name_for_crc = name ^ key in + Utils.string_crc_hex32 name_for_crc in + name_up_to_cutoff ^ Char.to_string crc_token ^ crc_str + +let strip_crc str = + (* Strip 32 characters of digest, plus 1 character of crc_token *) + String.sub ~pos:0 ~len:(String.length str - 33) str + (** string encoding of a source file (including path) as a single filename *) let encoding source_file = let prefix = match source_file with @@ -92,7 +109,7 @@ let encoding source_file = | `Enc_crc -> let base = Filename.basename source_file_s in let dir = prefix ^ Filename.dirname source_file_s in - Utils.string_append_crc_cutoff ~key:dir base + append_crc_cutoff ~key:dir base let empty = Absolute "" @@ -153,3 +170,11 @@ let changed_files_set = ) ~init:Set.empty ) + +module UNSAFE = struct + let from_string str = + if Filename.is_relative str then + RelativeProjectRoot str + else + Absolute str +end diff --git a/infer/src/base/SourceFile.mli b/infer/src/base/SourceFile.mli index 66f561752..2ab7eb9a5 100644 --- a/infer/src/base/SourceFile.mli +++ b/infer/src/base/SourceFile.mli @@ -60,3 +60,18 @@ val of_header : t -> t option NOTE: it may include extra source_files if --changed-files-index contains paths to header files *) val changed_files_set : Set.t option + +(** Append a crc to the string, using string_crc_hex32. + Cut the string if it exceeds the cutoff limit. + Use an optional key to compute the crc. *) +val append_crc_cutoff : ?key:string -> string -> string + +(** Strip any crc attached to any string generated by string_append_crc_cutoff *) +val strip_crc : string -> string + +module UNSAFE : sig + (** Create a SourceFile from any path. This is unchecked and should not be + used when the existence of source files is a requirement. Furthermore, + absolute paths won't be made relative to project root.*) + val from_string : string -> t +end diff --git a/infer/src/base/Utils.ml b/infer/src/base/Utils.ml index f133b73be..e639c41c0 100644 --- a/infer/src/base/Utils.ml +++ b/infer/src/base/Utils.ml @@ -185,19 +185,8 @@ let remove_directory_tree path = | _ -> Unix.remove (Fts.FTSENT.name ent) ) - let string_crc_hex32 s = Digest.to_hex (Digest.string s) -let string_append_crc_cutoff ?(cutoff=100) ?(key="") name = - let name_up_to_cutoff = - if String.length name <= cutoff - then name - else String.sub name ~pos:0 ~len:cutoff in - let crc_str = - let name_for_crc = name ^ key in - string_crc_hex32 name_for_crc in - name_up_to_cutoff ^ "." ^ crc_str - let read_optional_json_file path = if Sys.file_exists path = `Yes then try diff --git a/infer/src/base/Utils.mli b/infer/src/base/Utils.mli index 54592cb61..46a4378f2 100644 --- a/infer/src/base/Utils.mli +++ b/infer/src/base/Utils.mli @@ -18,11 +18,6 @@ val initial_timeofday : float (** Compute a 32-character hexadecimal crc using the Digest module *) val string_crc_hex32 : string -> string -(** Append a crc to the string, using string_crc_hex32. - Cut the string if it exceeds the cutoff limit. - Use an optional key to compute the crc. *) -val string_append_crc_cutoff : ?cutoff:int -> ?key:string -> string -> string - (** copy a source file, return the number of lines, or None in case of error *) val copy_file : string -> string -> int option diff --git a/infer/src/unit/DifferentialFiltersTests.ml b/infer/src/unit/DifferentialFiltersTests.ml new file mode 100644 index 000000000..6263d8c2e --- /dev/null +++ b/infer/src/unit/DifferentialFiltersTests.ml @@ -0,0 +1,523 @@ +(* + * Copyright (c) 2017 - 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 +open OUnit2 + +open DifferentialTestsUtils + +type 'a outcome = Return of 'a | Raise of exn + +let test_file_renamings_from_json = + let create_test test_input expected_output _ = + let test_output input = DifferentialFilters.FileRenamings.from_json input in + let pp_diff fmt (expected, actual) = + let pp = DifferentialFilters.FileRenamings.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.pp in + Format.fprintf fmt "Expected %a but got %a" pp expected pp actual in + match expected_output with + | Return exp -> + assert_equal + ~pp_diff + ~cmp:DifferentialFilters.FileRenamings.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.equal + exp + (test_output test_input) + | Raise exc -> + assert_raises exc (fun () -> test_output test_input) in + [ + ( + "test_file_renamings_from_json_with_good_input", + "[" ^ + "{\"current\": \"aaa.java\", \"previous\": \"BBB.java\"}," ^ + "{\"current\": \"ccc.java\", \"previous\": \"DDD.java\"}," ^ + "{\"current\": \"eee.java\", \"previous\": \"FFF.java\"}" ^ + "]", + Return ( + DifferentialFilters.FileRenamings.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.from_renamings [ + {DifferentialFilters.FileRenamings.current = "aaa.java"; previous = "BBB.java"}; + {DifferentialFilters.FileRenamings.current = "ccc.java"; previous = "DDD.java"}; + {DifferentialFilters.FileRenamings.current = "eee.java"; previous = "FFF.java"}; + ] + ) + ); + ( + "test_file_renamings_from_json_with_good_empty_input", + "[]", + Return ( + DifferentialFilters.FileRenamings.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.from_renamings [] + ) + ); + ( + "test_file_renamings_from_json_with_well_formed_but_unexpected_input", + "{}", + Raise ( + Failure "Expected JSON list but got '{}'" + ) + ); + ( + "test_file_renamings_from_json_with_well_formed_but_unexpected_value", + "[{\"current\": 1, \"previous\": \"BBB.java\"}]", + Raise ( + Failure ("Expected JSON object of the following form: " ^ + "'{\"current\": \"aaa.java\", \"previous\": \"BBB.java\"}', " ^ + "but instead got: '{\"current\":1,\"previous\":\"BBB.java\"}'") + ) + ); + ( + "test_file_renamings_from_json_with_malformed_input", + "A", + Raise ( + Yojson.Json_error "Line 1, bytes 0-1:\nInvalid token 'A'" + ) + ); + ] + |> List.map + ~f:(fun (name, test_input, expected_output) -> + name >:: create_test test_input expected_output) + +let test_file_renamings_find_previous = + let renamings = + DifferentialFilters.FileRenamings.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.from_renamings [ + {DifferentialFilters.FileRenamings.current = "aaa.java"; previous = "BBB.java"}; + {DifferentialFilters.FileRenamings.current = "ccc.java"; previous = "DDD.java"}; + {DifferentialFilters.FileRenamings.current = "eee.java"; previous = "FFF.java"}; + ] in + let cmp s1 s2 = [%compare.equal : string option] s1 s2 in + let find_previous = DifferentialFilters.FileRenamings.find_previous in + let pp_diff fmt (expected, actual) = + let pp_str_opt fmt str_opt = + let out = match str_opt with + | Some str -> "Some " ^ str + | None -> "None" in + Format.fprintf fmt "%s" out in + Format.fprintf fmt "Expected '%a' but got '%a'" pp_str_opt expected pp_str_opt actual in + let create_test input expected_previous _ = + assert_equal ~cmp ~pp_diff expected_previous (find_previous renamings input) in + [ + ( + "test_file_renamings_find_previous_with_existing_value", + "ccc.java", + Some "DDD.java" + ); + ( + "test_file_renamings_find_previous_with_existing_value", + "abc.java", + None + ) + ] + |> List.map + ~f:(fun (name, test_input, expected_output) -> + name >:: create_test test_input expected_output) + +let test_relative_complements = + let create_test pred (l1, l2) (expected_l1, expected_l2) _ = + let cmp = Int.compare in + let output_l1, output_l2 = + DifferentialFilters.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.relative_complements + ~cmp ~pred l1 l2 in + let list_equal l1 l2 = List.equal ~equal:(fun v1 v2 -> Int.equal (cmp v1 v2) 0) l1 l2 in + assert_equal + ~pp_diff:(pp_diff_of_int_list "First list") ~cmp:list_equal expected_l1 output_l1; + assert_equal + ~pp_diff:(pp_diff_of_int_list "Second list") ~cmp:list_equal expected_l2 output_l2 in + [ + ( + "test_relative_complements_with_always_true_pred", + (fun _ -> true), + ([0;1;2;3;4;5], [5;3;7;1;1;2]), + ([4;0], [7]) + ); + ( + "test_relative_complements_with_even_numbers_pred", + (fun i -> Int.equal (i mod 2) 0), (* skip when even, keep odd *) + ([0;1;2;3;4;5], [5;3;7;1;1;2]), + ([5;4;3;1;0], [7;5;3;1;1]) + ); + ( + "test_relative_complements_with_even_numbers_pred_2", + (fun i -> Int.equal (i mod 2) 0), (* skip when even, keep odd *) + ([0;1;2;3;5;5], [1;1;2;3;4;7]), + ([5;5;3;1;0], [7;4;3;1;1]) + ); + ( + "test_relative_complements_with_always_true_pred_and_disjoint_lists_of_different_length", + (fun _ -> true), + ([0;3;2;3;5], [9;7;6;8;4;6;9]), + ([5;3;3;2;0], [9;9;8;7;6;6;4]) + ); + ( + "test_relative_complements_with_always_true_pred_and_lists_of_different_length", + (fun _ -> true), + ([0;3;2;3], [9;7;3;8;0;6;9;4]), + ([2], [9;9;8;7;6;4]) + ); + ( + "test_relative_complements_with_odd_numbers_on_lists_of_different_length", + (fun i -> Int.equal (i mod 2) 1), (* skip when odd, keep even *) + ([0;3;2;3], [9;7;3;8;0;6;9;4]), + ([2;0], [9;9;8;7;6;4;0]) + ); + ( + "test_relative_complements_with_singleton_lists1", + (fun _ -> true), + ([0], [0;1;0;0]), + ([], [1]) + ); + ( + "test_relative_complements_with_singleton_lists2", + (fun _ -> true), + ([0;1;0;0], [0]), + ([1], []) + ); + ( + "test_relative_complements_with_singleton_lists3", + (fun _ -> true), + ([0], [0]), + ([], []) + ); + ( + "test_relative_complements_with_singleton_lists4", + (fun _ -> true), + ([0], [1]), + ([0], [1]) + ); + ( + "test_relative_complements_with_empty_lists1", + (fun _ -> true), + ([], [0;1;0;0]), + ([], [1;0;0;0]) + ); + ( + "test_relative_complements_with_empty_lists2", + (fun _ -> true), + ([0;1;0;0], []), + ([1;0;0;0], []) + ); + ( + "test_relative_complements_with_empty_lists3", + (fun _ -> true), + ([], []), + ([], []) + ); + ] + |> List.map + ~f:(fun (name, pred, test_input, expected_output) -> + name >:: create_test pred test_input expected_output) + +let test_skip_duplicated_types_on_filenames = + let current_report = [ + create_fake_jsonbug ~bug_type:"bug_type_1" ~file:"file_2'.java" ~hash:22 (); + create_fake_jsonbug ~bug_type:"bug_type_1" ~file:"file_1'.java" ~hash:11 (); + create_fake_jsonbug ~bug_type:"bug_type_1" ~file:"file_1'.java" ~hash:111 (); + create_fake_jsonbug ~bug_type:"bug_type_2" ~file:"file_4.java" ~hash:4 (); + create_fake_jsonbug ~bug_type:"bug_type_1" ~file:"file_2'.java" ~hash:222 (); + create_fake_jsonbug ~bug_type:"bug_type_2" ~file:"file_5.java" ~hash:55 (); + ] in + let previous_report = [ + create_fake_jsonbug ~bug_type:"bug_type_1" ~file:"file_2'.java" ~hash:222 (); + create_fake_jsonbug ~bug_type:"bug_type_2" ~file:"file_5.java" ~hash:5 (); + create_fake_jsonbug ~bug_type:"bug_type_1" ~file:"file_1.java" ~hash:1 (); + create_fake_jsonbug ~bug_type:"bug_type_2" ~file:"file_3.java" ~hash:3 (); + create_fake_jsonbug ~bug_type:"bug_type_1" ~file:"file_2.java" ~hash:2 (); + ] in + let renamings = + DifferentialFilters.FileRenamings.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.from_renamings [ + {DifferentialFilters.FileRenamings.current = "file_2'.java"; previous = "file_2.java"}; + {DifferentialFilters.FileRenamings.current = "file_1'.java"; previous = "file_1.java"}; + ] in + let diff = Differential.of_reports ~current_report ~previous_report in + let diff' = + DifferentialFilters.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.skip_duplicated_types_on_filenames + renamings diff in + let do_assert _ = + assert_equal + ~pp_diff:(pp_diff_of_int_list "Hashes of introduced") + [4] (sorted_hashes_of_issues diff'.introduced); + assert_equal + ~pp_diff:(pp_diff_of_int_list "Hashes of fixed") + [3] (sorted_hashes_of_issues diff'.fixed); + assert_equal + ~pp_diff:(pp_diff_of_int_list "Hashes of preexisting") + [222] (sorted_hashes_of_issues diff'.preexisting) in + "test_skip_duplicated_types_on_filenames" >:: do_assert + +let test_value_of_qualifier_tag = + let qts = + [{Jsonbug_t.tag = "tag1"; value = "value1"}; {Jsonbug_t.tag = "tag2"; value = "value2"}] in + let pp_diff fmt (expected, actual) = + let to_str v = Option.value v ~default:"NONE" in + Format.fprintf fmt "Expected: %s Found: %s" (to_str expected) (to_str actual) in + let do_assert _ = + assert_equal + ~cmp:(Option.equal String.equal) + ~pp_diff + (Some "value2") + (DifferentialFilters.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.value_of_qualifier_tag + qts "tag2"); + assert_equal + ~cmp:(Option.equal String.equal) + ~pp_diff + None + (DifferentialFilters.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.value_of_qualifier_tag + qts "tag3"); + assert_equal + ~cmp:(Option.equal String.equal) + ~pp_diff + (Some "value1") + (DifferentialFilters.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.value_of_qualifier_tag + qts "tag1") in + "test_value_of_qualifier_tag" >:: do_assert + +let test_skip_anonymous_class_renamings = + let qt1 = [{Jsonbug_t.tag = "call_procedure"; value = "aValue1"}] in + let qt2 = [{Jsonbug_t.tag = "call_procedure"; value = "aValue2"}] in + + let create_test input_diff (exp_introduced, exp_fixed, exp_preexisting) _ = + let diff' = + DifferentialFilters.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.skip_anonymous_class_renamings + input_diff in + assert_equal + ~pp_diff:(pp_diff_of_int_list "Hashes of introduced") + exp_introduced (sorted_hashes_of_issues diff'.introduced); + assert_equal + ~pp_diff:(pp_diff_of_int_list "Hashes of fixed") + exp_fixed (sorted_hashes_of_issues diff'.fixed); + assert_equal + ~pp_diff:(pp_diff_of_int_list "Hashes of preexisting") + exp_preexisting (sorted_hashes_of_issues diff'.preexisting) in + (* [(test_name, diff, expected hashes); ...] *) + [ + ("test_skip_anonymous_class_renamings_with_long_procedure_ids", + Differential.of_reports + ~current_report:[ + create_fake_jsonbug + ~bug_type:"bug_type_1" + ~procedure_id: + ("com.whatever.package00.abcd." ^ + "ABasicExampleFragment$83.onMenuItemActionExpand(android.view.MenuItem):b." ^ + "5ab5e18cae498c35d887ce88f3d5fa82") + ~file:"a.java" + ~key:1 + ~qualifier_tags:qt1 + ~hash:3 (); + create_fake_jsonbug + ~bug_type:"bug_type_1" + ~procedure_id: + ("com.whatever.package00.abcd." ^ + "ABasicExampleFragment$83$7.onMenuItemActionExpand(android.view.MenuItem)." ^ + "522cc747174466169781c9d2fc980dbc") + ~file:"a.java" + ~key:1 + ~qualifier_tags:qt1 + ~hash:4 (); + create_fake_jsonbug + ~bug_type:"bug_type_2" + ~procedure_id:"procid5.c854fd4a98113d9ab5b82deb3545de89" + ~file:"b.java" + ~key:5 + ~hash:5 (); + ] + ~previous_report:[ + create_fake_jsonbug + ~bug_type:"bug_type_1" + ~procedure_id: + ("com.whatever.package00.abcd." ^ + "ABasicExampleFragment$9.onMenuItemActionExpand(android.view.MenuItem):bo." ^ + "ba1776155fba2899542401da5bc779a5") + ~file:"a.java" + ~key:1 + ~qualifier_tags:qt1 + ~hash:1 (); + create_fake_jsonbug + ~bug_type:"bug_type_2" + ~procedure_id:"procid2.92095aee3f1884c37e96feae031f4931" + ~file:"b.java" + ~key:2 + ~hash:2 (); + ], + ([4;5], [2], [])); + ("test_skip_anonymous_class_renamings_with_empty_qualifier_tags", + Differential.of_reports + ~current_report:[ + create_fake_jsonbug + ~bug_type:"bug_type_1" + ~procedure_id:"com.whatever.package.Class$1.foo():bool.bf13089cf4c47ff8ff089a1a4767324f" + ~file:"a.java" + ~key:1 + ~hash:1 (); + create_fake_jsonbug + ~bug_type:"bug_type_2" + ~procedure_id:"com.whatever.package.Class$1.foo():bool.bf13089cf4c47ff8ff089a1a4767324f" + ~file:"a.java" + ~key:1 + ~hash:3 (); + ] + ~previous_report:[ + create_fake_jsonbug + ~bug_type:"bug_type_1" + ~procedure_id: + "com.whatever.package.Class$21$1.foo():bool.db89561ad9dab28587c8c04833f09b03" + ~file:"a.java" + ~key:1 + ~hash:2 (); + create_fake_jsonbug + ~bug_type:"bug_type_2" + ~procedure_id:"com.whatever.package.Class$8.foo():bool.cffd4e941668063eb802183dbd3e856d" + ~file:"a.java" + ~key:1 + ~hash:4 (); + ], + ([1], [2], [])); + ("test_skip_anonymous_class_renamings_with_matching_non_anonymous_procedure_ids", + Differential.of_reports + ~current_report:[ + create_fake_jsonbug + ~bug_type:"bug_type_1" + ~procedure_id:"com.whatever.package.Class.foo():bool.919f37fd0993058a01f438210ba8a247" + ~file:"a.java" + ~key:1 + ~hash:1 (); + create_fake_jsonbug + ~bug_type:"bug_type_1" + ~procedure_id:"com.whatever.package.Class.foo():bool.919f37fd0993058a01f438210ba8a247" + ~file:"a.java" + ~key:1 + ~hash:3 (); + ] + ~previous_report:[ + create_fake_jsonbug + ~bug_type:"bug_type_1" + ~procedure_id:"com.whatever.package.Class.foo():bool.919f37fd0993058a01f438210ba8a247" + ~file:"a.java" + ~key:1 + ~hash:2 (); + create_fake_jsonbug + ~bug_type:"bug_type_1" + ~procedure_id:"com.whatever.package.Class.foo():bool.919f37fd0993058a01f438210ba8a247" + ~file:"a.java" + ~key:1 + ~hash:4 (); + ], + ([1;3], [2;4], [])); + ("test_skip_anonymous_class_renamings_with_non_java_files", + Differential.of_reports + ~current_report:[ + create_fake_jsonbug + ~bug_type:"bug_type_1" + ~procedure_id: + "com.whatever.package.Class$3$1.foo():bool.9ff39eb5c53c81da9f6a7ade324345b6" + ~file:"a.java" + ~key:1 + ~hash:1 (); + create_fake_jsonbug + ~bug_type:"bug_type_2" + ~procedure_id:"com.whatever.package.Class$1.foo():bool.bf13089cf4c47ff8ff089a1a4767324f" + ~file:"a.mm" + ~key:1 + ~hash:3 (); + ] + ~previous_report:[ + create_fake_jsonbug + ~bug_type:"bug_type_1" + ~procedure_id: + "com.whatever.package.Class$21$1.foo():bool.db89561ad9dab28587c8c04833f09b03" + ~file:"a.java" + ~key:1 + ~hash:2 (); + create_fake_jsonbug + ~bug_type:"bug_type_2" + ~procedure_id:"com.whatever.package.Class$8.foo():bool.cffd4e941668063eb802183dbd3e856d" + ~file:"a.mm" + ~key:1 + ~hash:4 (); + ], + ([3], [4], [])); + ("test_skip_anonymous_class_renamings_with_different_call_procedure_qualifier_tags", + Differential.of_reports + ~current_report:[ + create_fake_jsonbug + ~bug_type:"bug_type_1" + ~procedure_id: + "com.whatever.package.Class$3$1.foo():bool.9ff39eb5c53c81da9f6a7ade324345b6" + ~file:"a.java" + ~key:1 + ~qualifier_tags:qt1 + ~hash:1 (); + ] + ~previous_report:[ + create_fake_jsonbug + ~bug_type:"bug_type_1" + ~procedure_id: + "com.whatever.package.Class$21$1.foo():bool.db89561ad9dab28587c8c04833f09b03" + ~file:"a.java" + ~key:1 + ~qualifier_tags:qt2 + ~hash:2 (); + ], + ([1], [2], [])); + ] |> List.map + ~f:(fun (name, diff, expected_output) -> + name >:: create_test diff expected_output) + +let test_resolve_infer_eradicate_conflict = + let fake_filters_factory analyzer = + match analyzer with + | Config.Eradicate -> + { + Inferconfig.path_filter = (function _ -> true); (* all paths are whitelisted *) + error_filter = (function _ -> failwith "error_filter is not needed"); + proc_filter = (function _ -> failwith "proc_filter is not needed"); + } + | _ -> failwith "This mock only supports Eradicate" in + let create_test analyzer (exp_introduced, exp_fixed, exp_preexisting) _ = + let null_dereference = Localise.to_string Localise.null_dereference in + let current_report = [ + create_fake_jsonbug ~bug_type:"bug_type_1" ~file:"file_1.java" ~hash:1 (); + create_fake_jsonbug ~bug_type:null_dereference ~file:"file_2.java" ~hash:2 (); + create_fake_jsonbug ~bug_type:"bug_type_1" ~file:"file_4.java" ~hash:4 (); + ] in + let previous_report = [ + create_fake_jsonbug ~bug_type:"bug_type_1" ~file:"file_1.java" ~hash:11 (); + create_fake_jsonbug ~bug_type:null_dereference ~file:"file_3.java" ~hash:3 (); + create_fake_jsonbug ~bug_type:"bug_type_1" ~file:"file_4.java" ~hash:4 (); + ] in + let diff = Differential.of_reports ~current_report ~previous_report in + let diff' = + DifferentialFilters.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.resolve_infer_eradicate_conflict + analyzer fake_filters_factory diff in + assert_equal + ~pp_diff:(pp_diff_of_int_list "Hashes of introduced") + exp_introduced (sorted_hashes_of_issues diff'.introduced); + assert_equal + ~pp_diff:(pp_diff_of_int_list "Hashes of fixed") + exp_fixed (sorted_hashes_of_issues diff'.fixed); + assert_equal + ~pp_diff:(pp_diff_of_int_list "Hashes of preexisting") + exp_preexisting (sorted_hashes_of_issues diff'.preexisting) in + (* [(test_name, analyzer, expected_hashes); ...] *) + [ + ("test_resolve_infer_eradicate_conflict_runs_with_infer_analyzer", + Config.Infer, + ([1], [11], [4])); + ("test_resolve_infer_eradicate_conflict_skips_with_checkers_analyzer", + Config.Checkers, + ([1;2], [3;11], [4])); + ("test_resolve_infer_eradicate_conflict_skips_with_linters_analyzer", + Config.Linters, + ([1;2], [3;11], [4])); + ] |> List.map + ~f:(fun (name, analyzer, expected_output) -> + name >:: create_test analyzer expected_output) + +let tests = "differential_filters_suite" >::: + test_file_renamings_from_json @ + test_file_renamings_find_previous @ + test_relative_complements @ + test_skip_anonymous_class_renamings @ + test_resolve_infer_eradicate_conflict @ + [test_skip_duplicated_types_on_filenames; test_value_of_qualifier_tag] diff --git a/infer/src/unit/DifferentialTests.ml b/infer/src/unit/DifferentialTests.ml new file mode 100644 index 000000000..38b7f6c7b --- /dev/null +++ b/infer/src/unit/DifferentialTests.ml @@ -0,0 +1,61 @@ +(* + * Copyright (c) 2017 - 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 + +open OUnit2 + +open DifferentialTestsUtils + +let current_report = [ + create_fake_jsonbug ~hash:3 (); + create_fake_jsonbug ~hash:1 (); + create_fake_jsonbug ~hash:2 (); + create_fake_jsonbug ~hash:2 (); + create_fake_jsonbug ~hash:2 (); +] + +let previous_report = [ + create_fake_jsonbug ~hash:1 (); + create_fake_jsonbug ~hash:4 (); + create_fake_jsonbug ~hash:1 (); +] + +let diff = Differential.of_reports ~current_report ~previous_report + +(* Sets operations should keep duplicated issues with identical hashes *) +let test_diff_keeps_duplicated_hashes = + let hashes_expected = 3 in + let hashes_found = List.fold + ~init:0 + ~f:(fun acc i -> if Int.equal i.Jsonbug_t.hash 2 then acc + 1 else acc) + diff.introduced in + let pp_diff fmt (expected, actual) = + Format.fprintf fmt + "Expected %d issues with hash=2 among the introduced, but got %d instead" + expected + actual in + let do_assert _ = assert_equal ~pp_diff hashes_expected hashes_found in + "test_diff_keeps_duplicated_hashes" >:: do_assert + +(* Sets operations to compute introduced, fixed and preexisting issues are correct *) +let test_set_operations = + let do_assert _ = + assert_equal + ~pp_diff:(pp_diff_of_int_list "Hashes of introduced") + [2;2;2;3] (sorted_hashes_of_issues diff.introduced); + assert_equal + ~pp_diff:(pp_diff_of_int_list "Hashes of fixed") + [4] (sorted_hashes_of_issues diff.fixed); + assert_equal + ~pp_diff:(pp_diff_of_int_list "Hashes of preexisting") + [1] (sorted_hashes_of_issues diff.preexisting) in + "test_set_operations" >:: do_assert + +let tests = "differential_suite" >::: [test_diff_keeps_duplicated_hashes; test_set_operations] diff --git a/infer/src/unit/DifferentialTestsUtils.ml b/infer/src/unit/DifferentialTestsUtils.ml new file mode 100644 index 000000000..e39e0b929 --- /dev/null +++ b/infer/src/unit/DifferentialTestsUtils.ml @@ -0,0 +1,62 @@ +(* + * Copyright (c) 2017 - 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 + +let create_fake_jsonbug + ?(bug_class="bug_class") + ?(kind="kind") + ?(bug_type="bug_type") + ?(qualifier="qualifier") + ?(severity="severity") + ?(visibility="visibility") + ?(line=1) + ?(column=1) + ?(procedure="procedure") + ?(procedure_id="procedure_id") + ?(procedure_start_line=1) + ?(file="file/at/a/certain/path.java") + ?(bug_trace=[]) + ?(key=1234) + ?(qualifier_tags=[]) + ?(hash=1) + ?(dotty=None) + ?(infer_source_loc=None) () : Jsonbug_t.jsonbug = + { + bug_class; + kind; + bug_type; + qualifier; + severity; + visibility; + line; + column; + procedure; + procedure_id; + procedure_start_line; + file; + bug_trace; + key; + qualifier_tags; + hash; + dotty; + infer_source_loc; + } + +let pp_diff_of_int_list group_name fmt (expected, actual) = + Format.fprintf fmt + "[%s]: Expected: [%a] Found: [%a]" + group_name + (Pp.comma_seq Format.pp_print_int) expected + (Pp.comma_seq Format.pp_print_int) actual + +(* Sort hashes to make things easier to compare *) +let sorted_hashes_of_issues issues = + let hash i = i.Jsonbug_t.hash in + List.sort ~cmp:Int.compare (List.map ~f:hash issues) diff --git a/infer/src/unit/inferunit.ml b/infer/src/unit/inferunit.ml index 113746d07..d9b8c7fe5 100644 --- a/infer/src/unit/inferunit.ml +++ b/infer/src/unit/inferunit.ml @@ -20,6 +20,8 @@ let () = AddressTakenTests.tests; BoundedCallTreeTests.tests; CopyPropagationTests.tests; + DifferentialTests.tests; + DifferentialFiltersTests.tests; ProcCfgTests.tests; LivenessTests.tests; SchedulerTests.tests; diff --git a/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/.inferconfig b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/.inferconfig new file mode 100644 index 000000000..77db692a8 --- /dev/null +++ b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/.inferconfig @@ -0,0 +1,5 @@ +{ + "eradicate-whitelist-path-regex": [ + "src/com" + ] +} diff --git a/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/Makefile b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/Makefile new file mode 100644 index 000000000..e28fe2f88 --- /dev/null +++ b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/Makefile @@ -0,0 +1,26 @@ +# Copyright (c) 2017 - 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. + +# E2E test involving the resolve_infer_eradicate_conflict filter + +TESTS_DIR = ../.. +DIFFERENTIAL_ARGS = --resolve-infer-eradicate-conflict +CLEAN_EXTRA = src/com/example/Diff*.java src/Diff*.java *.class com/ + +include ../../differential.make + +$(CURRENT_REPORT): + cp src/com/example/DiffExample.java.current src/com/example/DiffExample.java + cp src/DiffExampleTwo.java.current src/DiffExampleTwo.java + $(call silent_on_success,\ + $(INFER_BIN) \ + -o $(CURRENT_DIR) -- javac src/com/example/DiffExample.java src/DiffExampleTwo.java) + +$(PREVIOUS_REPORT): + cp src/com/example/DiffExample.java.previous src/com/example/DiffExample.java + $(call silent_on_success,\ + $(INFER_BIN) -o $(PREVIOUS_DIR) -- javac src/com/example/DiffExample.java) diff --git a/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/fixed.exp b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/fixed.exp new file mode 100644 index 000000000..e69de29bb diff --git a/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/introduced.exp b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/introduced.exp new file mode 100644 index 000000000..5c3f4d0fc --- /dev/null +++ b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/introduced.exp @@ -0,0 +1,2 @@ +NULL_DEREFERENCE, src/DiffExampleTwo.java, void DiffExampleTwo.doSomethingTwo(), 1, DiffExampleTwo.doSomethingTwo():void.d8149869686ac2ef26a75ac4829094a7, DiffExampleTwo.doSomethingTwo():void +RESOURCE_LEAK, src/com/example/DiffExample.java, void DiffExample.openResource(), 5, com.example.DiffExample.openResource():void.75390f44594cb95db15fe8db9d07c4be, com.example.DiffExample.openResource():void diff --git a/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/preexisting.exp b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/preexisting.exp new file mode 100644 index 000000000..e69de29bb diff --git a/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/src/DiffExampleTwo.java.current b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/src/DiffExampleTwo.java.current new file mode 100644 index 000000000..ee716da18 --- /dev/null +++ b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/src/DiffExampleTwo.java.current @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2017 - 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. + */ + +// This example tests the resolve_infer_eradicate_conflict filter +class DiffExampleTwo { + private String genString() { + return null; + } + + private void doSomethingTwo() { + int i = this.genString().length(); + } +} diff --git a/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/src/com/example/DiffExample.java.current b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/src/com/example/DiffExample.java.current new file mode 100644 index 000000000..5e9686458 --- /dev/null +++ b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/src/com/example/DiffExample.java.current @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2017 - 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. + */ + +package com.example; + +import java.io.FileInputStream; + +// This example tests the resolve_infer_eradicate_conflict filter +class DiffExample { + private String genString() { + return null; + } + + + private int triggerNpe() { + return this.genString().length(); + } + + + private void openResource() { + try { + FileInputStream fis = new FileInputStream("AAA"); + fis.read(); + fis.close(); + } catch (Exception exc) { + // do nothing + } + } +} diff --git a/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/src/com/example/DiffExample.java.previous b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/src/com/example/DiffExample.java.previous new file mode 100644 index 000000000..b63a61fa0 --- /dev/null +++ b/infer/tests/build_systems/differential_resolve_infer_eradicate_conflict/src/com/example/DiffExample.java.previous @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2017 - 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. + */ + +package com.example; + +import java.io.FileInputStream; + +// This example tests the resolve_infer_eradicate_conflict filter +class DiffExample { + private String genString() { + return null; + } + + + private int triggerNpe() { + return this.genString().length(); + } +} diff --git a/infer/tests/build_systems/differential_skip_anonymous_class_renamings/Makefile b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/Makefile new file mode 100644 index 000000000..74c4f40c4 --- /dev/null +++ b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/Makefile @@ -0,0 +1,21 @@ +# Copyright (c) 2017 - 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. + +# E2E test involving the skip_anonymous_class_renamings filter + +TESTS_DIR = ../.. +CLEAN_EXTRA = src/Diff*.java *.class + +include ../../differential.make + +$(CURRENT_REPORT): + cp src/DiffExample.java.current src/DiffExample.java + $(call silent_on_success, $(INFER_BIN) -o $(CURRENT_DIR) -- javac src/*.java) + +$(PREVIOUS_REPORT): + cp src/DiffExample.java.previous src/DiffExample.java + $(call silent_on_success, $(INFER_BIN) -o $(PREVIOUS_DIR) -- javac src/*.java) diff --git a/infer/tests/build_systems/differential_skip_anonymous_class_renamings/fixed.exp b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/fixed.exp new file mode 100644 index 000000000..e69de29bb diff --git a/infer/tests/build_systems/differential_skip_anonymous_class_renamings/introduced.exp b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/introduced.exp new file mode 100644 index 000000000..66f6b9e2b --- /dev/null +++ b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/introduced.exp @@ -0,0 +1 @@ +NULL_DEREFERENCE, src/DiffExample.java, void DiffExample$3$1.doSomething(), 1, DiffExample$3$1.doSomething():void.64afb6aca478af18163141bbb8999018, DiffExample$3$1.doSomething():void diff --git a/infer/tests/build_systems/differential_skip_anonymous_class_renamings/preexisting.exp b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/preexisting.exp new file mode 100644 index 000000000..e69de29bb diff --git a/infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/DiffExample.java.current b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/DiffExample.java.current new file mode 100644 index 000000000..5c3a44a69 --- /dev/null +++ b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/DiffExample.java.current @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2017 - 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. + */ + +// This example tests the skip_anonymous_class_renamings filter +class DiffExample { + private int checkAnonymousClasses() { + SimpleInterfaceExample sie1 = new SimpleInterfaceExample() { + public String getString() { return "111"; } + public int aaa() { return this.getString().length(); } + }; + SimpleInterfaceExample sie2 = new SimpleInterfaceExample() { + public String getString() { return "111"; } + public int aaa() { return this.getString().length(); } + }; + + + new SimpleInterfaceExample() { + public String getString() { return null; } + public int aaa() { + new SimpleNestedInterface() { + public void doSomething() { + int a = getString().length(); + } + }; + return 1; + } + }; + return new SimpleInterfaceExample() { + public String getString() { return null; } + public int aaa() { return this.getString().length(); } + }.aaa(); + } +} diff --git a/infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/DiffExample.java.previous b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/DiffExample.java.previous new file mode 100644 index 000000000..44bb5f967 --- /dev/null +++ b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/DiffExample.java.previous @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2017 - 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. + */ + +// This example tests the skip_anonymous_class_renamings filter +class DiffExample { + private int checkAnonymousClasses() { + SimpleInterfaceExample sie1 = new SimpleInterfaceExample() { + public String getString() { return "111"; } + public int aaa() { return this.getString().length(); } + }; + SimpleInterfaceExample sie2 = new SimpleInterfaceExample() { + public String getString() { return "111"; } + public int aaa() { return this.getString().length(); } + }; + + return new SimpleInterfaceExample() { + public String getString() { return null; } + public int aaa() { return this.getString().length(); } + }.aaa(); + } +} diff --git a/infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/SimpleInterfaceExample.java b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/SimpleInterfaceExample.java new file mode 100644 index 000000000..b172badc0 --- /dev/null +++ b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/SimpleInterfaceExample.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2017 - 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. + */ + +// This example tests the skip_anonymous_class_renamings filter +public interface SimpleInterfaceExample { + public String getString(); +} diff --git a/infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/SimpleNestedInterface.java b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/SimpleNestedInterface.java new file mode 100644 index 000000000..7ff7bb59f --- /dev/null +++ b/infer/tests/build_systems/differential_skip_anonymous_class_renamings/src/SimpleNestedInterface.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2017 - 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. + */ + +// This example tests the skip_anonymous_class_renamings filter +public interface SimpleNestedInterface { + public void doSomething(); +} diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/Makefile b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/Makefile new file mode 100644 index 000000000..4c13893c8 --- /dev/null +++ b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/Makefile @@ -0,0 +1,24 @@ +# Copyright (c) 2017 - 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. + +# E2E test involving the skip_duplicated_types_on_filenames filter + +TESTS_DIR = ../.. +CLEAN_EXTRA = src/Diff*.java src/Diff*.java *.class + +include ../../differential.make + +$(CURRENT_REPORT): + cp src/DiffExample.java.current src/DiffExample.java + cp src/DiffExampleTwo.java.current src/DiffExampleTwo.java + $(call silent_on_success,\ + $(INFER_BIN) -o $(CURRENT_DIR) -- javac src/DiffExample.java src/DiffExampleTwo.java) + +$(PREVIOUS_REPORT): + cp src/DiffExample.java.previous src/DiffExample.java + $(call silent_on_success,\ + $(INFER_BIN) -o $(PREVIOUS_DIR) -- javac src/DiffExample.java) diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/fixed.exp b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/fixed.exp new file mode 100644 index 000000000..e69de29bb diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/introduced.exp b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/introduced.exp new file mode 100644 index 000000000..ba2bc0bce --- /dev/null +++ b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/introduced.exp @@ -0,0 +1,2 @@ +RESOURCE_LEAK, src/DiffExample.java, void DiffExample.openResource(), 5, DiffExample.openResource():void.c57b56c0042a220d7416e229c4e61b99, DiffExample.openResource():void +NULL_DEREFERENCE, src/DiffExampleTwo.java, void DiffExampleTwo.doSomethingTwo(), 1, DiffExampleTwo.doSomethingTwo():void.d8149869686ac2ef26a75ac4829094a7, DiffExampleTwo.doSomethingTwo():void diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/preexisting.exp b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/preexisting.exp new file mode 100644 index 000000000..e69de29bb diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/src/DiffExample.java.current b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/src/DiffExample.java.current new file mode 100644 index 000000000..72a75a2aa --- /dev/null +++ b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/src/DiffExample.java.current @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2017 - 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. + */ + +import java.io.FileInputStream; + +// This example tests the skip_duplicated_types_on_filenames filter +class DiffExample { + private String genString() { + return null; + } + + + private int triggerNpeRenamed() { + return this.genString().length(); + } + + + private void openResource() { + try { + FileInputStream fis = new FileInputStream("AAA"); + fis.read(); + fis.close(); + } catch (Exception exc) { + // do nothing + } + } +} diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/src/DiffExample.java.previous b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/src/DiffExample.java.previous new file mode 100644 index 000000000..c12e15e1b --- /dev/null +++ b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/src/DiffExample.java.previous @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2017 - 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. + */ + +import java.io.FileInputStream; + +// This example tests the skip_duplicated_types_on_filenames filter +class DiffExample { + private String genString() { + return null; + } + + // rename this method to change its bug_hash + private int triggerNPE() { + return this.genString().length(); + } +} diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/src/DiffExampleTwo.java.current b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/src/DiffExampleTwo.java.current new file mode 100644 index 000000000..9f71e7df0 --- /dev/null +++ b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames/src/DiffExampleTwo.java.current @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2017 - 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. + */ + +// This example tests the skip_duplicated_types_on_filenames filter +class DiffExampleTwo { + private String genString() { + return null; + } + + private void doSomethingTwo() { + int i = this.genString().length(); + } +} diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/Makefile b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/Makefile new file mode 100644 index 000000000..5a2873536 --- /dev/null +++ b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/Makefile @@ -0,0 +1,24 @@ +# Copyright (c) 2017 - 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. + +# E2E test involving the skip_duplicated_types_on_filenames filter + +TESTS_DIR = ../.. +DIFFERENTIAL_ARGS = --file-renamings file_renamings.json +CLEAN_EXTRA = src/Diff*.java *.class + +include ../../differential.make + +$(CURRENT_REPORT): + cp src/DiffExampleRenamed.java.current src/DiffExampleRenamed.java + $(call silent_on_success,\ + $(INFER_BIN) -o $(CURRENT_DIR) -- javac src/DiffExampleRenamed.java) + +$(PREVIOUS_REPORT): + cp src/DiffExample.java.previous src/DiffExample.java + $(call silent_on_success,\ + $(INFER_BIN) -o $(PREVIOUS_DIR) -- javac src/DiffExample.java) diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/file_renamings.json b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/file_renamings.json new file mode 100644 index 000000000..259e7e6ff --- /dev/null +++ b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/file_renamings.json @@ -0,0 +1,6 @@ +[ + { + "current": "src/DiffExampleRenamed.java", + "previous": "src/DiffExample.java" + } +] diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/fixed.exp b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/fixed.exp new file mode 100644 index 000000000..e69de29bb diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/introduced.exp b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/introduced.exp new file mode 100644 index 000000000..e69de29bb diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/preexisting.exp b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/preexisting.exp new file mode 100644 index 000000000..e69de29bb diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/src/DiffExample.java.previous b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/src/DiffExample.java.previous new file mode 100644 index 000000000..9f9bd7dd6 --- /dev/null +++ b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/src/DiffExample.java.previous @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2017 - 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. + */ + +import java.io.FileInputStream; + +// This example tests the skip_duplicated_types_on_filenames filter +class DiffExample { + private String genString() { + return null; + } + + + private int triggerNpe() { + return this.genString().length(); + } + + + private void openResource() { + try { + FileInputStream fis = new FileInputStream("AAA"); + fis.read(); + fis.close(); + } catch (Exception exc) { + // do nothing + } + } +} diff --git a/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/src/DiffExampleRenamed.java.current b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/src/DiffExampleRenamed.java.current new file mode 100644 index 000000000..6f3ed5cf5 --- /dev/null +++ b/infer/tests/build_systems/differential_skip_duplicated_types_on_filenames_with_renamings/src/DiffExampleRenamed.java.current @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2017 - 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. + */ + +import java.io.FileInputStream; + +// This example tests the skip_duplicated_types_on_filenames filter +class DiffExampleRenamed { + private String genString() { + return null; + } + + + private int triggerNpe() { + return this.genString().length(); + } + + + private void openResource() { + try { + FileInputStream fis = new FileInputStream("AAA"); + fis.read(); + fis.close(); + } catch (Exception exc) { + // do nothing + } + } +} diff --git a/infer/tests/differential.make b/infer/tests/differential.make new file mode 100644 index 000000000..5fade819f --- /dev/null +++ b/infer/tests/differential.make @@ -0,0 +1,65 @@ +# Copyright (c) 2017 - 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. + +# Targets that must be defined: CURRENT_REPORT and PREVIOUS_REPORT +# Optional variables: DIFFERENTIAL_ARGS, CLEAN_EXTRA + +ROOT_DIR = $(TESTS_DIR)/../.. +include $(ROOT_DIR)/Makefile.config + +INFER_OUT = infer-out +DIFFERENTIAL_REPORT = $(INFER_OUT)/differential/introduced.json +EXPECTED_TEST_OUTPUT = introduced.exp.test +INFERPRINT_ISSUES_FIELDS = \ + "bug_type,file,procedure,line_offset,procedure_id,procedure_id_without_crc" + +CURRENT_DIR = infer-current +PREVIOUS_DIR = infer-previous +CURRENT_REPORT = $(CURRENT_DIR)/report.json +PREVIOUS_REPORT = $(PREVIOUS_DIR)/report.json + +default: analyze + +# the following dependency is to guarantee that the computation of +# PREVIOUS_REPORT and CURRENT_REPORT will be serialized +$(PREVIOUS_REPORT): $(CURRENT_REPORT) + +.PHONY: analyze +analyze: $(CURRENT_REPORT) $(PREVIOUS_REPORT) + +$(DIFFERENTIAL_REPORT): $(CURRENT_REPORT) $(PREVIOUS_REPORT) + $(INFER_BIN) -o $(INFER_OUT) --project-root $(CURDIR) --diff \ + --report-current $(CURRENT_REPORT) --report-previous $(PREVIOUS_REPORT) \ + $(DIFFERENTIAL_ARGS) + +$(EXPECTED_TEST_OUTPUT): $(DIFFERENTIAL_REPORT) $(INFERPRINT_BIN) + $(INFERPRINT_BIN) \ + --issues-fields $(INFERPRINT_ISSUES_FIELDS) \ + --from-json-report $(INFER_OUT)/differential/introduced.json \ + --issues-tests introduced.exp.test + $(INFERPRINT_BIN) \ + --issues-fields $(INFERPRINT_ISSUES_FIELDS) \ + --from-json-report $(INFER_OUT)/differential/fixed.json \ + --issues-tests fixed.exp.test + $(INFERPRINT_BIN) \ + --issues-fields $(INFERPRINT_ISSUES_FIELDS) \ + --from-json-report $(INFER_OUT)/differential/preexisting.json \ + --issues-tests preexisting.exp.test + +.PHONY: print +print: $(EXPECTED_TEST_OUTPUT) + +.PHONY: test +test: print + diff -u introduced.exp introduced.exp.test + diff -u fixed.exp fixed.exp.test + diff -u preexisting.exp preexisting.exp.test + +.PHONY: clean +clean: + $(REMOVE_DIR) *.exp.test $(INFER_OUT) $(CURRENT_DIR) $(PREVIOUS_DIR) \ + $(CLEAN_EXTRA)