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
master
Martino Luca 8 years ago committed by Facebook Github Bot
parent bc852ec0d1
commit f8a65e698c

4
.gitignore vendored

@ -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

@ -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 \

@ -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 */

@ -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)

@ -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

@ -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

@ -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

@ -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 "@."

@ -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 ()

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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]

@ -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]

@ -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)

@ -20,6 +20,8 @@ let () =
AddressTakenTests.tests;
BoundedCallTreeTests.tests;
CopyPropagationTests.tests;
DifferentialTests.tests;
DifferentialFiltersTests.tests;
ProcCfgTests.tests;
LivenessTests.tests;
SchedulerTests.tests;

@ -0,0 +1,5 @@
{
"eradicate-whitelist-path-regex": [
"src/com"
]
}

@ -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)

@ -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

@ -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();
}
}

@ -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
}
}
}

@ -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();
}
}

@ -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)

@ -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

@ -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();
}
}

@ -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();
}
}

@ -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();
}

@ -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();
}

@ -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)

@ -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

@ -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
}
}
}

@ -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();
}
}

@ -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();
}
}

@ -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)

@ -0,0 +1,6 @@
[
{
"current": "src/DiffExampleRenamed.java",
"previous": "src/DiffExample.java"
}
]

@ -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
}
}
}

@ -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
}
}
}

@ -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)
Loading…
Cancel
Save