[nullsafe] Signature and nulllability of a third party method

Summary:
This is a helper module for reading info from a 3rd party nullability repository.
Next diffs are going to use it for reading nullability repository from
disk.

Reviewed By: artempyanykh

Differential Revision: D18225473

fbshipit-source-id: 06a2dc97e
master
Mitya Lyubarskiy 5 years ago committed by Facebook Github Bot
parent 7b4f8a4bbc
commit 688deb0936

@ -46,7 +46,7 @@ depend:
ocamldep -native \ ocamldep -native \
-I IR -I absint -I al -I atd -I backend -I base -I biabduction -I bufferoverrun \ -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 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 \ -I test_determinator \
$(ml_src_files) > deadcode/.depend $(ml_src_files) > deadcode/.depend

@ -28,7 +28,8 @@ let source_dirs =
; "scuba" ; "scuba"
; "test_determinator" ; "test_determinator"
; "topl" ; "topl"
; "unit" ] ) ; "unit"
; "unit" ^/ "nullsafe" ] )
let infer_binaries = let infer_binaries =

@ -134,6 +134,8 @@ let option pp fmt = function
let to_string ~f fmt x = F.pp_print_string fmt (f x) 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 cli_args fmt args =
let pp_args fmt args = let pp_args fmt args =
F.fprintf fmt "@[<hov2> " ; F.fprintf fmt "@[<hov2> " ;

@ -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 val to_string : f:('a -> string) -> F.formatter -> 'a -> unit
(** turn a "to_string" function into a "pp_foo" *) (** 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 val current_time : F.formatter -> unit -> unit
(** Print the current time and date in a format similar to the "date" command *) (** Print the current time and date in a format similar to the "date" command *)

@ -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 "<init>" -> 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 <params>)<nullability>, 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 <Class>#<method>(<params>)<ret_nullability>,
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

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

@ -44,7 +44,7 @@ let () =
; TaintTests.tests ; TaintTests.tests
; TraceTests.tests ; TraceTests.tests
; WeakTopologicalOrderTests.tests ] ; WeakTopologicalOrderTests.tests ]
@ ClangTests.tests ) @ ClangTests.tests @ AllNullsafeTests.tests )
in in
let test_suite = "all" >::: tests in let test_suite = "all" >::: tests in
OUnit2.run_test_tt_main test_suite OUnit2.run_test_tt_main test_suite

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

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

@ -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#<init>(@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]
Loading…
Cancel
Save