diff --git a/infer/src/deadcode/Makefile b/infer/src/deadcode/Makefile index 298ccddf1..79bbdf6d8 100644 --- a/infer/src/deadcode/Makefile +++ b/infer/src/deadcode/Makefile @@ -46,7 +46,7 @@ depend: ocamldep -native \ -I IR -I absint -I al -I atd -I backend -I base -I biabduction -I bufferoverrun \ -I checkers -I clang -I concurrency -I facebook -I integration -I istd -I java \ - -I labs -I nullsafe -I pulse -I scuba -I quandary -I topl -I unit -I unit/clang -I deadcode \ + -I labs -I nullsafe -I pulse -I scuba -I quandary -I topl -I unit -I unit/clang -I unit/nullsafe -I deadcode \ -I test_determinator \ $(ml_src_files) > deadcode/.depend diff --git a/infer/src/dune.in b/infer/src/dune.in index a95bb1ab5..d1c5f84ff 100644 --- a/infer/src/dune.in +++ b/infer/src/dune.in @@ -28,7 +28,8 @@ let source_dirs = ; "scuba" ; "test_determinator" ; "topl" - ; "unit" ] ) + ; "unit" + ; "unit" ^/ "nullsafe" ] ) let infer_binaries = diff --git a/infer/src/istd/Pp.ml b/infer/src/istd/Pp.ml index a59f7999d..7a2c2f2d4 100644 --- a/infer/src/istd/Pp.ml +++ b/infer/src/istd/Pp.ml @@ -134,6 +134,8 @@ let option pp fmt = function let to_string ~f fmt x = F.pp_print_string fmt (f x) +let string_of_pp pp = Format.asprintf "%a" pp + let cli_args fmt args = let pp_args fmt args = F.fprintf fmt "@[ " ; diff --git a/infer/src/istd/Pp.mli b/infer/src/istd/Pp.mli index a6c9b10d2..630393dce 100644 --- a/infer/src/istd/Pp.mli +++ b/infer/src/istd/Pp.mli @@ -88,6 +88,9 @@ val semicolon_seq : ?print_env:env -> (F.formatter -> 'a -> unit) -> F.formatter val to_string : f:('a -> string) -> F.formatter -> 'a -> unit (** turn a "to_string" function into a "pp_foo" *) +val string_of_pp : (F.formatter -> 'a -> unit) -> 'a -> string +(** turn "pp_foo" to "to_string" function *) + val current_time : F.formatter -> unit -> unit (** Print the current time and date in a format similar to the "date" command *) diff --git a/infer/src/nullsafe/ThirdPartyMethod.ml b/infer/src/nullsafe/ThirdPartyMethod.ml new file mode 100644 index 000000000..eba0d381a --- /dev/null +++ b/infer/src/nullsafe/ThirdPartyMethod.ml @@ -0,0 +1,134 @@ +(* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) +open! IStd +module F = Format +open Result.Monad_infix + +type fully_qualified_type = string [@@deriving sexp] + +type unique_repr = + { class_name: fully_qualified_type + ; method_name: method_name + ; param_types: fully_qualified_type list } +[@@deriving sexp] + +and method_name = Constructor | Method of string + +type type_nullability = Nullable | Nonnull [@@deriving sexp] + +type nullability = {ret_nullability: type_nullability; param_nullability: type_nullability list} +[@@deriving sexp] + +type parsing_error = BadStructure | MalformedNullability | LackingParam | MalformedParam + +let string_of_parsing_error = function + | BadStructure -> + "BadStructure" + | MalformedNullability -> + "MalformedNullability" + | LackingParam -> + "LackingParam" + | MalformedParam -> + "MalformedParam" + + +let pp_unique_repr fmt signature = Sexp.pp fmt (sexp_of_unique_repr signature) + +let pp_nullability fmt nullability = Sexp.pp fmt (sexp_of_nullability nullability) + +let parse_nullability str = + match String.strip str with + | "@Nullable" -> + Ok Nullable + | "" -> + Ok Nonnull + | _ -> + Error MalformedNullability + + +let whitespace_no_line_break = lazy (Str.regexp "[ \t]+") + +let parse_param str = + let trimmed_param = String.strip str in + match Str.split (Lazy.force whitespace_no_line_break) trimmed_param with + | [nullability_str; typ] -> + parse_nullability nullability_str >>= fun nullability -> Ok (typ, nullability) + | [typ] -> + Ok (typ, Nonnull) + | [] -> + Error LackingParam + | _ -> + Error MalformedParam + + +(* Given a list of results and a binding function, returns Ok (results) if all results + are Ok or the first error if any of results has Error *) +let bind_list list_of_results ~f = + List.fold list_of_results ~init:(Ok []) ~f:(fun acc element -> + acc + >>= fun accumulated_success_results -> + f element >>= fun success_result -> Ok (accumulated_success_results @ [success_result]) ) + + +let split_params str = + let stripped = String.strip str in + (* Empty case is the special one: lack of params mean an empty list, + not a list of a single empty string *) + if String.is_empty stripped then [] else String.split stripped ~on:',' + + +let parse_params str = split_params str |> bind_list ~f:parse_param + +let parse_method_name str = + match String.strip str with "" -> Constructor | _ as method_name -> Method method_name + + +let match_after_close_brace (split_result : Str.split_result list) = + (* After close brace there can be either nothing or return type nullability information *) + match split_result with + | [] -> + parse_nullability "" + | [Text nullability] -> + parse_nullability nullability + | _ -> + Error BadStructure + + +let match_after_open_brace (split_result : Str.split_result list) = + (* We expect to see ), so let's look for `)` *) + ( match split_result with + | Text params :: Delim ")" :: rest -> + Ok (params, rest) + | Delim ")" :: rest -> + Ok ("", rest) + | _ -> + Error BadStructure ) + >>= fun (params, rest) -> + parse_params params + >>= fun parsed_params -> + match_after_close_brace rest >>= fun ret_nullability -> Ok (parsed_params, ret_nullability) + + +let hashsign_and_parentheses = lazy (Str.regexp "[#()]") + +let parse str = + (* Expected string is #(), + let's look what is between #, (, and ) *) + match Str.full_split (Lazy.force hashsign_and_parentheses) str with + | Text class_name_str :: Delim "#" :: Text method_name_str :: Delim "(" :: rest -> + let method_name = parse_method_name method_name_str in + let class_name = String.strip class_name_str in + match_after_open_brace rest + >>= fun (parsed_params, ret_nullability) -> + let param_types, param_nullability = List.unzip parsed_params in + Ok ({class_name; method_name; param_types}, {ret_nullability; param_nullability}) + | _ -> + Error BadStructure + + +let pp_parse_result fmt (unique_repr, nullability) = + F.fprintf fmt "(%a; %a)" pp_unique_repr unique_repr pp_nullability nullability diff --git a/infer/src/nullsafe/ThirdPartyMethod.mli b/infer/src/nullsafe/ThirdPartyMethod.mli new file mode 100644 index 000000000..9271f167c --- /dev/null +++ b/infer/src/nullsafe/ThirdPartyMethod.mli @@ -0,0 +1,45 @@ +(* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +(** A helper module responsible for representing nullability information for a single 3rd party method, + as well with functionality to read this information from the 3rd party nullability repository. + *) + +open! IStd + +(** E.g. "full.package.name.TypeName$NestedTypeName1$NestedTypeName2" + *) +type fully_qualified_type = string + +(** The minimum information that is needed to _uniquely_ identify the method. + That why we don't include e.g. return type, access quilifiers, or whether the method is static + (because Java overload resolution rules ignore these things). + In contrast, parameter types are essential, because Java allows several methods with different types. + *) +type unique_repr = + { class_name: fully_qualified_type + ; method_name: method_name + ; param_types: fully_qualified_type list } + +and method_name = Constructor | Method of string + +type nullability = {ret_nullability: type_nullability; param_nullability: type_nullability list} + +and type_nullability = Nullable | Nonnull + +type parsing_error + +val string_of_parsing_error : parsing_error -> string + +val parse : string -> (unique_repr * nullability, parsing_error) result +(** Given a string representing nullability information for a given third-party method, + return the method signature and nullability of its params and return values. + The string should come from a repository storing 3rd party annotations. + E.g. + "package.name.Class$NestedClass#foo(package.name.SomeClass, @Nullable package.name.OtherClass) @Nullable" *) + +val pp_parse_result : Format.formatter -> unique_repr * nullability -> unit diff --git a/infer/src/unit/inferunit.ml b/infer/src/unit/inferunit.ml index 55cafb5e7..2e4ae5610 100644 --- a/infer/src/unit/inferunit.ml +++ b/infer/src/unit/inferunit.ml @@ -44,7 +44,7 @@ let () = ; TaintTests.tests ; TraceTests.tests ; WeakTopologicalOrderTests.tests ] - @ ClangTests.tests ) + @ ClangTests.tests @ AllNullsafeTests.tests ) in let test_suite = "all" >::: tests in OUnit2.run_test_tt_main test_suite diff --git a/infer/src/unit/nullsafe/AllNullsafeTests.ml b/infer/src/unit/nullsafe/AllNullsafeTests.ml new file mode 100644 index 000000000..b52d5123b --- /dev/null +++ b/infer/src/unit/nullsafe/AllNullsafeTests.ml @@ -0,0 +1,10 @@ +(* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +open! IStd + +let tests = [ThirdPartyMethodTests.test] diff --git a/infer/src/unit/nullsafe/AllNullsafeTests.mli b/infer/src/unit/nullsafe/AllNullsafeTests.mli new file mode 100644 index 000000000..38e35a830 --- /dev/null +++ b/infer/src/unit/nullsafe/AllNullsafeTests.mli @@ -0,0 +1,10 @@ +(* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +open! IStd + +val tests : OUnit2.test list diff --git a/infer/src/unit/nullsafe/ThirdPartyMethodTests.ml b/infer/src/unit/nullsafe/ThirdPartyMethodTests.ml new file mode 100644 index 000000000..0ebb3942b --- /dev/null +++ b/infer/src/unit/nullsafe/ThirdPartyMethodTests.ml @@ -0,0 +1,113 @@ +(* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +open! IStd +module F = Format +open OUnit2 +open ThirdPartyMethod + +let assert_parse_ok input expected_output = + let result = ThirdPartyMethod.parse input in + match result with + | Ok output -> + assert_equal output expected_output ~printer:(fun parse_result -> + Pp.string_of_pp pp_parse_result parse_result ) + | Error error -> + assert_failure + (F.asprintf "Expected '%s' to be parsed as %a, but got error %s instead" input + ThirdPartyMethod.pp_parse_result expected_output (string_of_parsing_error error)) + + +let assert_parse_bad input = + let result = ThirdPartyMethod.parse input in + match result with + | Ok output -> + assert_failure + (F.asprintf "Expected '%s' to be NOT parsed, but was parsed as %a instead" input + ThirdPartyMethod.pp_parse_result output) + | Error _ -> + () + + +let success_cases = + "success_cases" + >:: fun _ -> + (* No params *) + assert_parse_ok "a.b.C#foo()" + ( {class_name= "a.b.C"; method_name= Method "foo"; param_types= []} + , {ret_nullability= Nonnull; param_nullability= []} ) ; + assert_parse_ok " a.b.C # foo ( ) " + ( {class_name= "a.b.C"; method_name= Method "foo"; param_types= []} + , {ret_nullability= Nonnull; param_nullability= []} ) ; + assert_parse_ok "a.b.C#foo() @Nullable" + ( {class_name= "a.b.C"; method_name= Method "foo"; param_types= []} + , {ret_nullability= Nullable; param_nullability= []} ) ; + assert_parse_ok "a.b.C # foo( ) @Nullable " + ( {class_name= "a.b.C"; method_name= Method "foo"; param_types= []} + , {ret_nullability= Nullable; param_nullability= []} ) ; + (* One param *) + assert_parse_ok "a.b.C#foo(c.d.E)" + ( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"]} + , {ret_nullability= Nonnull; param_nullability= [Nonnull]} ) ; + assert_parse_ok "a.b.C#foo(@Nullable c.d.E)" + ( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"]} + , {ret_nullability= Nonnull; param_nullability= [Nullable]} ) ; + assert_parse_ok "a.b.C#foo(c.d.E) @Nullable" + ( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"]} + , {ret_nullability= Nullable; param_nullability= [Nonnull]} ) ; + assert_parse_ok "a.b.C#foo(@Nullable c.d.E) @Nullable" + ( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"]} + , {ret_nullability= Nullable; param_nullability= [Nullable]} ) ; + assert_parse_ok " a.b.C # foo ( @Nullable c.d.E ) @Nullable " + ( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"]} + , {ret_nullability= Nullable; param_nullability= [Nullable]} ) ; + (* Many params *) + assert_parse_ok "a.b.C#foo(c.d.E,a.b.C,x.y.Z)" + ( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"; "a.b.C"; "x.y.Z"]} + , {ret_nullability= Nonnull; param_nullability= [Nonnull; Nonnull; Nonnull]} ) ; + assert_parse_ok "a.b.C#foo(c.d.E, @Nullable a.b.C, x.y.Z)" + ( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"; "a.b.C"; "x.y.Z"]} + , {ret_nullability= Nonnull; param_nullability= [Nonnull; Nullable; Nonnull]} ) ; + assert_parse_ok "a.b.C#foo(@Nullable c.d.E, a.b.C, @Nullable x.y.Z) @Nullable" + ( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"; "a.b.C"; "x.y.Z"]} + , {ret_nullability= Nullable; param_nullability= [Nullable; Nonnull; Nullable]} ) ; + assert_parse_ok + "a.b.C # foo ( @Nullable c.d.E , a.b.C , @Nullable x.y.Z ) @Nullable " + ( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"; "a.b.C"; "x.y.Z"]} + , {ret_nullability= Nullable; param_nullability= [Nullable; Nonnull; Nullable]} ) ; + (* Constructor *) + assert_parse_ok "a.b.C#(@Nullable c.d.E, a.b.C, x.y.Z) @Nullable" + ( {class_name= "a.b.C"; method_name= Constructor; param_types= ["c.d.E"; "a.b.C"; "x.y.Z"]} + , {ret_nullability= Nullable; param_nullability= [Nullable; Nonnull; Nonnull]} ) + + +(* We intentionally don't test all bad cases. + It is generally OK for nullsafe to allow something that is not really valid: + We let other tools to make thorough linting. + Also we don't test exact error type, because this is an implementation detail + needed merely to simplify diagnostics + *) +let bad_cases = + "bad_cases" + >:: fun _ -> + assert_parse_bad "" ; + assert_parse_bad " " ; + assert_parse_bad "blablabla" ; + assert_parse_bad "a.b.C.f()" ; + (* no # delimiter *) + assert_parse_bad "a.b.C#f(())" ; + (* nested parenthesis *) + assert_parse_bad "a.b.C#f(int param)" ; + (* param names are not accepted *) + assert_parse_bad "a.b.C#f(Nullable param)" ; + (* Missed @ in annotation*) + assert_parse_bad "a.b.C#f(@Nullable int param)" + + +(* param names are not accepted *) + +let test = "ThirdPartyMethodTests" >::: [success_cases; bad_cases]